# Copyright (c) 2026 Dominic Masters # # This software is released under the MIT License. # https://opensource.org/licenses/MIT """ Generates DCF + companion DMF files from raw chunk JSON files, or upgrades legacy DCF files (version 1 or 2) to version 3. JSON input (assetsraw/chunks/chunk_X_Y_Z.json): { "tiles": [ { "pos": [x, y, z], "type": , "tile": } ], "meshes": [ { "file": "house_5_3.dmf", "pos": [x, y, z] } ] } Tiles absent from the array default to TILE_SHAPE_NULL. Mesh files are located by searching under assets/meshes/ and referenced by path from the assets root in the DCF. Output DCF is derived automatically: assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf Version 3 DCF format (after 8-byte header + tiles): uint8_t meshCount for each mesh: null-terminated string (relative asset path to .dmf) float32[3] (x, y, z offset) DMF format: Bytes 0-3: DMF\\x00 Bytes 4-7: uint32_t version = 1 (little-endian) Bytes 8-11: uint32_t vertCount (little-endian) Bytes 12+: meshvertex_t vertices[vertCount] Each vertex: uv[2] + pos[3] = 5 x float32 LE = 20 bytes Usage: python3 -m tools.asset.chunk # process all assetsraw/chunks/*.json python3 -m tools.asset.chunk # process one JSON file python3 -m tools.asset.chunk # upgrade legacy DCF in-place """ import json import os import struct import sys _HERE = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.normpath(os.path.join(_HERE, '..', '..', '..')) ASSETSRAW_DIR = os.path.join(PROJECT_ROOT, 'assetsraw') ASSETS_DIR = os.path.join(PROJECT_ROOT, 'assets') CHUNK_WIDTH = 16 CHUNK_HEIGHT = 16 CHUNK_DEPTH = 32 CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 8192 CHUNK_MESH_COUNT_MAX = 10 CHUNK_MESH_NAME_MAX = 64 TILE_SIZE = 4 VERTEX_SIZE = 20 TILE_SHAPE_NULL = 0 TILE_SHAPE_GROUND = 1 TILE_SHAPE_RAMP_NORTH = 2 TILE_SHAPE_RAMP_EAST = 3 TILE_SHAPE_RAMP_SOUTH = 4 TILE_SHAPE_RAMP_WEST = 5 TILE_SHAPE_RAMP_NORTHEAST = 6 TILE_SHAPE_RAMP_NORTHWEST = 7 TILE_SHAPE_RAMP_SOUTHEAST = 8 TILE_SHAPE_RAMP_SOUTHWEST = 9 FILE_MAGIC = b'DCF' DMF_MAGIC = b'DMF\x00' VERSION_OUT = 3 DMF_VERSION = 1 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def tile_index(x, y, z): return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT def find_mesh(filename): """Search ASSETS_DIR/meshes/ recursively for filename. Returns the path relative to ASSETS_DIR (forward slashes), or None.""" meshes_root = os.path.join(ASSETS_DIR, 'meshes') for dirpath, _, filenames in os.walk(meshes_root): if filename in filenames: rel = os.path.relpath( os.path.join(dirpath, filename), ASSETS_DIR ) return rel.replace('\\', '/') return None def derive_dcf_path(json_path): """assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf""" base = os.path.splitext(os.path.basename(json_path))[0] if base.startswith('chunk_'): base = base[len('chunk_'):] rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR) return os.path.join(ASSETS_DIR, rel_dir, base + '.dcf') def write_dmf(path, vertex_bytes): vert_count = len(vertex_bytes) // VERTEX_SIZE buf = bytearray() buf += DMF_MAGIC buf += struct.pack('= CHUNK_MESH_NAME_MAX: raise ValueError( f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}" ) buf += encoded + b'\x00' buf += struct.pack('<3f', offset[0], offset[1], offset[2]) with open(dcf_path, 'wb') as f: f.write(buf) print( f' Wrote DCF {dcf_path}: ' f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes' ) # --------------------------------------------------------------------------- # JSON -> DCF + DMF # --------------------------------------------------------------------------- def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z): """Six vertices (2 CCW triangles) for a quad with per-corner Z heights. Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1). NORTH=+Y, EAST=+X (matches entityDirGetRelative).""" verts = [ (u0, v0, fx, fy, sw_z), (u1, v0, fx+1, fy, se_z), (u1, v1, fx+1, fy+1, ne_z), (u0, v0, fx, fy, sw_z), (u1, v1, fx+1, fy+1, ne_z), (u0, v1, fx, fy+1, nw_z), ] buf = bytearray() for v in verts: buf += struct.pack('<5f', *v) return bytes(buf) # Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z. # NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1. # Diagonal ramps: opposite corner at 0/1, adjacent corners at 0.5. _RAMP_CORNERS = { TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0), TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high TILE_SHAPE_RAMP_SOUTH: (1.0, 1.0, 0.0, 0.0), # south=high, north=low TILE_SHAPE_RAMP_EAST: (0.0, 1.0, 1.0, 0.0), # west=low, east=high TILE_SHAPE_RAMP_WEST: (1.0, 0.0, 0.0, 1.0), # west=high, east=low TILE_SHAPE_RAMP_NORTHEAST: (0.0, 0.0, 1.0, 0.0), # only NE raised TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised } def build_terrain_verts(tiles_bytes): """Generate quads for every non-null tile, shaped to match tile type.""" buf = bytearray() for z in range(CHUNK_DEPTH): for y in range(CHUNK_HEIGHT): for x in range(CHUNK_WIDTH): idx = tile_index(x, y, z) tile_type = struct.unpack_from( ' {rel}') write_v3(dcf_path, bytes(tiles), mesh_names, mesh_offsets) def process_json(json_path): dcf_path = derive_dcf_path(json_path) os.makedirs(os.path.dirname(dcf_path), exist_ok=True) print(f"{json_path} -> {dcf_path}") from_json(json_path, dcf_path) # --------------------------------------------------------------------------- # Legacy DCF (v1/v2) -> v3 # --------------------------------------------------------------------------- def read_legacy_dcf(path): with open(path, 'rb') as f: data = f.read() if data[:3] != FILE_MAGIC: raise ValueError(f"{path}: not a DCF file") version = struct.unpack_from(' 0: meshes.append(verts) else: mesh_count = data[offset] offset += 1 for _ in range(mesh_count): vert_count = struct.unpack_from(' 0: meshes.append(verts) return tiles, meshes def upgrade_dcf(path): print(f"Upgrading legacy DCF {path} ...") tiles, meshes = read_legacy_dcf(path) print( f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, " f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}" ) base = os.path.splitext(os.path.basename(path))[0] terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks') os.makedirs(terrain_dir, exist_ok=True) mesh_names = [] for idx, verts in enumerate(meshes): dmf_name = f'chunk_{base}_{idx}.dmf' write_dmf(os.path.join(terrain_dir, dmf_name), verts) mesh_names.append(f'meshes/chunks/{dmf_name}') write_v3(path, tiles, mesh_names) # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- def main(): args = sys.argv[1:] if not args: chunks_dir = os.path.join(ASSETSRAW_DIR, 'chunks') if not os.path.isdir(chunks_dir): print(f"No directory found: {chunks_dir}") sys.exit(1) json_files = sorted( os.path.join(chunks_dir, f) for f in os.listdir(chunks_dir) if f.endswith('.json') ) if not json_files: print(f"No JSON files found in {chunks_dir}") sys.exit(0) for p in json_files: process_json(p) return src = args[0] if os.path.splitext(src)[1].lower() == '.json': process_json(src) else: upgrade_dcf(src) if __name__ == '__main__': main()