Compare commits
	
		
			26 Commits
		
	
	
		
			3e3dbcdda8
			...
			feat/graph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6ffc11f607 | |||
| 713b7c32ca | |||
| b2904c6b85 | |||
| 9a3bbc95d9 | |||
| a4249899e9 | |||
| 9ca2ea7e1d | |||
| 6265a4e9b2 | |||
| f04a014478 | |||
| b3366e5cfe | |||
| f6b4be1485 | |||
| 029d24ce61 | |||
| ecdf3d30eb | |||
| 60416e447c | |||
| 0077a768e8 | |||
| b9bcbf829d | |||
| 691cb7da73 | |||
| efc88658ee | |||
| 5d99a0d2c5 | |||
| 258b2613d5 | |||
| efe158f83f | |||
| 77f11a7440 | |||
| df8198ef94 | |||
| c09db757bc | |||
| f53dab338a | |||
| 25f1b20a7a | |||
| 28a0ad26ce | 
							
								
								
									
										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) | ||||
							
								
								
									
										555
									
								
								src/editor.py
									
									
									
									
									
								
							
							
						
						
									
										555
									
								
								src/editor.py
									
									
									
									
									
								
							| @@ -3,102 +3,191 @@ from enum import Enum, auto | ||||
| from math import floor | ||||
| from typing import Optional | ||||
|  | ||||
| import platformdirs | ||||
| import pygame | ||||
|  | ||||
| from src.image_handler import ImageHandler | ||||
| from src.config import Config | ||||
| from src.graph.graph import Graph | ||||
| from src.image_handler import ImageHandler | ||||
| from src.utils.paths import CONFIG_DIR, CACHE_DIR | ||||
|  | ||||
|  | ||||
| class Editor: | ||||
|     APP_NAME: str = "lycacraft-paths" | ||||
|     APP_AUTHOR: str = "Lycacraft" | ||||
|     WIDTH: int = 800 | ||||
|     HEIGHT: int = 600 | ||||
|     MAP_SIZE: int = 1024 | ||||
|     MAPS_DIR: str = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps") | ||||
|     ZOOMS: tuple[float] = (0.25, 0.5, 1, 2, 4) | ||||
|     CONFIG_PATH: str = os.path.join(CONFIG_DIR, "config.json") | ||||
|     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 | ||||
|  | ||||
|     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): | ||||
|         pygame.init() | ||||
|         self.config: Config = Config(self.CONFIG_PATH) | ||||
|         self.width: int = self.WIDTH | ||||
|         self.height: int = self.HEIGHT | ||||
|         self.win: pygame.Surface = pygame.display.set_mode([self.width, self.height], pygame.RESIZABLE) | ||||
|         pygame.display.set_caption("Lycacraft Map Editor") | ||||
|         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.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.left_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.loading_font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 30) | ||||
|         self.zooms_texts: list[pygame.Surface] = list(map( | ||||
|             lambda z: self.font.render(str(z), True, (255, 255, 255)), | ||||
|             self.ZOOMS | ||||
|         )) | ||||
|         self.is_creating_node: bool = False | ||||
|         self.typing: bool = False | ||||
|         self.zooms_texts: list[pygame.Surface] = [] | ||||
|         for zoom in 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.graph = Graph() | ||||
|         self.typing_text: str = "" | ||||
|         self.node_candidate_pos: tuple[int, int] = None | ||||
|         self.node_radius: int = 10 | ||||
|         self.node_radius: int = 5 | ||||
|         self.line_size: int = 3 | ||||
|         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: | ||||
|         self.state = State.LOADING | ||||
|         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() | ||||
|             if self.state == State.LOADING: | ||||
|                 self.render_loading() | ||||
|                 if not self.image_handler.loading: | ||||
|                     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.image_handler.clean() | ||||
|             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: | ||||
|         events = pygame.event.get() | ||||
|  | ||||
|         keys = pygame.key.get_pressed() | ||||
|         for event in events: | ||||
|             if event.type == pygame.QUIT: | ||||
|                 self.state = State.STOPPING | ||||
|                 self.quit() | ||||
|             elif event.type == pygame.WINDOWRESIZED: | ||||
|                 self.width = event.x | ||||
|                 self.height = event.y | ||||
|             elif event.type == pygame.KEYDOWN: | ||||
|                 if self.typing: | ||||
|                 if self.is_renaming_node: | ||||
|                     if event.key == pygame.K_ESCAPE: | ||||
|                         self.typing = False | ||||
|                         self.is_creating_node = False | ||||
|                         self.node_candidate_pos = None | ||||
|                         self.is_renaming_node = False | ||||
|                         self.typing_text = "" | ||||
|                     elif event.key == pygame.K_RETURN: | ||||
|                         self.create_node() | ||||
|                         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: | ||||
|                         self.state = State.STOPPING | ||||
|                         if self.selected_nodes != [] or self.selected_edges != []: | ||||
|                             self.clear_selection() | ||||
|                             self.previously_created_nodes = [] | ||||
|                         else: | ||||
|                             self.quit() | ||||
|                     elif event.key == pygame.K_PAGEUP: | ||||
|                         self.zoom_in() | ||||
|                     elif event.key == pygame.K_PAGEDOWN: | ||||
|                         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: | ||||
|                 if event.button == 2: | ||||
|                     self.mid_drag_pos = event.pos | ||||
|                 elif event.button == 1: | ||||
|                     if keys[pygame.K_LCTRL]: | ||||
|                         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.node_candidate_pos = self.screen_to_world(event.pos[0], event.pos[1]) | ||||
|                         self.is_creating_node = True | ||||
|                         self.typing = True | ||||
|                         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: | ||||
|                     self.zoom_in() | ||||
|                 elif event.button == 5: | ||||
| @@ -107,7 +196,15 @@ class Editor: | ||||
|                 if event.button == 2: | ||||
|                     self.mid_drag_pos = None | ||||
|                 elif event.button == 1: | ||||
|                     if keys[pygame.K_LCTRL]: | ||||
|                         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]: | ||||
|             self.center[0] -= 4 / self.zoom | ||||
| @@ -131,8 +228,13 @@ class Editor: | ||||
|             if mbtns[1]: | ||||
|                 self.mid_drag_pos = mpos | ||||
|  | ||||
|  | ||||
|     # ========================= | ||||
|     # RENDERING | ||||
|     # ========================= | ||||
|  | ||||
|     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_y = (self.center[1] * self.zoom) % self.MAP_SIZE | ||||
|  | ||||
| @@ -168,7 +270,9 @@ class Editor: | ||||
|                     oy + y * self.MAP_SIZE | ||||
|                 ]) | ||||
|  | ||||
|         self.render_nodes() | ||||
|         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, h2 - self.CROSSHAIR_SIZE], [w2, h2 + self.CROSSHAIR_SIZE]) | ||||
| @@ -180,19 +284,19 @@ class Editor: | ||||
|         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]) | ||||
|  | ||||
|         if self.is_creating_node: | ||||
|             self.render_node_creator() | ||||
|         if self.is_renaming_node: | ||||
|             self.render_node_renamer() | ||||
|  | ||||
|         pygame.display.flip() | ||||
|  | ||||
|     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_v_margin = self.height * 0.05 | ||||
|         zoom_x = self.width - zoom_h_margin | ||||
|         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 | ||||
|         pygame.draw.rect(self.win, (80, 80, 80), [ | ||||
|             zoom_x + zoom_r - zoom_width - 5, | ||||
| @@ -209,6 +313,7 @@ class Editor: | ||||
|  | ||||
|     def render_loading(self) -> None: | ||||
|         self.win.fill((0, 0, 0)) | ||||
|         self.win.blit(self.loading_bg, [0, 0]) | ||||
|         count = self.image_handler.count | ||||
|         total = self.image_handler.total | ||||
|         txt = self.loading_font.render(f"Loading maps - {count}/{total}", True, (255, 255, 255)) | ||||
| @@ -226,7 +331,7 @@ class Editor: | ||||
|  | ||||
|         pygame.display.flip() | ||||
|  | ||||
|     def render_node_creator(self) -> None: | ||||
|     def render_node_renamer(self) -> None: | ||||
|         width = self.width / 2 | ||||
|         height = self.height / 2 | ||||
|         x0 = (self.width - width) / 2 | ||||
| @@ -234,8 +339,8 @@ class Editor: | ||||
|  | ||||
|         line_height = height / 6 | ||||
|  | ||||
|         nc_txt = self.loading_font.render("NODE CREATOR", True, (255, 255, 255)) | ||||
|         name_txt = self.loading_font.render("Name:", True, (255, 255, 255)) | ||||
|         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]) | ||||
| @@ -243,21 +348,57 @@ class Editor: | ||||
|         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) | ||||
|             pygame.draw.circle(self.win, (255, 0, 0), (blitpos[0], blitpos[1]), self.node_radius) | ||||
|         self.render_mouse_hover_node_text() | ||||
|             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_mouse_hover_node_text(self): | ||||
|         mouse_pos = pygame.mouse.get_pos() | ||||
|  | ||||
|         for node in self.graph.nodes: | ||||
|     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) | ||||
|             hovering = ((mouse_pos[0] - node_pos[0]) ** 2 + (mouse_pos[1] - node_pos[1]) ** 2) < self.node_radius ** 2 | ||||
|             if (hovering): | ||||
|                 txt = self.loading_font.render(node.name, True, (0, 0, 0)) | ||||
|                 self.win.blit(txt, [node_pos[0] - txt.get_width(), node_pos[1] - txt.get_height()]) | ||||
|         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: | ||||
|         self.zoom_i = max(0, min(len(self.ZOOMS) - 1, zoom_i)) | ||||
| @@ -269,6 +410,259 @@ class Editor: | ||||
|     def zoom_out(self) -> None: | ||||
|         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]: | ||||
|         w2 = self.width / 2 | ||||
|         h2 = self.height / 2 | ||||
| @@ -285,18 +679,75 @@ class Editor: | ||||
|  | ||||
|         return int(screen_x), int(screen_y) | ||||
|  | ||||
|     def create_node(self) -> None: | ||||
|         self.graph.add_node(self.node_candidate_pos[0], self.node_candidate_pos[1], self.typing_text) | ||||
|         self.typing_text = "" | ||||
|         self.node_candidate_pos = None | ||||
|         self.typing = False | ||||
|         self.is_creating_node = False | ||||
|     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): | ||||
|     STOPPING = auto() | ||||
|     LOADING = auto() | ||||
|     RUNNING = auto() | ||||
|     CREATING_NODE = auto() | ||||
							
								
								
									
										0
									
								
								src/graph/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/graph/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,5 +1,8 @@ | ||||
| class Edge: | ||||
|     def __init__(self, start: int, end: int, length: float): | ||||
|  | ||||
|     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 | ||||
|   | ||||
| @@ -1,23 +1,99 @@ | ||||
| from math import inf | ||||
| 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, y: int, name: str) -> None: | ||||
|         self.nodes.append(Node(x, y, name)) | ||||
|     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, length: float) -> None: | ||||
|         self.edges.append(Edge(start_index, end_index, length)) | ||||
|     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) | ||||
|  | ||||
| @@ -53,3 +129,33 @@ class Graph: | ||||
|                     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 | ||||
|   | ||||
| @@ -1,5 +1,18 @@ | ||||
| class Node: | ||||
|     def __init__(self, x: int, z: int, name: str): | ||||
|     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 threading | ||||
| import time | ||||
| from math import floor | ||||
| from typing import Optional | ||||
|  | ||||
| @@ -7,10 +8,13 @@ import pygame | ||||
|  | ||||
|  | ||||
| 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.base_size: int = base_size | ||||
|         self.ttl: int = ttl | ||||
|         self.cache: dict = {} | ||||
|         self.history: dict[tuple[float, tuple[int, int]], float] = {} | ||||
|         self.size: int = 0 | ||||
|         self.count: int = 0 | ||||
|         self.total: int = 0 | ||||
|         self.loading: bool = False | ||||
| @@ -29,6 +33,7 @@ class ImageHandler: | ||||
|             name, x, y = path.split(".")[0].split("_") | ||||
|             cache[(int(x), int(y))] = pygame.image.load(fullpath).convert_alpha() | ||||
|             self.count += 1 | ||||
|             self.size += 1 | ||||
|  | ||||
|         self.cache = { | ||||
|             1: cache | ||||
| @@ -65,6 +70,19 @@ class ImageHandler: | ||||
|                     img = pygame.transform.scale_by(img, zoom) | ||||
|  | ||||
|             cache[pos] = img | ||||
|             self.size += 1 | ||||
|  | ||||
|         self.cache[zoom] = cache | ||||
|         self.history[(zoom, pos)] = time.time() | ||||
|         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 | ||||
| DEFAULT_PATH = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps") | ||||
| 
 | ||||
| 
 | ||||
| def clamp(mn, value, mx): | ||||
|     return max(mn, min(mx, value)) | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|     input_path = input("Input image: ") | ||||
|     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) | ||||
		Reference in New Issue
	
	Block a user