added basic auto mapper
This commit is contained in:
		
							
								
								
									
										134
									
								
								src/utils/auto_mapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/utils/auto_mapper.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | |||||||
|  | import json | ||||||
|  | import os | ||||||
|  | import tempfile | ||||||
|  |  | ||||||
|  | import platformdirs | ||||||
|  | from ftplib import FTP | ||||||
|  |  | ||||||
|  | import pygame | ||||||
|  |  | ||||||
|  | from src.utils.minecraft.chunk import Chunk | ||||||
|  | from src.utils.minecraft.region import Region | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AutoMapper: | ||||||
|  |     APP_NAME: str = "lycacraft-paths" | ||||||
|  |     APP_AUTHOR: str = "Lycacraft" | ||||||
|  |     CONFIG_DIR = platformdirs.user_config_dir(appname=APP_NAME, appauthor=APP_AUTHOR) | ||||||
|  |     CONFIG_PATH = os.path.join(CONFIG_DIR, "mapper.json") | ||||||
|  |     CACHE_DIR = platformdirs.user_cache_dir(appname=APP_NAME, appauthor=APP_AUTHOR) | ||||||
|  |     CACHE_PATH = os.path.join(CACHE_DIR, "regions.json") | ||||||
|  |     COLORS_PATH = os.path.join(CACHE_DIR, "colors.json") | ||||||
|  |  | ||||||
|  |     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(prefix="regions") | ||||||
|  |         self.colors: dict[str, tuple[float, float, float, float]] = {} | ||||||
|  |         self.load_colors() | ||||||
|  |  | ||||||
|  |     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 list_available_regions(self) -> list[tuple[int, int]]: | ||||||
|  |         files = self.ftp.nlst() | ||||||
|  |         regions = [] | ||||||
|  |         for f in files: | ||||||
|  |             _, x, z, _ = f.split(".") | ||||||
|  |             regions.append((int(x), int(z))) | ||||||
|  |         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) -> pygame.Surface: | ||||||
|  |         print(f"[Fetching region ({rx},{rz})]") | ||||||
|  |         path = self.fetch_region(rx, rz) | ||||||
|  |         region = Region(path) | ||||||
|  |         surf = pygame.Surface([512, 512], pygame.SRCALPHA) | ||||||
|  |  | ||||||
|  |         for cz in range(32): | ||||||
|  |             for cx in range(32): | ||||||
|  |                 chunk = region.get_chunk(rx * 32 + cx, rz * 32 + cz) | ||||||
|  |                 if chunk is not None: | ||||||
|  |                     self.render_chunk(chunk, surf, cx * 16, cz * 16) | ||||||
|  |  | ||||||
|  |         return surf | ||||||
|  |  | ||||||
|  |     def map_region_group(self, x: int, z: int) -> pygame.Surface: | ||||||
|  |         surf = pygame.Surface([1024, 1024], pygame.SRCALPHA) | ||||||
|  |         for dz in range(2): | ||||||
|  |             for dx in range(2): | ||||||
|  |                 region = self.map_region(x * 2 + dx, z * 2 + dz) | ||||||
|  |                 surf.blit(region, [dx * 512, dz * 512]) | ||||||
|  |         return surf | ||||||
|  |  | ||||||
|  |     def render_chunk(self, chunk: Chunk, surf: pygame.Surface, ox: int, oy: int): | ||||||
|  |         #blocks, hmap_surf = chunk.get_top_blocks() | ||||||
|  |         blocks = chunk.get_top_blocks() | ||||||
|  |         # surf.blit(hmap_surf, [ox, oy]) | ||||||
|  |         # return | ||||||
|  |         for z in range(16): | ||||||
|  |             for x in range(16): | ||||||
|  |                 color = self.get_color(blocks[z][x]) | ||||||
|  |                 surf.set_at((ox + x, oy + z), color) | ||||||
|  |  | ||||||
|  |     def get_color(self, block: str) -> tuple[float, float, float]: | ||||||
|  |         return self.colors.get(block, (0, 0, 0)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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__': | ||||||
|  |     from math import floor | ||||||
|  |     pygame.init() | ||||||
|  |     x = -1 | ||||||
|  |     z = 0 | ||||||
|  |     with AutoMapper() as mapper: | ||||||
|  |         # print(mapper.list_available_regions()) | ||||||
|  |         surf = mapper.map_region_group(x, z) | ||||||
|  |         pygame.image.save(surf, f"/tmp/map_{x}_{z}.png") | ||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										107
									
								
								src/utils/minecraft/chunk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/utils/minecraft/chunk.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import numpy as np | ||||||
|  | from nbt.nbt import NBTFile, TAG_Compound | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PositionOutOfBounds(ValueError): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BlockNotFound(Exception): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Chunk: | ||||||
|  |     def __init__(self, x: int, z: int, nbt: NBTFile): | ||||||
|  |         self.x: int = x | ||||||
|  |         self.z: int = 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[str]] = [] | ||||||
|  |         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[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] | ||||||
|  |             if blockstates is None: | ||||||
|  |                 return palette[0].get("Name").value | ||||||
|  |  | ||||||
|  |             bits = max((len(palette) - 1).bit_length(), 4) | ||||||
|  |             ids_per_long = 64 // bits | ||||||
|  |  | ||||||
|  |             rel_y = y - oy | ||||||
|  |             block_i = rel_y * 256 + z * 16 + x | ||||||
|  |             long_i = block_i // ids_per_long | ||||||
|  |             long_val = blockstates[long_i] | ||||||
|  |  | ||||||
|  |             if long_val < 0: | ||||||
|  |                 long_val += 2**64 | ||||||
|  |  | ||||||
|  |             bit_i = (block_i % ids_per_long) * bits | ||||||
|  |             state = (long_val >> bit_i) & ((1 << bits) - 1) | ||||||
|  |             block = palette[state] | ||||||
|  |             return block.get("Name").value | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             raise PositionOutOfBounds(f"Coordinates x and z should be in range [0:16[") | ||||||
|  |  | ||||||
|  |     def get_top_blocks(self) -> list[list[str]]: | ||||||
|  |         blocks = [[] for _ in range(16)] | ||||||
|  |         heightmap_longs = self.nbt.get("Heightmaps").get("MOTION_BLOCKING") | ||||||
|  |         if heightmap_longs is None: | ||||||
|  |             return [["minecraft:air"]*16 for _ in range(16)] | ||||||
|  |         heightmap_longs = heightmap_longs.value | ||||||
|  |         # heightmap = [[0]*16 for _ in range(16)] | ||||||
|  |         # hmap_surf = pygame.Surface((16, 16)) | ||||||
|  |  | ||||||
|  |         i = 0 | ||||||
|  |         for z in range(16): | ||||||
|  |             for x in range(16): | ||||||
|  |                 # i = z * 16 + x | ||||||
|  |                 long_i = i // 7 | ||||||
|  |                 long_val = heightmap_longs[long_i] | ||||||
|  |                 bit_i = (i % 7) * 9 | ||||||
|  |                 height = (long_val >> bit_i) & 0b1_1111_1111 | ||||||
|  |  | ||||||
|  |                 # heightmap[z][x] = height | ||||||
|  |                 # col = 255 * height / 384 | ||||||
|  |                 # hmap_surf.set_at((x, z), (col, col, col)) | ||||||
|  |                 y = self.oy + height - 1 | ||||||
|  |                 if y < 0: | ||||||
|  |                     block = "minecraft:air" | ||||||
|  |                 else: | ||||||
|  |                     block = self.get_block(x, y, z) | ||||||
|  |                 blocks[z].append(block) | ||||||
|  |  | ||||||
|  |                 i += 1 | ||||||
|  |  | ||||||
|  |         return blocks | ||||||
							
								
								
									
										67
									
								
								src/utils/minecraft/region.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/utils/minecraft/region.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | import zlib | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import nbt | ||||||
|  |  | ||||||
|  | from src.utils.minecraft.block import Block | ||||||
|  | from src.utils.minecraft.chunk import Chunk | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def to_int(bytes_): | ||||||
|  |     return int.from_bytes(bytes_, byteorder="big") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Region: | ||||||
|  |     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) -> Optional[Chunk]: | ||||||
|  |         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: | ||||||
|  |             data = zlib.decompress(data) | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             print(f"{compression} is not a valid compression type") | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         chunk = nbt.nbt.NBTFile(buffer=nbt.chunk.BytesIO(data)) | ||||||
|  |         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) -> Block: | ||||||
|  |         chunk_x, chunk_z = x//16, z//16 | ||||||
|  |         x_rel, z_rel = x % 16, z % 16 | ||||||
|  |  | ||||||
|  |         chunk = self.get_chunk(chunk_x, chunk_z) | ||||||
|  |         return chunk.get_block(x_rel, y, z_rel) | ||||||
		Reference in New Issue
	
	Block a user