From f6b4be14850738e25272b994b318d00211445d4b Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Wed, 3 Jul 2024 01:55:20 +0200 Subject: [PATCH] added basic auto mapper --- src/utils/auto_mapper.py | 134 ++++++++++++++++++++++++++++++++ src/utils/minecraft/__init__.py | 0 src/utils/minecraft/block.py | 19 +++++ src/utils/minecraft/chunk.py | 107 +++++++++++++++++++++++++ src/utils/minecraft/region.py | 67 ++++++++++++++++ 5 files changed, 327 insertions(+) create mode 100644 src/utils/auto_mapper.py create mode 100644 src/utils/minecraft/__init__.py create mode 100644 src/utils/minecraft/block.py create mode 100644 src/utils/minecraft/chunk.py create mode 100644 src/utils/minecraft/region.py diff --git a/src/utils/auto_mapper.py b/src/utils/auto_mapper.py new file mode 100644 index 0000000..50b041c --- /dev/null +++ b/src/utils/auto_mapper.py @@ -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") diff --git a/src/utils/minecraft/__init__.py b/src/utils/minecraft/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/minecraft/block.py b/src/utils/minecraft/block.py new file mode 100644 index 0000000..17862d9 --- /dev/null +++ b/src/utils/minecraft/block.py @@ -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 diff --git a/src/utils/minecraft/chunk.py b/src/utils/minecraft/chunk.py new file mode 100644 index 0000000..a131df1 --- /dev/null +++ b/src/utils/minecraft/chunk.py @@ -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 diff --git a/src/utils/minecraft/region.py b/src/utils/minecraft/region.py new file mode 100644 index 0000000..68ccbaa --- /dev/null +++ b/src/utils/minecraft/region.py @@ -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)