added basic auto mapper
This commit is contained in:
parent
029d24ce61
commit
f6b4be1485
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)
|
Loading…
Reference in New Issue
Block a user