This commit is contained in:
2026-06-27 08:50:55 -05:00
parent 2a85c9503f
commit f17b0bfcfb
9 changed files with 390 additions and 65 deletions
+120
View File
@@ -0,0 +1,120 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
"""
Converts DCF chunk files from version 1 to version 2.
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]
Usage:
python3 -m tools.asset.chunk <input.dcf> [output.dcf]
If output is omitted the input file is updated in place.
"""
import struct
import sys
import os
# Must match src/dusk/rpg/overworld/chunk.h
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
# 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
FILE_MAGIC = b'DCF'
VERSION_IN = 1
VERSION_OUT = 2
def read_v1(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 != VERSION_IN:
raise ValueError(f"{path}: expected version {VERSION_IN}, got {version}")
offset = 8
tiles_bytes = CHUNK_TILE_COUNT * TILE_SIZE
tiles = data[offset:offset + tiles_bytes]
offset += tiles_bytes
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")
return tiles, vert_count, verts
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]
mesh_count = 1 if vert_count > 0 else 0
buf = bytearray()
buf += FILE_MAGIC
buf += b'\x00'
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:
f.write(buf)
print(
f" Wrote {path}: version {VERSION_OUT}, "
f"{mesh_count} mesh(es), {vert_count} vertices."
)
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, vert_count, verts = read_v1(src)
print(f" tiles={CHUNK_TILE_COUNT}, vertices={vert_count}")
print(f"Writing {dst} ...")
write_v2(dst, tiles, vert_count, verts)
if __name__ == '__main__':
main()
+160
View File
@@ -0,0 +1,160 @@
# 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')