5 Commits

Author SHA1 Message Date
YourWishes a6f449bb93 example building 2 2026-06-30 16:21:47 -05:00
YourWishes 0614bfc446 example building 2026-06-30 16:21:46 -05:00
YourWishes bb020c36c1 CHUNK STUFF 2026-06-27 17:19:44 -05:00
YourWishes f17b0bfcfb Tiles 2026-06-27 08:50:55 -05:00
YourWishes 2a85c9503f npc interact turn to face player 2026-06-27 06:30:45 -05:00
35 changed files with 3004 additions and 227 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+1
View File
@@ -16,3 +16,4 @@ add_subdirectory(display)
add_subdirectory(locale) add_subdirectory(locale)
add_subdirectory(json) add_subdirectory(json)
add_subdirectory(chunk) add_subdirectory(chunk)
add_subdirectory(dmf)
+6
View File
@@ -45,4 +45,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
.loadAsync = assetChunkLoaderAsync, .loadAsync = assetChunkLoaderAsync,
.dispose = assetChunkDispose .dispose = assetChunkDispose
}, },
[ASSET_LOADER_TYPE_DMF] = {
.loadSync = assetDmfLoaderSync,
.loadAsync = assetDmfLoaderAsync,
.dispose = assetDmfDispose
},
}; };
+5
View File
@@ -12,6 +12,7 @@
#include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h" #include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/chunk/assetchunkloader.h" #include "asset/loader/chunk/assetchunkloader.h"
#include "asset/loader/dmf/assetdmfloader.h"
typedef enum { typedef enum {
ASSET_LOADER_TYPE_NULL, ASSET_LOADER_TYPE_NULL,
@@ -22,6 +23,7 @@ typedef enum {
ASSET_LOADER_TYPE_LOCALE, ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON, ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_CHUNK, ASSET_LOADER_TYPE_CHUNK,
ASSET_LOADER_TYPE_DMF,
ASSET_LOADER_TYPE_COUNT ASSET_LOADER_TYPE_COUNT
} assetloadertype_t; } assetloadertype_t;
@@ -33,6 +35,7 @@ typedef union {
assetlocaleloaderinput_t locale; assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json; assetjsonloaderinput_t json;
assetchunkloaderinput_t chunk; assetchunkloaderinput_t chunk;
assetdmfloaderinput_t dmf;
} assetloaderinput_t; } assetloaderinput_t;
typedef union { typedef union {
@@ -42,6 +45,7 @@ typedef union {
assetlocaleloaderloading_t locale; assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json; assetjsonloaderloading_t json;
assetchunkloaderloading_t chunk; assetchunkloaderloading_t chunk;
assetdmfloaderloading_t dmf;
} assetloaderloading_t; } assetloaderloading_t;
typedef union { typedef union {
@@ -51,6 +55,7 @@ typedef union {
assetlocaleoutput_t locale; assetlocaleoutput_t locale;
assetjsonoutput_t json; assetjsonoutput_t json;
assetchunkoutput_t chunk; assetchunkoutput_t chunk;
assetdmfoutput_t dmf;
} assetloaderoutput_t; } assetloaderoutput_t;
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
+19 -8
View File
@@ -85,19 +85,30 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
memoryCopy(out->tiles, data + offset, tileSize); memoryCopy(out->tiles, data + offset, tileSize);
offset += tileSize; offset += tileSize;
out->vertCount = endianLittleToHost32(*(uint32_t *)(data + offset)); out->meshCount = data[offset];
offset += sizeof(uint32_t); offset += sizeof(uint8_t);
assertTrue( assertTrue(
out->vertCount <= CHUNK_VERTEX_COUNT, out->meshCount <= CHUNK_MESH_COUNT_MAX,
"Chunk vertex count exceeds maximum." "Chunk mesh count exceeds maximum."
); );
for(uint8_t m = 0; m < out->meshCount; m++) {
uint8_t nameLen = 0;
while(
data[offset + nameLen] != '\0' &&
nameLen < CHUNK_MESH_NAME_MAX - 1
) {
nameLen++;
}
memoryCopy(out->meshNames[m], data + offset, nameLen);
out->meshNames[m][nameLen] = '\0';
offset += nameLen + 1;
memoryCopy( memoryCopy(
out->vertices, out->meshOffsets[m], data + offset, sizeof(vec3)
data + offset,
out->vertCount * sizeof(meshvertex_t)
); );
offset += sizeof(vec3);
}
memoryFree(data); memoryFree(data);
loading->loading.chunk.data = NULL; loading->loading.chunk.data = NULL;
@@ -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 1 #define ASSET_CHUNK_FILE_VERSION 3
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -33,8 +33,9 @@ typedef struct {
typedef struct { typedef struct {
tile_t tiles[CHUNK_TILE_COUNT]; tile_t tiles[CHUNK_TILE_COUNT];
uint32_t vertCount; uint8_t meshCount;
meshvertex_t vertices[CHUNK_VERTEX_COUNT]; char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
} assetchunkoutput_t; } assetchunkoutput_t;
/** /**
@@ -49,7 +50,8 @@ errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/** /**
* Synchronous loader for chunk assets. Validates the DCF binary previously * Synchronous loader for chunk assets. Validates the DCF binary previously
* read by the async phase and populates the output assetchunkoutput_t. * read by the async phase and populates the output assetchunkoutput_t with
* tile data and DMF mesh names.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetdmfloader.c
)
+136
View File
@@ -0,0 +1,136 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetdmfloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetDmfLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.dmf.state != ASSET_DMF_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.dmf.data, "Data already defined?");
assetfile_t *file = &loading->loading.dmf.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
assertTrue(
file->lastRead == file->size,
"Failed to read entire DMF file."
);
loading->loading.dmf.data = data;
loading->loading.dmf.state = ASSET_DMF_LOADING_STATE_CREATE_MESH;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetDmfLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_DMF, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.dmf.state) {
case ASSET_DMF_LOADING_STATE_INITIAL:
loading->loading.dmf.state = ASSET_DMF_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_DMF_LOADING_STATE_CREATE_MESH:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.dmf.data;
assertNotNull(data, "DMF data should have been loaded by now.");
if(data[0] != 'D' || data[1] != 'M' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid DMF file header");
}
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
if(version != ASSET_DMF_FILE_VERSION) {
memoryFree(data);
assetLoaderErrorThrow(
loading, "Unsupported DMF version %u", version
);
}
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(data + 8));
assetdmfoutput_t *out = &loading->entry->data.dmf;
if(vertCount == 0) {
memoryFree(data);
loading->loading.dmf.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
out->vertices = memoryAllocate(vertCount * sizeof(meshvertex_t));
memoryCopy(
out->vertices, data + 12, vertCount * sizeof(meshvertex_t)
);
memoryFree(data);
loading->loading.dmf.data = NULL;
errorret_t ret = meshInit(
&out->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
(int32_t)vertCount,
out->vertices
);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh);
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetDmfDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_DMF, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetdmfoutput_t *out = &entry->data.dmf;
if(out->vertices != NULL) {
errorChain(meshDispose(&out->mesh));
memoryFree(out->vertices);
out->vertices = NULL;
}
errorOk();
}
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/mesh/mesh.h"
#define ASSET_DMF_FILE_VERSION 1
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assetdmfloaderinput_t;
typedef enum {
ASSET_DMF_LOADING_STATE_INITIAL,
ASSET_DMF_LOADING_STATE_READ_FILE,
ASSET_DMF_LOADING_STATE_CREATE_MESH,
ASSET_DMF_LOADING_STATE_DONE
} assetdmfloadingstate_t;
typedef struct {
assetfile_t file;
assetdmfloadingstate_t state;
uint8_t *data;
} assetdmfloaderloading_t;
typedef struct {
mesh_t mesh;
meshvertex_t *vertices;
} assetdmfoutput_t;
/**
* Asynchronous loader for DMF mesh assets. Reads the raw DMF file bytes
* into the loading buffer so the sync phase can parse without blocking
* the main thread on I/O.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure.
*/
errorret_t assetDmfLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for DMF mesh assets. Parses the DMF binary read by
* the async phase, then initializes and flushes the mesh to the GPU.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure.
*/
errorret_t assetDmfLoaderSync(assetloading_t *loading);
/**
* Disposer for DMF mesh assets. Disposes the mesh and frees the vertex
* buffer.
*
* @param entry Asset entry containing the DMF data to dispose.
* @return Error code indicating success or failure.
*/
errorret_t assetDmfDispose(assetentry_t *entry);
+1 -1
View File
@@ -21,7 +21,7 @@ add_subdirectory(texture)
# Color definitions # Color definitions
dusk_run_python( dusk_run_python(
dusk_color_defs dusk_color_defs
tools.color.csv tools.color
--csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv --csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h --output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
) )
+1 -1
View File
@@ -14,7 +14,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Input Action Definitions # Input Action Definitions
dusk_run_python( dusk_run_python(
dusk_input_csv_defs dusk_input_csv_defs
tools.input.csv tools.input
--csv ${CMAKE_CURRENT_SOURCE_DIR}/input.csv --csv ${CMAKE_CURRENT_SOURCE_DIR}/input.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/input/inputactiondefs.h --output ${DUSK_GENERATED_HEADERS_DIR}/input/inputactiondefs.h
) )
-3
View File
@@ -43,9 +43,6 @@ void entityUpdate(entity_t *entity) {
entityAnimUpdate(entity); entityAnimUpdate(entity);
// Movement code. // Movement code.
if(ENTITY_CALLBACKS[entity->type].freeMovement != NULL) {
ENTITY_CALLBACKS[entity->type].freeMovement(entity);
}
if( if(
cutsceneModeIsInputAllowed() && cutsceneModeIsInputAllowed() &&
ENTITY_CALLBACKS[entity->type].movement != NULL ENTITY_CALLBACKS[entity->type].movement != NULL
-16
View File
@@ -37,20 +37,6 @@ typedef struct {
* @param entity Pointer to the entity to move. * @param entity Pointer to the entity to move.
*/ */
void (*movement)(entity_t *entity); void (*movement)(entity_t *entity);
/**
* Free movement callback. Always runs regardless of cutscene state.
* @param entity Pointer to the entity to move.
*/
void (*freeMovement)(entity_t *entity);
/**
* Interaction callback for the entity type.
* @param player Pointer to the player entity.
* @param entity Pointer to the entity to interact with.
* @return True if the entity handled the interaction, false otherwise.
*/
bool_t (*interact)(entity_t *player, entity_t *entity);
} entitycallback_t; } entitycallback_t;
static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = { static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
@@ -64,7 +50,5 @@ static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
[ENTITY_TYPE_NPC] = { [ENTITY_TYPE_NPC] = {
.init = npcInit, .init = npcInit,
.movement = npcMovement, .movement = npcMovement,
.freeMovement = npcFreeMovement,
.interact = npcInteract
} }
}; };
+11 -5
View File
@@ -21,11 +21,20 @@ void entityInteractWith(entity_t *player, entity_t *target) {
"Interact cutscene pointer cannot be NULL" "Interact cutscene pointer cannot be NULL"
); );
cutsceneSystemStartCutscene(target->interact.data.cutscene); cutsceneSystemStartCutscene(target->interact.data.cutscene);
return; break;
case ENTITY_INTERACT_PRINT: case ENTITY_INTERACT_PRINT:
uiTextboxMainSetText(target->interact.data.message); uiTextboxMainSetText(target->interact.data.message);
return;
// If NPC turn to face player.
if(target->type == ENTITY_TYPE_NPC) {
target->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING;
target->animation = ENTITY_ANIM_IDLE;
entityTurn(target, entityDirGetOpposite(player->direction));
}
// entityTurn(player, player->direction); // Redundant (for now)
break;
case ENTITY_INTERACT_NULL: case ENTITY_INTERACT_NULL:
break; break;
@@ -34,7 +43,4 @@ void entityInteractWith(entity_t *player, entity_t *target) {
assertUnreachable("Unknown entity interact type"); assertUnreachable("Unknown entity interact type");
break; break;
} }
if(ENTITY_CALLBACKS[target->type].interact == NULL) return;
ENTITY_CALLBACKS[target->type].interact(player, target);
} }
@@ -16,8 +16,11 @@ typedef struct entity_s entity_t;
*/ */
typedef enum { typedef enum {
ENTITY_INTERACT_NULL = 0, ENTITY_INTERACT_NULL = 0,
ENTITY_INTERACT_CUTSCENE, ENTITY_INTERACT_CUTSCENE,
ENTITY_INTERACT_PRINT, ENTITY_INTERACT_PRINT,
ENTITY_INTERACT_CALLBACK,
ENTITY_INTERACT_COUNT ENTITY_INTERACT_COUNT
} entityinteracttype_t; } entityinteracttype_t;
@@ -27,6 +30,7 @@ typedef enum {
typedef union { typedef union {
const cutscene_t *cutscene; const cutscene_t *cutscene;
const char_t *message; const char_t *message;
void (*callback)(entity_t *player, entity_t *target);
} entityinteractdata_t; } entityinteractdata_t;
/** /**
+17 -20
View File
@@ -12,17 +12,27 @@
#include "rpg/rpgtextbox.h" #include "rpg/rpgtextbox.h"
const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT] = { const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT] = {
[NPC_MOVE_TYPE_NULL] = { NULL, NULL }, [NPC_MOVE_TYPE_NULL] = { 0 },
[NPC_MOVE_TYPE_RANDOM_TURN] = { npcRandomTurnInit, npcRandomTurnMovement },
[NPC_MOVE_TYPE_RANDOM_TURN] = {
npcRandomTurnInit,
npcRandomTurnMovement
},
[NPC_MOVE_TYPE_RANDOM_WALK] = { [NPC_MOVE_TYPE_RANDOM_WALK] = {
npcRandomWalkInit, npcRandomWalkInit,
npcRandomWalkMovement npcRandomWalkMovement
}, },
[NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK] = { [NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK] = {
npcRandomTurnAndWalkInit, npcRandomTurnAndWalkInit,
npcRandomTurnAndWalkMovement npcRandomTurnAndWalkMovement
}, },
[NPC_MOVE_TYPE_PATH] = { npcPathInit, npcPathMovement, true },
[NPC_MOVE_TYPE_PATH] = {
npcPathInit,
npcPathMovement
},
}; };
void npcInit(entity_t *entity) { void npcInit(entity_t *entity) {
@@ -40,23 +50,10 @@ void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) {
void npcMovement(entity_t *entity) { void npcMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL"); assertNotNull(entity, "Entity pointer cannot be NULL");
npc_t *npc = &entity->data.npc; npc_t *npc = &entity->data.npc;
if(npc->interactState != NPC_INTERACT_STATE_NONE) return;
const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType]; const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType];
if(!cb->alwaysRun && cb->movement != NULL) cb->movement(entity); if(cb->movement != NULL) cb->movement(entity);
}
void npcFreeMovement(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
npc_t *npc = &entity->data.npc;
const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType];
if(cb->alwaysRun && cb->movement != NULL) cb->movement(entity);
}
bool_t npcInteract(entity_t *player, entity_t *npc) {
assertNotNull(player, "Player entity pointer cannot be NULL");
assertNotNull(npc, "NPC entity pointer cannot be NULL");
cutsceneSystemStartCutscene(&TEST_CUTSCENE);
// rpgTextboxShow(RPG_TEXTBOX_POS_BOTTOM, "Hello World!");
return false;
} }
+7 -10
View File
@@ -13,6 +13,12 @@
typedef struct entity_s entity_t; typedef struct entity_s entity_t;
typedef enum {
NPC_INTERACT_STATE_NONE,
NPC_INTERACT_STATE_CONVERSING,
NPC_INTERACT_STATE_COUNT
} npcinteractstate_t;
typedef enum { typedef enum {
NPC_MOVE_TYPE_NULL, NPC_MOVE_TYPE_NULL,
NPC_MOVE_TYPE_RANDOM_TURN, NPC_MOVE_TYPE_RANDOM_TURN,
@@ -30,6 +36,7 @@ typedef union {
} npcmovedata_t; } npcmovedata_t;
typedef struct npc_s { typedef struct npc_s {
npcinteractstate_t interactState;
npcmovetype_t moveType; npcmovetype_t moveType;
npcmovedata_t moveData; npcmovedata_t moveData;
} npc_t; } npc_t;
@@ -39,8 +46,6 @@ typedef struct {
void (*init)(npc_t *npc); void (*init)(npc_t *npc);
/** Called each movement tick. */ /** Called each movement tick. */
void (*movement)(entity_t *entity); void (*movement)(entity_t *entity);
/** True if movement runs regardless of cutscene state. */
bool_t alwaysRun;
} npcmovecallback_t; } npcmovecallback_t;
extern const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT]; extern const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT];
@@ -74,11 +79,3 @@ void npcMovement(entity_t *entity);
* @param entity Pointer to the entity structure to update. * @param entity Pointer to the entity structure to update.
*/ */
void npcFreeMovement(entity_t *entity); void npcFreeMovement(entity_t *entity);
/**
* Handles interaction with an NPC entity.
*
* @param player Pointer to the player entity.
* @param npc Pointer to the NPC entity.
*/
bool_t npcInteract(entity_t *player, entity_t *npc);
+1 -1
View File
@@ -13,7 +13,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Item Definitions # Item Definitions
dusk_run_python( dusk_run_python(
dusk_item_csv_defs dusk_item_csv_defs
tools.item.csv tools.item
--csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv --csv ${CMAKE_CURRENT_SOURCE_DIR}/item.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/item.h
) )
+9 -11
View File
@@ -1,5 +1,5 @@
/** /**
* Copyright (c) 2025 Dominic Masters * Copyright (c) 2026 Dominic Masters
* *
* This software is released under the MIT License. * This software is released under the MIT License.
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
@@ -8,24 +8,22 @@
#pragma once #pragma once
#include "rpg/overworld/tile.h" #include "rpg/overworld/tile.h"
#include "worldpos.h" #include "worldpos.h"
#include "display/mesh/quad.h"
#include "display/spritebatch/spritebatch.h"
// #define CHUNK_MESH_COUNT_MAX 3 #define CHUNK_MESH_COUNT_MAX 10
#define CHUNK_VERTEX_COUNT (QUAD_VERTEX_COUNT * CHUNK_WIDTH * CHUNK_HEIGHT * 2) #define CHUNK_MESH_NAME_MAX 64
#define CHUNK_ENTITY_COUNT_MAX 10 #define CHUNK_ENTITY_COUNT_MAX 10
typedef struct assetentry_s assetentry_t;
typedef struct chunk_s { typedef struct chunk_s {
chunkpos_t position; chunkpos_t position;
tile_t tiles[CHUNK_TILE_COUNT]; tile_t tiles[CHUNK_TILE_COUNT];
meshvertex_t vertices[CHUNK_VERTEX_COUNT]; uint8_t meshCount;
uint32_t vertCount; char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
mesh_t mesh; vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
assetentry_t *meshEntries[CHUNK_MESH_COUNT_MAX];
// uint8_t meshCount;
// meshvertex_t vertices[CHUNK_VERTEX_COUNT_MAX];
// mesh_t meshes[CHUNK_MESH_COUNT_MAX];
uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
} chunk_t; } chunk_t;
+43 -91
View File
@@ -9,6 +9,7 @@
#include "util/memory.h" #include "util/memory.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "asset/asset.h" #include "asset/asset.h"
#include "asset/loader/assetloader.h"
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "util/string.h" #include "util/string.h"
@@ -17,18 +18,6 @@ map_t MAP;
errorret_t mapInit() { errorret_t mapInit() {
memoryZero(&MAP, sizeof(map_t)); memoryZero(&MAP, sizeof(map_t));
// Setup chunk meshes
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
errorChain(meshInit(
&chunk->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
CHUNK_VERTEX_COUNT,
chunk->vertices
));
}
// Perform "initial load"
MAP.loaded = true; MAP.loaded = true;
int32_t i = 0; int32_t i = 0;
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) { for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
@@ -52,61 +41,6 @@ bool_t mapIsLoaded() {
return MAP.loaded; return MAP.loaded;
} }
// errorret_t mapLoad(const char_t *path, const chunkpos_t position) {
// assertStrLenMin(path, 1, "Map file path cannot be empty");
// assertStrLenMax(path, MAP_FILE_PATH_MAX - 1, "Map file path too long");
// if(stringCompare(MAP.filePath, path) == 0) {
// // Same map, no need to reload
// errorOk();
// }
// chunkindex_t i;
// // Unload all loaded chunks
// if(mapIsLoaded()) {
// for(i = 0; i < MAP_CHUNK_COUNT; i++) {
// mapChunkUnload(&MAP.chunks[i]);
// }
// }
// // Store the map file path
// stringCopy(MAP.filePath, path, MAP_FILE_PATH_MAX);
// // Determine directory path (it is dirname)
// stringCopy(MAP.dirPath, path, MAP_FILE_PATH_MAX);
// char_t *last = stringFindLastChar(MAP.dirPath, '/');
// if(last == NULL) errorThrow("Invalid map file path");
// // Store filename, sans extension
// stringCopy(MAP.fileName, last + 1, MAP_FILE_PATH_MAX);
// *last = '\0'; // Terminate to get directory path
// last = stringFindLastChar(MAP.fileName, '.');
// if(last == NULL) errorThrow("Map file name has no extension");
// *last = '\0'; // Terminate to remove extension
// // Reset map position
// MAP.chunkPosition = position;
// // Perform "initial load"
// i = 0;
// for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
// for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
// for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
// chunk_t *chunk = &MAP.chunks[i];
// chunk->position.x = x + position.x;
// chunk->position.y = y + position.y;
// chunk->position.z = z + position.z;
// MAP.chunkOrder[i] = chunk;
// errorChain(mapChunkLoad(chunk));
// i++;
// }
// }
// }
// errorOk();
// }
errorret_t mapPositionSet(const chunkpos_t newPos) { errorret_t mapPositionSet(const chunkpos_t newPos) {
if(!mapIsLoaded()) errorThrow("No map loaded"); if(!mapIsLoaded()) errorThrow("No map loaded");
@@ -115,7 +49,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
errorOk(); errorOk();
} }
// Determine which chunks remain loaded
chunkindex_t chunksRemaining[MAP_CHUNK_COUNT] = {0}; chunkindex_t chunksRemaining[MAP_CHUNK_COUNT] = {0};
chunkindex_t chunksFreed[MAP_CHUNK_COUNT] = {0}; chunkindex_t chunksFreed[MAP_CHUNK_COUNT] = {0};
@@ -123,7 +56,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
uint32_t freedCount = 0; uint32_t freedCount = 0;
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
// Will this chunk remain loaded?
chunk_t *chunk = &MAP.chunks[i]; chunk_t *chunk = &MAP.chunks[i];
if( if(
chunk->position.x >= newPos.x && chunk->position.x >= newPos.x &&
@@ -135,23 +67,18 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
chunk->position.z >= newPos.z && chunk->position.z >= newPos.z &&
chunk->position.z < newPos.z + MAP_CHUNK_DEPTH chunk->position.z < newPos.z + MAP_CHUNK_DEPTH
) { ) {
// Stays loaded
chunksRemaining[remainingCount++] = i; chunksRemaining[remainingCount++] = i;
continue; continue;
} }
// Not remaining loaded
chunksFreed[freedCount++] = i; chunksFreed[freedCount++] = i;
} }
// Unload the freed chunks
for(chunkindex_t i = 0; i < freedCount; i++) { for(chunkindex_t i = 0; i < freedCount; i++) {
chunk_t *chunk = &MAP.chunks[chunksFreed[i]]; chunk_t *chunk = &MAP.chunks[chunksFreed[i]];
mapChunkUnload(chunk); mapChunkUnload(chunk);
} }
// This can probably be optimized later, for now we check each chunk and see
// if it needs loading or not, and update the chunk order
chunkindex_t orderIndex = 0; chunkindex_t orderIndex = 0;
for(chunkunit_t zOff = 0; zOff < MAP_CHUNK_DEPTH; zOff++) { for(chunkunit_t zOff = 0; zOff < MAP_CHUNK_DEPTH; zOff++) {
for(chunkunit_t yOff = 0; yOff < MAP_CHUNK_HEIGHT; yOff++) { for(chunkunit_t yOff = 0; yOff < MAP_CHUNK_HEIGHT; yOff++) {
@@ -160,7 +87,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
newPos.x + xOff, newPos.y + yOff, newPos.z + zOff newPos.x + xOff, newPos.y + yOff, newPos.z + zOff
}; };
// Is this chunk already loaded (was not unloaded earlier)?
chunkindex_t chunkIndex = -1; chunkindex_t chunkIndex = -1;
for(chunkindex_t i = 0; i < remainingCount; i++) { for(chunkindex_t i = 0; i < remainingCount; i++) {
chunk_t *chunk = &MAP.chunks[chunksRemaining[i]]; chunk_t *chunk = &MAP.chunks[chunksRemaining[i]];
@@ -169,9 +95,7 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
break; break;
} }
// Need to load this chunk
if(chunkIndex == -1) { if(chunkIndex == -1) {
// Find a freed chunk to reuse
chunkIndex = chunksFreed[--freedCount]; chunkIndex = chunksFreed[--freedCount];
chunk_t *chunk = &MAP.chunks[chunkIndex]; chunk_t *chunk = &MAP.chunks[chunkIndex];
chunk->position = newChunkPos; chunk->position = newChunkPos;
@@ -183,7 +107,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) {
} }
} }
// Update map position
MAP.chunkPosition = newPos; MAP.chunkPosition = newPos;
errorOk(); errorOk();
@@ -196,7 +119,6 @@ void mapUpdate() {
errorret_t mapDispose() { errorret_t mapDispose() {
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
mapChunkUnload(&MAP.chunks[i]); mapChunkUnload(&MAP.chunks[i]);
errorChain(meshDispose(&MAP.chunks[i].mesh));
} }
errorOk(); errorOk();
} }
@@ -216,14 +138,20 @@ void mapChunkUnload(chunk_t* chunk) {
entity->type = ENTITY_TYPE_NULL; entity->type = ENTITY_TYPE_NULL;
} }
} }
chunk->vertCount = 0;
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
assetUnlockEntry(chunk->meshEntries[m]);
chunk->meshEntries[m] = NULL;
}
chunk->meshCount = 0;
} }
errorret_t mapChunkLoad(chunk_t *chunk) { errorret_t mapChunkLoad(chunk_t *chunk) {
if(!mapIsLoaded()) errorThrow("No map loaded"); if(!mapIsLoaded()) errorThrow("No map loaded");
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
chunk->vertCount = 0; chunk->meshCount = 0;
char_t name[64]; char_t name[64];
stringFormat( stringFormat(
@@ -248,19 +176,43 @@ errorret_t mapChunkLoad(chunk_t* chunk) {
return ret; return ret;
} }
memoryCopy( memoryCopy(chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles));
chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles) uint8_t meshCount = entry->data.chunk.meshCount;
); for(uint8_t m = 0; m < meshCount; m++) {
chunk->vertCount = entry->data.chunk.vertCount; stringCopy(
memoryCopy( chunk->meshNames[m],
chunk->vertices, entry->data.chunk.meshNames[m],
entry->data.chunk.vertices, CHUNK_MESH_NAME_MAX
chunk->vertCount * sizeof(meshvertex_t)
); );
glm_vec3_copy(entry->data.chunk.meshOffsets[m], chunk->meshOffsets[m]);
}
assetUnlockEntry(entry); assetUnlockEntry(entry);
if(chunk->vertCount == 0) errorOk(); for(uint8_t m = 0; m < meshCount; m++) {
errorChain(meshFlush(&chunk->mesh, 0, chunk->vertCount)); assetentry_t *meshEntry = assetLock(
chunk->meshNames[m], ASSET_LOADER_TYPE_DMF, NULL
);
if(meshEntry == NULL) {
for(uint8_t j = 0; j < m; j++) {
assetUnlockEntry(chunk->meshEntries[j]);
chunk->meshEntries[j] = NULL;
}
errorThrow("Failed to lock mesh: %s", chunk->meshNames[m]);
}
ret = assetRequireLoaded(meshEntry);
if(errorIsNotOk(ret)) {
assetUnlockEntry(meshEntry);
for(uint8_t j = 0; j < m; j++) {
assetUnlockEntry(chunk->meshEntries[j]);
chunk->meshEntries[j] = NULL;
}
return ret;
}
chunk->meshEntries[m] = meshEntry;
}
chunk->meshCount = meshCount;
errorOk(); errorOk();
} }
+1
View File
@@ -6,6 +6,7 @@
*/ */
#pragma once #pragma once
#include "error/error.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#define MAP_FILE_PATH_MAX 128 #define MAP_FILE_PATH_MAX 128
+8 -7
View File
@@ -35,6 +35,7 @@ errorret_t rpgInit(void) {
assertTrue(entIndex != 0xFF, "No available entity slots!."); assertTrue(entIndex != 0xFF, "No available entity slots!.");
entity_t *ent = &ENTITIES[entIndex]; entity_t *ent = &ENTITIES[entIndex];
entityInit(ent, ENTITY_TYPE_PLAYER); entityInit(ent, ENTITY_TYPE_PLAYER);
ent->position = (worldpos_t){ 10, 2, 0 };
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
RPG_CAMERA.followEntity.followEntityId = ent->id; RPG_CAMERA.followEntity.followEntityId = ent->id;
{ {
@@ -48,7 +49,7 @@ errorret_t rpgInit(void) {
entity_t *npc = &ENTITIES[npcIndex]; entity_t *npc = &ENTITIES[npcIndex];
entityInit(npc, ENTITY_TYPE_NPC); entityInit(npc, ENTITY_TYPE_NPC);
npcSetMoveType(npc, NPC_MOVE_TYPE_PATH); npcSetMoveType(npc, NPC_MOVE_TYPE_PATH);
npc->position = (worldpos_t){ 3, 3, 0 }; npc->position = (worldpos_t){ 8, 8, 0 };
npc->interact.type = ENTITY_INTERACT_PRINT; npc->interact.type = ENTITY_INTERACT_PRINT;
npc->interact.data.message = "hello world"; npc->interact.data.message = "hello world";
{ {
@@ -58,12 +59,12 @@ errorret_t rpgInit(void) {
if(ci != -1) entitySetChunk(npc, (uint8_t)ci); if(ci != -1) entitySetChunk(npc, (uint8_t)ci);
} }
npcpath_t *path = &npc->data.npc.moveData.path; // npcpath_t *path = &npc->data.npc.moveData.path;
path->positions[0] = (worldpos_t){ 3, 3, 0 }; // path->positions[0] = (worldpos_t){ 3, 3, 0 };
path->positions[1] = (worldpos_t){ 10, 3, 0 }; // path->positions[1] = (worldpos_t){ 10, 3, 0 };
path->positions[2] = (worldpos_t){ 10, 10, 0 }; // path->positions[2] = (worldpos_t){ 10, 10, 0 };
path->positions[3] = (worldpos_t){ 3, 10, 0 }; // path->positions[3] = (worldpos_t){ 3, 10, 0 };
path->count = 4; // path->count = 4;
// All Good! // All Good!
errorOk(); errorOk();
+1 -1
View File
@@ -12,7 +12,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
# Story Flag Definitions # Story Flag Definitions
dusk_run_python( dusk_run_python(
dusk_story_defs dusk_story_defs
tools.story.csv tools.story
--csv ${CMAKE_CURRENT_SOURCE_DIR}/storyflag.csv --csv ${CMAKE_CURRENT_SOURCE_DIR}/storyflag.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/story/storyflagvalue.h --output ${DUSK_GENERATED_HEADERS_DIR}/rpg/story/storyflagvalue.h
) )
+48 -25
View File
@@ -9,6 +9,8 @@
#include "console/console.h" #include "console/console.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "display/display.h"
#include "display/displaystate.h"
#include "display/shader/shader.h" #include "display/shader/shader.h"
#include "display/screen/screen.h" #include "display/screen/screen.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
@@ -19,6 +21,9 @@
#include "rpg/entity/entity.h" #include "rpg/entity/entity.h"
#include "rpg/rpgcamera.h" #include "rpg/rpgcamera.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/assetentry.h"
#define TEXTURE_CHUNK_SIZE 16 #define TEXTURE_CHUNK_SIZE 16
static texture_t TEXTURE_CHUNK; static texture_t TEXTURE_CHUNK;
@@ -54,6 +59,10 @@ errorret_t sceneOverworldUpdate(scenedata_t *sceneData) {
errorret_t sceneOverworldRender(scenedata_t *sceneData) { errorret_t sceneOverworldRender(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
}));
mat4 proj, model, eye; mat4 proj, model, eye;
@@ -97,31 +106,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye)); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye));
// Chunks // Chunks
{ errorChain(sceneOverworldDrawChunks());
shadermaterial_t chunkMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_CHUNK
}
};
uint32_t i = 0;
for(uint8_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
for(uint8_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
for(uint8_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->vertCount == 0) {
i++;
continue;
}
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
errorChain(meshDraw(&chunk->mesh, 0, chunk->vertCount));
i++;
}
}
}
}
// Entities // Entities
{ {
@@ -156,6 +141,44 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) {
errorOk(); errorOk();
} }
errorret_t sceneOverworldDrawChunks() {
shadermaterial_t chunkMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_CHUNK
}
};
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
chunk_t *chunk = &MAP.chunks[i];
if(chunk->meshCount == 0) continue;
worldpos_t wp;
chunkPosToWorldPos(&chunk->position, &wp);
vec3 wpf = { (float_t)wp.x, (float_t)wp.y, (float_t)wp.z };
for(uint8_t m = 0; m < chunk->meshCount; m++) {
if(chunk->meshEntries[m] == NULL) continue;
vec3 pos;
glm_vec3_add(wpf, chunk->meshOffsets[m], pos);
mat4 model;
glm_translate_make(model, pos);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial));
errorChain(meshDraw(&chunk->meshEntries[m]->data.dmf.mesh, 0, -1));
}
}
// Restore identity model so subsequent renders (e.g. entities) are
// not affected by the last chunk transform.
mat4 identity;
glm_mat4_identity(identity);
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, identity));
errorOk();
}
errorret_t sceneOverworldDispose(scenedata_t *sceneData) { errorret_t sceneOverworldDispose(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null"); assertNotNull(sceneData, "Scene data cannot be null");
@@ -28,6 +28,14 @@ errorret_t sceneOverworldInit(scenedata_t *sceneData);
*/ */
errorret_t sceneOverworldUpdate(scenedata_t *sceneData); errorret_t sceneOverworldUpdate(scenedata_t *sceneData);
/**
* Draws all loaded chunks in two passes: base meshes first (shared texture,
* no binds between chunks), then each chunk's additional meshes.
*
* @return An error if drawing failed, or errorOk() on success.
*/
errorret_t sceneOverworldDrawChunks();
/** /**
* Renders the overworld scene. * Renders the overworld scene.
* *
+318
View File
@@ -0,0 +1,318 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
"""
Generates DCF + companion DMF files from raw chunk JSON files, or upgrades
legacy DCF files (version 1 or 2) to version 3.
JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
{
"tiles": [
{ "pos": [x, y, z], "type": <tile_shape_int>, "tile": <uv_tile_int> }
],
"meshes": [
{ "file": "house_5_3.dmf", "pos": [x, y, z] }
]
}
Tiles absent from the array default to TILE_SHAPE_NULL.
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):
uint8_t meshCount
for each mesh:
null-terminated string (relative asset path to .dmf)
float32[3] (x, y, z offset)
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]
Each vertex: uv[2] + pos[3] = 5 x float32 LE = 20 bytes
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
"""
import json
import os
import struct
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.normpath(os.path.join(_HERE, '..', '..', '..'))
ASSETSRAW_DIR = os.path.join(PROJECT_ROOT, 'assetsraw')
ASSETS_DIR = os.path.join(PROJECT_ROOT, 'assets')
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_MESH_NAME_MAX = 64
TILE_SIZE = 4
VERTEX_SIZE = 20
TILE_SHAPE_NULL = 0
TILE_SHAPE_GROUND = 1
FILE_MAGIC = b'DCF'
DMF_MAGIC = b'DMF\x00'
VERSION_OUT = 3
DMF_VERSION = 1
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def tile_index(x, y, z):
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
def find_mesh(filename):
"""Search ASSETS_DIR/meshes/ recursively for filename.
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
meshes_root = os.path.join(ASSETS_DIR, 'meshes')
for dirpath, _, filenames in os.walk(meshes_root):
if filename in filenames:
rel = os.path.relpath(
os.path.join(dirpath, filename), ASSETS_DIR
)
return rel.replace('\\', '/')
return None
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')
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')
def write_v3(dcf_path, tiles, mesh_names, mesh_offsets=None):
"""Write a v3 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
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])
with open(dcf_path, 'wb') as f:
f.write(buf)
print(
f' Wrote DCF {dcf_path}: '
f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes'
)
# ---------------------------------------------------------------------------
# JSON -> DCF + DMF
# ---------------------------------------------------------------------------
def build_terrain_verts(tiles_bytes):
"""Generate top-face quads for every TILE_SHAPE_GROUND tile."""
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]
if tile_type != TILE_SHAPE_GROUND:
continue
fx, fy, fz = float(x), float(y), float(z)
uv_u0 = x / CHUNK_WIDTH
uv_u1 = (x + 1) / CHUNK_WIDTH
uv_v0 = y / CHUNK_HEIGHT
uv_v1 = (y + 1) / CHUNK_HEIGHT
for v in [
(uv_u0, uv_v0, fx, fy, fz),
(uv_u1, uv_v0, fx+1, fy, fz),
(uv_u1, uv_v1, fx+1, fy+1, fz),
(uv_u0, uv_v0, fx, fy, fz),
(uv_u1, uv_v1, fx+1, fy+1, fz),
(uv_u0, uv_v1, fx, fy+1, fz),
]:
buf += struct.pack('<5f', *v)
return bytes(buf)
def from_json(json_path, dcf_path):
with open(json_path) as f:
data = json.load(f)
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
for tile in data.get('tiles', []):
pos = tile['pos']
x, y, z = int(pos[0]), int(pos[1]), int(pos[2])
struct.pack_into(
'<I', tiles, tile_index(x, y, z) * TILE_SIZE, int(tile['type'])
)
terrain_verts = build_terrain_verts(tiles)
mesh_names = []
mesh_offsets = []
# Terrain mesh (only written if non-empty)
dcf_base = os.path.splitext(os.path.basename(dcf_path))[0]
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
os.makedirs(terrain_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_names.append(f'meshes/chunks/{dmf_name}')
mesh_offsets.append((0.0, 0.0, 0.0))
# External mesh references
for mesh in data.get('meshes', []):
filename = mesh['file']
pos = mesh.get('pos', [0, 0, 0])
rel = find_mesh(filename)
if rel is None:
raise ValueError(f"Mesh not found under assets/meshes/: {filename}")
mesh_names.append(rel)
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
print(f' Resolved {filename} -> {rel}')
write_v3(dcf_path, bytes(tiles), mesh_names, mesh_offsets)
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)
# ---------------------------------------------------------------------------
# Legacy DCF (v1/v2) -> v3
# ---------------------------------------------------------------------------
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}")
offset = 8
tiles_size = CHUNK_TILE_COUNT * TILE_SIZE
tiles = data[offset:offset + tiles_size]
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
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_v3(path, tiles, mesh_names)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main():
args = sys.argv[1:]
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
src = args[0]
if os.path.splitext(src)[1].lower() == '.json':
process_json(src)
else:
upgrade_dcf(src)
if __name__ == '__main__':
main()
+4
View File
@@ -0,0 +1,4 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
+69
View File
@@ -0,0 +1,69 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
"""
Writes DMF (Dusk Mesh Format) files.
DMF format:
Bytes 0-3: DMF\x00 (magic)
Bytes 4-7: uint32_t version = 1 (little-endian)
Bytes 8-11: uint32_t vertCount (little-endian)
Bytes 12+: meshvertex_t vertices[vertCount]
Each vertex is 20 bytes: uv[2] + pos[3] (5 floats, LE)
Usage:
python3 -m tools.asset.dmf <output.dmf> <vertices.bin>
Reads raw vertex bytes from vertices.bin and writes a DMF file.
"""
import struct
import sys
import os
MAGIC = b'DMF\x00'
VERSION = 1
VERTEX_SIZE = 20
def write_dmf(path, vertex_bytes):
if len(vertex_bytes) % VERTEX_SIZE != 0:
raise ValueError(
f"Vertex data size {len(vertex_bytes)} is not a "
f"multiple of {VERTEX_SIZE}"
)
vert_count = len(vertex_bytes) // VERTEX_SIZE
buf = bytearray()
buf += MAGIC
buf += struct.pack('<I', VERSION)
buf += struct.pack('<I', vert_count)
buf += vertex_bytes
with open(path, 'wb') as f:
f.write(buf)
print(
f'Wrote {path}: {vert_count} vertices, {len(buf)} bytes'
)
return vert_count
def main():
args = sys.argv[1:]
if len(args) != 2:
print(
"Usage: python3 -m tools.asset.dmf "
"<output.dmf> <vertices.bin>"
)
sys.exit(1)
dst = args[0]
src = args[1]
with open(src, 'rb') as f:
vertex_bytes = f.read()
write_dmf(dst, vertex_bytes)
if __name__ == '__main__':
main()