Fixed a bunch of code inconsistencies
This commit is contained in:
+352
-343
@@ -90,6 +90,7 @@ Usage:
|
||||
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
@@ -160,121 +161,121 @@ DMF_VERSION = 1
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def tile_index(x, y):
|
||||
return x + y * CHUNK_WIDTH
|
||||
return x + y * CHUNK_WIDTH
|
||||
|
||||
|
||||
def find_model(filename):
|
||||
"""Search ASSETS_DIR/models/ recursively for filename.
|
||||
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
|
||||
models_root = os.path.join(ASSETS_DIR, 'models')
|
||||
for dirpath, _, filenames in os.walk(models_root):
|
||||
if filename in filenames:
|
||||
rel = os.path.relpath(
|
||||
os.path.join(dirpath, filename), ASSETS_DIR
|
||||
)
|
||||
return rel.replace('\\', '/')
|
||||
return None
|
||||
"""Search ASSETS_DIR/models/ recursively for filename.
|
||||
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
|
||||
models_root = os.path.join(ASSETS_DIR, 'models')
|
||||
for dirpath, _, filenames in os.walk(models_root):
|
||||
if filename in filenames:
|
||||
rel = os.path.relpath(
|
||||
os.path.join(dirpath, filename), ASSETS_DIR
|
||||
)
|
||||
return rel.replace('\\', '/')
|
||||
return None
|
||||
|
||||
|
||||
def write_model_json(path, mesh_rel, texture_rel, color):
|
||||
"""Write a model JSON descriptor file."""
|
||||
obj = {'mesh': mesh_rel, 'color': color}
|
||||
if texture_rel:
|
||||
obj['texture'] = texture_rel
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w') as f:
|
||||
json.dump(obj, f, indent=2)
|
||||
print(f' Wrote model JSON {path}')
|
||||
"""Write a model JSON descriptor file."""
|
||||
obj = {'mesh': mesh_rel, 'color': color}
|
||||
if texture_rel:
|
||||
obj['texture'] = texture_rel
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, 'w') as f:
|
||||
json.dump(obj, f, indent=2)
|
||||
print(f' Wrote model JSON {path}')
|
||||
|
||||
|
||||
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')
|
||||
"""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):
|
||||
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}: {vert_count} vertices, {len(buf)} 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}: {vert_count} vertices, {len(buf)} bytes')
|
||||
|
||||
|
||||
def write_dcf(
|
||||
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
||||
entity_spawns=None, area_spawns=None
|
||||
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
||||
entity_spawns=None, area_spawns=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
|
||||
if entity_spawns is None:
|
||||
entity_spawns = []
|
||||
if area_spawns is None:
|
||||
area_spawns = []
|
||||
"""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
|
||||
if entity_spawns is None:
|
||||
entity_spawns = []
|
||||
if area_spawns is None:
|
||||
area_spawns = []
|
||||
|
||||
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
||||
f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}"
|
||||
)
|
||||
if len(area_spawns) > CHUNK_AREA_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many area spawns ({len(area_spawns)}) - max "
|
||||
f"{CHUNK_AREA_COUNT_MAX}"
|
||||
)
|
||||
|
||||
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])
|
||||
|
||||
buf += struct.pack('<B', len(entity_spawns))
|
||||
for spawn in entity_spawns:
|
||||
kind = spawn['kind']
|
||||
x, y, z = spawn['pos']
|
||||
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
||||
a, b = spawn['globalId'], 0
|
||||
else:
|
||||
a, b = spawn['itemId'], spawn['quantity']
|
||||
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
||||
|
||||
buf += struct.pack('<B', len(area_spawns))
|
||||
for area in area_spawns:
|
||||
minX, minY, minZ = area['min']
|
||||
maxX, maxY, maxZ = area['max']
|
||||
buf += struct.pack(
|
||||
'<6hHBB',
|
||||
minX, minY, minZ, maxX, maxY, maxZ,
|
||||
area['callbackId'], area['notify'], area['trigger']
|
||||
)
|
||||
|
||||
with open(dcf_path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DCF {dcf_path}: '
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
||||
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
||||
f'area(s), {len(buf)} bytes'
|
||||
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
||||
f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}"
|
||||
)
|
||||
if len(area_spawns) > CHUNK_AREA_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many area spawns ({len(area_spawns)}) - max "
|
||||
f"{CHUNK_AREA_COUNT_MAX}"
|
||||
)
|
||||
|
||||
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])
|
||||
|
||||
buf += struct.pack('<B', len(entity_spawns))
|
||||
for spawn in entity_spawns:
|
||||
kind = spawn['kind']
|
||||
x, y, z = spawn['pos']
|
||||
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
||||
a, b = spawn['globalId'], 0
|
||||
else:
|
||||
a, b = spawn['itemId'], spawn['quantity']
|
||||
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
||||
|
||||
buf += struct.pack('<B', len(area_spawns))
|
||||
for area in area_spawns:
|
||||
minX, minY, minZ = area['min']
|
||||
maxX, maxY, maxZ = area['max']
|
||||
buf += struct.pack(
|
||||
'<6hHBB',
|
||||
minX, minY, minZ, maxX, maxY, maxZ,
|
||||
area['callbackId'], area['notify'], area['trigger']
|
||||
)
|
||||
|
||||
with open(dcf_path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DCF {dcf_path}: '
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
||||
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
||||
f'area(s), {len(buf)} bytes'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -282,21 +283,21 @@ def write_dcf(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
||||
"""Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
|
||||
Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
|
||||
NORTH=+Y, EAST=+X (matches entityDirGetRelative)."""
|
||||
verts = [
|
||||
(u0, v0, fx, fy, sw_z),
|
||||
(u1, v0, fx+1, fy, se_z),
|
||||
(u1, v1, fx+1, fy+1, ne_z),
|
||||
(u0, v0, fx, fy, sw_z),
|
||||
(u1, v1, fx+1, fy+1, ne_z),
|
||||
(u0, v1, fx, fy+1, nw_z),
|
||||
]
|
||||
buf = bytearray()
|
||||
for v in verts:
|
||||
buf += struct.pack('<5f', *v)
|
||||
return bytes(buf)
|
||||
"""Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
|
||||
Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
|
||||
NORTH=+Y, EAST=+X (matches entityDirGetRelative)."""
|
||||
verts = [
|
||||
(u0, v0, fx, fy, sw_z),
|
||||
(u1, v0, fx+1, fy, se_z),
|
||||
(u1, v1, fx+1, fy+1, ne_z),
|
||||
(u0, v0, fx, fy, sw_z),
|
||||
(u1, v1, fx+1, fy+1, ne_z),
|
||||
(u0, v1, fx, fy+1, nw_z),
|
||||
]
|
||||
buf = bytearray()
|
||||
for v in verts:
|
||||
buf += struct.pack('<5f', *v)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
# Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z.
|
||||
@@ -305,150 +306,150 @@ def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
||||
# Diagonal inner-corner ramps: only the corner opposite the named one is
|
||||
# lowered to 0, the rest (including the named corner) stay raised at 1.
|
||||
_RAMP_CORNERS = {
|
||||
TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0),
|
||||
TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high
|
||||
TILE_SHAPE_RAMP_SOUTH: (1.0, 1.0, 0.0, 0.0), # south=high, north=low
|
||||
TILE_SHAPE_RAMP_EAST: (0.0, 1.0, 1.0, 0.0), # west=low, east=high
|
||||
TILE_SHAPE_RAMP_WEST: (1.0, 0.0, 0.0, 1.0), # west=high, east=low
|
||||
TILE_SHAPE_RAMP_NORTHEAST: (0.0, 0.0, 1.0, 0.0), # only NE raised
|
||||
TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised
|
||||
TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised
|
||||
TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised
|
||||
TILE_SHAPE_RAMP_NORTHEAST_INNER: (0.0, 1.0, 1.0, 1.0), # only SW low
|
||||
TILE_SHAPE_RAMP_NORTHWEST_INNER: (1.0, 0.0, 1.0, 1.0), # only SE low
|
||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER: (1.0, 1.0, 1.0, 0.0), # only NW low
|
||||
TILE_SHAPE_RAMP_SOUTHWEST_INNER: (1.0, 1.0, 0.0, 1.0), # only NE low
|
||||
TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0),
|
||||
TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high
|
||||
TILE_SHAPE_RAMP_SOUTH: (1.0, 1.0, 0.0, 0.0), # south=high, north=low
|
||||
TILE_SHAPE_RAMP_EAST: (0.0, 1.0, 1.0, 0.0), # west=low, east=high
|
||||
TILE_SHAPE_RAMP_WEST: (1.0, 0.0, 0.0, 1.0), # west=high, east=low
|
||||
TILE_SHAPE_RAMP_NORTHEAST: (0.0, 0.0, 1.0, 0.0), # only NE raised
|
||||
TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised
|
||||
TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised
|
||||
TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised
|
||||
TILE_SHAPE_RAMP_NORTHEAST_INNER: (0.0, 1.0, 1.0, 1.0), # only SW low
|
||||
TILE_SHAPE_RAMP_NORTHWEST_INNER: (1.0, 0.0, 1.0, 1.0), # only SE low
|
||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER: (1.0, 1.0, 1.0, 0.0), # only NW low
|
||||
TILE_SHAPE_RAMP_SOUTHWEST_INNER: (1.0, 1.0, 0.0, 1.0), # only NE low
|
||||
}
|
||||
|
||||
|
||||
def build_terrain_verts(tiles_bytes):
|
||||
"""Generate quads for every non-null tile, shaped to match tile type."""
|
||||
buf = bytearray()
|
||||
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)
|
||||
"""Generate quads for every non-null tile, shaped to match tile type."""
|
||||
buf = bytearray()
|
||||
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)
|
||||
|
||||
|
||||
def from_json(json_path, dcf_path):
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
with open(json_path) as f:
|
||||
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(
|
||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||
int(tile['type']), z
|
||||
)
|
||||
|
||||
terrain_verts = build_terrain_verts(tiles)
|
||||
model_names = []
|
||||
mesh_offsets = []
|
||||
|
||||
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
|
||||
|
||||
# Terrain mesh + model (only written if non-empty)
|
||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||
models_dir = os.path.join(ASSETS_DIR, 'models', 'chunks')
|
||||
os.makedirs(terrain_dir, exist_ok=True)
|
||||
os.makedirs(models_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_rel = f'meshes/chunks/{dmf_name}'
|
||||
json_name = f'chunk_{dcf_base}_0.json'
|
||||
write_model_json(
|
||||
os.path.join(models_dir, json_name),
|
||||
mesh_rel,
|
||||
'tiles.png',
|
||||
[255, 255, 255, 255]
|
||||
)
|
||||
model_names.append(f'models/chunks/{json_name}')
|
||||
mesh_offsets.append((0.0, 0.0, 0.0))
|
||||
|
||||
# External mesh references -> look up matching model JSON
|
||||
for mesh in data.get('meshes', []):
|
||||
filename = mesh['file']
|
||||
pos = mesh.get('pos', [0, 0, 0])
|
||||
model_filename = os.path.splitext(filename)[0] + '.json'
|
||||
rel = find_model(model_filename)
|
||||
if rel is None:
|
||||
raise ValueError(
|
||||
f"Model not found under assets/models/: {model_filename}"
|
||||
)
|
||||
model_names.append(rel)
|
||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||
print(f' Resolved {filename} -> {rel}')
|
||||
|
||||
entity_spawns = []
|
||||
for spawn in data.get('entities', []):
|
||||
pos = tuple(int(v) for v in spawn['pos'])
|
||||
if spawn['type'] == 'global':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_GLOBAL,
|
||||
'globalId': int(spawn['globalId']),
|
||||
'pos': pos,
|
||||
})
|
||||
elif spawn['type'] == 'item':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_ITEM,
|
||||
'itemId': int(spawn['itemId']),
|
||||
'quantity': int(spawn['quantity']),
|
||||
'pos': pos,
|
||||
})
|
||||
else:
|
||||
raise ValueError(f"Unknown entity spawn type: {spawn['type']}")
|
||||
|
||||
area_spawns = []
|
||||
for area in data.get('areas', []):
|
||||
area_spawns.append({
|
||||
'min': tuple(int(v) for v in area['min']),
|
||||
'max': tuple(int(v) for v in area['max']),
|
||||
'callbackId': int(area['callbackId']),
|
||||
'notify': int(area['notify']),
|
||||
'trigger': int(area['trigger']),
|
||||
})
|
||||
|
||||
write_dcf(
|
||||
dcf_path, bytes(tiles), model_names, mesh_offsets,
|
||||
entity_spawns, area_spawns
|
||||
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(
|
||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||
int(tile['type']), z
|
||||
)
|
||||
|
||||
terrain_verts = build_terrain_verts(tiles)
|
||||
model_names = []
|
||||
mesh_offsets = []
|
||||
|
||||
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
|
||||
|
||||
# Terrain mesh + model (only written if non-empty)
|
||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||
models_dir = os.path.join(ASSETS_DIR, 'models', 'chunks')
|
||||
os.makedirs(terrain_dir, exist_ok=True)
|
||||
os.makedirs(models_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_rel = f'meshes/chunks/{dmf_name}'
|
||||
json_name = f'chunk_{dcf_base}_0.json'
|
||||
write_model_json(
|
||||
os.path.join(models_dir, json_name),
|
||||
mesh_rel,
|
||||
'tiles.png',
|
||||
[255, 255, 255, 255]
|
||||
)
|
||||
model_names.append(f'models/chunks/{json_name}')
|
||||
mesh_offsets.append((0.0, 0.0, 0.0))
|
||||
|
||||
# External mesh references -> look up matching model JSON
|
||||
for mesh in data.get('meshes', []):
|
||||
filename = mesh['file']
|
||||
pos = mesh.get('pos', [0, 0, 0])
|
||||
model_filename = os.path.splitext(filename)[0] + '.json'
|
||||
rel = find_model(model_filename)
|
||||
if rel is None:
|
||||
raise ValueError(
|
||||
f"Model not found under assets/models/: {model_filename}"
|
||||
)
|
||||
model_names.append(rel)
|
||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||
print(f' Resolved {filename} -> {rel}')
|
||||
|
||||
entity_spawns = []
|
||||
for spawn in data.get('entities', []):
|
||||
pos = tuple(int(v) for v in spawn['pos'])
|
||||
if spawn['type'] == 'global':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_GLOBAL,
|
||||
'globalId': int(spawn['globalId']),
|
||||
'pos': pos,
|
||||
})
|
||||
elif spawn['type'] == 'item':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_ITEM,
|
||||
'itemId': int(spawn['itemId']),
|
||||
'quantity': int(spawn['quantity']),
|
||||
'pos': pos,
|
||||
})
|
||||
else:
|
||||
raise ValueError(f"Unknown entity spawn type: {spawn['type']}")
|
||||
|
||||
area_spawns = []
|
||||
for area in data.get('areas', []):
|
||||
area_spawns.append({
|
||||
'min': tuple(int(v) for v in area['min']),
|
||||
'max': tuple(int(v) for v in area['max']),
|
||||
'callbackId': int(area['callbackId']),
|
||||
'notify': int(area['notify']),
|
||||
'trigger': int(area['trigger']),
|
||||
})
|
||||
|
||||
write_dcf(
|
||||
dcf_path, bytes(tiles), model_names, mesh_offsets,
|
||||
entity_spawns, area_spawns
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -456,90 +457,90 @@ def process_json(json_path):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
"""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()
|
||||
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}")
|
||||
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 = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
|
||||
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
|
||||
offset += tiles_size
|
||||
offset = 8
|
||||
tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
|
||||
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
|
||||
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
|
||||
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_dcf(path, tiles, mesh_names)
|
||||
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_dcf(path, tiles, mesh_names)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -547,31 +548,39 @@ def upgrade_dcf(path):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate DCF + companion DMF files from raw chunk JSON, "
|
||||
"or upgrade legacy DCF files to the current version"
|
||||
)
|
||||
parser.add_argument(
|
||||
'path', nargs='?',
|
||||
help='Chunk JSON file to process, or legacy DCF file to upgrade. '
|
||||
'If omitted, processes all assetsraw/chunks/*.json'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args:
|
||||
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')
|
||||
)
|
||||
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
|
||||
if args.path is None:
|
||||
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')
|
||||
)
|
||||
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]
|
||||
if os.path.splitext(src)[1].lower() == '.json':
|
||||
process_json(src)
|
||||
else:
|
||||
upgrade_dcf(src)
|
||||
if os.path.splitext(args.path)[1].lower() == '.json':
|
||||
process_json(args.path)
|
||||
else:
|
||||
upgrade_dcf(args.path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user