Change to use the Z tile system
This commit is contained in:
+100
-41
@@ -5,7 +5,7 @@
|
||||
|
||||
"""
|
||||
Generates DCF + companion DMF files from raw chunk JSON files, or upgrades
|
||||
legacy DCF files (version 1 or 2) to version 3.
|
||||
legacy DCF files (version 1 or 2) to the current version.
|
||||
|
||||
JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
||||
{
|
||||
@@ -17,14 +17,21 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
||||
]
|
||||
}
|
||||
|
||||
Tiles absent from the array default to TILE_SHAPE_NULL.
|
||||
Tiles absent from the array default to TILE_SHAPE_NULL. Each (x, y)
|
||||
column may only have ONE real tile - chunks store one tile per column,
|
||||
not one per (x, y, z), and that tile records its own local z (see the
|
||||
version 4 tile format below). If two entries share the same (x, y) with
|
||||
different z, the later one in the array wins and a warning is printed.
|
||||
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):
|
||||
Version 4 DCF format (after 8-byte header):
|
||||
tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column)
|
||||
each tile: uint32_t shape, uint8_t z, 3 padding bytes (8 bytes total,
|
||||
matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; })
|
||||
uint8_t meshCount
|
||||
for each model:
|
||||
null-terminated string (relative asset path to .json model)
|
||||
@@ -40,7 +47,7 @@ DMF format:
|
||||
Usage:
|
||||
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
|
||||
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -56,8 +63,10 @@ ASSETS_DIR = os.path.join(PROJECT_ROOT, 'assets')
|
||||
|
||||
CHUNK_WIDTH = 16
|
||||
CHUNK_HEIGHT = 16
|
||||
CHUNK_DEPTH = 4
|
||||
CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 1024
|
||||
CHUNK_DEPTH = 8
|
||||
# One tile per (x, y) column - the column's local Z (0..CHUNK_DEPTH-1) is
|
||||
# carried on the tile itself instead of storing a tile per (x, y, z).
|
||||
CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT # 256
|
||||
|
||||
# World-space Z distance of one Z-layer/story, in world-float space.
|
||||
# Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h.
|
||||
@@ -66,9 +75,20 @@ WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2)
|
||||
CHUNK_MESH_COUNT_MAX = 10
|
||||
CHUNK_MESH_NAME_MAX = 64
|
||||
|
||||
TILE_SIZE = 4
|
||||
# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded
|
||||
# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment).
|
||||
TILE_STRUCT_FORMAT = '<IB3x'
|
||||
TILE_SIZE = struct.calcsize(TILE_STRUCT_FORMAT)
|
||||
VERTEX_SIZE = 20
|
||||
|
||||
# Legacy (v1/v2) DCFs stored one uint32 shape per (x, y, z) triple across
|
||||
# the full 3D grid, at whatever CHUNK_DEPTH was in effect when they were
|
||||
# written (4). Frozen here, independent of the live CHUNK_DEPTH above, so
|
||||
# a future depth change can't corrupt parsing of genuinely old files.
|
||||
_LEGACY_CHUNK_DEPTH = 4
|
||||
_LEGACY_TILE_SIZE = 4
|
||||
_LEGACY_CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * _LEGACY_CHUNK_DEPTH
|
||||
|
||||
TILE_SHAPE_NULL = 0
|
||||
TILE_SHAPE_GROUND = 1
|
||||
TILE_SHAPE_RAMP_NORTH = 2
|
||||
@@ -86,7 +106,7 @@ TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 3
|
||||
VERSION_OUT = 4
|
||||
DMF_VERSION = 1
|
||||
|
||||
|
||||
@@ -94,8 +114,8 @@ DMF_VERSION = 1
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def tile_index(x, y, z):
|
||||
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
||||
def tile_index(x, y):
|
||||
return x + y * CHUNK_WIDTH
|
||||
|
||||
|
||||
def find_model(filename):
|
||||
@@ -143,8 +163,8 @@ def write_dmf(path, vertex_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 referencing the given DMF asset paths."""
|
||||
def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
||||
"""Write a current-version 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
|
||||
@@ -218,29 +238,28 @@ _RAMP_CORNERS = {
|
||||
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(
|
||||
'<I', tiles_bytes, idx * TILE_SIZE
|
||||
)[0]
|
||||
corners = _RAMP_CORNERS.get(tile_type)
|
||||
if corners is None:
|
||||
continue
|
||||
fx, fy, fz = float(x), float(y), float(z)
|
||||
u0 = x / CHUNK_WIDTH
|
||||
u1 = (x + 1) / CHUNK_WIDTH
|
||||
v0 = y / CHUNK_HEIGHT
|
||||
v1 = (y + 1) / CHUNK_HEIGHT
|
||||
sw, se, ne, nw = corners
|
||||
buf += _tile_quad(
|
||||
u0, u1, v0, v1, fx, fy,
|
||||
(fz + sw) * WORLD_LAYER_HEIGHT,
|
||||
(fz + se) * WORLD_LAYER_HEIGHT,
|
||||
(fz + ne) * WORLD_LAYER_HEIGHT,
|
||||
(fz + nw) * WORLD_LAYER_HEIGHT
|
||||
)
|
||||
for y in range(CHUNK_HEIGHT):
|
||||
for x in range(CHUNK_WIDTH):
|
||||
idx = tile_index(x, y)
|
||||
tile_type, z = struct.unpack_from(
|
||||
'<IB', tiles_bytes, idx * TILE_SIZE
|
||||
)
|
||||
corners = _RAMP_CORNERS.get(tile_type)
|
||||
if corners is None:
|
||||
continue
|
||||
fx, fy, fz = float(x), float(y), float(z)
|
||||
u0 = x / CHUNK_WIDTH
|
||||
u1 = (x + 1) / CHUNK_WIDTH
|
||||
v0 = y / CHUNK_HEIGHT
|
||||
v1 = (y + 1) / CHUNK_HEIGHT
|
||||
sw, se, ne, nw = corners
|
||||
buf += _tile_quad(
|
||||
u0, u1, v0, v1, fx, fy,
|
||||
(fz + sw) * WORLD_LAYER_HEIGHT,
|
||||
(fz + se) * WORLD_LAYER_HEIGHT,
|
||||
(fz + ne) * WORLD_LAYER_HEIGHT,
|
||||
(fz + nw) * WORLD_LAYER_HEIGHT
|
||||
)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
@@ -249,11 +268,20 @@ def from_json(json_path, dcf_path):
|
||||
data = json.load(f)
|
||||
|
||||
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
||||
column_z = {}
|
||||
for tile in data.get('tiles', []):
|
||||
pos = tile['pos']
|
||||
x, y, z = int(pos[0]), int(pos[1]), int(pos[2])
|
||||
if (x, y) in column_z and column_z[(x, y)] != z:
|
||||
print(
|
||||
f' WARNING: {json_path}: column ({x}, {y}) has tiles at '
|
||||
f'z={column_z[(x, y)]} and z={z} - only one tile per '
|
||||
f'column is kept, z={z} wins (last one in the array)'
|
||||
)
|
||||
column_z[(x, y)] = z
|
||||
struct.pack_into(
|
||||
'<I', tiles, tile_index(x, y, z) * TILE_SIZE, int(tile['type'])
|
||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||
int(tile['type']), z
|
||||
)
|
||||
|
||||
terrain_verts = build_terrain_verts(tiles)
|
||||
@@ -295,7 +323,7 @@ def from_json(json_path, dcf_path):
|
||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||
print(f' Resolved {filename} -> {rel}')
|
||||
|
||||
write_v3(dcf_path, bytes(tiles), model_names, mesh_offsets)
|
||||
write_dcf(dcf_path, bytes(tiles), model_names, mesh_offsets)
|
||||
|
||||
|
||||
def process_json(json_path):
|
||||
@@ -306,9 +334,40 @@ def process_json(json_path):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy DCF (v1/v2) -> v3
|
||||
# Legacy DCF (v1/v2) -> current version
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def collapse_legacy_tiles(legacy_tiles, path):
|
||||
"""Collapse the old one-tile-per-(x,y,z) grid into the current
|
||||
one-tile-per-(x,y) format. If a column has more than one non-null tile
|
||||
across its Z layers, the highest Z wins and a warning is printed."""
|
||||
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
||||
for y in range(CHUNK_HEIGHT):
|
||||
for x in range(CHUNK_WIDTH):
|
||||
found = []
|
||||
for z in range(_LEGACY_CHUNK_DEPTH):
|
||||
legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
||||
shape = struct.unpack_from(
|
||||
'<I', legacy_tiles, legacy_idx * _LEGACY_TILE_SIZE
|
||||
)[0]
|
||||
if shape != TILE_SHAPE_NULL:
|
||||
found.append((z, shape))
|
||||
if not found:
|
||||
continue
|
||||
if len(found) > 1:
|
||||
print(
|
||||
f' WARNING: {path}: column ({x}, {y}) has tiles at '
|
||||
f'z={[z for z, _ in found]} - only one tile per column '
|
||||
f'is kept, z={found[-1][0]} wins (highest Z)'
|
||||
)
|
||||
z, shape = found[-1]
|
||||
struct.pack_into(
|
||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||
shape, z
|
||||
)
|
||||
return bytes(tiles)
|
||||
|
||||
|
||||
def read_legacy_dcf(path):
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
@@ -319,8 +378,8 @@ def read_legacy_dcf(path):
|
||||
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]
|
||||
tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
|
||||
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
|
||||
offset += tiles_size
|
||||
|
||||
meshes = []
|
||||
@@ -362,7 +421,7 @@ def upgrade_dcf(path):
|
||||
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)
|
||||
write_dcf(path, tiles, mesh_names)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user