Change to use the Z tile system

This commit is contained in:
2026-07-04 08:40:53 -05:00
parent 88ddf429b7
commit a292f1992b
21 changed files with 269 additions and 160 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,7 @@
#include "asset/assetfile.h" #include "asset/assetfile.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 3 #define ASSET_CHUNK_FILE_VERSION 4
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
+1 -1
View File
@@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
}; };
float_t entityAnimTileZOffset(const worldpos_t pos) { float_t entityAnimTileZOffset(const worldpos_t pos) {
return tileIsRamp(mapGetTile(pos)) ? 0.5f : 0.0f; return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
} }
void entityAnimUpdate(entity_t *entity) { void entityAnimUpdate(entity_t *entity) {
+30 -25
View File
@@ -95,38 +95,38 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
// Are we walking up a ramp? // Are we walking up a ramp?
if( if(
tileIsRamp(tileCurrent) && tileShapeIsRamp(tileCurrent.shape) &&
( (
// Can only walk UP the direction the ramp faces. // Can only walk UP the direction the ramp faces.
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent || (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
// If diagonal ramp, can go up one of two ways only. Inner ramps // If diagonal ramp, can go up one of two ways only. Inner ramps
// share the same allowed directions as their outer counterparts. // share the same allowed directions as their outer counterparts.
( (
( (
( (
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST || tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
( (
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST || tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
( (
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST || tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
( (
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST || tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST_INNER tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) )
@@ -135,55 +135,60 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
) )
) { ) {
tile_t tileNewSaved = tileNew; tile_t tileNewSaved = tileNew;
tileNew = TILE_SHAPE_NULL; tileNew = TILE_NULL;
worldpos_t abovePos = newPos; worldpos_t abovePos = newPos;
abovePos.z += 1; abovePos.z += 1;
tile_t tileAbove = mapGetTile(abovePos); tile_t tileAbove = mapGetTile(abovePos);
if(tileAbove != TILE_SHAPE_NULL && tileIsWalkable(tileAbove)) { if(
tileAbove.shape != TILE_SHAPE_NULL &&
tileShapeIsWalkable(tileAbove.shape)
) {
raise = true; raise = true;
} else { } else {
tileNew = tileNewSaved; tileNew = tileNewSaved;
} }
} else if(tileNew == TILE_SHAPE_NULL && newPos.z > 0) { } else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
// Falling down? // Falling down?
worldpos_t belowPos = newPos; worldpos_t belowPos = newPos;
belowPos.z -= 1; belowPos.z -= 1;
tile_t tileBelow = mapGetTile(belowPos); tile_t tileBelow = mapGetTile(belowPos);
if( if(
tileBelow != TILE_SHAPE_NULL && tileBelow.shape != TILE_SHAPE_NULL &&
tileIsRamp(tileBelow) && tileShapeIsRamp(tileBelow.shape) &&
( (
// This handles regular cardinal ramps // This handles regular cardinal ramps
(entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow || (
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
) == tileBelow.shape ||
// This handles diagonal ramps. Inner ramps share the same // This handles diagonal ramps. Inner ramps share the same
// allowed directions as their outer counterparts. // allowed directions as their outer counterparts.
( (
( (
( (
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST || tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST_INNER tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
( (
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST || tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST_INNER tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
( (
tileBelow == TILE_SHAPE_RAMP_NORTHEAST || tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow == TILE_SHAPE_RAMP_NORTHEAST_INNER tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
( (
tileBelow == TILE_SHAPE_RAMP_NORTHWEST || tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow == TILE_SHAPE_RAMP_NORTHWEST_INNER tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
) && ) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) )
@@ -196,7 +201,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
} }
// Can we walk here? // Can we walk here?
if(!raise && !fall && !tileIsWalkable(tileNew)) return;// Blocked if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
// Raise/fall must be applied before checking for blocking entities, // Raise/fall must be applied before checking for blocking entities,
// otherwise the check compares against the wrong z-level. // otherwise the check compares against the wrong z-level.
+1
View File
@@ -10,5 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
map.c map.c
worldpos.c worldpos.c
tile.c tile.c
tileshape.c
) )
+1 -5
View File
@@ -8,11 +8,7 @@
#include "chunk.h" #include "chunk.h"
uint32_t chunkGetTileIndex(const chunkpos_t position) { uint32_t chunkGetTileIndex(const chunkpos_t position) {
return ( return (position.y * CHUNK_WIDTH) + position.x;
(position.z * CHUNK_WIDTH * CHUNK_HEIGHT) +
(position.y * CHUNK_WIDTH) +
position.x
);
} }
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) { bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
+11 -6
View File
@@ -188,7 +188,10 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
); );
if(!assetFileExists(name)) { if(!assetFileExists(name)) {
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles)); for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
// chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
}
errorOk(); errorOk();
} }
@@ -229,17 +232,19 @@ chunk_t *mapGetChunk(const uint8_t index) {
} }
tile_t mapGetTile(const worldpos_t position) { tile_t mapGetTile(const worldpos_t position) {
if(!mapIsLoaded()) return TILE_SHAPE_NULL; if(!mapIsLoaded()) return TILE_NULL;
chunkpos_t chunkPos; chunkpos_t chunkPos;
worldPosToChunkPos(&position, &chunkPos); worldPosToChunkPos(&position, &chunkPos);
chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos); chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos);
if(chunkIndex == -1) return TILE_SHAPE_NULL; if(chunkIndex == -1) return TILE_NULL;
chunk_t *chunk = mapGetChunk(chunkIndex); chunk_t *chunk = mapGetChunk(chunkIndex);
assertNotNull(chunk, "Chunk pointer cannot be NULL"); assertNotNull(chunk, "Chunk pointer cannot be NULL");
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
return chunk->tiles[tileIndex]; tile_t tile = chunk->tiles[tileIndex];
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
return tile;
} }
bool_t mapGetWalkableZNear( bool_t mapGetWalkableZNear(
@@ -255,7 +260,7 @@ bool_t mapGetWalkableZNear(
}; };
for(uint8_t i = 0; i < 3; i++) { for(uint8_t i = 0; i < 3; i++) {
const worldpos_t pos = { x, y, candidates[i] }; const worldpos_t pos = { x, y, candidates[i] };
if(!tileIsWalkable(mapGetTile(pos))) continue; if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue;
*outZ = candidates[i]; *outZ = candidates[i];
return true; return true;
} }
@@ -294,7 +299,7 @@ void mapChunkLoadError(void *params, void *user) {
); );
assetUnlockEntry(chunk->dcfEntry); assetUnlockEntry(chunk->dcfEntry);
chunk->dcfEntry = NULL; chunk->dcfEntry = NULL;
memorySet(chunk->tiles, TILE_SHAPE_GROUND, sizeof(chunk->tiles)); memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
} }
void mapChunkLoaded(void *params, void *user) { void mapChunkLoaded(void *params, void *user) {
-31
View File
@@ -6,34 +6,3 @@
*/ */
#include "tile.h" #include "tile.h"
bool_t tileIsWalkable(const tile_t tile) {
switch(tile) {
case TILE_SHAPE_NULL:
return false;
default:
return true;
}
}
bool_t tileIsRamp(const tile_t tile) {
switch(tile) {
case TILE_SHAPE_RAMP_NORTH:
case TILE_SHAPE_RAMP_SOUTH:
case TILE_SHAPE_RAMP_EAST:
case TILE_SHAPE_RAMP_WEST:
case TILE_SHAPE_RAMP_NORTHEAST:
case TILE_SHAPE_RAMP_NORTHWEST:
case TILE_SHAPE_RAMP_SOUTHEAST:
case TILE_SHAPE_RAMP_SOUTHWEST:
case TILE_SHAPE_RAMP_NORTHEAST_INNER:
case TILE_SHAPE_RAMP_NORTHWEST_INNER:
case TILE_SHAPE_RAMP_SOUTHEAST_INNER:
case TILE_SHAPE_RAMP_SOUTHWEST_INNER:
return true;
default:
return false;
}
}
+8 -34
View File
@@ -6,41 +6,15 @@
*/ */
#pragma once #pragma once
#include "rpg/entity/entitydir.h" #include "tileshape.h"
typedef struct {
tileshape_t shape;
typedef enum { // Local Z layer (0..CHUNK_DEPTH-1) this tile occupies within its chunk's
TILE_SHAPE_NULL = 0, // depth slab. Used for entity navigation since chunk_t only stores one
// tile per X/Y column rather than one per X/Y/Z.
TILE_SHAPE_GROUND = 1, uint8_t z;
TILE_SHAPE_RAMP_NORTH = 2,
TILE_SHAPE_RAMP_EAST = 3,
TILE_SHAPE_RAMP_SOUTH = 4,
TILE_SHAPE_RAMP_WEST = 5,
TILE_SHAPE_RAMP_NORTHEAST = 6,
TILE_SHAPE_RAMP_NORTHWEST = 7,
TILE_SHAPE_RAMP_SOUTHEAST = 8,
TILE_SHAPE_RAMP_SOUTHWEST = 9,
TILE_SHAPE_RAMP_NORTHEAST_INNER = 10,
TILE_SHAPE_RAMP_NORTHWEST_INNER = 11,
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12,
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13,
TILE_SHAPE_COUNT
} tile_t; } tile_t;
/** #define TILE_NULL ((tile_t){ .shape = TILE_SHAPE_NULL, .z = 0 })
* Returns whether or not the given tile is walkable.
*
* @param tile The tile to check.
* @return bool_t True if walkable, false if not.
*/
bool_t tileIsWalkable(const tile_t tile);
/**
* Returns whether or not the given tile is a ramp tile.
*
* @param tile The tile to check.
* @return bool_t True if ramp, false if not.
*/
bool_t tileIsRamp(const tile_t tile);
+39
View File
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "tileshape.h"
bool_t tileShapeIsWalkable(const tileshape_t shape) {
switch(shape) {
case TILE_SHAPE_NULL:
return false;
default:
return true;
}
}
bool_t tileShapeIsRamp(const tileshape_t shape) {
switch(shape) {
case TILE_SHAPE_RAMP_NORTH:
case TILE_SHAPE_RAMP_SOUTH:
case TILE_SHAPE_RAMP_EAST:
case TILE_SHAPE_RAMP_WEST:
case TILE_SHAPE_RAMP_NORTHEAST:
case TILE_SHAPE_RAMP_NORTHWEST:
case TILE_SHAPE_RAMP_SOUTHEAST:
case TILE_SHAPE_RAMP_SOUTHWEST:
case TILE_SHAPE_RAMP_NORTHEAST_INNER:
case TILE_SHAPE_RAMP_NORTHWEST_INNER:
case TILE_SHAPE_RAMP_SOUTHEAST_INNER:
case TILE_SHAPE_RAMP_SOUTHWEST_INNER:
return true;
default:
return false;
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitydir.h"
typedef enum {
TILE_SHAPE_NULL = 0,
TILE_SHAPE_GROUND = 1,
TILE_SHAPE_RAMP_NORTH = 2,
TILE_SHAPE_RAMP_EAST = 3,
TILE_SHAPE_RAMP_SOUTH = 4,
TILE_SHAPE_RAMP_WEST = 5,
TILE_SHAPE_RAMP_NORTHEAST = 6,
TILE_SHAPE_RAMP_NORTHWEST = 7,
TILE_SHAPE_RAMP_SOUTHEAST = 8,
TILE_SHAPE_RAMP_SOUTHWEST = 9,
TILE_SHAPE_RAMP_NORTHEAST_INNER = 10,
TILE_SHAPE_RAMP_NORTHWEST_INNER = 11,
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12,
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13,
TILE_SHAPE_COUNT
} tileshape_t;
/**
* Returns whether or not the given tile shape is a ramp.
*
* @param shape The tile shape to check.
* @return bool_t True if ramp, false if not.
*/
bool_t tileShapeIsRamp(const tileshape_t shape);
/**
* Returns whether or not the given tile shape is walkable.
*
* @param shape The tile shape to check.
* @return bool_t True if walkable, false if not.
*/
bool_t tileShapeIsWalkable(const tileshape_t shape);
+13 -12
View File
@@ -47,7 +47,7 @@ void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out) {
chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) { chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
assertNotNull(worldPos, "World position pointer cannot be NULL"); assertNotNull(worldPos, "World position pointer cannot be NULL");
uint8_t localX, localY, localZ; uint8_t localX, localY;
if(worldPos->x < 0) { if(worldPos->x < 0) {
localX = (uint8_t)( localX = (uint8_t)(
(CHUNK_WIDTH - 1) - ((-worldPos->x - 1) % CHUNK_WIDTH) (CHUNK_WIDTH - 1) - ((-worldPos->x - 1) % CHUNK_WIDTH)
@@ -64,18 +64,8 @@ chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
localY = (uint8_t)(worldPos->y % CHUNK_HEIGHT); localY = (uint8_t)(worldPos->y % CHUNK_HEIGHT);
} }
if(worldPos->z < 0) {
localZ = (uint8_t)(
(CHUNK_DEPTH - 1) - ((-worldPos->z - 1) % CHUNK_DEPTH)
);
} else {
localZ = (uint8_t)(worldPos->z % CHUNK_DEPTH);
}
chunktileindex_t chunkTileIndex = (chunktileindex_t)( chunktileindex_t chunkTileIndex = (chunktileindex_t)(
(localZ * CHUNK_WIDTH * CHUNK_HEIGHT) + (localY * CHUNK_WIDTH) + localX
(localY * CHUNK_WIDTH) +
localX
); );
assertTrue( assertTrue(
chunkTileIndex < CHUNK_TILE_COUNT, chunkTileIndex < CHUNK_TILE_COUNT,
@@ -84,6 +74,17 @@ chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) {
return chunkTileIndex; return chunkTileIndex;
} }
uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos) {
assertNotNull(worldPos, "World position pointer cannot be NULL");
if(worldPos->z < 0) {
return (uint8_t)(
(CHUNK_DEPTH - 1) - ((-worldPos->z - 1) % CHUNK_DEPTH)
);
}
return (uint8_t)(worldPos->z % CHUNK_DEPTH);
}
chunkindex_t chunkPosToIndex(const chunkpos_t* pos) { chunkindex_t chunkPosToIndex(const chunkpos_t* pos) {
assertNotNull(pos, "Chunk position pointer cannot be NULL"); assertNotNull(pos, "Chunk position pointer cannot be NULL");
+17 -2
View File
@@ -19,9 +19,11 @@
#define CHUNK_WIDTH 16 #define CHUNK_WIDTH 16
#define CHUNK_HEIGHT 16 #define CHUNK_HEIGHT 16
#define CHUNK_DEPTH 4 #define CHUNK_DEPTH 8
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) // Chunks store one tile per X/Y column, not one per X/Y/Z - each tile
// records its own local Z layer (see tile_t) instead.
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT)
#define MAP_CHUNK_WIDTH 3 #define MAP_CHUNK_WIDTH 3
#define MAP_CHUNK_HEIGHT 3 #define MAP_CHUNK_HEIGHT 3
@@ -48,6 +50,10 @@ typedef uint32_t chunktileindex_t;
typedef int32_t worldunits_t; typedef int32_t worldunits_t;
typedef int32_t chunkunits_t; typedef int32_t chunkunits_t;
typedef struct worldpos2d_s {
worldunit_t x, y;
} worldpos2d_t;
typedef struct worldpos_s { typedef struct worldpos_s {
worldunit_t x, y, z; worldunit_t x, y, z;
} worldpos_t; } worldpos_t;
@@ -90,6 +96,15 @@ void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out);
*/ */
chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos); chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos);
/**
* Converts a world-space Z coordinate to the local Z layer (0..CHUNK_DEPTH-1)
* within the chunk that owns it, for comparison against tile_t.z.
*
* @param worldPos The world position.
* @return The local Z layer within the owning chunk.
*/
uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos);
/** /**
* Converts a chunk position to a world position. * Converts a chunk position to a world position.
* *
+100 -41
View File
@@ -5,7 +5,7 @@
""" """
Generates DCF + companion DMF files from raw chunk JSON files, or upgrades 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): 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 Mesh files are located by searching under assets/meshes/ and referenced by
path from the assets root in the DCF. path from the assets root in the DCF.
Output DCF is derived automatically: Output DCF is derived automatically:
assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf 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 uint8_t meshCount
for each model: for each model:
null-terminated string (relative asset path to .json model) null-terminated string (relative asset path to .json model)
@@ -40,7 +47,7 @@ DMF format:
Usage: Usage:
python3 -m tools.asset.chunk # process all assetsraw/chunks/*.json 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.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 import json
@@ -56,8 +63,10 @@ ASSETS_DIR = os.path.join(PROJECT_ROOT, 'assets')
CHUNK_WIDTH = 16 CHUNK_WIDTH = 16
CHUNK_HEIGHT = 16 CHUNK_HEIGHT = 16
CHUNK_DEPTH = 4 CHUNK_DEPTH = 8
CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 1024 # 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. # 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. # 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_COUNT_MAX = 10
CHUNK_MESH_NAME_MAX = 64 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 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_NULL = 0
TILE_SHAPE_GROUND = 1 TILE_SHAPE_GROUND = 1
TILE_SHAPE_RAMP_NORTH = 2 TILE_SHAPE_RAMP_NORTH = 2
@@ -86,7 +106,7 @@ TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
FILE_MAGIC = b'DCF' FILE_MAGIC = b'DCF'
DMF_MAGIC = b'DMF\x00' DMF_MAGIC = b'DMF\x00'
VERSION_OUT = 3 VERSION_OUT = 4
DMF_VERSION = 1 DMF_VERSION = 1
@@ -94,8 +114,8 @@ DMF_VERSION = 1
# Helpers # Helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def tile_index(x, y, z): def tile_index(x, y):
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT return x + y * CHUNK_WIDTH
def find_model(filename): 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') print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
def write_v3(dcf_path, tiles, mesh_names, mesh_offsets=None): def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
"""Write a v3 DCF referencing the given DMF asset paths.""" """Write a current-version DCF referencing the given DMF asset paths."""
mesh_count = len(mesh_names) mesh_count = len(mesh_names)
if mesh_offsets is None: if mesh_offsets is None:
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
@@ -218,29 +238,28 @@ _RAMP_CORNERS = {
def build_terrain_verts(tiles_bytes): def build_terrain_verts(tiles_bytes):
"""Generate quads for every non-null tile, shaped to match tile type.""" """Generate quads for every non-null tile, shaped to match tile type."""
buf = bytearray() buf = bytearray()
for z in range(CHUNK_DEPTH): for y in range(CHUNK_HEIGHT):
for y in range(CHUNK_HEIGHT): for x in range(CHUNK_WIDTH):
for x in range(CHUNK_WIDTH): idx = tile_index(x, y)
idx = tile_index(x, y, z) tile_type, z = struct.unpack_from(
tile_type = struct.unpack_from( '<IB', tiles_bytes, idx * TILE_SIZE
'<I', tiles_bytes, idx * TILE_SIZE )
)[0] corners = _RAMP_CORNERS.get(tile_type)
corners = _RAMP_CORNERS.get(tile_type) if corners is None:
if corners is None: continue
continue fx, fy, fz = float(x), float(y), float(z)
fx, fy, fz = float(x), float(y), float(z) u0 = x / CHUNK_WIDTH
u0 = x / CHUNK_WIDTH u1 = (x + 1) / CHUNK_WIDTH
u1 = (x + 1) / CHUNK_WIDTH v0 = y / CHUNK_HEIGHT
v0 = y / CHUNK_HEIGHT v1 = (y + 1) / CHUNK_HEIGHT
v1 = (y + 1) / CHUNK_HEIGHT sw, se, ne, nw = corners
sw, se, ne, nw = corners buf += _tile_quad(
buf += _tile_quad( u0, u1, v0, v1, fx, fy,
u0, u1, v0, v1, fx, fy, (fz + sw) * WORLD_LAYER_HEIGHT,
(fz + sw) * WORLD_LAYER_HEIGHT, (fz + se) * WORLD_LAYER_HEIGHT,
(fz + se) * WORLD_LAYER_HEIGHT, (fz + ne) * WORLD_LAYER_HEIGHT,
(fz + ne) * WORLD_LAYER_HEIGHT, (fz + nw) * WORLD_LAYER_HEIGHT
(fz + nw) * WORLD_LAYER_HEIGHT )
)
return bytes(buf) return bytes(buf)
@@ -249,11 +268,20 @@ def from_json(json_path, dcf_path):
data = json.load(f) data = json.load(f)
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE) tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
column_z = {}
for tile in data.get('tiles', []): for tile in data.get('tiles', []):
pos = tile['pos'] pos = tile['pos']
x, y, z = int(pos[0]), int(pos[1]), int(pos[2]) 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( 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) 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]))) mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
print(f' Resolved {filename} -> {rel}') 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): 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): def read_legacy_dcf(path):
with open(path, 'rb') as f: with open(path, 'rb') as f:
data = f.read() data = f.read()
@@ -319,8 +378,8 @@ def read_legacy_dcf(path):
raise ValueError(f"{path}: expected version 1 or 2, got {version}") raise ValueError(f"{path}: expected version 1 or 2, got {version}")
offset = 8 offset = 8
tiles_size = CHUNK_TILE_COUNT * TILE_SIZE tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
tiles = data[offset:offset + tiles_size] tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
offset += tiles_size offset += tiles_size
meshes = [] meshes = []
@@ -362,7 +421,7 @@ def upgrade_dcf(path):
dmf_name = f'chunk_{base}_{idx}.dmf' dmf_name = f'chunk_{base}_{idx}.dmf'
write_dmf(os.path.join(terrain_dir, dmf_name), verts) write_dmf(os.path.join(terrain_dir, dmf_name), verts)
mesh_names.append(f'meshes/chunks/{dmf_name}') mesh_names.append(f'meshes/chunks/{dmf_name}')
write_v3(path, tiles, mesh_names) write_dcf(path, tiles, mesh_names)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------