diff --git a/assets/chunks/0_0_0.dcf b/assets/chunks/0_0_0.dcf index 43672168..c14c4c90 100644 Binary files a/assets/chunks/0_0_0.dcf and b/assets/chunks/0_0_0.dcf differ diff --git a/assets/meshes/chunk_0_0_0_0.dmf b/assets/meshes/chunk_0_0_0_0.dmf new file mode 100644 index 00000000..ada49deb Binary files /dev/null and b/assets/meshes/chunk_0_0_0_0.dmf differ diff --git a/src/dusk/asset/loader/CMakeLists.txt b/src/dusk/asset/loader/CMakeLists.txt index 556ccd17..30f97bc1 100644 --- a/src/dusk/asset/loader/CMakeLists.txt +++ b/src/dusk/asset/loader/CMakeLists.txt @@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} add_subdirectory(display) add_subdirectory(locale) add_subdirectory(json) -add_subdirectory(chunk) \ No newline at end of file +add_subdirectory(chunk) +add_subdirectory(dmf) \ No newline at end of file diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c index 902731b6..96adaf56 100644 --- a/src/dusk/asset/loader/assetloader.c +++ b/src/dusk/asset/loader/assetloader.c @@ -45,4 +45,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = { .loadAsync = assetChunkLoaderAsync, .dispose = assetChunkDispose }, + + [ASSET_LOADER_TYPE_DMF] = { + .loadSync = assetDmfLoaderSync, + .loadAsync = assetDmfLoaderAsync, + .dispose = assetDmfDispose + }, }; diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h index 61ba9696..09454560 100644 --- a/src/dusk/asset/loader/assetloader.h +++ b/src/dusk/asset/loader/assetloader.h @@ -12,6 +12,7 @@ #include "asset/loader/locale/assetlocaleloader.h" #include "asset/loader/json/assetjsonloader.h" #include "asset/loader/chunk/assetchunkloader.h" +#include "asset/loader/dmf/assetdmfloader.h" typedef enum { ASSET_LOADER_TYPE_NULL, @@ -22,6 +23,7 @@ typedef enum { ASSET_LOADER_TYPE_LOCALE, ASSET_LOADER_TYPE_JSON, ASSET_LOADER_TYPE_CHUNK, + ASSET_LOADER_TYPE_DMF, ASSET_LOADER_TYPE_COUNT } assetloadertype_t; @@ -33,6 +35,7 @@ typedef union { assetlocaleloaderinput_t locale; assetjsonloaderinput_t json; assetchunkloaderinput_t chunk; + assetdmfloaderinput_t dmf; } assetloaderinput_t; typedef union { @@ -42,6 +45,7 @@ typedef union { assetlocaleloaderloading_t locale; assetjsonloaderloading_t json; assetchunkloaderloading_t chunk; + assetdmfloaderloading_t dmf; } assetloaderloading_t; typedef union { @@ -51,6 +55,7 @@ typedef union { assetlocaleoutput_t locale; assetjsonoutput_t json; assetchunkoutput_t chunk; + assetdmfoutput_t dmf; } assetloaderoutput_t; typedef struct assetloading_s assetloading_t; diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c index 92a491d0..56fadf49 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.c +++ b/src/dusk/asset/loader/chunk/assetchunkloader.c @@ -92,22 +92,22 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) { "Chunk mesh count exceeds maximum." ); - uint32_t poolOffset = 0; for(uint8_t m = 0; m < out->meshCount; m++) { - uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(data + offset)); - offset += sizeof(uint32_t); - assertTrue( - poolOffset + vertCount <= CHUNK_VERTEX_COUNT, - "Chunk vertex data exceeds pool." - ); - out->meshVertCounts[m] = vertCount; + 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( - &out->vertices[poolOffset], - data + offset, - vertCount * sizeof(meshvertex_t) + out->meshOffsets[m], data + offset, sizeof(vec3) ); - offset += vertCount * sizeof(meshvertex_t); - poolOffset += vertCount; + offset += sizeof(vec3); } memoryFree(data); diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h index ecedef40..8504b2c2 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.h +++ b/src/dusk/asset/loader/chunk/assetchunkloader.h @@ -9,7 +9,7 @@ #include "asset/assetfile.h" #include "rpg/overworld/chunk.h" -#define ASSET_CHUNK_FILE_VERSION 2 +#define ASSET_CHUNK_FILE_VERSION 3 typedef struct assetloading_s assetloading_t; typedef struct assetentry_s assetentry_t; @@ -34,8 +34,8 @@ typedef struct { typedef struct { tile_t tiles[CHUNK_TILE_COUNT]; uint8_t meshCount; - uint32_t meshVertCounts[CHUNK_MESH_COUNT_MAX]; - 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; /** @@ -50,7 +50,8 @@ errorret_t assetChunkLoaderAsync(assetloading_t *loading); /** * 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. * @return Error code indicating success or failure of the load operation. diff --git a/src/dusk/asset/loader/dmf/CMakeLists.txt b/src/dusk/asset/loader/dmf/CMakeLists.txt new file mode 100644 index 00000000..4ffb6016 --- /dev/null +++ b/src/dusk/asset/loader/dmf/CMakeLists.txt @@ -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 +) diff --git a/src/dusk/asset/loader/dmf/assetdmfloader.c b/src/dusk/asset/loader/dmf/assetdmfloader.c new file mode 100644 index 00000000..3c217c43 --- /dev/null +++ b/src/dusk/asset/loader/dmf/assetdmfloader.c @@ -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(); +} diff --git a/src/dusk/asset/loader/dmf/assetdmfloader.h b/src/dusk/asset/loader/dmf/assetdmfloader.h new file mode 100644 index 00000000..f53954e1 --- /dev/null +++ b/src/dusk/asset/loader/dmf/assetdmfloader.h @@ -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); diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h index a701d544..4aa602b2 100644 --- a/src/dusk/rpg/overworld/chunk.h +++ b/src/dusk/rpg/overworld/chunk.h @@ -8,21 +8,21 @@ #pragma once #include "rpg/overworld/tile.h" #include "worldpos.h" -#include "display/mesh/mesh.h" -#include "display/spritebatch/spritebatch.h" #define CHUNK_MESH_COUNT_MAX 10 -#define CHUNK_VERTEX_COUNT 8192 +#define CHUNK_MESH_NAME_MAX 64 #define CHUNK_ENTITY_COUNT_MAX 10 +typedef struct assetentry_s assetentry_t; + typedef struct chunk_s { chunkpos_t position; tile_t tiles[CHUNK_TILE_COUNT]; uint8_t meshCount; - uint32_t meshVertCounts[CHUNK_MESH_COUNT_MAX]; - meshvertex_t vertices[CHUNK_VERTEX_COUNT]; - mesh_t meshes[CHUNK_MESH_COUNT_MAX]; + char_t meshNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; + vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; + assetentry_t *meshEntries[CHUNK_MESH_COUNT_MAX]; uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; } chunk_t; @@ -37,9 +37,9 @@ uint32_t chunkGetTileIndex(const chunkpos_t position); /** * Checks if two chunk positions are equal. - * + * * @param a The first chunk position. * @param b The second chunk position. * @return true if equal, false otherwise. */ -bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b); \ No newline at end of file +bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b); diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index 2f7598dc..bbccccaa 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -1,6 +1,6 @@ /** * Copyright (c) 2025 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -9,6 +9,7 @@ #include "util/memory.h" #include "assert/assert.h" #include "asset/asset.h" +#include "asset/loader/assetloader.h" #include "rpg/entity/entity.h" #include "util/string.h" @@ -17,20 +18,6 @@ map_t MAP; errorret_t mapInit() { memoryZero(&MAP, sizeof(map_t)); - // Setup chunk meshes - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = &MAP.chunks[i]; - for(uint8_t j = 0; j < CHUNK_MESH_COUNT_MAX; j++) { - errorChain(meshInit( - &chunk->meshes[j], - MESH_PRIMITIVE_TYPE_TRIANGLES, - CHUNK_VERTEX_COUNT, - chunk->vertices - )); - } - } - - // Perform "initial load" MAP.loaded = true; int32_t i = 0; for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) { @@ -54,61 +41,6 @@ bool_t mapIsLoaded() { 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) { if(!mapIsLoaded()) errorThrow("No map loaded"); @@ -117,7 +49,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) { errorOk(); } - // Determine which chunks remain loaded chunkindex_t chunksRemaining[MAP_CHUNK_COUNT] = {0}; chunkindex_t chunksFreed[MAP_CHUNK_COUNT] = {0}; @@ -125,7 +56,6 @@ errorret_t mapPositionSet(const chunkpos_t newPos) { uint32_t freedCount = 0; for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - // Will this chunk remain loaded? chunk_t *chunk = &MAP.chunks[i]; if( chunk->position.x >= newPos.x && @@ -137,23 +67,18 @@ errorret_t mapPositionSet(const chunkpos_t newPos) { chunk->position.z >= newPos.z && chunk->position.z < newPos.z + MAP_CHUNK_DEPTH ) { - // Stays loaded chunksRemaining[remainingCount++] = i; continue; } - // Not remaining loaded chunksFreed[freedCount++] = i; } - // Unload the freed chunks for(chunkindex_t i = 0; i < freedCount; i++) { chunk_t *chunk = &MAP.chunks[chunksFreed[i]]; 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; for(chunkunit_t zOff = 0; zOff < MAP_CHUNK_DEPTH; zOff++) { for(chunkunit_t yOff = 0; yOff < MAP_CHUNK_HEIGHT; yOff++) { @@ -161,8 +86,7 @@ errorret_t mapPositionSet(const chunkpos_t newPos) { const chunkpos_t newChunkPos = { newPos.x + xOff, newPos.y + yOff, newPos.z + zOff }; - - // Is this chunk already loaded (was not unloaded earlier)? + chunkindex_t chunkIndex = -1; for(chunkindex_t i = 0; i < remainingCount; i++) { chunk_t *chunk = &MAP.chunks[chunksRemaining[i]]; @@ -171,9 +95,7 @@ errorret_t mapPositionSet(const chunkpos_t newPos) { break; } - // Need to load this chunk if(chunkIndex == -1) { - // Find a freed chunk to reuse chunkIndex = chunksFreed[--freedCount]; chunk_t *chunk = &MAP.chunks[chunkIndex]; chunk->position = newChunkPos; @@ -185,27 +107,23 @@ errorret_t mapPositionSet(const chunkpos_t newPos) { } } - // Update map position MAP.chunkPosition = newPos; errorOk(); } void mapUpdate() { - + } errorret_t mapDispose() { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { mapChunkUnload(&MAP.chunks[i]); - for(uint8_t j = 0; j < CHUNK_MESH_COUNT_MAX; j++) { - errorChain(meshDispose(&MAP.chunks[i].meshes[j])); - } } errorOk(); } -void mapChunkUnload(chunk_t* chunk) { +void mapChunkUnload(chunk_t *chunk) { uint8_t chunkIndex = (uint8_t)(chunk - MAP.chunks); for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { if(chunk->entities[i] == 0xFF) continue; @@ -220,10 +138,16 @@ void mapChunkUnload(chunk_t* chunk) { entity->type = ENTITY_TYPE_NULL; } } + + 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"); memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); @@ -253,25 +177,42 @@ errorret_t mapChunkLoad(chunk_t* chunk) { } memoryCopy(chunk->tiles, entry->data.chunk.tiles, sizeof(chunk->tiles)); - memoryCopy( - chunk->vertices, entry->data.chunk.vertices, sizeof(chunk->vertices) - ); - memoryCopy( - chunk->meshVertCounts, - entry->data.chunk.meshVertCounts, - sizeof(chunk->meshVertCounts) - ); uint8_t meshCount = entry->data.chunk.meshCount; + for(uint8_t m = 0; m < meshCount; m++) { + stringCopy( + chunk->meshNames[m], + entry->data.chunk.meshNames[m], + CHUNK_MESH_NAME_MAX + ); + glm_vec3_copy(entry->data.chunk.meshOffsets[m], chunk->meshOffsets[m]); + } assetUnlockEntry(entry); - if(meshCount == 0) errorOk(); - chunk->meshCount = meshCount; for(uint8_t m = 0; m < meshCount; m++) { - if(chunk->meshVertCounts[m] == 0) continue; - errorChain(meshFlush( - &chunk->meshes[m], 0, (int32_t)chunk->meshVertCounts[m] - )); + 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(); } @@ -296,7 +237,7 @@ chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) { return chunkPosToIndex(&relPos); } -chunk_t* mapGetChunk(const uint8_t index) { +chunk_t *mapGetChunk(const uint8_t index) { if(index >= MAP_CHUNK_COUNT) return NULL; if(!mapIsLoaded()) return NULL; return MAP.chunkOrder[index]; @@ -314,4 +255,4 @@ tile_t mapGetTile(const worldpos_t position) { assertNotNull(chunk, "Chunk pointer cannot be NULL"); chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); return chunk->tiles[tileIndex]; -} \ No newline at end of file +} diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h index 78eb5682..87f44e7d 100644 --- a/src/dusk/rpg/overworld/map.h +++ b/src/dusk/rpg/overworld/map.h @@ -6,6 +6,7 @@ */ #pragma once +#include "error/error.h" #include "rpg/overworld/chunk.h" #define MAP_FILE_PATH_MAX 128 diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index 97d9e26a..0f01870c 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -1,6 +1,6 @@ /** * Copyright (c) 2026 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -19,6 +19,9 @@ #include "rpg/entity/entity.h" #include "rpg/rpgcamera.h" +#include "asset/loader/assetloader.h" +#include "asset/loader/assetentry.h" + #define TEXTURE_CHUNK_SIZE 16 static texture_t TEXTURE_CHUNK; @@ -73,7 +76,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { proj ); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); - + // Camera Eye float_t pixelsPerUnit = TILE_SIZE_PIXELS; float_t worldH = (float_t)SCREEN.height / pixelsPerUnit; @@ -95,7 +98,7 @@ errorret_t sceneOverworldRender(scenedata_t *sceneData) { eye ); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, eye)); - + // Chunks errorChain(sceneOverworldDrawChunks()); @@ -140,35 +143,33 @@ errorret_t sceneOverworldDrawChunks() { } }; - // Pass 1: draw all base meshes with the shared chunk texture (no mid-loop - // texture swaps). - errorChain(shaderSetMaterial(&SHADER_UNLIT, &chunkMaterial)); for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { chunk_t *chunk = &MAP.chunks[i]; if(chunk->meshCount == 0) continue; - if(chunk->meshVertCounts[0] == 0) continue; - errorChain(meshDraw( - &chunk->meshes[0], 0, (int32_t)chunk->meshVertCounts[0] - )); - } - // Pass 2: draw each chunk's additional meshes (indices 1..meshCount-1). - // Vertices are packed sequentially in the pool, so accumulate the offset. - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = &MAP.chunks[i]; - uint32_t vertOffset = chunk->meshVertCounts[0]; - for(uint8_t m = 1; m < chunk->meshCount; m++) { - if(chunk->meshVertCounts[m] > 0) { - errorChain(meshDraw( - &chunk->meshes[m], - (int32_t)vertOffset, - (int32_t)chunk->meshVertCounts[m] - )); - } - vertOffset += chunk->meshVertCounts[m]; + 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(); } @@ -178,4 +179,4 @@ errorret_t sceneOverworldDispose(scenedata_t *sceneData) { errorChain(textureDispose(&TEXTURE_CHUNK)); errorOk(); -} \ No newline at end of file +} diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py index ce152a4d..8bc9c289 100644 --- a/tools/asset/chunk/__main__.py +++ b/tools/asset/chunk/__main__.py @@ -4,7 +4,8 @@ # https://opensource.org/licenses/MIT """ -Converts DCF chunk files from version 1 to version 2. +Converts DCF chunk files (version 1 or 2) to version 3 and generates the +companion DMF (Dusk Mesh Format) files that version 3 references. Version 1 format (after 8-byte header + tiles): uint32_t vertCount @@ -13,37 +14,51 @@ Version 1 format (after 8-byte header + tiles): Version 2 format (after 8-byte header + tiles): uint8_t meshCount for each mesh: - uint32_t vertCount + uint32_t vertCount meshvertex_t vertices[vertCount] +Version 3 format (after 8-byte header + tiles): + uint8_t meshCount + for each mesh: + null-terminated string (path to companion .dmf asset) + +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] + Usage: python3 -m tools.asset.chunk [output.dcf] If output is omitted the input file is updated in place. + DMF files are written to assets/meshes/ beside the chunks/ directory. """ import struct import sys import os -# Must match src/dusk/rpg/overworld/chunk.h -CHUNK_WIDTH = 16 -CHUNK_HEIGHT = 16 -CHUNK_DEPTH = 32 +CHUNK_WIDTH = 16 +CHUNK_HEIGHT = 16 +CHUNK_DEPTH = 32 CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH # 8192 CHUNK_MESH_COUNT_MAX = 10 CHUNK_VERTEX_COUNT = 8192 +CHUNK_MESH_NAME_MAX = 64 -# C enum (int) = 4 bytes; meshvertex_t = uv[2]+pos[3] floats = 20 bytes TILE_SIZE = 4 -VERTEX_SIZE = 20 # 2 floats UV + 3 floats pos, MESH_ENABLE_COLOR=0 +VERTEX_SIZE = 20 FILE_MAGIC = b'DCF' -VERSION_IN = 1 -VERSION_OUT = 2 +DMF_MAGIC = b'DMF\x00' +VERSION_OUT = 3 +DMF_VERSION = 1 -def read_v1(path): +def read_dcf(path): + """Read a v1 or v2 DCF file. Returns (tiles, meshes) where meshes is a + list of raw vertex byte strings, one per mesh.""" with open(path, 'rb') as f: data = f.read() @@ -51,34 +66,66 @@ def read_v1(path): raise ValueError(f"{path}: not a DCF file") version = struct.unpack_from(' 0: + meshes.append(verts) + else: + mesh_count = data[offset] + offset += 1 + for _ in range(mesh_count): + vert_count = struct.unpack_from(' 0: + meshes.append(verts) - return tiles, vert_count, verts + return tiles, meshes -def write_v2(path, tiles, vert_count, verts): - if vert_count > CHUNK_VERTEX_COUNT: - print( - f" Warning: {vert_count} vertices exceeds pool " - f"({CHUNK_VERTEX_COUNT}); truncating." - ) - vert_count = CHUNK_VERTEX_COUNT - verts = verts[:vert_count * VERTEX_SIZE] +def write_dmf(path, vertex_bytes): + vert_count = len(vertex_bytes) // VERTEX_SIZE + buf = bytearray() + buf += DMF_MAGIC + buf += struct.pack(' 0 else 0 + +def write_v3(dcf_path, tiles, mesh_names, mesh_offsets=None): + """Write a v3 DCF that references the given DMF asset paths. + + mesh_offsets is an optional list of (x, y, z) tuples, one per mesh. + Defaults to (0, 0, 0) for each mesh when omitted. + """ + 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 @@ -86,34 +133,63 @@ def write_v2(path, tiles, vert_count, verts): buf += struct.pack(' 0: - buf += struct.pack('= 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 {path}: version {VERSION_OUT}, " - f"{mesh_count} mesh(es), {vert_count} vertices." + f' Wrote DCF {dcf_path}: ' + f'version {VERSION_OUT}, {mesh_count} mesh(es)' ) def main(): args = sys.argv[1:] if not args: - print("Usage: python3 -m tools.asset.chunk [output.dcf]") + print( + "Usage: python3 -m tools.asset.chunk " + " [output.dcf]" + ) sys.exit(1) src = args[0] dst = args[1] if len(args) > 1 else src print(f"Reading {src} ...") - tiles, vert_count, verts = read_v1(src) - print(f" tiles={CHUNK_TILE_COUNT}, vertices={vert_count}") + tiles, meshes = read_dcf(src) + print( + f" tiles={CHUNK_TILE_COUNT}, " + f"meshes={len(meshes)}, " + f"total_verts=" + f"{sum(len(m) // VERTEX_SIZE for m in meshes)}" + ) + + # Derive chunk base name from the DCF filename (e.g. "0_0_0" from + # "0_0_0.dcf") to name DMF files "chunk_0_0_0_0.dmf" etc. + base = os.path.splitext(os.path.basename(dst))[0] + + # assets/meshes/ sits beside assets/chunks/ (one dir up from the DCF). + meshes_dir = os.path.normpath( + os.path.join(os.path.dirname(os.path.abspath(dst)), '..', 'meshes') + ) + os.makedirs(meshes_dir, exist_ok=True) + + print(f"Writing DMF files to {meshes_dir} ...") + mesh_names = [] + for idx, verts in enumerate(meshes): + dmf_filename = f'chunk_{base}_{idx}.dmf' + dmf_path = os.path.join(meshes_dir, dmf_filename) + write_dmf(dmf_path, verts) + mesh_names.append(f'meshes/{dmf_filename}') print(f"Writing {dst} ...") - write_v2(dst, tiles, vert_count, verts) + write_v3(dst, tiles, mesh_names) if __name__ == '__main__': diff --git a/tools/asset/chunk/hillgen.py b/tools/asset/chunk/hillgen.py deleted file mode 100644 index 13ec593c..00000000 --- a/tools/asset/chunk/hillgen.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -""" -Generates chunk 0_0_0.dcf with a small hill in the centre. - -Hill layout (tile coordinates, 0-based): - y=5: . . . . . . N N . . . . . . . . RAMP_NORTH (south slope) - y=6: . . . . . E H H W . . . . . . . Hill top (H), RAMP_EAST/WEST - y=7: . . . . . E H H W . . . . . . . - y=8: . . . . . . S S . . . . . . . . RAMP_SOUTH (north slope) - x=6 x=7 -""" - -import struct, os - -# Must match src/dusk/rpg/overworld/chunk.h and tile.h -CHUNK_WIDTH = 16 -CHUNK_HEIGHT = 16 -CHUNK_DEPTH = 32 -CHUNK_W_F = float(CHUNK_WIDTH) - -TILE_NULL = 0 -TILE_GROUND = 1 -TILE_RAMP_NORTH = 2 -TILE_RAMP_SOUTH = 3 -TILE_RAMP_EAST = 4 -TILE_RAMP_WEST = 5 - -TILE_SIZE = 4 # sizeof(tile_t) = sizeof(int) -VERT_SIZE = 20 # sizeof(meshvertex_t): uv[2] + pos[3] floats -FILE_VER = 2 - -# Hill geometry parameters -HILL_X = frozenset({6, 7}) -HILL_Y = frozenset({6, 7}) -HILL_H = 1.0 - - -def tile_idx(cx, cy, cz): - return cz * CHUNK_WIDTH * CHUNK_HEIGHT + cy * CHUNK_WIDTH + cx - - -def make_vert(u, v, px, py, pz): - return struct.pack('<5f', u, v, px, py, pz) - - -def quad_verts(cx, cy, z_sw, z_se, z_ne, z_nw): - """ - Build 6 vertices (2 triangles) for a tile quad. - Heights at each corner: SW=south-west, SE=south-east, - NE=north-east, NW=north-west. - UV formula (verified against existing DCF data): - u = (cy + within_x) / CHUNK_WIDTH where within_x in {0,1} - v = (cx + within_y) / CHUNK_HEIGHT where within_y in {0,1} - """ - u0 = cy / CHUNK_W_F - u1 = (cy + 1) / CHUNK_W_F - v0 = cx / CHUNK_W_F - v1 = (cx + 1) / CHUNK_W_F - x0, x1 = float(cx), float(cx + 1) - y0, y1 = float(cy), float(cy + 1) - - SW = make_vert(u0, v0, x0, y0, float(z_sw)) - SE = make_vert(u1, v0, x1, y0, float(z_se)) - NE = make_vert(u1, v1, x1, y1, float(z_ne)) - NW = make_vert(u0, v1, x0, y1, float(z_nw)) - - return SW + SE + NE + SW + NE + NW - - -def flat(cx, cy, z): - return quad_verts(cx, cy, z, z, z, z) - - -def ramp_north(cx, cy): - return quad_verts(cx, cy, 0, 0, HILL_H, HILL_H) - - -def ramp_south(cx, cy): - return quad_verts(cx, cy, HILL_H, HILL_H, 0, 0) - - -def ramp_east(cx, cy): - return quad_verts(cx, cy, 0, HILL_H, HILL_H, 0) - - -def ramp_west(cx, cy): - return quad_verts(cx, cy, HILL_H, 0, 0, HILL_H) - - -def generate(): - tiles = [TILE_GROUND] * (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH) - - ramps_n = frozenset((cx, 5) for cx in HILL_X) - ramps_s = frozenset((cx, 8) for cx in HILL_X) - ramps_e = frozenset((5, cy) for cy in HILL_Y) - ramps_w = frozenset((8, cy) for cy in HILL_Y) - - for cx, cy in ramps_n: - tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_NORTH - for cx, cy in ramps_s: - tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_SOUTH - for cx, cy in ramps_e: - tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_EAST - for cx, cy in ramps_w: - tiles[tile_idx(cx, cy, 0)] = TILE_RAMP_WEST - - for cx in HILL_X: - for cy in HILL_Y: - tiles[tile_idx(cx, cy, 1)] = TILE_GROUND - - verts = bytearray() - - for cx in range(CHUNK_WIDTH): - for cy in range(CHUNK_HEIGHT): - pos = (cx, cy) - if cx in HILL_X and cy in HILL_Y: - continue - if pos in ramps_n: - verts += ramp_north(cx, cy) - elif pos in ramps_s: - verts += ramp_south(cx, cy) - elif pos in ramps_e: - verts += ramp_east(cx, cy) - elif pos in ramps_w: - verts += ramp_west(cx, cy) - else: - verts += flat(cx, cy, 0) - - for cx in sorted(HILL_X): - for cy in sorted(HILL_Y): - verts += flat(cx, cy, HILL_H) - - vert_count = len(verts) // VERT_SIZE - tile_bytes = struct.pack(f'<{len(tiles)}i', *tiles) - - buf = bytearray() - buf += b'DCF\x00' - buf += struct.pack(' + 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(' " + ) + 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()