197 lines
5.6 KiB
Python
197 lines
5.6 KiB
Python
# Copyright (c) 2026 Dominic Masters
|
|
#
|
|
# This software is released under the MIT License.
|
|
# 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.
|
|
|
|
Version 1 format (after 8-byte header + tiles):
|
|
uint32_t vertCount
|
|
meshvertex_t vertices[vertCount]
|
|
|
|
Version 2 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)
|
|
|
|
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]
|
|
|
|
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.
|
|
"""
|
|
|
|
import struct
|
|
import sys
|
|
import os
|
|
|
|
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_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
|
|
|
|
|
|
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()
|
|
|
|
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 write_dmf(path, vertex_bytes):
|
|
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
|
buf = bytearray()
|
|
buf += DMF_MAGIC
|
|
buf += struct.pack('<I', DMF_VERSION)
|
|
buf += struct.pack('<I', vert_count)
|
|
buf += vertex_bytes
|
|
with open(path, 'wb') as f:
|
|
f.write(buf)
|
|
print(
|
|
f' Wrote DMF {path}: '
|
|
f'{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.
|
|
"""
|
|
mesh_count = len(mesh_names)
|
|
if mesh_offsets is None:
|
|
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
|
|
|
buf = bytearray()
|
|
buf += FILE_MAGIC
|
|
buf += b'\x00'
|
|
buf += struct.pack('<I', VERSION_OUT)
|
|
buf += tiles
|
|
buf += struct.pack('<B', mesh_count)
|
|
for name, offset in zip(mesh_names, mesh_offsets):
|
|
encoded = name.encode('ascii')
|
|
if len(encoded) >= 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)'
|
|
)
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
if not args:
|
|
print(
|
|
"Usage: python3 -m tools.asset.chunk "
|
|
"<input.dcf> [output.dcf]"
|
|
)
|
|
sys.exit(1)
|
|
|
|
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 __name__ == '__main__':
|
|
main()
|