Compare commits
	
		
			28 Commits
		
	
	
		
			main
			...
			b2904c6b85
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b2904c6b85 | |||
| 9a3bbc95d9 | |||
| a4249899e9 | |||
| 9ca2ea7e1d | |||
| 6265a4e9b2 | |||
| f04a014478 | |||
| b3366e5cfe | |||
| f6b4be1485 | |||
| 029d24ce61 | |||
| ecdf3d30eb | |||
| 60416e447c | |||
| 0077a768e8 | |||
| b9bcbf829d | |||
| 691cb7da73 | |||
| efc88658ee | |||
| 5d99a0d2c5 | |||
| 258b2613d5 | |||
| efe158f83f | |||
| 77f11a7440 | |||
| df8198ef94 | |||
| c09db757bc | |||
| f53dab338a | |||
| 25f1b20a7a | |||
| 28a0ad26ce | |||
| 3e3dbcdda8 | |||
| 6351381789 | |||
| d1bcdcd530 | |||
| 9870a643dd | 
							
								
								
									
										16
									
								
								res/color_overrides.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								res/color_overrides.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | { | ||||||
|  | 	"minecraft:seagrass": [97, 113, 54], | ||||||
|  | 	"minecraft:chest": [171, 121, 45], | ||||||
|  | 	"minecraft:ender_chest": [48, 67, 71], | ||||||
|  | 	"minecraft:lily_pad": [32, 128, 48], | ||||||
|  | 	"minecraft:redstone_wire": [189, 32, 8], | ||||||
|  | 	"minecraft:cocoa": [150, 87, 26], | ||||||
|  | 	"minecraft:wheat": [220, 187, 101], | ||||||
|  | 	"minecraft:carrots": [227, 138, 29], | ||||||
|  | 	"minecraft:potatoes": [200, 162, 75], | ||||||
|  | 	"minecraft:beetroots": [191, 37,41], | ||||||
|  | 	"minecraft:nether_wart": [131, 28, 32], | ||||||
|  | 	"minecraft:sweet_berry_bush": [60, 110, 66], | ||||||
|  | 	"minecraft:torchflower_crop": [130, 158, 85], | ||||||
|  | 	"minecraft:pitcher_crop": [112, 134, 181] | ||||||
|  | } | ||||||
							
								
								
									
										262
									
								
								save.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								save.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,262 @@ | |||||||
|  | n -220 -374 3 Port Pickle | ||||||
|  | n -240 -519 3 Pickledill | ||||||
|  | n -266 -454 4 Pickledill Valley | ||||||
|  | n -125 -518 2 Pickledill Waterfall | ||||||
|  | n -224 -562 1 Yeti Cave | ||||||
|  | n -225 -352 1 Port Pickle Observatory | ||||||
|  | n -227 -364 1 Port Pickle Smithery | ||||||
|  | n -212 -372 1 Pickledill Ferry – Port Pickle | ||||||
|  | n -212 -97 1 Pickledill Ferry – Shopping District | ||||||
|  | n -70 -67 1 Witch Farm | ||||||
|  | n -70 -77 0  | ||||||
|  | n -79 -77 0  | ||||||
|  | n -86 -75 0  | ||||||
|  | n -92 -71 0  | ||||||
|  | n -108 -61 0  | ||||||
|  | n -127 -55 0  | ||||||
|  | n -151 -52 0  | ||||||
|  | n -168 -55 0  | ||||||
|  | n -188 -61 0  | ||||||
|  | n -198 -71 0  | ||||||
|  | n -208 -84 0  | ||||||
|  | n -213 -376 0  | ||||||
|  | n -208 -372 0  | ||||||
|  | n -200 -371 0  | ||||||
|  | n -193 -373 0  | ||||||
|  | n -216 -379 0  | ||||||
|  | n -222 -379 0  | ||||||
|  | n -226 -372 0  | ||||||
|  | n -222 -369 0  | ||||||
|  | n -222 -364 0  | ||||||
|  | n -221 -359 0  | ||||||
|  | n -219 -353 0  | ||||||
|  | n -220 -349 0  | ||||||
|  | n -225 -348 0  | ||||||
|  | n -257 -479 1 Pickledill Mill | ||||||
|  | n -383 -499 1 Pickledill Pass | ||||||
|  | n -398 -517 1 Pickledill Bridge | ||||||
|  | n -279 -524 1 Pickledill Portal | ||||||
|  | n -407 -17 1 Portail du Shopping District | ||||||
|  | n -390 -22 4 Shopping District | ||||||
|  | n -480 -56 3 Shopping District Ouest | ||||||
|  | n -396 -4 1 Slime Shop | ||||||
|  | n -387 -3 1 Slime Shop | ||||||
|  | n -383 -13 1 Sand Shop | ||||||
|  | n -394 -29 1 Magasin de Livres | ||||||
|  | n -407 -37 1 Magasin de la Mer | ||||||
|  | n -370 -10 1 Shop de Fer | ||||||
|  | n -362 -17 1 Living Is A Right | ||||||
|  | n -399 -17 3 Shopping District Centre | ||||||
|  | n -373 -77 1 Magasin de Glace | ||||||
|  | n -315 -91 1 Puits du Shopping District | ||||||
|  | n -335 -43 1 Nether Goods | ||||||
|  | n -680 -199 4 Lycapark | ||||||
|  | n -392 -10 0  | ||||||
|  | n -397 -12 0  | ||||||
|  | n -396 -5 0  | ||||||
|  | n -401 -26 0  | ||||||
|  | n -406 -33 0  | ||||||
|  | n -412 -35 0  | ||||||
|  | n -396 -23 0  | ||||||
|  | n -392 -18 0  | ||||||
|  | n -385 -17 0  | ||||||
|  | n -375 -16 0  | ||||||
|  | n -376 -6 0  | ||||||
|  | n -374 -2 0  | ||||||
|  | n -373 2 0  | ||||||
|  | n -368 6 0  | ||||||
|  | n -370 -16 0  | ||||||
|  | n -367 -21 0  | ||||||
|  | n -361 -24 0  | ||||||
|  | n -354 -24 0  | ||||||
|  | n -352 -31 0  | ||||||
|  | n -348 -39 0  | ||||||
|  | n -343 -44 0  | ||||||
|  | n -350 -19 0  | ||||||
|  | n -348 -14 0  | ||||||
|  | n -341 -13 0  | ||||||
|  | n -339 -17 0  | ||||||
|  | n -336 -22 0  | ||||||
|  | n -341 -25 0  | ||||||
|  | n -345 -29 0  | ||||||
|  | n -331 -16 0  | ||||||
|  | n -328 -11 0  | ||||||
|  | n -324 -7 0  | ||||||
|  | n -320 -3 0  | ||||||
|  | n -317 2 0  | ||||||
|  | n -315 7 0  | ||||||
|  | n -310 6 0  | ||||||
|  | n -306 4 0  | ||||||
|  | n -299 3 0  | ||||||
|  | n -295 -4 0  | ||||||
|  | n -296 -14 0  | ||||||
|  | n -299 -23 0  | ||||||
|  | n -304 -31 0  | ||||||
|  | n -311 -28 0  | ||||||
|  | n -319 -24 0  | ||||||
|  | n -328 -22 0  | ||||||
|  | n -299 -37 0  | ||||||
|  | n -297 -41 0  | ||||||
|  | n -297 -51 0  | ||||||
|  | n -291 -61 0  | ||||||
|  | n -296 -68 0  | ||||||
|  | n -300 -64 0  | ||||||
|  | n -304 -60 0  | ||||||
|  | n -307 -58 0  | ||||||
|  | n -315 -57 0  | ||||||
|  | n -320 -53 0  | ||||||
|  | n -324 -52 0  | ||||||
|  | n -330 -53 0  | ||||||
|  | n -337 -53 0  | ||||||
|  | n -291 -72 0  | ||||||
|  | n -286 -73 0  | ||||||
|  | n -282 -76 0  | ||||||
|  | n -279 -77 0  | ||||||
|  | n -273 -83 0  | ||||||
|  | n -269 -90 0  | ||||||
|  | n -260 -92 0  | ||||||
|  | n -253 -96 0  | ||||||
|  | n -239 -96 0  | ||||||
|  | n -231 -94 0  | ||||||
|  | n -223 -97 0  | ||||||
|  | n -419 -32 0  | ||||||
|  | n -423 -29 0  | ||||||
|  | n -428 -28 0  | ||||||
|  | n -434 -26 0  | ||||||
|  | n -435 -22 0  | ||||||
|  | n -442 -17 0  | ||||||
|  | n -446 -19 0  | ||||||
|  | n -452 -21 0  | ||||||
|  | n -457 -24 0  | ||||||
|  | n -462 -26 0  | ||||||
|  | n -465 -31 0  | ||||||
|  | n -469 -37 0  | ||||||
|  | n -471 -44 0  | ||||||
|  | n -475 -51 0  | ||||||
|  | n -286 -91 3 Shopping District Est | ||||||
|  |  | ||||||
|  | e 7 8 2 | ||||||
|  | e 10 9 0 | ||||||
|  | e 10 11 3 | ||||||
|  | e 11 12 3 | ||||||
|  | e 12 13 3 | ||||||
|  | e 13 14 3 | ||||||
|  | e 14 15 3 | ||||||
|  | e 15 16 3 | ||||||
|  | e 16 17 3 | ||||||
|  | e 17 18 3 | ||||||
|  | e 18 19 3 | ||||||
|  | e 19 20 3 | ||||||
|  | e 20 8 3 | ||||||
|  | e 7 21 0 | ||||||
|  | e 21 22 0 | ||||||
|  | e 22 23 0 | ||||||
|  | e 23 24 0 | ||||||
|  | e 21 25 0 | ||||||
|  | e 25 26 0 | ||||||
|  | e 26 27 0 | ||||||
|  | e 27 28 0 | ||||||
|  | e 28 29 0 | ||||||
|  | e 29 6 0 | ||||||
|  | e 29 30 0 | ||||||
|  | e 30 31 0 | ||||||
|  | e 31 32 0 | ||||||
|  | e 32 33 0 | ||||||
|  | e 33 5 0 | ||||||
|  | e 42 53 0 | ||||||
|  | e 53 54 0 | ||||||
|  | e 54 55 0 | ||||||
|  | e 54 48 0 | ||||||
|  | e 48 38 0 | ||||||
|  | e 48 56 0 | ||||||
|  | e 56 57 0 | ||||||
|  | e 57 58 0 | ||||||
|  | e 57 45 0 | ||||||
|  | e 45 48 0 | ||||||
|  | e 48 59 0 | ||||||
|  | e 59 44 0 | ||||||
|  | e 48 60 0 | ||||||
|  | e 60 61 0 | ||||||
|  | e 61 43 0 | ||||||
|  | e 61 62 0 | ||||||
|  | e 63 64 0 | ||||||
|  | e 64 65 0 | ||||||
|  | e 65 66 0 | ||||||
|  | e 63 62 0 | ||||||
|  | e 62 67 0 | ||||||
|  | e 67 46 0 | ||||||
|  | e 67 47 0 | ||||||
|  | e 67 68 0 | ||||||
|  | e 68 69 0 | ||||||
|  | e 69 70 0 | ||||||
|  | e 70 71 0 | ||||||
|  | e 71 72 0 | ||||||
|  | e 72 73 0 | ||||||
|  | e 73 51 0 | ||||||
|  | e 70 74 0 | ||||||
|  | e 74 75 0 | ||||||
|  | e 75 76 0 | ||||||
|  | e 76 77 0 | ||||||
|  | e 77 78 0 | ||||||
|  | e 78 79 0 | ||||||
|  | e 79 80 0 | ||||||
|  | e 80 71 0 | ||||||
|  | e 77 81 0 | ||||||
|  | e 81 82 0 | ||||||
|  | e 82 83 0 | ||||||
|  | e 83 84 0 | ||||||
|  | e 84 85 0 | ||||||
|  | e 85 86 0 | ||||||
|  | e 86 87 0 | ||||||
|  | e 87 88 0 | ||||||
|  | e 88 89 0 | ||||||
|  | e 89 90 0 | ||||||
|  | e 90 91 0 | ||||||
|  | e 91 92 0 | ||||||
|  | e 92 93 0 | ||||||
|  | e 93 94 0 | ||||||
|  | e 94 95 0 | ||||||
|  | e 95 96 0 | ||||||
|  | e 96 78 0 | ||||||
|  | e 93 97 0 | ||||||
|  | e 97 98 0 | ||||||
|  | e 98 99 0 | ||||||
|  | e 99 100 0 | ||||||
|  | e 100 101 0 | ||||||
|  | e 101 102 0 | ||||||
|  | e 102 103 0 | ||||||
|  | e 103 104 0 | ||||||
|  | e 104 105 0 | ||||||
|  | e 105 106 0 | ||||||
|  | e 106 107 0 | ||||||
|  | e 107 108 0 | ||||||
|  | e 108 109 0 | ||||||
|  | e 109 73 0 | ||||||
|  | e 101 110 0 | ||||||
|  | e 110 111 0 | ||||||
|  | e 111 112 0 | ||||||
|  | e 112 113 0 | ||||||
|  | e 113 114 0 | ||||||
|  | e 114 115 0 | ||||||
|  | e 115 116 0 | ||||||
|  | e 116 117 0 | ||||||
|  | e 117 118 0 | ||||||
|  | e 118 119 0 | ||||||
|  | e 119 120 0 | ||||||
|  | e 120 8 0 | ||||||
|  | e 58 121 0 | ||||||
|  | e 121 122 0 | ||||||
|  | e 122 123 0 | ||||||
|  | e 123 124 0 | ||||||
|  | e 124 125 0 | ||||||
|  | e 125 126 0 | ||||||
|  | e 126 127 0 | ||||||
|  | e 127 128 0 | ||||||
|  | e 128 129 0 | ||||||
|  | e 129 130 0 | ||||||
|  | e 130 131 0 | ||||||
|  | e 131 132 0 | ||||||
|  | e 132 133 0 | ||||||
|  | e 133 134 0 | ||||||
|  | e 134 40 0 | ||||||
|  | e 101 112 0 | ||||||
							
								
								
									
										28
									
								
								src/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import json | ||||||
|  | import os.path | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Config: | ||||||
|  |     LAST_OPENED_FILE = "" | ||||||
|  |     AUTOSAVE_INTERVAL = 5 * 60 * 1000 | ||||||
|  |     CACHE_TTL = 10 | ||||||
|  |  | ||||||
|  |     def __init__(self, path: str): | ||||||
|  |         self._path: str = path | ||||||
|  |  | ||||||
|  |         self.load() | ||||||
|  |  | ||||||
|  |     def load(self) -> None: | ||||||
|  |         if os.path.exists(self._path): | ||||||
|  |             with open(self._path, "r") as f: | ||||||
|  |                 config = json.load(f) | ||||||
|  |  | ||||||
|  |             self.LAST_OPENED_FILE = config["last_opened_file"] | ||||||
|  |             self.AUTOSAVE_INTERVAL = config["autosave_interval"] | ||||||
|  |  | ||||||
|  |     def save(self) -> None: | ||||||
|  |         with open(self._path, "w") as f: | ||||||
|  |             json.dump({ | ||||||
|  |                 "last_opened_file": self.LAST_OPENED_FILE, | ||||||
|  |                 "autosave_interval": self.AUTOSAVE_INTERVAL | ||||||
|  |             }, f, indent=4) | ||||||
							
								
								
									
										569
									
								
								src/editor.py
									
									
									
									
									
								
							
							
						
						
									
										569
									
								
								src/editor.py
									
									
									
									
									
								
							| @@ -3,77 +3,191 @@ from enum import Enum, auto | |||||||
| from math import floor | from math import floor | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| import platformdirs |  | ||||||
| import pygame | import pygame | ||||||
|  |  | ||||||
|  | from src.config import Config | ||||||
|  | from src.graph.graph import Graph | ||||||
| from src.image_handler import ImageHandler | from src.image_handler import ImageHandler | ||||||
|  | from src.utils.paths import CONFIG_DIR, CACHE_DIR | ||||||
|  |  | ||||||
|  |  | ||||||
| class Editor: | class Editor: | ||||||
|  |     APP_NAME: str = "lycacraft-paths" | ||||||
|  |     APP_AUTHOR: str = "Lycacraft" | ||||||
|     WIDTH: int = 800 |     WIDTH: int = 800 | ||||||
|     HEIGHT: int = 600 |     HEIGHT: int = 600 | ||||||
|     MAP_SIZE: int = 1024 |     MAP_SIZE: int = 1024 | ||||||
|     MAPS_DIR: str = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps") |     CONFIG_PATH: str = os.path.join(CONFIG_DIR, "config.json") | ||||||
|     ZOOMS: tuple[float] = (0.25, 0.5, 1, 2, 4) |     MAPS_DIR: str = os.path.join(CACHE_DIR, "maps") | ||||||
|  |     AUTOSAVE_PATH: str = os.path.join(CACHE_DIR, "AUTOSAVE.txt") | ||||||
|  |     AUTOSAVE_EVENT: int = pygame.event.custom_type() | ||||||
|  |     ZOOMS: tuple[float] = tuple(2**p for p in range(-6, 7)) | ||||||
|     CROSSHAIR_SIZE: int = 10 |     CROSSHAIR_SIZE: int = 10 | ||||||
|  |  | ||||||
|  |     EDGE_TYPE_KEYS: dict[int, str] = { | ||||||
|  |         pygame.K_p: "path", | ||||||
|  |         pygame.K_n: "narrow_path", | ||||||
|  |         pygame.K_f: "ferry", | ||||||
|  |         pygame.K_b: "boat", | ||||||
|  |         pygame.K_r: "rails" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     NODE_TYPE_KEYS: list[int] = [ | ||||||
|  |         pygame.K_0, | ||||||
|  |         pygame.K_1, | ||||||
|  |         pygame.K_2, | ||||||
|  |         pygame.K_3, | ||||||
|  |         pygame.K_4 | ||||||
|  |     ] | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         pygame.init() |         pygame.init() | ||||||
|  |         self.config: Config = Config(self.CONFIG_PATH) | ||||||
|         self.width: int = self.WIDTH |         self.width: int = self.WIDTH | ||||||
|         self.height: int = self.HEIGHT |         self.height: int = self.HEIGHT | ||||||
|         self.win: pygame.Surface = pygame.display.set_mode([self.width, self.height], pygame.RESIZABLE) |         self.win: pygame.Surface = pygame.display.set_mode([self.width, self.height], pygame.RESIZABLE) | ||||||
|         pygame.display.set_caption("Lycacraft Map Editor") |         pygame.display.set_caption("Lycacraft Map Editor") | ||||||
|         self.center: list[int] = [0, 0] |         self.center: list[int] = [0, 0] | ||||||
|         self.zoom_i: int = 2 |         self.zoom_i: int = self.ZOOMS.index(1) | ||||||
|         self.zoom: float = self.ZOOMS[self.zoom_i] |         self.zoom: float = self.ZOOMS[self.zoom_i] | ||||||
|         self.running: bool = False |         self.running: bool = False | ||||||
|         self.image_handler: ImageHandler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE) |         self.image_handler: ImageHandler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE, self.config.CACHE_TTL) | ||||||
|         self.clock: pygame.time.Clock = pygame.time.Clock() |         self.clock: pygame.time.Clock = pygame.time.Clock() | ||||||
|         self.left_drag_pos: Optional[tuple[int, int]] = None |         self.left_drag_pos: Optional[tuple[int, int]] = None | ||||||
|         self.mid_drag_pos: Optional[tuple[int, int]] = None |         self.mid_drag_pos: Optional[tuple[int, int]] = None | ||||||
|         self.font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 20) |         self.font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 20) | ||||||
|         self.loading_font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 30) |         self.loading_font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 30) | ||||||
|         self.zooms_texts: list[pygame.Surface] = list(map( |         self.zooms_texts: list[pygame.Surface] = [] | ||||||
|             lambda z: self.font.render(str(z), True, (255, 255, 255)), |         for zoom in self.ZOOMS: | ||||||
|             self.ZOOMS |             txt = str(zoom) | ||||||
|         )) |             if zoom < 1: | ||||||
|  |                 txt = f"1/{int(1/zoom):d}" | ||||||
|  |             self.zooms_texts.append(self.font.render(txt, True, (255, 255, 255))) | ||||||
|  |         self.is_renaming_node: bool = False | ||||||
|         self.state: State = State.STOPPING |         self.state: State = State.STOPPING | ||||||
|  |         self.graph = Graph() | ||||||
|  |         self.typing_text: str = "" | ||||||
|  |         self.node_radius: int = 10 | ||||||
|  |         self.line_size: int = int(self.node_radius / 5) | ||||||
|  |         self.edge_detect_radius: int = 3 * self.line_size | ||||||
|  |         self.selected_nodes: list[int] = [] | ||||||
|  |         self.selected_edges: list[int] = [] | ||||||
|  |         self.previously_created_nodes: list[int] = [] | ||||||
|  |         self.selection_rectangle: Optional[list[tuple[int, int], tuple[int, int]]] = None | ||||||
|  |         self.original_move_pos: Optional[tuple[int, int]] = None | ||||||
|  |         self.move_old_poses: Optional[dict[int, tuple[int, int]]] = None | ||||||
|  |         self.dirty: bool = False | ||||||
|  |         self.loading_bg: pygame.Surface = pygame.Surface([self.width, self.height]) | ||||||
|  |         pygame.time.set_timer(self.AUTOSAVE_EVENT, self.config.AUTOSAVE_INTERVAL) | ||||||
|  |  | ||||||
|  |         if os.path.exists(self.AUTOSAVE_PATH): | ||||||
|  |             self.load(self.AUTOSAVE_PATH, False) | ||||||
|  |             self.dirty = True | ||||||
|  |         elif self.config.LAST_OPENED_FILE != "": | ||||||
|  |             self.load(self.config.LAST_OPENED_FILE) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # EVENTS | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|     def mainloop(self) -> None: |     def mainloop(self) -> None: | ||||||
|         self.state = State.LOADING |         self.state = State.LOADING | ||||||
|         while self.state != State.STOPPING: |         while self.state != State.STOPPING: | ||||||
|             pygame.display.set_caption(f"Lycacraft Map Editor - {self.clock.get_fps():.2f}fps") |             caption = f"Lycacraft Map Editor - {self.clock.get_fps():.2f}fps - {self.image_handler.size} images" | ||||||
|  |             if self.dirty: | ||||||
|  |                 caption += " (unsaved)" | ||||||
|  |             pygame.display.set_caption(caption) | ||||||
|             self.process_events() |             self.process_events() | ||||||
|             if self.state == State.LOADING: |             if self.state == State.LOADING: | ||||||
|                 self.render_loading() |                 self.render_loading() | ||||||
|                 if not self.image_handler.loading: |                 if not self.image_handler.loading: | ||||||
|                     self.state = State.RUNNING |                     self.state = State.RUNNING | ||||||
|             elif self.state == State.RUNNING: |             elif self.state == State.RUNNING: | ||||||
|  |                 if self.selection_rectangle is not None: | ||||||
|  |                     self.expand_selection_rect() | ||||||
|  |                 if self.original_move_pos is not None: | ||||||
|  |                     self.move_poses() | ||||||
|                 self.render() |                 self.render() | ||||||
|  |                 self.image_handler.clean() | ||||||
|             self.clock.tick(30) |             self.clock.tick(30) | ||||||
|  |  | ||||||
|  |     def quit(self) -> None: | ||||||
|  |         if self.dirty: | ||||||
|  |             self.save(self.AUTOSAVE_PATH, False) | ||||||
|  |         self.state = State.STOPPING | ||||||
|  |  | ||||||
|     def process_events(self) -> None: |     def process_events(self) -> None: | ||||||
|         events = pygame.event.get() |         events = pygame.event.get() | ||||||
|  |  | ||||||
|         keys = pygame.key.get_pressed() |         keys = pygame.key.get_pressed() | ||||||
|         for event in events: |         for event in events: | ||||||
|             if event.type == pygame.QUIT: |             if event.type == pygame.QUIT: | ||||||
|                 self.state = State.STOPPING |                 self.quit() | ||||||
|             elif event.type == pygame.WINDOWRESIZED: |             elif event.type == pygame.WINDOWRESIZED: | ||||||
|                 self.width = event.x |                 self.width = event.x | ||||||
|                 self.height = event.y |                 self.height = event.y | ||||||
|             elif event.type == pygame.KEYDOWN: |             elif event.type == pygame.KEYDOWN: | ||||||
|  |                 if self.is_renaming_node: | ||||||
|                     if event.key == pygame.K_ESCAPE: |                     if event.key == pygame.K_ESCAPE: | ||||||
|                     self.state = State.STOPPING |                         self.is_renaming_node = False | ||||||
|  |                         self.typing_text = "" | ||||||
|  |                     elif event.key == pygame.K_RETURN: | ||||||
|  |                         self.rename_nodes() | ||||||
|  |                     elif event.key == pygame.K_BACKSPACE: | ||||||
|  |                         self.typing_text = self.typing_text[:-1] | ||||||
|  |                     else: | ||||||
|  |                         self.typing_text += event.unicode | ||||||
|  |                 else: | ||||||
|  |                     if event.key == pygame.K_ESCAPE: | ||||||
|  |                         if self.selected_nodes != [] or self.selected_edges != []: | ||||||
|  |                             self.clear_selection() | ||||||
|  |                             self.previously_created_nodes = [] | ||||||
|  |                         else: | ||||||
|  |                             self.quit() | ||||||
|                     elif event.key == pygame.K_PAGEUP: |                     elif event.key == pygame.K_PAGEUP: | ||||||
|                         self.zoom_in() |                         self.zoom_in() | ||||||
|                     elif event.key == pygame.K_PAGEDOWN: |                     elif event.key == pygame.K_PAGEDOWN: | ||||||
|                         self.zoom_out() |                         self.zoom_out() | ||||||
|  |                     elif event.key == pygame.K_BACKSPACE: | ||||||
|  |                         self.deleted_selected_objects() | ||||||
|  |                     elif event.key == pygame.K_s and event.mod & (pygame.KMOD_CTRL | pygame.KMOD_META): | ||||||
|  |                         self.save() | ||||||
|  |                     elif event.key == pygame.K_l and event.mod & (pygame.KMOD_CTRL | pygame.KMOD_META): | ||||||
|  |                         self.load() | ||||||
|  |                     elif event.key == pygame.K_RETURN: | ||||||
|  |                         if len(self.selected_nodes) > 0: | ||||||
|  |                             self.typing_text = "" | ||||||
|  |                             if len(self.selected_nodes) == 1: | ||||||
|  |                                 self.typing_text = self.graph.nodes[self.selected_nodes[0]].name | ||||||
|  |                             self.is_renaming_node = True | ||||||
|  |                     elif event.key == pygame.K_HOME: | ||||||
|  |                         self.center = [0, 0] | ||||||
|  |                     elif event.key == pygame.K_F5: | ||||||
|  |                         self.reload() | ||||||
|  |                     elif event.key in self.EDGE_TYPE_KEYS.keys(): | ||||||
|  |                         self.change_edge_types(self.EDGE_TYPE_KEYS[event.key]) | ||||||
|  |                     elif event.key in self.NODE_TYPE_KEYS: | ||||||
|  |                         self.change_node_types(self.NODE_TYPE_KEYS.index(event.key)) | ||||||
|  |             elif event.type == pygame.KEYUP: | ||||||
|  |                 if event.key == pygame.K_m: | ||||||
|  |                     if self.original_move_pos is not None: | ||||||
|  |                         self.reset_move_poses() | ||||||
|             elif event.type == pygame.MOUSEBUTTONDOWN: |             elif event.type == pygame.MOUSEBUTTONDOWN: | ||||||
|                 if event.button == 2: |                 if event.button == 2: | ||||||
|                     self.mid_drag_pos = event.pos |                     self.mid_drag_pos = event.pos | ||||||
|                 elif event.button == 1 and keys[pygame.K_LCTRL]: |                 elif event.button == 1: | ||||||
|  |                     if keys[pygame.K_LCTRL]: | ||||||
|                         self.left_drag_pos = event.pos |                         self.left_drag_pos = event.pos | ||||||
|  |                     elif keys[pygame.K_LALT]: | ||||||
|  |                         self.create_selection_rect(keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]) | ||||||
|  |                     elif keys[pygame.K_m]: | ||||||
|  |                         self.original_move_pos = event.pos | ||||||
|  |                         self.start_moving() | ||||||
|  |                     else: | ||||||
|  |                         self.select_object(keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]) | ||||||
|  |                 elif event.button == 3: | ||||||
|  |                     self.create_node(self.screen_to_world(event.pos[0], event.pos[1])) | ||||||
|                 elif event.button == 4: |                 elif event.button == 4: | ||||||
|                     self.zoom_in() |                     self.zoom_in() | ||||||
|                 elif event.button == 5: |                 elif event.button == 5: | ||||||
| @@ -82,7 +196,15 @@ class Editor: | |||||||
|                 if event.button == 2: |                 if event.button == 2: | ||||||
|                     self.mid_drag_pos = None |                     self.mid_drag_pos = None | ||||||
|                 elif event.button == 1: |                 elif event.button == 1: | ||||||
|  |                     if keys[pygame.K_LCTRL]: | ||||||
|                         self.left_drag_pos = None |                         self.left_drag_pos = None | ||||||
|  |                     elif keys[pygame.K_m] and self.original_move_pos is not None: | ||||||
|  |                         self.confirm_move_poses() | ||||||
|  |                     elif self.selection_rectangle is not None: | ||||||
|  |                         self.release_selection_rect(keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]) | ||||||
|  |             elif event.type == self.AUTOSAVE_EVENT: | ||||||
|  |                 if self.dirty: | ||||||
|  |                     self.save(self.AUTOSAVE_PATH, False) | ||||||
|  |  | ||||||
|         if keys[pygame.K_LEFT]: |         if keys[pygame.K_LEFT]: | ||||||
|             self.center[0] -= 4 / self.zoom |             self.center[0] -= 4 / self.zoom | ||||||
| @@ -106,8 +228,13 @@ class Editor: | |||||||
|             if mbtns[1]: |             if mbtns[1]: | ||||||
|                 self.mid_drag_pos = mpos |                 self.mid_drag_pos = mpos | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # RENDERING | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|     def render(self) -> None: |     def render(self) -> None: | ||||||
|         self.win.fill((0, 0, 0)) |         self.win.fill((50, 50, 50)) | ||||||
|         off_x = (self.center[0] * self.zoom) % self.MAP_SIZE |         off_x = (self.center[0] * self.zoom) % self.MAP_SIZE | ||||||
|         off_y = (self.center[1] * self.zoom) % self.MAP_SIZE |         off_y = (self.center[1] * self.zoom) % self.MAP_SIZE | ||||||
|  |  | ||||||
| @@ -143,6 +270,10 @@ class Editor: | |||||||
|                     oy + y * self.MAP_SIZE |                     oy + y * self.MAP_SIZE | ||||||
|                 ]) |                 ]) | ||||||
|  |  | ||||||
|  |         self.render_graph() | ||||||
|  |  | ||||||
|  |         self.render_selection_rect() | ||||||
|  |  | ||||||
|         pygame.draw.line(self.win, (150, 150, 150), [w2 - self.CROSSHAIR_SIZE, h2], [w2 + self.CROSSHAIR_SIZE, h2]) |         pygame.draw.line(self.win, (150, 150, 150), [w2 - self.CROSSHAIR_SIZE, h2], [w2 + self.CROSSHAIR_SIZE, h2]) | ||||||
|         pygame.draw.line(self.win, (150, 150, 150), [w2, h2 - self.CROSSHAIR_SIZE], [w2, h2 + self.CROSSHAIR_SIZE]) |         pygame.draw.line(self.win, (150, 150, 150), [w2, h2 - self.CROSSHAIR_SIZE], [w2, h2 + self.CROSSHAIR_SIZE]) | ||||||
|         self.render_zoom_slider() |         self.render_zoom_slider() | ||||||
| @@ -153,16 +284,19 @@ class Editor: | |||||||
|         pygame.draw.rect(self.win, (80, 80, 80), [0, 0, mouse_txt.get_width() + 10, mouse_txt.get_height() + 10]) |         pygame.draw.rect(self.win, (80, 80, 80), [0, 0, mouse_txt.get_width() + 10, mouse_txt.get_height() + 10]) | ||||||
|         self.win.blit(mouse_txt, [5, 5]) |         self.win.blit(mouse_txt, [5, 5]) | ||||||
|  |  | ||||||
|  |         if self.is_renaming_node: | ||||||
|  |             self.render_node_renamer() | ||||||
|  |  | ||||||
|         pygame.display.flip() |         pygame.display.flip() | ||||||
|  |  | ||||||
|     def render_zoom_slider(self) -> None: |     def render_zoom_slider(self) -> None: | ||||||
|         zoom_height = self.height * 0.2 |         zoom_r = self.height / 80 | ||||||
|  |         zoom_space = zoom_r * 4 | ||||||
|  |         zoom_height = zoom_space * (len(self.ZOOMS) - 1) | ||||||
|         zoom_h_margin = self.width * 0.02 |         zoom_h_margin = self.width * 0.02 | ||||||
|         zoom_v_margin = self.height * 0.05 |         zoom_v_margin = self.height * 0.05 | ||||||
|         zoom_x = self.width - zoom_h_margin |         zoom_x = self.width - zoom_h_margin | ||||||
|         zoom_y = self.height - zoom_v_margin - zoom_height |         zoom_y = self.height - zoom_v_margin - zoom_height | ||||||
|         zoom_space = zoom_height / 4 |  | ||||||
|         zoom_r = zoom_space / 4 |  | ||||||
|         zoom_width = max(s.get_width() for s in self.zooms_texts) + 2 * zoom_r + 5 |         zoom_width = max(s.get_width() for s in self.zooms_texts) + 2 * zoom_r + 5 | ||||||
|         pygame.draw.rect(self.win, (80, 80, 80), [ |         pygame.draw.rect(self.win, (80, 80, 80), [ | ||||||
|             zoom_x + zoom_r - zoom_width - 5, |             zoom_x + zoom_r - zoom_width - 5, | ||||||
| @@ -179,6 +313,7 @@ class Editor: | |||||||
|  |  | ||||||
|     def render_loading(self) -> None: |     def render_loading(self) -> None: | ||||||
|         self.win.fill((0, 0, 0)) |         self.win.fill((0, 0, 0)) | ||||||
|  |         self.win.blit(self.loading_bg, [0, 0]) | ||||||
|         count = self.image_handler.count |         count = self.image_handler.count | ||||||
|         total = self.image_handler.total |         total = self.image_handler.total | ||||||
|         txt = self.loading_font.render(f"Loading maps - {count}/{total}", True, (255, 255, 255)) |         txt = self.loading_font.render(f"Loading maps - {count}/{total}", True, (255, 255, 255)) | ||||||
| @@ -189,12 +324,82 @@ class Editor: | |||||||
|         h2 = self.height / 2 |         h2 = self.height / 2 | ||||||
|         x0 = w2 - width / 2 |         x0 = w2 - width / 2 | ||||||
|         y0 = h2 - height / 2 |         y0 = h2 - height / 2 | ||||||
|  |         loaded_width = 0 if total == 0 else width * count / total | ||||||
|         pygame.draw.rect(self.win, (160, 160, 160), [x0, y0, width, height]) |         pygame.draw.rect(self.win, (160, 160, 160), [x0, y0, width, height]) | ||||||
|         pygame.draw.rect(self.win, (90, 250, 90), [x0, y0, width * count / total, height]) |         pygame.draw.rect(self.win, (90, 250, 90), [x0, y0, loaded_width, height]) | ||||||
|         self.win.blit(txt, [w2 - txt.get_width() / 2, y0 - txt.get_height() - 5]) |         self.win.blit(txt, [w2 - txt.get_width() / 2, y0 - txt.get_height() - 5]) | ||||||
|  |  | ||||||
|         pygame.display.flip() |         pygame.display.flip() | ||||||
|  |  | ||||||
|  |     def render_node_renamer(self) -> None: | ||||||
|  |         width = self.width / 2 | ||||||
|  |         height = self.height / 2 | ||||||
|  |         x0 = (self.width - width) / 2 | ||||||
|  |         y0 = (self.height - height) / 2 | ||||||
|  |  | ||||||
|  |         line_height = height / 6 | ||||||
|  |  | ||||||
|  |         nc_txt = self.loading_font.render("RENAME NODE", True, (255, 255, 255)) | ||||||
|  |         name_txt = self.loading_font.render("New name:", True, (255, 255, 255)) | ||||||
|  |         txt = self.loading_font.render(self.typing_text, True, (255, 255, 255)) | ||||||
|  |  | ||||||
|  |         pygame.draw.rect(self.win, (0, 0, 0), [x0, y0, width, height]) | ||||||
|  |         self.win.blit(nc_txt, [self.width / 2 - nc_txt.get_width() / 2, y0 + line_height]) | ||||||
|  |         self.win.blit(name_txt, [self.width / 2 - name_txt.get_width() / 2, y0 + 3 * line_height]) | ||||||
|  |         self.win.blit(txt, [self.width / 2 - txt.get_width() / 2, y0 + 4 * line_height]) | ||||||
|  |  | ||||||
|  |     def render_graph(self) -> None: | ||||||
|  |         self.render_edges() | ||||||
|  |         self.render_nodes() | ||||||
|  |  | ||||||
|  |     def render_edges(self) -> None: | ||||||
|  |         hover_index, is_node = self.get_hover_object() | ||||||
|  |         for edge in self.graph.edges: | ||||||
|  |             node_1, node_2 = self.graph.get_edge_nodes(edge) | ||||||
|  |             color = self.graph.TYPE_COLORS[edge.type] | ||||||
|  |             start = self.world_to_screen(node_1.x, node_1.z) | ||||||
|  |             end = self.world_to_screen(node_2.x, node_2.z) | ||||||
|  |             if not is_node and edge.index == hover_index: | ||||||
|  |                 pygame.draw.line(self.win, (0, 0, 0), start, end, self.edge_detect_radius) | ||||||
|  |             elif edge.index in self.selected_edges: | ||||||
|  |                 pygame.draw.line(self.win, (255, 255, 255), start, end, self.edge_detect_radius) | ||||||
|  |             pygame.draw.line(self.win, color, start, end, self.line_size) | ||||||
|  |  | ||||||
|  |     def render_nodes(self) -> None: | ||||||
|  |         hover_index, is_node = self.get_hover_object() | ||||||
|  |         for node in self.graph.nodes: | ||||||
|  |             blitpos = self.world_to_screen(node.x, node.z) | ||||||
|  |             color = self.graph.TYPE_COLORS[node.type] | ||||||
|  |             if is_node and node.index == hover_index: | ||||||
|  |                 self.render_hover_node(node.index) | ||||||
|  |             elif node.index in self.selected_nodes: | ||||||
|  |                 pygame.draw.circle(self.win, (255, 255, 255), (blitpos[0], blitpos[1]), self.node_radius + self.line_size) | ||||||
|  |             pygame.draw.circle(self.win, color, (blitpos[0], blitpos[1]), self.node_radius) | ||||||
|  |  | ||||||
|  |     def render_hover_node(self, node_index: int) -> None: | ||||||
|  |         node = self.graph.nodes[node_index] | ||||||
|  |         txt = self.loading_font.render(node.name, True, (255, 255, 255)) | ||||||
|  |         node_pos = self.world_to_screen(node.x, node.z) | ||||||
|  |         xpos = node_pos[0] - txt.get_width() - self.node_radius * (2 ** -0.5) | ||||||
|  |         ypos = node_pos[1] - txt.get_height() - self.node_radius * (2 ** -0.5) | ||||||
|  |         pygame.draw.rect(self.win, (0, 0, 0), pygame.Rect(xpos, ypos, txt.get_width(), txt.get_height())) | ||||||
|  |         self.win.blit(txt, [xpos, ypos]) | ||||||
|  |         pygame.draw.circle(self.win, (0, 0, 0), (node_pos[0], node_pos[1]), self.node_radius + self.line_size) | ||||||
|  |  | ||||||
|  |     def render_selection_rect(self) -> None: | ||||||
|  |         rect = self.selection_rectangle | ||||||
|  |         if rect is not None: | ||||||
|  |             left = min(rect[0][0], rect[1][0]) | ||||||
|  |             top = min(rect[0][1], rect[1][1]) | ||||||
|  |             width = abs(rect[0][0] - rect[1][0]) | ||||||
|  |             height = abs(rect[0][1] - rect[1][1]) | ||||||
|  |             pygame.draw.rect(self.win, (32, 32, 32), pygame.Rect(left, top, width, height), self.line_size) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # ZOOMING | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|     def set_zoom(self, zoom_i: int) -> None: |     def set_zoom(self, zoom_i: int) -> None: | ||||||
|         self.zoom_i = max(0, min(len(self.ZOOMS) - 1, zoom_i)) |         self.zoom_i = max(0, min(len(self.ZOOMS) - 1, zoom_i)) | ||||||
|         self.zoom = self.ZOOMS[self.zoom_i] |         self.zoom = self.ZOOMS[self.zoom_i] | ||||||
| @@ -205,6 +410,259 @@ class Editor: | |||||||
|     def zoom_out(self) -> None: |     def zoom_out(self) -> None: | ||||||
|         self.set_zoom(self.zoom_i - 1) |         self.set_zoom(self.zoom_i - 1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # SELECTION | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|  |     def select_object(self, shifting: bool = False) -> None: | ||||||
|  |         hover_index, is_node = self.get_hover_object() | ||||||
|  |          | ||||||
|  |         self.previously_created_nodes = [] | ||||||
|  |  | ||||||
|  |         if is_node: | ||||||
|  |             self.select_node(hover_index, shifting) | ||||||
|  |         elif hover_index != -1: | ||||||
|  |             self.select_edge(hover_index, shifting) | ||||||
|  |         else: | ||||||
|  |             self.clear_selection() | ||||||
|  |      | ||||||
|  |     def select_node(self, node: int, shifting: bool = False) -> None: | ||||||
|  |         if shifting: | ||||||
|  |             if node in self.selected_nodes: | ||||||
|  |                 self.selected_nodes.remove(node) | ||||||
|  |                 return | ||||||
|  |             if node != -1: | ||||||
|  |                 self.selected_nodes.append(node) | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         if node in self.selected_nodes: | ||||||
|  |             self.clear_selection() | ||||||
|  |             self.selected_nodes.append(node) | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         if node != -1: | ||||||
|  |             for sel_node in self.selected_nodes: | ||||||
|  |                 self.link_nodes(sel_node, node) | ||||||
|  |         self.selected_nodes = [] if node == -1 else [node] | ||||||
|  |         self.selected_edges = [] | ||||||
|  |  | ||||||
|  |     def select_edge(self, edge: int, shifting: bool = False) -> None: | ||||||
|  |         if shifting: | ||||||
|  |             if edge in self.selected_edges: | ||||||
|  |                 self.selected_edges.remove(edge) | ||||||
|  |                 return | ||||||
|  |             if edge != -1: | ||||||
|  |                 self.selected_edges.append(edge) | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         if edge in self.selected_edges: | ||||||
|  |             self.clear_selection() | ||||||
|  |             self.selected_edges.append(edge) | ||||||
|  |             return | ||||||
|  |         self.selected_edges = [] if edge == -1 else [edge] | ||||||
|  |         self.selected_nodes = [] | ||||||
|  |  | ||||||
|  |     def clear_selection(self) -> None: | ||||||
|  |         self.selected_nodes = [] | ||||||
|  |         self.selected_edges = [] | ||||||
|  |  | ||||||
|  |     def clear_node_selection(self) -> None: | ||||||
|  |         self.selected_nodes = [] | ||||||
|  |  | ||||||
|  |     def clear_edge_selection(self) -> None: | ||||||
|  |         self.selected_edges = [] | ||||||
|  |  | ||||||
|  |     def create_selection_rect(self, shifting: bool = False): | ||||||
|  |         if not shifting: | ||||||
|  |             self.clear_selection() | ||||||
|  |             self.previously_created_nodes = [] | ||||||
|  |         mouse_pos = pygame.mouse.get_pos() | ||||||
|  |         self.selection_rectangle = [mouse_pos, mouse_pos] | ||||||
|  |  | ||||||
|  |     def expand_selection_rect(self): | ||||||
|  |         self.selection_rectangle[1] = pygame.mouse.get_pos() | ||||||
|  |  | ||||||
|  |     def release_selection_rect(self, shifting: bool = False): | ||||||
|  |         if not shifting: | ||||||
|  |             self.clear_selection() | ||||||
|  |         self.previously_created_nodes = [] | ||||||
|  |         rect = self.selection_rectangle | ||||||
|  |         left = min(rect[0][0], rect[1][0]) | ||||||
|  |         top = min(rect[0][1], rect[1][1]) | ||||||
|  |         right = max(rect[0][0], rect[1][0]) | ||||||
|  |         bottom = max(rect[0][1], rect[1][1]) | ||||||
|  |         for node in self.graph.nodes: | ||||||
|  |             pos = self.world_to_screen(node.x, node.z) | ||||||
|  |             if left <= pos[0] <= right and top <= pos[1] <= bottom: | ||||||
|  |                 if node.index not in self.selected_nodes: | ||||||
|  |                     self.selected_nodes.append(node.index) | ||||||
|  |         for edge in self.graph.edges: | ||||||
|  |             pos = self.world_to_screen(*self.graph.get_edge_center(edge.index)) | ||||||
|  |             if left <= pos[0] <= right and top <= pos[1] <= bottom: | ||||||
|  |                 if edge.index not in self.selected_edges: | ||||||
|  |                     self.selected_edges.append(edge.index) | ||||||
|  |         self.selection_rectangle = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # HOVERING | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|  |     def get_hovering_nodes(self) -> tuple[list[int], list[float]]: | ||||||
|  |         hovering = [] | ||||||
|  |         dists = [] | ||||||
|  |         mouse_pos = pygame.mouse.get_pos() | ||||||
|  |  | ||||||
|  |         for node in self.graph.nodes: | ||||||
|  |             dist = self.get_node_distance(node.index, mouse_pos[0], mouse_pos[1]) | ||||||
|  |             if dist < self.node_radius: | ||||||
|  |                 hovering.append(node.index) | ||||||
|  |                 dists.append(dist) | ||||||
|  |          | ||||||
|  |         return hovering, dists | ||||||
|  |      | ||||||
|  |     def get_hover_node(self) -> int: | ||||||
|  |         hover_nodes, distances = self.get_hovering_nodes() | ||||||
|  |         return -1 if len(hover_nodes) == 0 else hover_nodes[distances.index(min(distances))] | ||||||
|  |      | ||||||
|  |     def get_hovering_edges(self) -> tuple[list[int], list[float]]: | ||||||
|  |         hovering = [] | ||||||
|  |         dists = [] | ||||||
|  |         mouse_pos = pygame.mouse.get_pos() | ||||||
|  |  | ||||||
|  |         for edge in self.graph.edges: | ||||||
|  |             dist = self.get_edge_distance(edge.index, mouse_pos[0], mouse_pos[1]) | ||||||
|  |             if dist < self.edge_detect_radius: | ||||||
|  |                 hovering.append(edge.index) | ||||||
|  |                 dists.append(dist) | ||||||
|  |          | ||||||
|  |         return hovering, dists | ||||||
|  |      | ||||||
|  |     def get_hover_edge(self) -> int: | ||||||
|  |         hover_edges, distances = self.get_hovering_edges() | ||||||
|  |         return -1 if len(hover_edges) == 0 else hover_edges[distances.index(min(distances))] | ||||||
|  |      | ||||||
|  |     def get_hover_object(self) -> tuple[int, bool]: | ||||||
|  |         node = self.get_hover_node() | ||||||
|  |         if node != -1: | ||||||
|  |             return node, True | ||||||
|  |          | ||||||
|  |         edge = self.get_hover_edge() | ||||||
|  |         if edge != -1: | ||||||
|  |             return edge, False | ||||||
|  |          | ||||||
|  |         return -1, False | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # CREATION | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|  |     def link_nodes(self, node_1: int, node_2: int) -> None: | ||||||
|  |         if not self.graph.edge_exists(node_1, node_2): | ||||||
|  |             self.create_edge(node_1, node_2) | ||||||
|  |  | ||||||
|  |     def create_node(self, pos: tuple[int, int], typing_text: str = "") -> None: | ||||||
|  |         self.dirty = True | ||||||
|  |         self.graph.add_node(pos[0], pos[1], typing_text, type=self.get_node_type()) | ||||||
|  |         if len(self.selected_nodes) == 1: | ||||||
|  |             self.previously_created_nodes.append(self.selected_nodes[0]) | ||||||
|  |         self.select_node(self.graph.number_of_nodes() - 1) | ||||||
|  |  | ||||||
|  |     def create_edge(self, node_1: int, node_2: int) -> None: | ||||||
|  |         self.dirty = True | ||||||
|  |         self.graph.add_edge(node_1, node_2, type=self.get_edge_type()) | ||||||
|  |  | ||||||
|  |     def get_edge_type(self) -> str: | ||||||
|  |         type = "" | ||||||
|  |         for key, value in self.EDGE_TYPE_KEYS.items(): | ||||||
|  |             if pygame.key.get_pressed()[key]: | ||||||
|  |                 type = value | ||||||
|  |                 break | ||||||
|  |         return type | ||||||
|  |  | ||||||
|  |     def get_node_type(self) -> int: | ||||||
|  |         type = 0 | ||||||
|  |         for i in range(len(self.NODE_TYPE_KEYS)): | ||||||
|  |             if pygame.key.get_pressed()[self.NODE_TYPE_KEYS[i]]: | ||||||
|  |                 type = i | ||||||
|  |                 break | ||||||
|  |         return type | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # EDITION | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|  |     def rename_nodes(self) -> None: | ||||||
|  |         self.dirty = True | ||||||
|  |         for node in self.selected_nodes: | ||||||
|  |             self.graph.nodes[node].rename_node(self.typing_text) | ||||||
|  |         self.typing_text = "" | ||||||
|  |         self.is_renaming_node = False | ||||||
|  |  | ||||||
|  |     def start_moving(self): | ||||||
|  |         self.move_old_poses = {} | ||||||
|  |         for node_index in self.selected_nodes: | ||||||
|  |             node = self.graph.nodes[node_index] | ||||||
|  |             self.move_old_poses[node_index] = (node.x, node.z) | ||||||
|  |  | ||||||
|  |     def move_poses(self): | ||||||
|  |         mouse_pos = pygame.mouse.get_pos() | ||||||
|  |         start_pos = self.original_move_pos | ||||||
|  |         delta_x = mouse_pos[0] - start_pos[0] | ||||||
|  |         delta_z = mouse_pos[1] - start_pos[1] | ||||||
|  |         for node_index in self.move_old_poses.keys(): | ||||||
|  |             node = self.graph.nodes[node_index] | ||||||
|  |             old_pos = self.move_old_poses[node_index] | ||||||
|  |             node.x = old_pos[0] + delta_x | ||||||
|  |             node.z = old_pos[1] + delta_z | ||||||
|  |  | ||||||
|  |     def reset_move_poses(self): | ||||||
|  |         self.original_move_pos = None | ||||||
|  |         for node_index in self.move_old_poses.keys(): | ||||||
|  |             node = self.graph.nodes[node_index] | ||||||
|  |             old_pos = self.move_old_poses[node_index] | ||||||
|  |             node.x = old_pos[0] | ||||||
|  |             node.z = old_pos[1] | ||||||
|  |         self.move_old_poses = None | ||||||
|  |  | ||||||
|  |     def confirm_move_poses(self): | ||||||
|  |         self.dirty = True | ||||||
|  |         self.original_move_pos = None | ||||||
|  |         self.move_old_poses = None | ||||||
|  |  | ||||||
|  |     def change_edge_types(self, type: str = "path"): | ||||||
|  |         self.dirty = True | ||||||
|  |         for edge in self.selected_edges: | ||||||
|  |             self.graph.set_edge_type(edge, type) | ||||||
|  |  | ||||||
|  |     def change_node_types(self, type: int = 0): | ||||||
|  |         self.dirty = True | ||||||
|  |         for node_index in self.selected_nodes: | ||||||
|  |             node = self.graph.nodes[node_index] | ||||||
|  |             node.set_type(type) | ||||||
|  |  | ||||||
|  |     def deleted_selected_objects(self): | ||||||
|  |         self.dirty = True | ||||||
|  |         edges_to_delete = [self.graph.edges[i] for i in self.selected_edges] | ||||||
|  |         nodes_to_delete = [self.graph.nodes[i] for i in self.selected_nodes] | ||||||
|  |         for edge in edges_to_delete: | ||||||
|  |             self.graph.delete_edge(edge) | ||||||
|  |         for node in nodes_to_delete: | ||||||
|  |             self.graph.delete_node(node) | ||||||
|  |         self.clear_selection() | ||||||
|  |         n = len(self.previously_created_nodes) | ||||||
|  |         if n != 0: | ||||||
|  |             self.selected_nodes.append(self.previously_created_nodes[n - 1]) | ||||||
|  |             self.previously_created_nodes.pop() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # ========================= | ||||||
|  |     # CALCULATION | ||||||
|  |     # ========================= | ||||||
|  |  | ||||||
|     def screen_to_world(self, x: int, y: int) -> tuple[int, int]: |     def screen_to_world(self, x: int, y: int) -> tuple[int, int]: | ||||||
|         w2 = self.width / 2 |         w2 = self.width / 2 | ||||||
|         h2 = self.height / 2 |         h2 = self.height / 2 | ||||||
| @@ -213,6 +671,81 @@ class Editor: | |||||||
|  |  | ||||||
|         return int(world_x), int(world_z) |         return int(world_x), int(world_z) | ||||||
|  |  | ||||||
|  |     def world_to_screen(self, world_x: int, world_z: int) -> tuple[int, int]: | ||||||
|  |         w2 = self.width / 2 | ||||||
|  |         h2 = self.height / 2 | ||||||
|  |         screen_x = (world_x - self.center[0]) * self.zoom + w2 | ||||||
|  |         screen_y = (world_z - self.center[1]) * self.zoom + h2 | ||||||
|  |  | ||||||
|  |         return int(screen_x), int(screen_y) | ||||||
|  |  | ||||||
|  |     def get_edge_distance(self, edge_i: int, px: int, pz: int) -> float: | ||||||
|  |         start_n, end_n = self.graph.get_edge_nodes(self.graph.edges[edge_i]) | ||||||
|  |         start_p = self.world_to_screen(start_n.x, start_n.z) | ||||||
|  |         end_p = self.world_to_screen(end_n.x, end_n.z) | ||||||
|  |  | ||||||
|  |         edge_vec = (end_p[0] - start_p[0], end_p[1] - start_p[1]) | ||||||
|  |         start_vec = (px - start_p[0], pz - start_p[1]) | ||||||
|  |  | ||||||
|  |         edge_vec_len = (edge_vec[0] ** 2 + edge_vec[1] ** 2) ** 0.5 | ||||||
|  |  | ||||||
|  |         if edge_vec_len == 0: | ||||||
|  |             return self.get_node_distance(start_n.index, px, pz) | ||||||
|  |  | ||||||
|  |         scal_prod = start_vec[0] * edge_vec[0] + start_vec[1] * edge_vec[1] | ||||||
|  |  | ||||||
|  |         proj_len = scal_prod / edge_vec_len | ||||||
|  |  | ||||||
|  |         if proj_len < 0: | ||||||
|  |             return self.get_node_distance(start_n.index, px, pz) | ||||||
|  |         if proj_len > edge_vec_len: | ||||||
|  |             return self.get_node_distance(end_n.index, px, pz) | ||||||
|  |         return abs((edge_vec[0] * start_vec[1] - edge_vec[1] * start_vec[0]) / edge_vec_len) | ||||||
|  |  | ||||||
|  |     def get_node_distance(self, node_i: int, px: int, pz: int) -> float: | ||||||
|  |         node = self.graph.nodes[node_i] | ||||||
|  |         node_pos = self.world_to_screen(node.x, node.z) | ||||||
|  |         return ((px - node_pos[0]) ** 2 + (pz - node_pos[1]) ** 2) ** 0.5 | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # SAVING | ||||||
|  |  | ||||||
|  |     def save(self, path: Optional[str] = None, save_config: bool = True) -> None: | ||||||
|  |         last_path = self.config.LAST_OPENED_FILE | ||||||
|  |         if path is None: | ||||||
|  |             path = input(f"Save as ({last_path}): ") | ||||||
|  |             if len(path.strip()) == 0: | ||||||
|  |                 path = last_path | ||||||
|  |  | ||||||
|  |         self.graph.save(path) | ||||||
|  |         if save_config: | ||||||
|  |             self.config.LAST_OPENED_FILE = path | ||||||
|  |             self.config.save() | ||||||
|  |             if os.path.exists(self.AUTOSAVE_PATH): | ||||||
|  |                 os.remove(self.AUTOSAVE_PATH) | ||||||
|  |         self.dirty = False | ||||||
|  |  | ||||||
|  |     def load(self, path: Optional[str] = None, save_config: bool = True) -> None: | ||||||
|  |         last_path = self.config.LAST_OPENED_FILE | ||||||
|  |         if path is None: | ||||||
|  |             path = input(f"Load from ({last_path}): ") | ||||||
|  |             if len(path.strip()) == 0: | ||||||
|  |                 path = last_path | ||||||
|  |  | ||||||
|  |         if os.path.exists(path): | ||||||
|  |             self.graph = Graph.load(path) | ||||||
|  |             if save_config: | ||||||
|  |                 self.config.LAST_OPENED_FILE = path | ||||||
|  |                 self.config.save() | ||||||
|  |             self.dirty = False | ||||||
|  |  | ||||||
|  |     def reload(self) -> None: | ||||||
|  |         self.state = State.LOADING | ||||||
|  |         self.loading_bg = self.win.copy() | ||||||
|  |         del self.image_handler | ||||||
|  |         self.image_handler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE, self.config.CACHE_TTL) | ||||||
|  |  | ||||||
|  |  | ||||||
| class State(Enum): | class State(Enum): | ||||||
|     STOPPING = auto() |     STOPPING = auto() | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								src/graph/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/graph/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										8
									
								
								src/graph/edge.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/graph/edge.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | class Edge: | ||||||
|  |  | ||||||
|  |     def __init__(self, start: int, end: int, length: float, index: int, type: int): | ||||||
|  |         self.length: float = length | ||||||
|  |         self.start: int = start | ||||||
|  |         self.end: int = end | ||||||
|  |         self.index: int = index | ||||||
|  |         self.type: int = type | ||||||
							
								
								
									
										161
									
								
								src/graph/graph.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/graph/graph.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  | from math import inf, sqrt | ||||||
|  | from typing import Iterator, Optional | ||||||
|  |  | ||||||
|  | from src.graph.node import Node | ||||||
|  | from src.graph.edge import Edge | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Graph: | ||||||
|  |  | ||||||
|  |     EDGE_TYPES: list[str] = [ | ||||||
|  |         "path", | ||||||
|  |         "narrow_path", | ||||||
|  |         "ferry", | ||||||
|  |         "boat", | ||||||
|  |         "rails" | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     TYPE_COLORS: list[tuple[int, int, int]] = [ | ||||||
|  |         (255, 0, 0), | ||||||
|  |         (255, 0, 255), | ||||||
|  |         (0, 255, 0), | ||||||
|  |         (255, 255, 0), | ||||||
|  |         (0, 0, 255) | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         self.edges: list[Edge] = [] | ||||||
|  |         self.nodes: list[Node] = [] | ||||||
|  |  | ||||||
|  |     def add_node(self, x: int, z: int, name: str = "", type: int = 0) -> None: | ||||||
|  |         self.nodes.append(Node(x, z, len(self.nodes), name, type)) | ||||||
|  |  | ||||||
|  |     def add_edge(self, start_index: int, end_index: int, type: str = "path", auto_length: bool = True) -> None: | ||||||
|  |         length = 0 | ||||||
|  |         if auto_length: | ||||||
|  |             n1, n2 = self.nodes[start_index], self.nodes[end_index] | ||||||
|  |             length = sqrt((n1.x - n2.x)**2 + (n1.z - n2.z)**2) | ||||||
|  |         type_n = 0 if type not in self.EDGE_TYPES else self.EDGE_TYPES.index(type) | ||||||
|  |         self.edges.append(Edge(start_index, end_index, length, len(self.edges), type_n)) | ||||||
|  |  | ||||||
|  |     def delete_edge(self, edge: Edge) -> None: | ||||||
|  |         self.edges.remove(edge) | ||||||
|  |         for ed in self.edges: | ||||||
|  |             ed.index = self.edges.index(ed) | ||||||
|  |  | ||||||
|  |     def delete_node(self, node: Node) -> None: | ||||||
|  |         edges_to_delete = [] | ||||||
|  |         for edge in self.edges: | ||||||
|  |             if node.index in (edge.start, edge.end): | ||||||
|  |                 edges_to_delete.append(edge) | ||||||
|  |                 continue | ||||||
|  |             if edge.start > node.index: | ||||||
|  |                 edge.start -= 1 | ||||||
|  |             if edge.end > node.index: | ||||||
|  |                 edge.end -= 1 | ||||||
|  |         for edge in edges_to_delete: | ||||||
|  |             self.delete_edge(edge) | ||||||
|  |         self.nodes.remove(node) | ||||||
|  |         for no in self.nodes: | ||||||
|  |             no.index = self.nodes.index(no) | ||||||
|  |  | ||||||
|  |     def recompute_lengths(self) -> None: | ||||||
|  |         for edge in self.edges: | ||||||
|  |             n1 = self.nodes[edge.start] | ||||||
|  |             n2 = self.nodes[edge.end] | ||||||
|  |             edge.length = sqrt((n1.x - n2.x)**2 + (n1.z - n2.z)**2) | ||||||
|  |  | ||||||
|  |     def number_of_nodes(self) -> int: | ||||||
|  |         return len(self.nodes) | ||||||
|  |      | ||||||
|  |     def get_edge(self, node_1: int, node_2: int) -> int: | ||||||
|  |         for edge in self.edges: | ||||||
|  |             if (edge.start == node_1 and edge.end == node_2) or (edge.start == node_2 and edge.end == node_1): | ||||||
|  |                 return self.edges.index(edge) | ||||||
|  |         return -1 | ||||||
|  |      | ||||||
|  |     def get_edge_nodes(self, edge: Edge) -> tuple[Node, Node]: | ||||||
|  |         return self.nodes[edge.start], self.nodes[edge.end] | ||||||
|  |  | ||||||
|  |     def get_edge_center(self, edge_index: int) -> tuple[float, float]: | ||||||
|  |         edge = self.edges[edge_index] | ||||||
|  |         start_n = self.nodes[edge.start] | ||||||
|  |         end_n = self.nodes[edge.end] | ||||||
|  |         return (start_n.x + end_n.x) / 2, (start_n.z + end_n.z) / 2 | ||||||
|  |  | ||||||
|  |     def edges_adjacent_to(self, node_i: int) -> Iterator[Edge]: | ||||||
|  |         return filter(lambda e: e.start == node_i or e.end == node_i, self.edges) | ||||||
|  |      | ||||||
|  |     def edge_exists(self, node_1: int, node_2: int) -> bool: | ||||||
|  |         return self.get_edge(node_1, node_2) != -1 | ||||||
|  |      | ||||||
|  |     def set_edge_type(self, edge_index: int, type: str = "path") -> None: | ||||||
|  |         edge = self.edges[edge_index] | ||||||
|  |         edge.type = 0 if type not in self.EDGE_TYPES else self.EDGE_TYPES.index(type) | ||||||
|  |  | ||||||
|  |     def dijkstra(self, source_index: int, target_index: int) -> Optional[list[int]]: | ||||||
|  |         n = len(self.nodes) | ||||||
|  |  | ||||||
|  |         if source_index < 0 or source_index >= n: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         if target_index < 0 or target_index >= n: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         unvisited = list(range(n)) | ||||||
|  |  | ||||||
|  |         distances_from_start = [inf] * n | ||||||
|  |         distances_from_start[source_index] = 0 | ||||||
|  |  | ||||||
|  |         node_sequences = [[] for _ in range(n)] | ||||||
|  |         node_sequences[source_index] = [source_index] | ||||||
|  |  | ||||||
|  |         while True: | ||||||
|  |             current_index = min(unvisited, key=lambda i: distances_from_start[i]) | ||||||
|  |  | ||||||
|  |             if current_index == target_index: | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |             unvisited.remove(current_index) | ||||||
|  |  | ||||||
|  |             for edge in self.edges_adjacent_to(current_index): | ||||||
|  |                 start = current_index | ||||||
|  |                 end = edge.end if edge.start == current_index else edge.start | ||||||
|  |  | ||||||
|  |                 if end in unvisited and distances_from_start[end] > distances_from_start[start] + edge.length: | ||||||
|  |                     distances_from_start[end] = distances_from_start[start] + edge.length | ||||||
|  |                     node_sequences[end] = node_sequences[start].copy() | ||||||
|  |                     node_sequences[end].append(end) | ||||||
|  |  | ||||||
|  |         return node_sequences[target_index] | ||||||
|  |  | ||||||
|  |     def save(self, path: str) -> None: | ||||||
|  |         with open(path, "w") as f: | ||||||
|  |             for node in self.nodes: | ||||||
|  |                 f.write(f"n {node.x} {node.z} {node.type} {node.name}\n") | ||||||
|  |             f.write("\n") | ||||||
|  |             for edge in self.edges: | ||||||
|  |                 f.write(f"e {edge.start} {edge.end} {edge.type}\n") | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def load(path: str) -> Graph: | ||||||
|  |         graph = Graph() | ||||||
|  |         with open(path, "r") as f: | ||||||
|  |             lines = f.read().splitlines() | ||||||
|  |             for line in lines: | ||||||
|  |                 if len(line.strip()) == 0: | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |                 entry_type, values = line.split(" ", 1) | ||||||
|  |                 if entry_type == "n": | ||||||
|  |                     x, z, type, name = values.split(" ", 3) | ||||||
|  |                     x, z, type = int(x), int(z), int(type) | ||||||
|  |                     graph.add_node(x, z, name, type) | ||||||
|  |                 elif entry_type == "e": | ||||||
|  |                     start, end, type = values.split(" ", 2) | ||||||
|  |                     start, end, type = int(start), int(end), int(type) | ||||||
|  |                     graph.add_edge(start, end, auto_length=False, type=graph.EDGE_TYPES[type]) | ||||||
|  |  | ||||||
|  |         graph.recompute_lengths() | ||||||
|  |         return graph | ||||||
							
								
								
									
										18
									
								
								src/graph/node.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/graph/node.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | class Node: | ||||||
|  |     def __init__(self, x: int, z: int, index: int, name: str = "", type: int = 0): | ||||||
|  |         self.x: int = x | ||||||
|  |         self.z: int = z | ||||||
|  |         self.index: int = index | ||||||
|  |         self.name: str = name | ||||||
|  |         self.type: int = (0 if name == "" else type) | ||||||
|  |  | ||||||
|  |     def rename_node(self, name: str) -> None: | ||||||
|  |         if self.name == "" and name != "": | ||||||
|  |             self.type = 1 | ||||||
|  |         self.name = name | ||||||
|  |         if name == "": | ||||||
|  |             self.type = 0 | ||||||
|  |      | ||||||
|  |     def set_type(self, type: int = 0) -> None: | ||||||
|  |         if type != 0: | ||||||
|  |             self.type = 0 if self.name == "" else type | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| import os | import os | ||||||
| import threading | import threading | ||||||
|  | import time | ||||||
| from math import floor | from math import floor | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| @@ -7,10 +8,13 @@ import pygame | |||||||
|  |  | ||||||
|  |  | ||||||
| class ImageHandler: | class ImageHandler: | ||||||
|     def __init__(self, maps_dir: str, base_size: int): |     def __init__(self, maps_dir: str, base_size: int, ttl: int): | ||||||
|         self.maps_dir: str = maps_dir |         self.maps_dir: str = maps_dir | ||||||
|         self.base_size: int = base_size |         self.base_size: int = base_size | ||||||
|  |         self.ttl: int = ttl | ||||||
|         self.cache: dict = {} |         self.cache: dict = {} | ||||||
|  |         self.history: dict[tuple[float, tuple[int, int]], float] = {} | ||||||
|  |         self.size: int = 0 | ||||||
|         self.count: int = 0 |         self.count: int = 0 | ||||||
|         self.total: int = 0 |         self.total: int = 0 | ||||||
|         self.loading: bool = False |         self.loading: bool = False | ||||||
| @@ -29,6 +33,7 @@ class ImageHandler: | |||||||
|             name, x, y = path.split(".")[0].split("_") |             name, x, y = path.split(".")[0].split("_") | ||||||
|             cache[(int(x), int(y))] = pygame.image.load(fullpath).convert_alpha() |             cache[(int(x), int(y))] = pygame.image.load(fullpath).convert_alpha() | ||||||
|             self.count += 1 |             self.count += 1 | ||||||
|  |             self.size += 1 | ||||||
|  |  | ||||||
|         self.cache = { |         self.cache = { | ||||||
|             1: cache |             1: cache | ||||||
| @@ -65,6 +70,19 @@ class ImageHandler: | |||||||
|                     img = pygame.transform.scale_by(img, zoom) |                     img = pygame.transform.scale_by(img, zoom) | ||||||
|  |  | ||||||
|             cache[pos] = img |             cache[pos] = img | ||||||
|  |             self.size += 1 | ||||||
|  |  | ||||||
|         self.cache[zoom] = cache |         self.cache[zoom] = cache | ||||||
|  |         self.history[(zoom, pos)] = time.time() | ||||||
|         return cache[pos] |         return cache[pos] | ||||||
|  |  | ||||||
|  |     def clean(self) -> None: | ||||||
|  |         t = time.time() | ||||||
|  |         new_history = {} | ||||||
|  |         for (zoom, pos), t0 in self.history.items(): | ||||||
|  |             if zoom != 1 and t0 + self.ttl < t: | ||||||
|  |                 del self.cache[zoom][pos] | ||||||
|  |                 self.size -= 1 | ||||||
|  |             else: | ||||||
|  |                 new_history[(zoom, pos)] = t0 | ||||||
|  |         self.history = new_history | ||||||
|   | |||||||
| Before Width: | Height: | Size: 20 MiB After Width: | Height: | Size: 20 MiB | 
							
								
								
									
										0
									
								
								src/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										285
									
								
								src/utils/auto_mapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								src/utils/auto_mapper.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | |||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | import tempfile | ||||||
|  | from datetime import datetime | ||||||
|  | from ftplib import FTP | ||||||
|  |  | ||||||
|  | import numpy as np | ||||||
|  | import pygame | ||||||
|  | from scipy.signal import convolve2d | ||||||
|  |  | ||||||
|  | from src.utils.minecraft.chunk import Chunk, MalformedChunk, OldChunk | ||||||
|  | from src.utils.minecraft.region import Region | ||||||
|  | from src.utils.paths import CONFIG_DIR, CACHE_DIR, get_project_path | ||||||
|  |  | ||||||
|  | CHUNK_SIZE = 16 | ||||||
|  | CHUNKS_IN_REGION = 32 | ||||||
|  | REGION_SIZE = CHUNK_SIZE * CHUNKS_IN_REGION | ||||||
|  | GRADIENT_MAT = np.array([ | ||||||
|  |     [0, 0, 0], | ||||||
|  |     [0, 1, 0.5], | ||||||
|  |     [0, -0.5, -1] | ||||||
|  | ]) | ||||||
|  | GRADIENT_RANGE = 2 | ||||||
|  | GAUSSIAN_MAT = np.array([ | ||||||
|  |     [1, 4, 7, 4, 1], | ||||||
|  |     [4, 16, 26, 16, 4], | ||||||
|  |     [7, 26, 41, 26, 7], | ||||||
|  |     [4, 16, 26, 16, 4], | ||||||
|  |     [1, 4, 7, 4, 1], | ||||||
|  | ]) | ||||||
|  | GAUSSIAN_FACT = GAUSSIAN_MAT.sum() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AutoMapper: | ||||||
|  |     CONFIG_PATH = os.path.join(CONFIG_DIR, "mapper.json") | ||||||
|  |     CACHE_PATH = os.path.join(CACHE_DIR, "regions.txt") | ||||||
|  |     COLORS_PATH = os.path.join(CACHE_DIR, "colors.json") | ||||||
|  |     MAPS_DIR = os.path.join(CACHE_DIR, "maps") | ||||||
|  |     MAP_SIZE = 1024 | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         self.config: FTPConfig = FTPConfig(self.CONFIG_PATH) | ||||||
|  |         self.ftp: FTP = FTP(self.config.HOST) | ||||||
|  |         self.regions: list[tuple[int, int]] = [] | ||||||
|  |         self.temp_dir: tempfile.TemporaryDirectory[str] = tempfile.TemporaryDirectory() | ||||||
|  |         self.colors: dict[str, tuple[float, float, float]] = {} | ||||||
|  |         self.cache: dict[tuple[int, int], int] = {} | ||||||
|  |         self.colormaps: dict[str, dict[str, tuple[float, float, float]]] = {} | ||||||
|  |         self.available_regions: list[tuple[int, int]] = [] | ||||||
|  |         self.load_colors() | ||||||
|  |         self.load_cache() | ||||||
|  |         self.load_colormaps() | ||||||
|  |         self.no_color: set[str] = set() | ||||||
|  |  | ||||||
|  |     def __enter__(self): | ||||||
|  |         self.ftp.login(self.config.USERNAME, self.config.PASSWORD) | ||||||
|  |         self.ftp.cwd(self.config.DIR) | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def __exit__(self, exc_type, exc_val, exc_tb): | ||||||
|  |         self.ftp.close() | ||||||
|  |  | ||||||
|  |     def load_colors(self) -> None: | ||||||
|  |         with open(self.COLORS_PATH, "r") as f: | ||||||
|  |             self.colors = json.load(f) | ||||||
|  |  | ||||||
|  |     def load_cache(self) -> None: | ||||||
|  |         if os.path.exists(self.CACHE_PATH): | ||||||
|  |             with open(self.CACHE_PATH, "r") as f: | ||||||
|  |                 lines = filter(lambda l: len(l.strip()) != 0, f.read().splitlines()) | ||||||
|  |                 self.cache = {} | ||||||
|  |                 for line in lines: | ||||||
|  |                     rx, rz, ts = map(int, line.split(" ")) | ||||||
|  |                     self.cache[(rx, rz)] = ts | ||||||
|  |  | ||||||
|  |     def load_colormaps(self) -> None: | ||||||
|  |         biome_ids = {} | ||||||
|  |         with open(get_project_path("res", "biome_ids.txt")) as f: | ||||||
|  |             lines = f.read().splitlines() | ||||||
|  |             for line in lines: | ||||||
|  |                 if len(line.strip()) == 0: | ||||||
|  |                     continue | ||||||
|  |                 biome_name, biome_id = line.split(" ") | ||||||
|  |                 biome_ids[f"minecraft:{biome_name}"] = int(biome_id) | ||||||
|  |  | ||||||
|  |         self.colormaps = {} | ||||||
|  |         maps_dir = get_project_path("res", "colormaps") | ||||||
|  |         for filename in os.listdir(maps_dir): | ||||||
|  |             if not filename.endswith(".png"): | ||||||
|  |                 continue | ||||||
|  |             img = pygame.image.load(os.path.join(maps_dir, filename)) | ||||||
|  |             block = filename.split(".")[0] | ||||||
|  |             colormap = {} | ||||||
|  |             for biome_name, biome_id in biome_ids.items(): | ||||||
|  |                 colormap[biome_name] = img.get_at((biome_id, 0))[:3] | ||||||
|  |             self.colormaps[f"minecraft:{block}"] = colormap | ||||||
|  |  | ||||||
|  |         self.colormaps["minecraft:grass"] = self.colormaps["minecraft:grass_block"] | ||||||
|  |         self.colormaps["minecraft:tall_grass"] = self.colormaps["minecraft:grass_block"] | ||||||
|  |         self.colormaps["minecraft:vine"] = self.colormaps["minecraft:oak_leaves"] | ||||||
|  |         self.colormaps["minecraft:large_fern"] = self.colormaps["minecraft:grass_block"] | ||||||
|  |         self.colormaps["minecraft:fern"] = self.colormaps["minecraft:grass_block"] | ||||||
|  |         self.colormaps["minecraft:melon_stem"] = self.colormaps["minecraft:foliage"] | ||||||
|  |         self.colormaps["minecraft:attached_melon_stem"] = self.colormaps["minecraft:foliage"] | ||||||
|  |         self.colormaps["minecraft:mangrove_leaves"] = self.colormaps["minecraft:foliage"] | ||||||
|  |         self.colormaps["minecraft:bubble_column"] = self.colormaps["minecraft:water"] | ||||||
|  |  | ||||||
|  |     def save_cache(self) -> None: | ||||||
|  |         with open(self.CACHE_PATH, "w") as f: | ||||||
|  |             for (rx, rz), ts in self.cache.items(): | ||||||
|  |                 f.write(f"{rx} {rz} {ts}\n") | ||||||
|  |  | ||||||
|  |     def get_available_regions(self) -> dict[tuple[int, int], int]: | ||||||
|  |         files = self.ftp.mlsd(facts=["modify"]) | ||||||
|  |         regions = {} | ||||||
|  |         self.available_regions = [] | ||||||
|  |         for filename, facts in files: | ||||||
|  |             m = re.match(r"r\.(-?\d+)\.(-?\d+)\.mca", filename) | ||||||
|  |             if m: | ||||||
|  |                 rx = int(m.group(1)) | ||||||
|  |                 rz = int(m.group(2)) | ||||||
|  |                 t = datetime.strptime(facts["modify"], "%Y%m%d%H%M%S") | ||||||
|  |                 regions[(rx, rz)] = int(t.timestamp()) | ||||||
|  |                 self.available_regions.append((rx, rz)) | ||||||
|  |         return regions | ||||||
|  |  | ||||||
|  |     def fetch_region(self, rx: int, rz: int) -> str: | ||||||
|  |         name = f"r.{rx}.{rz}.mca" | ||||||
|  |         outpath = os.path.join(self.temp_dir.name, name) | ||||||
|  |         with open(outpath, "wb") as f: | ||||||
|  |             self.ftp.retrbinary(f"RETR {name}", f.write, 1024) | ||||||
|  |  | ||||||
|  |         return outpath | ||||||
|  |  | ||||||
|  |     def map_region(self, rx: int, rz: int) -> tuple[pygame.Surface, np.array]: | ||||||
|  |         print(f"  [Fetching region ({rx},{rz})]") | ||||||
|  |         path = self.fetch_region(rx, rz) | ||||||
|  |         region = Region(path) | ||||||
|  |         print("  [Rendering]") | ||||||
|  |         surf = pygame.Surface([REGION_SIZE, REGION_SIZE], pygame.SRCALPHA) | ||||||
|  |         surf.fill((0, 0, 0, 0)) | ||||||
|  |         heightmap = np.zeros((REGION_SIZE, REGION_SIZE), dtype="uint16") | ||||||
|  |  | ||||||
|  |         for cz in range(CHUNKS_IN_REGION): | ||||||
|  |             for cx in range(CHUNKS_IN_REGION): | ||||||
|  |                 ox, oy = cx * CHUNK_SIZE, cz * CHUNK_SIZE | ||||||
|  |                 chunk = region.get_chunk(rx * CHUNKS_IN_REGION + cx, rz * CHUNKS_IN_REGION + cz) | ||||||
|  |                 if isinstance(chunk, Chunk): | ||||||
|  |                     hm = self.render_chunk(chunk, surf, ox, oy) | ||||||
|  |                     heightmap[oy:oy+CHUNK_SIZE, ox:ox+CHUNK_SIZE] = hm | ||||||
|  |                 elif isinstance(chunk, MalformedChunk): | ||||||
|  |                     pygame.draw.rect(surf, (92, 47, 32, 200), [ox, oy, CHUNK_SIZE, CHUNK_SIZE]) | ||||||
|  |                 elif isinstance(chunk, OldChunk): | ||||||
|  |                     pygame.draw.rect(surf, (32, 61, 92, 200), [ox, oy, CHUNK_SIZE, CHUNK_SIZE]) | ||||||
|  |  | ||||||
|  |         print() | ||||||
|  |         os.remove(path) | ||||||
|  |  | ||||||
|  |         return surf, heightmap | ||||||
|  |  | ||||||
|  |     def map_region_group(self, x: int, z: int) -> pygame.Surface: | ||||||
|  |         surf = pygame.Surface([self.MAP_SIZE, self.MAP_SIZE], pygame.SRCALPHA) | ||||||
|  |         surf.fill((0, 0, 0, 0)) | ||||||
|  |         n_regions = self.MAP_SIZE // REGION_SIZE | ||||||
|  |         heightmap = np.zeros((self.MAP_SIZE, self.MAP_SIZE), dtype="uint16") | ||||||
|  |         for dz in range(n_regions): | ||||||
|  |             for dx in range(n_regions): | ||||||
|  |                 rx, rz = x * n_regions + dx, z * n_regions + dz | ||||||
|  |                 if (rx, rz) in self.available_regions: | ||||||
|  |                     region, hm = self.map_region(rx, rz) | ||||||
|  |                     ox, oy = dx * REGION_SIZE, dz * REGION_SIZE | ||||||
|  |                     surf.blit(region, [ox, oy]) | ||||||
|  |                     heightmap[oy:oy+REGION_SIZE, ox:ox+REGION_SIZE] = hm | ||||||
|  |                     self.cache[(rx, rz)] = int(datetime.utcnow().timestamp()) | ||||||
|  |  | ||||||
|  |         gradient = convolve2d(heightmap, GRADIENT_MAT, boundary="symm")[1:self.MAP_SIZE+1, 1:self.MAP_SIZE+1] | ||||||
|  |         gradient = gradient.clip(-GRADIENT_RANGE, GRADIENT_RANGE) | ||||||
|  |         gradient = 1 + gradient / 2 / GRADIENT_RANGE | ||||||
|  |         gradient = np.array([gradient, gradient, gradient, np.ones(gradient.shape)]) | ||||||
|  |         gradient = np.swapaxes(gradient, 0, 2) | ||||||
|  |         surf_array = pygame.surfarray.array3d(surf) | ||||||
|  |         alpha_array = pygame.surfarray.array_alpha(surf).reshape((*surf_array.shape[0:2], 1)) | ||||||
|  |         surf_array = np.concatenate((surf_array, alpha_array), 2) | ||||||
|  |         res = surf_array * gradient | ||||||
|  |         res = res.clip(0, 255) | ||||||
|  |         buf = res.transpose((1, 0, 2)).astype("uint8").tobytes(order="C") | ||||||
|  |         surf = pygame.image.frombuffer(buf, surf.get_size(), "RGBA") | ||||||
|  |  | ||||||
|  |         return surf | ||||||
|  |  | ||||||
|  |     def render_chunk(self, chunk: Chunk, surf: pygame.Surface, ox: int, oy: int) -> np.array: | ||||||
|  |         blocks, heightmap, biomes, is_empty = chunk.get_top_blocks() | ||||||
|  |         if is_empty: | ||||||
|  |             pygame.draw.rect(surf, (0, 0, 0, 0), [ox, oy, CHUNK_SIZE, CHUNK_SIZE]) | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             for z in range(CHUNK_SIZE): | ||||||
|  |                 for x in range(CHUNK_SIZE): | ||||||
|  |                     color = self.get_color(blocks[z][x], biomes[z][x]) | ||||||
|  |                     surf.set_at((ox + x, oy + z), color) | ||||||
|  |         return heightmap | ||||||
|  |  | ||||||
|  |     def get_color(self, block: str, biome: str) -> tuple[float, float, float, float]: | ||||||
|  |         if block in self.colormaps: | ||||||
|  |             r, g, b = self.colormaps[block][biome] | ||||||
|  |             r2, g2, b2 = self.colors.get(block, (0, 0, 0, 0)) | ||||||
|  |             return min(255.0, r*r2/255), min(255.0, g*g2/255), min(255.0, b*b2/255), 255 | ||||||
|  |         if block not in self.colors: | ||||||
|  |             if block not in self.no_color: | ||||||
|  |                 print(f"    no color for {block}") | ||||||
|  |                 self.no_color.add(block) | ||||||
|  |         return self.colors.get(block, (0, 0, 0, 0)) | ||||||
|  |  | ||||||
|  |     def map_world(self) -> None: | ||||||
|  |         if not os.path.exists(self.MAPS_DIR): | ||||||
|  |             os.mkdir(self.MAPS_DIR) | ||||||
|  |  | ||||||
|  |         regions = self.get_available_regions() | ||||||
|  |         groups_to_map = set() | ||||||
|  |         n_regions = self.MAP_SIZE // REGION_SIZE | ||||||
|  |         for pos, modified_at in regions.items(): | ||||||
|  |             if pos in self.cache and modified_at <= self.cache[pos]: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             gx = pos[0] // n_regions | ||||||
|  |             gz = pos[1] // n_regions | ||||||
|  |             groups_to_map.add((gx, gz)) | ||||||
|  |  | ||||||
|  |         n_groups = len(groups_to_map) | ||||||
|  |         print(f"Groups to map: {n_groups}") | ||||||
|  |         proceed = input("Proceed ? y/[N] ") | ||||||
|  |         if proceed.strip().lower() == "y": | ||||||
|  |             groups_to_map = sorted(groups_to_map, key=lambda g: g[0]*g[0] + g[1]*g[1]) | ||||||
|  |             for i, (gx, gz) in enumerate(groups_to_map): | ||||||
|  |                 print(f"[Mapping group ({gx}, {gz}) ({i+1}/{n_groups})]") | ||||||
|  |                 try: | ||||||
|  |                     surf = self.map_region_group(gx, gz) | ||||||
|  |  | ||||||
|  |                     pygame.image.save(surf, os.path.join(self.MAPS_DIR, f"map_{gx}_{gz}.png")) | ||||||
|  |                     self.save_cache() | ||||||
|  |                 except Exception as e: | ||||||
|  |                     raise e | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FTPConfig: | ||||||
|  |     HOST = "" | ||||||
|  |     USERNAME = "" | ||||||
|  |     PASSWORD = "" | ||||||
|  |     DIR = "" | ||||||
|  |  | ||||||
|  |     def __init__(self, path: str): | ||||||
|  |         self._path: str = path | ||||||
|  |  | ||||||
|  |         self.load() | ||||||
|  |  | ||||||
|  |     def load(self) -> None: | ||||||
|  |         if os.path.exists(self._path): | ||||||
|  |             with open(self._path, "r") as f: | ||||||
|  |                 config = json.load(f) | ||||||
|  |  | ||||||
|  |             self.HOST = config["host"] | ||||||
|  |             self.USERNAME = config["username"] | ||||||
|  |             self.PASSWORD = config["password"] | ||||||
|  |             self.DIR = config["dir"] | ||||||
|  |  | ||||||
|  |     def save(self) -> None: | ||||||
|  |         with open(self._path, "w") as f: | ||||||
|  |             json.dump({ | ||||||
|  |                 "host": self.HOST, | ||||||
|  |                 "username": self.USERNAME, | ||||||
|  |                 "password": self.PASSWORD, | ||||||
|  |                 "dir": self.DIR | ||||||
|  |             }, f, indent=4) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     pygame.init() | ||||||
|  |     # x = -1 | ||||||
|  |     # z = 0 | ||||||
|  |     with AutoMapper() as mapper: | ||||||
|  |         # mapper.get_available_regions() | ||||||
|  |         # surf = mapper.map_region_group(x, z) | ||||||
|  |         # pygame.image.save(surf, f"/tmp/map_{x}_{z}.png") | ||||||
|  |         mapper.map_world() | ||||||
							
								
								
									
										244
									
								
								src/utils/color_calculator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								src/utils/color_calculator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | |||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import platform | ||||||
|  | import re | ||||||
|  | import tempfile | ||||||
|  | import zipfile | ||||||
|  |  | ||||||
|  | import pygame | ||||||
|  |  | ||||||
|  | from src.utils.paths import get_project_path, CACHE_DIR | ||||||
|  |  | ||||||
|  | # Textures with these parts will be excluded | ||||||
|  | EXCLUDE = { | ||||||
|  |     "bottom", "side", "front", "destroy", "on", "tip", "lit", "inner" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Textures with these names will be copied to their variants (variants listed in `VARIANTS`) | ||||||
|  | # Tuples indicate a change of name for variants | ||||||
|  | TO_VARIANT = [ | ||||||
|  |     ("nether_bricks", "nether_brick"), | ||||||
|  |     ("nether_bricks", "nether_brick"), | ||||||
|  |     ("oak_planks", "oak"), | ||||||
|  |     ("spruce_planks", "spruce"), | ||||||
|  |     ("birch_planks", "birch"), | ||||||
|  |     ("jungle_planks", "jungle"), | ||||||
|  |     ("acacia_planks", "acacia"), | ||||||
|  |     ("dark_oak_planks", "dark_oak"), | ||||||
|  |     ("mangrove_planks", "mangrove"), | ||||||
|  |     ("cherry_planks", "cherry"), | ||||||
|  |     ("bamboo_mosaic", "bamboo"), | ||||||
|  |     ("crimson_planks", "crimson"), | ||||||
|  |     ("warped_planks", "warped"), | ||||||
|  |     "stone", | ||||||
|  |     "cobblestone", | ||||||
|  |     "mossy_cobblestone", | ||||||
|  |     "smooth_stone", | ||||||
|  |     ("stone_bricks", "stone_brick"), | ||||||
|  |     ("mossy_stone_bricks", "mossy_stone_brick"), | ||||||
|  |     "granite", | ||||||
|  |     "polished_granite", | ||||||
|  |     "diorite", | ||||||
|  |     "polished_diorite", | ||||||
|  |     "andesite", | ||||||
|  |     "polished_andesite", | ||||||
|  |     "cobbled_deepslate", | ||||||
|  |     "polished_deepslate", | ||||||
|  |     ("deepslate_bricks", "deepslate_brick"), | ||||||
|  |     ("deepslate_tiles", "deepslate_tile"), | ||||||
|  |     ("bricks", "brick"), | ||||||
|  |     ("mud_bricks", "mud_brick"), | ||||||
|  |     "sandstone", | ||||||
|  |     "smooth_sandstone", | ||||||
|  |     "cut_sandstone", | ||||||
|  |     "red_sandstone", | ||||||
|  |     "smooth_red_sandstone", | ||||||
|  |     "cut_red_sandstone", | ||||||
|  |     "prismarine", | ||||||
|  |     ("prismarine_bricks", "prismarine_brick"), | ||||||
|  |     "dark_prismarine", | ||||||
|  |     ("red_nether_bricks", "red_nether_brick"), | ||||||
|  |     "blackstone", | ||||||
|  |     "polished_blackstone", | ||||||
|  |     ("polished_blackstone_bricks", "polished_blackstone_brick"), | ||||||
|  |     ("end_stone_bricks", "end_stone_brick"), | ||||||
|  |     ("purpur_block", "purpur"), | ||||||
|  |     ("quartz_block", "quartz"), | ||||||
|  |     "smooth_quartz", | ||||||
|  |     "cut_copper", | ||||||
|  |     "exposed_cut_copper", | ||||||
|  |     "weathered_cut_copper", | ||||||
|  |     "oxidized_cut_copper", | ||||||
|  |     "waxed_cut_copper", | ||||||
|  |     "waxed_exposed_cut_copper", | ||||||
|  |     "waxed_weathered_cut_copper", | ||||||
|  |     "waxed_oxidized_cut_copper", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # Variants of the textures in `TO_VARIANT` | ||||||
|  | VARIANTS = [ | ||||||
|  |     "slab", "stairs", | ||||||
|  |     "wall", "fence", "fence_gate", | ||||||
|  |     "pressure_plate", "button", | ||||||
|  |     "sign", "wall_sign", "hanging_sign", "wall_hanging_sign" | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # Colors to copy | ||||||
|  | TO_COPY = [ | ||||||
|  |     ("minecraft:furnace", "minecraft:dropper"), | ||||||
|  |     ("minecraft:furnace", "minecraft:dispenser"), | ||||||
|  |     ("minecraft:furnace", "minecraft:piston"), | ||||||
|  |     ("minecraft:furnace", "minecraft:sticky_piston"), | ||||||
|  |     ("minecraft:oak_planks", "minecraft:piston_head"), | ||||||
|  |     ("minecraft:oak_planks", "minecraft:sticky_piston_head"), | ||||||
|  |     ("minecraft:torch", "minecraft:wall_torch"), | ||||||
|  |     ("minecraft:soul_torch", "minecraft:soul_wall_torch"), | ||||||
|  |     ("minecraft:redstone_torch", "minecraft:redstone_wall_torch"), | ||||||
|  |     ("minecraft:snow", "minecraft:snow_block"), | ||||||
|  |     ("minecraft:water", "minecraft:bubble_column"), | ||||||
|  |     ("minecraft:sandstone", "minecraft:smooth_sandstone"), | ||||||
|  |     ("minecraft:red_sandstone", "minecraft:smooth_red_sandstone"), | ||||||
|  |     ("minecraft:quartz_block", "minecraft:smooth_quartz"), | ||||||
|  |     ("minecraft:dripstone_block", "minecraft:pointed_dripstone"), | ||||||
|  |     ("minecraft:oak_log", "minecraft:oak_wood"), | ||||||
|  |     ("minecraft:spruce_log", "minecraft:spruce_wood"), | ||||||
|  |     ("minecraft:birch_log", "minecraft:birch_wood"), | ||||||
|  |     ("minecraft:acacia_log", "minecraft:acacia_wood"), | ||||||
|  |     ("minecraft:jungle_log", "minecraft:jungle_wood"), | ||||||
|  |     ("minecraft:cherry_log", "minecraft:cherry_wood"), | ||||||
|  |     ("minecraft:mangrove_log", "minecraft:mangrove_wood"), | ||||||
|  |     ("minecraft:dark_oak_log", "minecraft:dark_oak_wood"), | ||||||
|  |     ("minecraft:stripped_oak_log", "minecraft:stripped_oak_wood"), | ||||||
|  |     ("minecraft:stripped_spruce_log", "minecraft:stripped_spruce_wood"), | ||||||
|  |     ("minecraft:stripped_birch_log", "minecraft:stripped_birch_wood"), | ||||||
|  |     ("minecraft:stripped_acacia_log", "minecraft:stripped_acacia_wood"), | ||||||
|  |     ("minecraft:stripped_jungle_log", "minecraft:stripped_jungle_wood"), | ||||||
|  |     ("minecraft:stripped_cherry_log", "minecraft:stripped_cherry_wood"), | ||||||
|  |     ("minecraft:stripped_mangrove_log", "minecraft:stripped_mangrove_wood"), | ||||||
|  |     ("minecraft:stripped_dark_oak_log", "minecraft:stripped_dark_oak_wood"), | ||||||
|  |     ("minecraft:magma", "minecraft:magma_block"), | ||||||
|  |     ("minecraft:cut_copper", "minecraft:waxed_cut_copper"), | ||||||
|  |     ("minecraft:exposed_cut_copper", "minecraft:waxed_exposed_cut_copper"), | ||||||
|  |     ("minecraft:weathered_cut_copper", "minecraft:waxed_weathered_cut_copper"), | ||||||
|  |     ("minecraft:oxidized_cut_copper", "minecraft:waxed_oxidized_cut_copper"), | ||||||
|  |     ("minecraft:iron_block", "minecraft:heavy_weighted_pressure_plate"), | ||||||
|  |     ("minecraft:gold_block", "minecraft:light_weighted_pressure_plate"), | ||||||
|  |     ("minecraft:bricks", "minecraft:flower_pot"), | ||||||
|  |     ("minecraft:oak_log", "minecraft:campfire"), | ||||||
|  |     ("minecraft:oak_log", "minecraft:soul_campfire"), | ||||||
|  |     ("minecraft:moss_block", "minecraft:moss_carpet"), | ||||||
|  |     ("minecraft:stone", "minecraft:infested_stone"), | ||||||
|  |     ("minecraft:cobblestone", "minecraft:infested_cobblestone"), | ||||||
|  |     ("minecraft:stone_bricks", "minecraft:infested_stone_bricks"), | ||||||
|  |     ("minecraft:mossy_stone_bricks", "minecraft:infested_mossy_stone_bricks"), | ||||||
|  |     ("minecraft:chiseled_stone_bricks", "minecraft:infested_chiseled_stone_bricks"), | ||||||
|  |     ("minecraft:deepslate", "minecraft:infested_deepslate"), | ||||||
|  |     ("minecraft:infested_stone_bricks", "minecraft:cracked_infested_stone_bricks"), | ||||||
|  |     ("minecraft:cauldron", "minecraft:water_cauldron"), | ||||||
|  |     ("minecraft:cauldron", "minecraft:lava_cauldron"), | ||||||
|  |     ("minecraft:cauldron", "minecraft:powder_snow_cauldron"), | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # Wool colors | ||||||
|  | WOOLS = [ | ||||||
|  |     "red", "blue", "cyan", "gray", | ||||||
|  |     "lime", "pink", "black", "brown", | ||||||
|  |     "green", "white", "orange", "purple", | ||||||
|  |     "yellow", "magenta", "light_blue", "light_gray" | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # Wool variants | ||||||
|  | WOOL_VARIANTS = [ | ||||||
|  |     "carpet", "bed", "banner", "wall_banner" | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | # These will be removed from the textures' names | ||||||
|  | TO_STRIP = ["_top", "_stalk", "_end", "_round", "_still"] | ||||||
|  |  | ||||||
|  | # Minecraft version | ||||||
|  | MC_VERSION = "1.20.1" | ||||||
|  |  | ||||||
|  | # Minecraft root directory (platform dependent) | ||||||
|  | MC_ROOT = os.path.expanduser({ | ||||||
|  |     "Linux": r"~/.minecraft", | ||||||
|  |     "Darwin": r"~/Library/Application Support/minecraft", | ||||||
|  |     "Windows": r"%APPDATA%\.minecraft" | ||||||
|  | }[platform.system()]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def extract_textures(jar_path: str, outdir: str) -> None: | ||||||
|  |     with zipfile.ZipFile(jar_path) as f: | ||||||
|  |         for info in f.infolist(): | ||||||
|  |             path = info.filename | ||||||
|  |             if not re.match(r"^assets/minecraft/textures/block/[^/]+\.png$", path): | ||||||
|  |                 continue | ||||||
|  |             info.filename = os.path.basename(path) | ||||||
|  |             f.extract(info, outdir) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main() -> None: | ||||||
|  |     print(f"[1/5] Extracting Minecraft {MC_VERSION} textures") | ||||||
|  |     jar_path = os.path.join(MC_ROOT, "versions", MC_VERSION, f"{MC_VERSION}.jar") | ||||||
|  |  | ||||||
|  |     if not os.path.exists(jar_path): | ||||||
|  |         print(f"Couldn't find Minecraft {MC_VERSION} JAR file") | ||||||
|  |         print(f"Not at {jar_path}") | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     workdir = tempfile.TemporaryDirectory() | ||||||
|  |     extract_textures(jar_path, workdir.name) | ||||||
|  |  | ||||||
|  |     pygame.init() | ||||||
|  |     pygame.display.set_mode((1, 1), pygame.NOFRAME | pygame.HIDDEN) | ||||||
|  |  | ||||||
|  |     colors = {} | ||||||
|  |     paths = os.listdir(workdir.name) | ||||||
|  |     total = len(paths) | ||||||
|  |     skipped = 0 | ||||||
|  |     print("[2/5] Averaging textures") | ||||||
|  |     for i, filename in enumerate(paths): | ||||||
|  |         print(f"\r{i+1}/{total} ({i/total*100:.2f}%) {filename}", end="") | ||||||
|  |         block, ext = filename.rsplit(".", 1) | ||||||
|  |         parts = set(block.split("_")) | ||||||
|  |         if not parts.isdisjoint(EXCLUDE): | ||||||
|  |             skipped += 1 | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         for s in TO_STRIP: | ||||||
|  |             block = block.replace(s, "") | ||||||
|  |         img = pygame.image.load(os.path.join(workdir.name, filename)).convert_alpha() | ||||||
|  |         color = pygame.transform.average_color(img, consider_alpha=True) | ||||||
|  |         color = color[:3] | ||||||
|  |         colors[f"minecraft:{block}"] = color | ||||||
|  |  | ||||||
|  |     print(f"\r{total}/{total} (100%) Finished") | ||||||
|  |     print(f"Skipped {skipped} files") | ||||||
|  |  | ||||||
|  |     print("[3/5] Applying overrides") | ||||||
|  |     with open(get_project_path("res", "color_overrides.json"), "r") as f: | ||||||
|  |         overrides = json.load(f) | ||||||
|  |     colors.update(overrides) | ||||||
|  |  | ||||||
|  |     print("[4/5] Generating variants") | ||||||
|  |     for to_variant in TO_VARIANT: | ||||||
|  |         src = to_variant[0] if isinstance(to_variant, tuple) else to_variant | ||||||
|  |         dst = to_variant[1] if isinstance(to_variant, tuple) else to_variant | ||||||
|  |  | ||||||
|  |         for variant in VARIANTS: | ||||||
|  |             TO_COPY.append((f"minecraft:{src}", f"minecraft:{dst}_{variant}")) | ||||||
|  |  | ||||||
|  |     for color in WOOLS: | ||||||
|  |         for variant in WOOL_VARIANTS: | ||||||
|  |             TO_COPY.append((f"minecraft:{color}_wool", f"minecraft:{color}_{variant}")) | ||||||
|  |  | ||||||
|  |     for src, dst in TO_COPY: | ||||||
|  |         colors[dst] = colors[src] | ||||||
|  |  | ||||||
|  |     print("[5/5] Exporting colors") | ||||||
|  |     outpath = os.path.join(CACHE_DIR, "colors.json") | ||||||
|  |     with open(outpath, "w") as f: | ||||||
|  |         json.dump(colors, f, indent=4) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     main() | ||||||
| @@ -8,12 +8,13 @@ Image.MAX_IMAGE_PIXELS = 200000000 | |||||||
| MAP_SIZE = 1024 | MAP_SIZE = 1024 | ||||||
| DEFAULT_PATH = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps") | DEFAULT_PATH = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps") | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def clamp(mn, value, mx): | def clamp(mn, value, mx): | ||||||
|     return max(mn, min(mx, value)) |     return max(mn, min(mx, value)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def main(): | def main(): | ||||||
|     # utils/2024-06-27_21.01.45_Lycacraft_minecraft~overworld_day.png |     # src/utils/2024-06-27_21.01.45_Lycacraft_minecraft~overworld_day.png | ||||||
|     # 6144,10240 |     # 6144,10240 | ||||||
|     input_path = input("Input image: ") |     input_path = input("Input image: ") | ||||||
|     output_path = input(f"Output dir (default: {DEFAULT_PATH}): ") |     output_path = input(f"Output dir (default: {DEFAULT_PATH}): ") | ||||||
							
								
								
									
										0
									
								
								src/utils/minecraft/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/utils/minecraft/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								src/utils/minecraft/block.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/utils/minecraft/block.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | from nbt.nbt import TAG | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Block: | ||||||
|  |     def __init__(self, | ||||||
|  |                  name: str, | ||||||
|  |                  nbt: Optional[TAG] = None, | ||||||
|  |                  tile_entity: Optional[TAG] = None, | ||||||
|  |                  x: int = 0, | ||||||
|  |                  y: int = 0, | ||||||
|  |                  z: int = 0): | ||||||
|  |         self.name: str = name | ||||||
|  |         self.nbt: Optional[TAG] = nbt | ||||||
|  |         self.tile_entity: Optional[TAG] = tile_entity | ||||||
|  |         self.x: int = x | ||||||
|  |         self.y: int = y | ||||||
|  |         self.z: int = z | ||||||
							
								
								
									
										138
									
								
								src/utils/minecraft/chunk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/utils/minecraft/chunk.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import numpy as np | ||||||
|  | from nbt.nbt import NBTFile, TAG_Compound, TAG_String | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PositionOutOfBounds(ValueError): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BlockNotFound(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ChunkBase: | ||||||
|  |     def __init__(self, x: int, z: int): | ||||||
|  |         self.x: int = x | ||||||
|  |         self.z: int = z | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Chunk(ChunkBase): | ||||||
|  |     def __init__(self, x: int, z: int, nbt: NBTFile): | ||||||
|  |         super().__init__(x, z) | ||||||
|  |  | ||||||
|  |         self.ox: int = x * 16 | ||||||
|  |         self.oy: int = nbt.get("yPos").value * 16 | ||||||
|  |         self.oz: int = z * 16 | ||||||
|  |  | ||||||
|  |         self.nbt: NBTFile = nbt | ||||||
|  |  | ||||||
|  |         self.palettes: list[list[TAG_Compound]] = [] | ||||||
|  |         self.blockstates: list[list[int]] = [] | ||||||
|  |         self.biome_palettes: list[list[TAG_String]] = [] | ||||||
|  |         self.biomes: list[list[int]] = [] | ||||||
|  |         self.get_sections() | ||||||
|  |  | ||||||
|  |     def get_sections(self) -> None: | ||||||
|  |         self.palettes = [] | ||||||
|  |         self.blockstates = [] | ||||||
|  |         self.biome_palettes = [] | ||||||
|  |         self.biomes = [] | ||||||
|  |         sections = self.nbt.get("sections") | ||||||
|  |         for s in range(1, len(sections)-1): | ||||||
|  |             section = sections[s] | ||||||
|  |             bs_tag = section.get("block_states") | ||||||
|  |             self.palettes.append(list(bs_tag.get("palette"))) | ||||||
|  |             bs = bs_tag.get("data") | ||||||
|  |             self.blockstates.append([] if bs is None else list(bs)) | ||||||
|  |             biomes_tag = section.get("biomes") | ||||||
|  |             self.biome_palettes.append(list(biomes_tag.get("palette"))) | ||||||
|  |             biomes = biomes_tag.get("data") | ||||||
|  |             self.biomes.append([] if biomes is None else list(biomes)) | ||||||
|  |  | ||||||
|  |     def get_block(self, x: int, y: int, z: int) -> Optional[tuple[str, str]]: | ||||||
|  |         if 0 <= x < 16 and 0 <= z < 16: | ||||||
|  |             section_i = y // 16 + 4 | ||||||
|  |             oy = y // 16 * 16 | ||||||
|  |             palette = self.palettes[section_i - 1] | ||||||
|  |             blockstates = self.blockstates[section_i - 1] | ||||||
|  |             biome_palette = self.biome_palettes[section_i - 1] | ||||||
|  |             biomes = self.biomes[section_i - 1] | ||||||
|  |             if blockstates is None or len(blockstates) == 0: | ||||||
|  |                 return palette[0].get("Name").value, biome_palette[0].value | ||||||
|  |  | ||||||
|  |             rel_y = y - oy | ||||||
|  |             block_bits = max((len(palette) - 1).bit_length(), 4) | ||||||
|  |             state = self.get_from_long_array(blockstates, block_bits, x, rel_y, z) | ||||||
|  |             block = palette[state] | ||||||
|  |             if len(biome_palette) == 1: | ||||||
|  |                 biome_state = 0 | ||||||
|  |             else: | ||||||
|  |                 biome_bits = (len(biome_palette) - 1).bit_length() | ||||||
|  |                 biome_state = self.get_from_long_array(biomes, biome_bits, x // 4, rel_y // 4, z // 4, 4) | ||||||
|  |             biome = biome_palette[biome_state] | ||||||
|  |             return block.get("Name").value, biome.value | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             raise PositionOutOfBounds(f"Coordinates x and z should be in range [0:16[") | ||||||
|  |  | ||||||
|  |     def get_from_long_array(self, long_array, bits, x, y, z, size=16) -> int: | ||||||
|  |         ids_per_long = 64 // bits | ||||||
|  |         block_i = y * size * size + z * size + x | ||||||
|  |         long_i = block_i // ids_per_long | ||||||
|  |         long_val = long_array[long_i] | ||||||
|  |         if long_val < 0: | ||||||
|  |             long_val += 2**64 | ||||||
|  |  | ||||||
|  |         bit_i = (block_i % ids_per_long) * bits | ||||||
|  |         value = (long_val >> bit_i) & ((1 << bits) - 1) | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def get_top_blocks(self) -> tuple[list[list[str]], np.array, np.array, bool]: | ||||||
|  |         blocks = [[] for _ in range(16)] | ||||||
|  |         heightmap_shadow = np.zeros((16, 16), dtype="uint16") | ||||||
|  |         heightmap = np.zeros((16, 16), dtype="uint16") | ||||||
|  |         biomes = [["minecraft:plains"]*16 for _ in range(16)] | ||||||
|  |         heightmap_shadow_longs = self.nbt.get("Heightmaps").get("MOTION_BLOCKING") | ||||||
|  |         heightmap_longs = self.nbt.get("Heightmaps").get("WORLD_SURFACE") | ||||||
|  |         if heightmap_longs is None: | ||||||
|  |             return [["minecraft:air"]*16 for _ in range(16)], heightmap_shadow, biomes, True | ||||||
|  |         heightmap_shadow_longs = heightmap_shadow_longs.value | ||||||
|  |         heightmap_longs = heightmap_longs.value | ||||||
|  |  | ||||||
|  |         i = 0 | ||||||
|  |         for z in range(16): | ||||||
|  |             for x in range(16): | ||||||
|  |                 # i = z * 16 + x | ||||||
|  |                 long_i = i // 7 | ||||||
|  |                 long1_val = heightmap_shadow_longs[long_i] | ||||||
|  |                 long2_val = heightmap_longs[long_i] | ||||||
|  |                 bit_i = (i % 7) * 9 | ||||||
|  |                 height_shadow = (long1_val >> bit_i) & 0b1_1111_1111 | ||||||
|  |                 height = (long2_val >> bit_i) & 0b1_1111_1111 | ||||||
|  |  | ||||||
|  |                 heightmap_shadow[z, x] = height_shadow | ||||||
|  |                 heightmap[z, x] = height | ||||||
|  |                 y = self.oy + height - 1 | ||||||
|  |                 if height == 0: | ||||||
|  |                     block = "minecraft:air" | ||||||
|  |                     biome = "minecraft:plains" | ||||||
|  |                 else: | ||||||
|  |                     block, biome = self.get_block(x, y, z) | ||||||
|  |                 blocks[z].append(block) | ||||||
|  |                 biomes[z][x] = biome | ||||||
|  |  | ||||||
|  |                 i += 1 | ||||||
|  |  | ||||||
|  |         return blocks, heightmap_shadow, biomes, False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OldChunk(ChunkBase): | ||||||
|  |     def __init__(self, x: int, z: int, data_version: int): | ||||||
|  |         super().__init__(x, z) | ||||||
|  |         self.data_version: int = data_version | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MalformedChunk(ChunkBase): | ||||||
|  |     pass | ||||||
							
								
								
									
										77
									
								
								src/utils/minecraft/region.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/utils/minecraft/region.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | import zlib | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import nbt | ||||||
|  |  | ||||||
|  | from src.utils.minecraft.chunk import Chunk, ChunkBase, MalformedChunk, OldChunk | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def to_int(bytes_): | ||||||
|  |     return int.from_bytes(bytes_, byteorder="big") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Region: | ||||||
|  |     DATA_VERSION = 3465 | ||||||
|  |  | ||||||
|  |     def __init__(self, filepath): | ||||||
|  |         self.file = open(filepath, "rb") | ||||||
|  |  | ||||||
|  |         self.locations: list[tuple[int, int]] = self.get_locations() | ||||||
|  |         self.timestamps: list[int] = self.get_timestamps() | ||||||
|  |  | ||||||
|  |         self.chunks = {} | ||||||
|  |  | ||||||
|  |     def get_locations(self) -> list[tuple[int, int]]: | ||||||
|  |         self.file.seek(0) | ||||||
|  |         locations = [] | ||||||
|  |  | ||||||
|  |         for c in range(1024): | ||||||
|  |             offset = to_int(self.file.read(3)) | ||||||
|  |             length = to_int(self.file.read(1)) | ||||||
|  |             locations.append((offset, length)) | ||||||
|  |  | ||||||
|  |         return locations | ||||||
|  |  | ||||||
|  |     def get_chunk(self, x, z) -> ChunkBase: | ||||||
|  |         if (x, z) in self.chunks.keys(): | ||||||
|  |             return self.chunks[(x, z)] | ||||||
|  |  | ||||||
|  |         loc = self.locations[(x % 32) + (z % 32) * 32] | ||||||
|  |  | ||||||
|  |         self.file.seek(loc[0] * 4096) | ||||||
|  |         length = to_int(self.file.read(4)) | ||||||
|  |         compression = to_int(self.file.read(1)) | ||||||
|  |         data = self.file.read(length-1) | ||||||
|  |  | ||||||
|  |         if compression == 2: | ||||||
|  |             try: | ||||||
|  |                 data = zlib.decompress(data) | ||||||
|  |             except zlib.error: | ||||||
|  |                 return MalformedChunk(x, z) | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             # print(f"{compression} is not a valid compression type") | ||||||
|  |             return MalformedChunk(x, z) | ||||||
|  |  | ||||||
|  |         chunk = nbt.nbt.NBTFile(buffer=nbt.chunk.BytesIO(data)) | ||||||
|  |         data_version = chunk.get("DataVersion").value | ||||||
|  |         if data_version != self.DATA_VERSION: | ||||||
|  |             print(f"\r    Invalid data version {data_version} for chunk ({x},{z})", end="") | ||||||
|  |             return OldChunk(x, z, data_version) | ||||||
|  |  | ||||||
|  |         chunk = Chunk(x, z, chunk) | ||||||
|  |         self.chunks[(x, z)] = chunk | ||||||
|  |  | ||||||
|  |         return chunk | ||||||
|  |  | ||||||
|  |     def get_timestamps(self) -> list[int]: | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |     def get_block(self, x: int, y: int, z: int) -> Optional[str]: | ||||||
|  |         chunk_x, chunk_z = x//16, z//16 | ||||||
|  |         x_rel, z_rel = x % 16, z % 16 | ||||||
|  |  | ||||||
|  |         chunk = self.get_chunk(chunk_x, chunk_z) | ||||||
|  |         if not isinstance(chunk, Chunk): | ||||||
|  |             return None | ||||||
|  |         return chunk.get_block(x_rel, y, z_rel) | ||||||
							
								
								
									
										14
									
								
								src/utils/paths.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/utils/paths.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import os.path | ||||||
|  |  | ||||||
|  | import platformdirs | ||||||
|  |  | ||||||
|  |  | ||||||
|  | APP_NAME = "lycacraft-paths" | ||||||
|  | APP_AUTHOR = "lycacraft" | ||||||
|  | CACHE_DIR = platformdirs.user_cache_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True) | ||||||
|  | CONFIG_DIR = platformdirs.user_config_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True) | ||||||
|  | ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_project_path(*elmts: str) -> str: | ||||||
|  |     return os.path.join(ROOT, *elmts) | ||||||
| @@ -1,88 +0,0 @@ | |||||||
| from math import inf |  | ||||||
| from typing import Iterator, Optional |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Node: |  | ||||||
|     def __init__(self, x: int, y: int): |  | ||||||
|         self.x: int = x |  | ||||||
|         self.y: int = y |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Edge: |  | ||||||
|     def __init__(self, start: int, end: int, length: float): |  | ||||||
|         self.length: float = length |  | ||||||
|         self.start: int = start |  | ||||||
|         self.end: int = end |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Graph: |  | ||||||
|     def __init__(self): |  | ||||||
|         self.edges: list[Edge] = [] |  | ||||||
|         self.nodes: list[Node] = [] |  | ||||||
|  |  | ||||||
|     def add_node(self, x: int, y: int) -> None: |  | ||||||
|         self.nodes.append(Node(x, y)) |  | ||||||
|  |  | ||||||
|     def add_edge(self, start_index: int, end_index: int, length: float) -> None: |  | ||||||
|         self.edges.append(Edge(start_index, end_index, length)) |  | ||||||
|  |  | ||||||
|     def edges_adjacent_to(self, node_i: int) -> Iterator[Edge]: |  | ||||||
|         return filter(lambda e: e.start == node_i or e.end == node_i, self.edges) |  | ||||||
|  |  | ||||||
|     def dijkstra(self, source_index: int, target_index: int) -> Optional[list[int]]: |  | ||||||
|         n = len(self.nodes) |  | ||||||
|  |  | ||||||
|         if source_index < 0 or source_index >= n: |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         if target_index < 0 or target_index >= n: |  | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         unvisited = list(range(n)) |  | ||||||
|  |  | ||||||
|         distances_from_start = [inf] * n |  | ||||||
|         distances_from_start[source_index] = 0 |  | ||||||
|  |  | ||||||
|         node_sequences = [[] for _ in range(n)] |  | ||||||
|         node_sequences[source_index] = [source_index] |  | ||||||
|  |  | ||||||
|         while True: |  | ||||||
|             current_index = min(unvisited, key=lambda i: distances_from_start[i]) |  | ||||||
|  |  | ||||||
|             if current_index == target_index: |  | ||||||
|                 break |  | ||||||
|  |  | ||||||
|             unvisited.remove(current_index) |  | ||||||
|  |  | ||||||
|             for edge in self.edges_adjacent_to(current_index): |  | ||||||
|                 start = current_index |  | ||||||
|                 end = edge.end if edge.start == current_index else edge.start |  | ||||||
|  |  | ||||||
|                 if end in unvisited and distances_from_start[end] > distances_from_start[start] + edge.length: |  | ||||||
|                     distances_from_start[end] = distances_from_start[start] + edge.length |  | ||||||
|                     node_sequences[end] = node_sequences[start].copy() |  | ||||||
|                     node_sequences[end].append(end) |  | ||||||
|  |  | ||||||
|         return node_sequences[target_index] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def main() -> None: |  | ||||||
|     graph = Graph() |  | ||||||
|  |  | ||||||
|     graph.add_node(1, 2) |  | ||||||
|     graph.add_node(4, 7) |  | ||||||
|     graph.add_node(3, 1) |  | ||||||
|     graph.add_node(-2, 0) |  | ||||||
|     graph.add_node(0, 0) |  | ||||||
|  |  | ||||||
|     graph.add_edge(0, 1, 1) |  | ||||||
|     graph.add_edge(1, 2, 2) |  | ||||||
|     graph.add_edge(2, 3, 3) |  | ||||||
|     graph.add_edge(3, 0, 1) |  | ||||||
|     graph.add_edge(1, 3, 3) |  | ||||||
|  |  | ||||||
|     print(graph.dijkstra(0, 3)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": |  | ||||||
|     main() |  | ||||||
		Reference in New Issue
	
	Block a user