CHUNK STUFF
This commit is contained in:
+119
-43
@@ -4,7 +4,8 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
"""
|
||||
Converts DCF chunk files from version 1 to version 2.
|
||||
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
|
||||
@@ -13,37 +14,51 @@ Version 1 format (after 8-byte header + tiles):
|
||||
Version 2 format (after 8-byte header + tiles):
|
||||
uint8_t meshCount
|
||||
for each mesh:
|
||||
uint32_t vertCount
|
||||
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
|
||||
|
||||
# Must match src/dusk/rpg/overworld/chunk.h
|
||||
CHUNK_WIDTH = 16
|
||||
CHUNK_HEIGHT = 16
|
||||
CHUNK_DEPTH = 32
|
||||
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
|
||||
|
||||
# C enum (int) = 4 bytes; meshvertex_t = uv[2]+pos[3] floats = 20 bytes
|
||||
TILE_SIZE = 4
|
||||
VERTEX_SIZE = 20 # 2 floats UV + 3 floats pos, MESH_ENABLE_COLOR=0
|
||||
VERTEX_SIZE = 20
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
VERSION_IN = 1
|
||||
VERSION_OUT = 2
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 3
|
||||
DMF_VERSION = 1
|
||||
|
||||
|
||||
def read_v1(path):
|
||||
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()
|
||||
|
||||
@@ -51,34 +66,66 @@ def read_v1(path):
|
||||
raise ValueError(f"{path}: not a DCF file")
|
||||
|
||||
version = struct.unpack_from('<I', data, 4)[0]
|
||||
if version != VERSION_IN:
|
||||
raise ValueError(f"{path}: expected version {VERSION_IN}, got {version}")
|
||||
if version not in (1, 2):
|
||||
raise ValueError(
|
||||
f"{path}: expected version 1 or 2, got {version}"
|
||||
)
|
||||
|
||||
offset = 8
|
||||
tiles_bytes = CHUNK_TILE_COUNT * TILE_SIZE
|
||||
tiles = data[offset:offset + tiles_bytes]
|
||||
offset += tiles_bytes
|
||||
tiles_size = CHUNK_TILE_COUNT * TILE_SIZE
|
||||
tiles = data[offset:offset + tiles_size]
|
||||
offset += tiles_size
|
||||
|
||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||
offset += 4
|
||||
meshes = []
|
||||
|
||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||
if len(verts) != vert_count * VERTEX_SIZE:
|
||||
raise ValueError(f"{path}: truncated vertex data")
|
||||
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, vert_count, verts
|
||||
return tiles, meshes
|
||||
|
||||
|
||||
def write_v2(path, tiles, vert_count, verts):
|
||||
if vert_count > CHUNK_VERTEX_COUNT:
|
||||
print(
|
||||
f" Warning: {vert_count} vertices exceeds pool "
|
||||
f"({CHUNK_VERTEX_COUNT}); truncating."
|
||||
)
|
||||
vert_count = CHUNK_VERTEX_COUNT
|
||||
verts = verts[:vert_count * VERTEX_SIZE]
|
||||
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'
|
||||
)
|
||||
|
||||
mesh_count = 1 if vert_count > 0 else 0
|
||||
|
||||
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
|
||||
@@ -86,34 +133,63 @@ def write_v2(path, tiles, vert_count, verts):
|
||||
buf += struct.pack('<I', VERSION_OUT)
|
||||
buf += tiles
|
||||
buf += struct.pack('<B', mesh_count)
|
||||
if mesh_count > 0:
|
||||
buf += struct.pack('<I', vert_count)
|
||||
buf += verts
|
||||
|
||||
with open(path, 'wb') as f:
|
||||
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 {path}: version {VERSION_OUT}, "
|
||||
f"{mesh_count} mesh(es), {vert_count} vertices."
|
||||
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]")
|
||||
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, vert_count, verts = read_v1(src)
|
||||
print(f" tiles={CHUNK_TILE_COUNT}, vertices={vert_count}")
|
||||
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_v2(dst, tiles, vert_count, verts)
|
||||
write_v3(dst, tiles, mesh_names)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
"""
|
||||
Generates chunk 0_0_0.dcf with a small hill in the centre.
|
||||
|
||||
Hill layout (tile coordinates, 0-based):
|
||||
y=5: . . . . . . N N . . . . . . . . RAMP_NORTH (south slope)
|
||||
y=6: . . . . . E H H W . . . . . . . Hill top (H), RAMP_EAST/WEST
|
||||
y=7: . . . . . E H H W . . . . . . .
|
||||
y=8: . . . . . . S S . . . . . . . . RAMP_SOUTH (north slope)
|
||||
x=6 x=7
|
||||
"""
|
||||
|
||||
import struct, os
|
||||
|
||||
# Must match src/dusk/rpg/overworld/chunk.h and tile.h
|
||||
CHUNK_WIDTH = 16
|
||||
CHUNK_HEIGHT = 16
|
||||
CHUNK_DEPTH = 32
|
||||
CHUNK_W_F = float(CHUNK_WIDTH)
|
||||
|
||||
TILE_NULL = 0
|
||||
TILE_GROUND = 1
|
||||
TILE_RAMP_NORTH = 2
|
||||
TILE_RAMP_SOUTH = 3
|
||||
TILE_RAMP_EAST = 4
|
||||
TILE_RAMP_WEST = 5
|
||||
|
||||
TILE_SIZE = 4 # sizeof(tile_t) = sizeof(int)
|
||||
VERT_SIZE = 20 # sizeof(meshvertex_t): uv[2] + pos[3] floats
|
||||
FILE_VER = 2
|
||||
|
||||
# Hill geometry parameters
|
||||
HILL_X = frozenset({6, 7})
|
||||
HILL_Y = frozenset({6, 7})
|
||||
HILL_H = 1.0
|
||||
|
||||
|
||||
def tile_idx(cx, cy, cz):
|
||||
return cz * CHUNK_WIDTH * CHUNK_HEIGHT + cy * CHUNK_WIDTH + cx
|
||||
|
||||
|
||||
def make_vert(u, v, px, py, pz):
|
||||
return struct.pack('<5f', u, v, px, py, pz)
|
||||
|
||||
|
||||
def quad_verts(cx, cy, z_sw, z_se, z_ne, z_nw):
|
||||
"""
|
||||
Build 6 vertices (2 triangles) for a tile quad.
|
||||
Heights at each corner: SW=south-west, SE=south-east,
|
||||
NE=north-east, NW=north-west.
|
||||
UV formula (verified against existing DCF data):
|
||||
u = (cy + within_x) / CHUNK_WIDTH where within_x in {0,1}
|
||||
v = (cx + within_y) / CHUNK_HEIGHT where within_y in {0,1}
|
||||
"""
|
||||
u0 = cy / CHUNK_W_F
|
||||
u1 = (cy + 1) / CHUNK_W_F
|
||||
v0 = cx / CHUNK_W_F
|
||||
v1 = (cx + 1) / CHUNK_W_F
|
||||
x0, x1 = float(cx), float(cx + 1)
|
||||
y0, y1 = float(cy), float(cy + 1)
|
||||
|
||||
SW = make_vert(u0, v0, x0, y0, float(z_sw))
|
||||
SE = make_vert(u1, v0, x1, y0, float(z_se))
|
||||
NE = make_vert(u1, v1, x1, y1, float(z_ne))
|
||||
NW = make_vert(u0, v1, x0, y1, float(z_nw))
|
||||
|
||||
return SW + SE + NE + SW + NE + NW
|
||||
|
||||
|
||||
def flat(cx, cy, z):
|
||||
return quad_verts(cx, cy, z, z, z, z)
|
||||
|
||||
|
||||
def ramp_north(cx, cy):
|
||||
return quad_verts(cx, cy, 0, 0, HILL_H, HILL_H)
|
||||
|
||||
|
||||
def ramp_south(cx, cy):
|
||||
return quad_verts(cx, cy, HILL_H, HILL_H, 0, 0)
|
||||
|
||||
|
||||
def ramp_east(cx, cy):
|
||||
return quad_verts(cx, cy, 0, HILL_H, HILL_H, 0)
|
||||
|
||||
|
||||
def ramp_west(cx, cy):
|
||||
return quad_verts(cx, cy, HILL_H, 0, 0, HILL_H)
|
||||
|
||||
|
||||
def generate():
|
||||
tiles = [TILE_GROUND] * (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH)
|
||||
|
||||
ramps_n = frozenset((cx, 5) for cx in HILL_X)
|
||||
ramps_s = frozenset((cx, 8) for cx in HILL_X)
|
||||
ramps_e = frozenset((5, cy) for cy in HILL_Y)
|
||||
ramps_w = frozenset((8, cy) for cy in HILL_Y)
|
||||
|
||||
for cx, cy in ramps_n:
|
||||
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_NORTH
|
||||
for cx, cy in ramps_s:
|
||||
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_SOUTH
|
||||
for cx, cy in ramps_e:
|
||||
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_EAST
|
||||
for cx, cy in ramps_w:
|
||||
tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_WEST
|
||||
|
||||
for cx in HILL_X:
|
||||
for cy in HILL_Y:
|
||||
tiles[tile_idx(cx, cy, 1)] = TILE_GROUND
|
||||
|
||||
verts = bytearray()
|
||||
|
||||
for cx in range(CHUNK_WIDTH):
|
||||
for cy in range(CHUNK_HEIGHT):
|
||||
pos = (cx, cy)
|
||||
if cx in HILL_X and cy in HILL_Y:
|
||||
continue
|
||||
if pos in ramps_n:
|
||||
verts += ramp_north(cx, cy)
|
||||
elif pos in ramps_s:
|
||||
verts += ramp_south(cx, cy)
|
||||
elif pos in ramps_e:
|
||||
verts += ramp_east(cx, cy)
|
||||
elif pos in ramps_w:
|
||||
verts += ramp_west(cx, cy)
|
||||
else:
|
||||
verts += flat(cx, cy, 0)
|
||||
|
||||
for cx in sorted(HILL_X):
|
||||
for cy in sorted(HILL_Y):
|
||||
verts += flat(cx, cy, HILL_H)
|
||||
|
||||
vert_count = len(verts) // VERT_SIZE
|
||||
tile_bytes = struct.pack(f'<{len(tiles)}i', *tiles)
|
||||
|
||||
buf = bytearray()
|
||||
buf += b'DCF\x00'
|
||||
buf += struct.pack('<I', FILE_VER)
|
||||
buf += tile_bytes
|
||||
buf += struct.pack('<B', 1)
|
||||
buf += struct.pack('<I', vert_count)
|
||||
buf += verts
|
||||
|
||||
return buf, vert_count
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
out = os.path.join(
|
||||
os.path.dirname(__file__), '..', '..', '..', 'assets', 'chunks',
|
||||
'0_0_0.dcf'
|
||||
)
|
||||
out = os.path.normpath(out)
|
||||
buf, vert_count = generate()
|
||||
with open(out, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(f'Wrote {out}: {vert_count} vertices, {len(buf)} bytes')
|
||||
Reference in New Issue
Block a user