Compare commits
	
		
			9 Commits
		
	
	
		
			6265a4e9b2
			...
			feat/graph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6ffc11f607 | |||
| 713b7c32ca | |||
| b2904c6b85 | |||
| 9a3bbc95d9 | |||
| a4249899e9 | |||
| 9ca2ea7e1d | |||
| f6b4be1485 | |||
| 029d24ce61 | |||
| ecdf3d30eb | 
							
								
								
									
										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] | ||||
| } | ||||
| @@ -5,6 +5,7 @@ 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 | ||||
|   | ||||
| @@ -3,12 +3,12 @@ from enum import Enum, auto | ||||
| from math import floor | ||||
| from typing import Optional | ||||
|  | ||||
| import platformdirs | ||||
| import pygame | ||||
|  | ||||
| from src.config import Config | ||||
| from src.image_handler import ImageHandler | ||||
| from src.graph.graph import Graph | ||||
| from src.image_handler import ImageHandler | ||||
| from src.utils.paths import CONFIG_DIR, CACHE_DIR | ||||
|  | ||||
|  | ||||
| class Editor: | ||||
| @@ -17,13 +17,11 @@ class Editor: | ||||
|     WIDTH: int = 800 | ||||
|     HEIGHT: int = 600 | ||||
|     MAP_SIZE: int = 1024 | ||||
|     CACHE_DIR: str = platformdirs.user_cache_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True) | ||||
|     CONFIG_DIR: str = platformdirs.user_config_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True) | ||||
|     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] = (0.25, 0.5, 1, 2, 4) | ||||
|     ZOOMS: tuple[float] = tuple(2**p for p in range(-6, 7)) | ||||
|     CROSSHAIR_SIZE: int = 10 | ||||
|  | ||||
|     EDGE_TYPE_KEYS: dict[int, str] = { | ||||
| @@ -50,25 +48,27 @@ class Editor: | ||||
|         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.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_radius: int = 10 | ||||
|         self.line_size: int = int(self.node_radius / 5) | ||||
|         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] = [] | ||||
| @@ -77,6 +77,7 @@ class Editor: | ||||
|         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): | ||||
| @@ -93,7 +94,7 @@ class Editor: | ||||
|     def mainloop(self) -> None: | ||||
|         self.state = State.LOADING | ||||
|         while self.state != State.STOPPING: | ||||
|             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) | ||||
| @@ -108,6 +109,7 @@ class Editor: | ||||
|                 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: | ||||
| @@ -159,6 +161,10 @@ class Editor: | ||||
|                             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: | ||||
| @@ -228,7 +234,7 @@ class Editor: | ||||
|     # ========================= | ||||
|  | ||||
|     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 | ||||
|  | ||||
| @@ -284,13 +290,13 @@ class Editor: | ||||
|         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, | ||||
| @@ -307,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)) | ||||
| @@ -726,12 +733,19 @@ class Editor: | ||||
|             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() | ||||
|   | ||||
							
								
								
									
										0
									
								
								src/graph/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/graph/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| 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() | ||||
| @@ -14,7 +14,7 @@ def clamp(mn, value, mx): | ||||
| 
 | ||||
| 
 | ||||
| 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) | ||||
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user