example building
This commit is contained in:
+229
-107
@@ -4,39 +4,54 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
"""
|
||||
Converts DCF chunk files (version 1 or 2) to version 3 and generates the
|
||||
companion DMF (Dusk Mesh Format) files that version 3 references.
|
||||
Generates DCF + companion DMF files from raw chunk JSON files, or upgrades
|
||||
legacy DCF files (version 1 or 2) to version 3.
|
||||
|
||||
Version 1 format (after 8-byte header + tiles):
|
||||
uint32_t vertCount
|
||||
meshvertex_t vertices[vertCount]
|
||||
JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
||||
{
|
||||
"tiles": [
|
||||
{ "pos": [x, y, z], "type": <tile_shape_int>, "tile": <uv_tile_int> }
|
||||
],
|
||||
"meshes": [
|
||||
{ "file": "house_5_3.dmf", "pos": [x, y, z] }
|
||||
]
|
||||
}
|
||||
|
||||
Version 2 format (after 8-byte header + tiles):
|
||||
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:
|
||||
uint32_t vertCount
|
||||
meshvertex_t vertices[vertCount]
|
||||
|
||||
Version 3 format (after 8-byte header + tiles):
|
||||
uint8_t meshCount
|
||||
for each mesh:
|
||||
null-terminated string (path to companion .dmf asset)
|
||||
null-terminated string (relative asset path to .dmf)
|
||||
float32[3] (x, y, z offset)
|
||||
|
||||
DMF format:
|
||||
Bytes 0-3: DMF\x00
|
||||
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 <input.dcf> [output.dcf]
|
||||
If output is omitted the input file is updated in place.
|
||||
DMF files are written to assets/meshes/ beside the chunks/ directory.
|
||||
python3 -m tools.asset.chunk # process all assetsraw/chunks/*.json
|
||||
python3 -m tools.asset.chunk <file.json> # process one JSON file
|
||||
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy DCF in-place
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
|
||||
_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
|
||||
@@ -44,62 +59,48 @@ CHUNK_DEPTH = 32
|
||||
CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 8192
|
||||
|
||||
CHUNK_MESH_COUNT_MAX = 10
|
||||
CHUNK_VERTEX_COUNT = 8192
|
||||
CHUNK_MESH_NAME_MAX = 64
|
||||
|
||||
TILE_SIZE = 4
|
||||
VERTEX_SIZE = 20
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 3
|
||||
DMF_VERSION = 1
|
||||
TILE_SHAPE_NULL = 0
|
||||
TILE_SHAPE_GROUND = 1
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 3
|
||||
DMF_VERSION = 1
|
||||
|
||||
|
||||
def read_dcf(path):
|
||||
"""Read a v1 or v2 DCF file. Returns (tiles, meshes) where meshes is a
|
||||
list of raw vertex byte strings, one per mesh."""
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if data[:3] != FILE_MAGIC:
|
||||
raise ValueError(f"{path}: not a DCF file")
|
||||
def tile_index(x, y, z):
|
||||
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
||||
|
||||
version = struct.unpack_from('<I', data, 4)[0]
|
||||
if version not in (1, 2):
|
||||
raise ValueError(
|
||||
f"{path}: expected version 1 or 2, got {version}"
|
||||
)
|
||||
|
||||
offset = 8
|
||||
tiles_size = CHUNK_TILE_COUNT * TILE_SIZE
|
||||
tiles = data[offset:offset + tiles_size]
|
||||
offset += tiles_size
|
||||
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
|
||||
|
||||
meshes = []
|
||||
|
||||
if version == 1:
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
else:
|
||||
mesh_count = data[offset]
|
||||
offset += 1
|
||||
for _ in range(mesh_count):
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
offset += vert_count * VERTEX_SIZE
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
|
||||
return tiles, meshes
|
||||
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):
|
||||
@@ -111,18 +112,11 @@ def write_dmf(path, vertex_bytes):
|
||||
buf += vertex_bytes
|
||||
with open(path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DMF {path}: '
|
||||
f'{vert_count} vertices, {len(buf)} bytes'
|
||||
)
|
||||
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
||||
|
||||
|
||||
def write_v3(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
||||
"""Write a v3 DCF that references the given DMF asset paths.
|
||||
|
||||
mesh_offsets is an optional list of (x, y, z) tuples, one per mesh.
|
||||
Defaults to (0, 0, 0) for each mesh when omitted.
|
||||
"""
|
||||
"""Write a v3 DCF referencing the given DMF asset paths."""
|
||||
mesh_count = len(mesh_names)
|
||||
if mesh_offsets is None:
|
||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||
@@ -145,51 +139,179 @@ def write_v3(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DCF {dcf_path}: '
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es)'
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON -> DCF + DMF
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_terrain_verts(tiles_bytes):
|
||||
"""Generate top-face quads for every TILE_SHAPE_GROUND tile."""
|
||||
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(
|
||||
'<I', tiles_bytes, idx * TILE_SIZE
|
||||
)[0]
|
||||
if tile_type != TILE_SHAPE_GROUND:
|
||||
continue
|
||||
fx, fy, fz = float(x), float(y), float(z)
|
||||
uv_u0 = x / CHUNK_WIDTH
|
||||
uv_u1 = (x + 1) / CHUNK_WIDTH
|
||||
uv_v0 = y / CHUNK_HEIGHT
|
||||
uv_v1 = (y + 1) / CHUNK_HEIGHT
|
||||
for v in [
|
||||
(uv_u0, uv_v0, fx, fy, fz),
|
||||
(uv_u1, uv_v0, fx+1, fy, fz),
|
||||
(uv_u1, uv_v1, fx+1, fy+1, fz),
|
||||
(uv_u0, uv_v0, fx, fy, fz),
|
||||
(uv_u1, uv_v1, fx+1, fy+1, fz),
|
||||
(uv_u0, uv_v1, fx, fy+1, fz),
|
||||
]:
|
||||
buf += struct.pack('<5f', *v)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def from_json(json_path, dcf_path):
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
||||
for tile in data.get('tiles', []):
|
||||
pos = tile['pos']
|
||||
x, y, z = int(pos[0]), int(pos[1]), int(pos[2])
|
||||
struct.pack_into(
|
||||
'<I', tiles, tile_index(x, y, z) * TILE_SIZE, int(tile['type'])
|
||||
)
|
||||
|
||||
terrain_verts = build_terrain_verts(tiles)
|
||||
mesh_names = []
|
||||
mesh_offsets = []
|
||||
|
||||
# Terrain mesh (only written if non-empty)
|
||||
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
|
||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||
os.makedirs(terrain_dir, exist_ok=True)
|
||||
if terrain_verts:
|
||||
dmf_name = f'chunk_{dcf_base}_0.dmf'
|
||||
write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts)
|
||||
mesh_names.append(f'meshes/chunks/{dmf_name}')
|
||||
mesh_offsets.append((0.0, 0.0, 0.0))
|
||||
|
||||
# External mesh references
|
||||
for mesh in data.get('meshes', []):
|
||||
filename = mesh['file']
|
||||
pos = mesh.get('pos', [0, 0, 0])
|
||||
rel = find_mesh(filename)
|
||||
if rel is None:
|
||||
raise ValueError(f"Mesh not found under assets/meshes/: {filename}")
|
||||
mesh_names.append(rel)
|
||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||
print(f' Resolved {filename} -> {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('<I', data, 4)[0]
|
||||
if version not in (1, 2):
|
||||
raise ValueError(f"{path}: expected version 1 or 2, got {version}")
|
||||
|
||||
offset = 8
|
||||
tiles_size = CHUNK_TILE_COUNT * TILE_SIZE
|
||||
tiles = data[offset:offset + tiles_size]
|
||||
offset += tiles_size
|
||||
|
||||
meshes = []
|
||||
if version == 1:
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
if vert_count > 0:
|
||||
meshes.append(verts)
|
||||
else:
|
||||
mesh_count = data[offset]
|
||||
offset += 1
|
||||
for _ in range(mesh_count):
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
offset += vert_count * VERTEX_SIZE
|
||||
if vert_count > 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:
|
||||
print(
|
||||
"Usage: python3 -m tools.asset.chunk "
|
||||
"<input.dcf> [output.dcf]"
|
||||
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')
|
||||
)
|
||||
sys.exit(1)
|
||||
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]
|
||||
dst = args[1] if len(args) > 1 else src
|
||||
|
||||
print(f"Reading {src} ...")
|
||||
tiles, meshes = read_dcf(src)
|
||||
print(
|
||||
f" tiles={CHUNK_TILE_COUNT}, "
|
||||
f"meshes={len(meshes)}, "
|
||||
f"total_verts="
|
||||
f"{sum(len(m) // VERTEX_SIZE for m in meshes)}"
|
||||
)
|
||||
|
||||
# Derive chunk base name from the DCF filename (e.g. "0_0_0" from
|
||||
# "0_0_0.dcf") to name DMF files "chunk_0_0_0_0.dmf" etc.
|
||||
base = os.path.splitext(os.path.basename(dst))[0]
|
||||
|
||||
# assets/meshes/ sits beside assets/chunks/ (one dir up from the DCF).
|
||||
meshes_dir = os.path.normpath(
|
||||
os.path.join(os.path.dirname(os.path.abspath(dst)), '..', 'meshes')
|
||||
)
|
||||
os.makedirs(meshes_dir, exist_ok=True)
|
||||
|
||||
print(f"Writing DMF files to {meshes_dir} ...")
|
||||
mesh_names = []
|
||||
for idx, verts in enumerate(meshes):
|
||||
dmf_filename = f'chunk_{base}_{idx}.dmf'
|
||||
dmf_path = os.path.join(meshes_dir, dmf_filename)
|
||||
write_dmf(dmf_path, verts)
|
||||
mesh_names.append(f'meshes/{dmf_filename}')
|
||||
|
||||
print(f"Writing {dst} ...")
|
||||
write_v3(dst, tiles, mesh_names)
|
||||
if os.path.splitext(src)[1].lower() == '.json':
|
||||
process_json(src)
|
||||
else:
|
||||
upgrade_dcf(src)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user