diff --git a/assets/chunks/-1_0_0.dcf b/assets/map/testmap/chunks/-1_0_0.dcf similarity index 100% rename from assets/chunks/-1_0_0.dcf rename to assets/map/testmap/chunks/-1_0_0.dcf diff --git a/assets/chunks/0_0_0.dcf b/assets/map/testmap/chunks/0_0_0.dcf similarity index 100% rename from assets/chunks/0_0_0.dcf rename to assets/map/testmap/chunks/0_0_0.dcf diff --git a/assets/chunks/0_1_0.dcf b/assets/map/testmap/chunks/0_1_0.dcf similarity index 100% rename from assets/chunks/0_1_0.dcf rename to assets/map/testmap/chunks/0_1_0.dcf diff --git a/assets/chunks/1_0_0.dcf b/assets/map/testmap/chunks/1_0_0.dcf similarity index 100% rename from assets/chunks/1_0_0.dcf rename to assets/map/testmap/chunks/1_0_0.dcf diff --git a/assets/chunks/1_1_0.dcf b/assets/map/testmap/chunks/1_1_0.dcf similarity index 100% rename from assets/chunks/1_1_0.dcf rename to assets/map/testmap/chunks/1_1_0.dcf diff --git a/assets/chunks/2_0_0.dcf b/assets/map/testmap/chunks/2_0_0.dcf similarity index 100% rename from assets/chunks/2_0_0.dcf rename to assets/map/testmap/chunks/2_0_0.dcf diff --git a/assets/map/testmap/map.json b/assets/map/testmap/map.json new file mode 100644 index 00000000..127f6066 --- /dev/null +++ b/assets/map/testmap/map.json @@ -0,0 +1,7 @@ +{ + "name": "Test Map", + "entities": [ + { "type": "player", "position": [10, 2, 0], "direction": "north" }, + { "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 } + ] +} diff --git a/assetsraw/chunks/chunk_-1_0_0.json b/assetsraw/map/testmap/chunks/chunk_-1_0_0.json similarity index 100% rename from assetsraw/chunks/chunk_-1_0_0.json rename to assetsraw/map/testmap/chunks/chunk_-1_0_0.json diff --git a/assetsraw/chunks/chunk_0_0_0.json b/assetsraw/map/testmap/chunks/chunk_0_0_0.json similarity index 100% rename from assetsraw/chunks/chunk_0_0_0.json rename to assetsraw/map/testmap/chunks/chunk_0_0_0.json diff --git a/assetsraw/chunks/chunk_0_1_0.json b/assetsraw/map/testmap/chunks/chunk_0_1_0.json similarity index 100% rename from assetsraw/chunks/chunk_0_1_0.json rename to assetsraw/map/testmap/chunks/chunk_0_1_0.json diff --git a/assetsraw/chunks/chunk_1_0_0.json b/assetsraw/map/testmap/chunks/chunk_1_0_0.json similarity index 100% rename from assetsraw/chunks/chunk_1_0_0.json rename to assetsraw/map/testmap/chunks/chunk_1_0_0.json diff --git a/assetsraw/chunks/chunk_1_1_0.json b/assetsraw/map/testmap/chunks/chunk_1_1_0.json similarity index 100% rename from assetsraw/chunks/chunk_1_1_0.json rename to assetsraw/map/testmap/chunks/chunk_1_1_0.json diff --git a/assetsraw/chunks/chunk_2_0_0.json b/assetsraw/map/testmap/chunks/chunk_2_0_0.json similarity index 100% rename from assetsraw/chunks/chunk_2_0_0.json rename to assetsraw/map/testmap/chunks/chunk_2_0_0.json diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c index 2b695545..7e4da000 100644 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c +++ b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c @@ -9,7 +9,7 @@ #include "rpg/cutscene/cutscenesystem.h" #include "rpg/entity/entity.h" #include "rpg/entity/entitypathstep.h" -#include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" void cutsceneEntityWalkToEntityStart( const cutsceneitem_t *item, @@ -35,7 +35,7 @@ bool_t cutsceneEntityWalkToEntityUpdate( }; worldunit_t z; - if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z; + if(chunkGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z; return entityPathStep(entity, dest, true); } diff --git a/src/dusk/rpg/entity/anim/entityanim.c b/src/dusk/rpg/entity/anim/entityanim.c index fa98628f..7d89f069 100644 --- a/src/dusk/rpg/entity/anim/entityanim.c +++ b/src/dusk/rpg/entity/anim/entityanim.c @@ -6,7 +6,7 @@ */ #include "rpg/entity/entity.h" -#include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" #include "rpg/overworld/tile.h" #include "time/time.h" #include "entityanimwalk.h" @@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = { }; float_t entityAnimTileZOffset(const worldpos_t pos) { - return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f; + return tileShapeIsRamp(chunkGetTile(pos).shape) ? 0.5f : 0.0f; } void entityAnimUpdate(entity_t *entity) { diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c index 4b2b4a65..f2dda4e4 100644 --- a/src/dusk/rpg/entity/entity.c +++ b/src/dusk/rpg/entity/entity.c @@ -8,12 +8,13 @@ #include "entity.h" #include "assert/assert.h" #include "util/memory.h" +#include "util/string.h" #include "time/time.h" #include "util/math.h" -#include "rpg/overworld/map.h" #include "rpg/overworld/maparea.h" #include "rpg/overworld/chunk.h" #include "rpg/overworld/tile.h" +#include "yyjson.h" entity_t ENTITIES[ENTITY_COUNT]; @@ -88,8 +89,8 @@ void entityWalk(entity_t *entity, const entitydir_t direction) { } // Get tile under foot - tile_t tileCurrent = mapGetTile(entity->position); - tile_t tileNew = mapGetTile(newPos); + tile_t tileCurrent = chunkGetTile(entity->position); + tile_t tileNew = chunkGetTile(newPos); bool_t fall = false; bool_t raise = false; @@ -138,7 +139,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) { tileNew = TILE_NULL; worldpos_t abovePos = newPos; abovePos.z += 1; - tile_t tileAbove = mapGetTile(abovePos); + tile_t tileAbove = chunkGetTile(abovePos); if( tileAbove.shape != TILE_SHAPE_NULL && @@ -152,7 +153,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) { // Falling down? worldpos_t belowPos = newPos; belowPos.z -= 1; - tile_t tileBelow = mapGetTile(belowPos); + tile_t tileBelow = chunkGetTile(belowPos); if( tileBelow.shape != TILE_SHAPE_NULL && tileShapeIsRamp(tileBelow.shape) && @@ -282,7 +283,7 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) { assertNotNull(entity, "Entity pointer cannot be NULL"); if(entity->chunkIndex != 0xFF) { - chunk_t *old = mapGetChunk(entity->chunkIndex); + chunk_t *old = chunkGet(entity->chunkIndex); if(old != NULL) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { if(old->entities[i] != entity->id) continue; @@ -295,7 +296,7 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) { entity->chunkIndex = chunkIndex; if(chunkIndex != 0xFF) { - chunk_t *next = mapGetChunk(chunkIndex); + chunk_t *next = chunkGet(chunkIndex); if(next != NULL) { for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { if(next->entities[i] != 0xFF) continue; @@ -311,6 +312,103 @@ void entityUpdateChunk(entity_t *entity) { chunkpos_t cp; worldPosToChunkPos(&entity->position, &cp); - chunkindex_t ci = mapGetChunkIndexAt(cp); + chunkindex_t ci = chunkGetIndexAt(cp); if(ci != -1) entitySetChunk(entity, (uint8_t)ci); +} + +errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity) { + assertNotNull(obj, "Entity JSON object cannot be NULL"); + assertNotNull(outEntity, "Output entity pointer cannot be NULL"); + + yyjson_val *typeVal = yyjson_obj_get(obj, "type"); + if(!typeVal || !yyjson_is_str(typeVal)) { + errorThrow("Entity JSON missing 'type' string"); + } + const char_t *typeStr = yyjson_get_str(typeVal); + + entitytype_t type; + if(stringEquals(typeStr, "player")) { + type = ENTITY_TYPE_PLAYER; + } else if(stringEquals(typeStr, "npc")) { + type = ENTITY_TYPE_NPC; + } else if(stringEquals(typeStr, "item")) { + type = ENTITY_TYPE_ITEM; + } else { + errorThrow("Entity JSON has unknown 'type': %s", typeStr); + } + + yyjson_val *posVal = yyjson_obj_get(obj, "position"); + if(!posVal || !yyjson_is_arr(posVal) || yyjson_arr_size(posVal) != 3) { + errorThrow("Entity JSON missing 'position' [x, y, z] array"); + } + worldunit_t pos[3]; + size_t posIdx, posLen; + yyjson_val *posElem; + yyjson_arr_foreach(posVal, posIdx, posLen, posElem) { + if(!yyjson_is_num(posElem)) { + errorThrow("Entity JSON 'position' elements must be numbers"); + } + pos[posIdx] = (worldunit_t)yyjson_get_num(posElem); + } + + entitydir_t direction = ENTITY_DIR_SOUTH; + yyjson_val *dirVal = yyjson_obj_get(obj, "direction"); + if(dirVal && yyjson_is_str(dirVal)) { + const char_t *dirStr = yyjson_get_str(dirVal); + if(stringEquals(dirStr, "north")) { + direction = ENTITY_DIR_NORTH; + } else if(stringEquals(dirStr, "east")) { + direction = ENTITY_DIR_EAST; + } else if(stringEquals(dirStr, "south")) { + direction = ENTITY_DIR_SOUTH; + } else if(stringEquals(dirStr, "west")) { + direction = ENTITY_DIR_WEST; + } else { + errorThrow("Entity JSON has unknown 'direction': %s", dirStr); + } + } + + // Item entities require a valid item reference, resolved up front so a + // bad reference fails before an entity slot is ever claimed. + itemid_t itemId = ITEM_ID_NULL; + uint8_t itemQuantity = 1; + if(type == ENTITY_TYPE_ITEM) { + yyjson_val *itemVal = yyjson_obj_get(obj, "item"); + if(!itemVal || !yyjson_is_str(itemVal)) { + errorThrow("Entity JSON with type 'item' missing 'item' string"); + } + const char_t *itemStr = yyjson_get_str(itemVal); + itemId = itemGetIdByName(itemStr); + if(itemId == ITEM_ID_NULL) { + errorThrow("Entity JSON references unknown item '%s'", itemStr); + } + + yyjson_val *quantityVal = yyjson_obj_get(obj, "quantity"); + if(quantityVal && yyjson_is_int(quantityVal)) { + itemQuantity = (uint8_t)yyjson_get_int(quantityVal); + } + } + + // Only one player may exist at a time - it always holds the reserved + // ENTITY_GLOBAL_ID_PLAYER global ID, so that's how callers (e.g. the + // camera) find it regardless of how it was spawned. + if( + type == ENTITY_TYPE_PLAYER && + entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER) != NULL + ) { + errorThrow("A player entity has already been spawned"); + } + + uint8_t index = entityGetAvailable(); + if(index == 0xFF) errorThrow("No available entity slots"); + + entity_t *entity = &ENTITIES[index]; + entityInit(entity, type); + entity->direction = direction; + entityPositionSet(entity, (worldpos_t){ pos[0], pos[1], pos[2] }); + if(type == ENTITY_TYPE_ITEM) entityItemSet(entity, itemId, itemQuantity); + if(type == ENTITY_TYPE_PLAYER) entity->globalId = ENTITY_GLOBAL_ID_PLAYER; + + *outEntity = entity; + errorOk(); } \ No newline at end of file diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h index e184b3b3..c721e8f5 100644 --- a/src/dusk/rpg/entity/entity.h +++ b/src/dusk/rpg/entity/entity.h @@ -13,6 +13,7 @@ #include "npc/npc.h" typedef struct map_s map_t; +typedef struct yyjson_val yyjson_val; typedef uint16_t entityglobalid_t; @@ -164,4 +165,31 @@ void entityUpdateChunk(entity_t *entity); * @param entity Pointer to the entity to move. * @param pos The world position to place the entity at. */ -void entityPositionSet(entity_t *entity, const worldpos_t pos); \ No newline at end of file +void entityPositionSet(entity_t *entity, const worldpos_t pos); + +/** + * Parses an entity descriptor from a yyjson object and spawns it as a new + * entity in an available slot. Expected shape: + * { "type": "npc", "position": [x, y, z], "direction": "south" } + * "type" must be one of "player", "npc", "item". "direction" is optional + * (one of "north", "east", "south", "west") and defaults to + * ENTITY_DIR_SOUTH when absent. + * + * When "type" is "item", two extra fields apply: + * { "type": "item", "position": [x, y, z], "item": "POTION", + * "quantity": 1 } + * "item" (required) is the item's string ID, resolved via + * itemGetIdByName. "quantity" (optional) defaults to 1. + * + * When "type" is "player", the spawned entity is assigned the reserved + * ENTITY_GLOBAL_ID_PLAYER global ID (so entityGetByGlobalId can find it + * regardless of how it was spawned), and it is an error to spawn a + * second one while one is already loaded. + * + * @param obj The yyjson object describing the entity. + * @param outEntity Output pointer, set to the newly spawned entity on + * success. + * @return Any error that occurs (missing/invalid fields, unknown item, + * duplicate player, no free slots). + */ +errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity); \ No newline at end of file diff --git a/src/dusk/rpg/item/backpack.c b/src/dusk/rpg/item/backpack.c index bf16afdc..5493204c 100644 --- a/src/dusk/rpg/item/backpack.c +++ b/src/dusk/rpg/item/backpack.c @@ -15,7 +15,7 @@ void backpackInit() { inventoryInit( &BACKPACK.inventories[i], BACKPACK.storage[i], - ITEM_TYPE_SLOT_COUNT_MAX + INVENTORY_CAPACITY_MAX ); } } diff --git a/src/dusk/rpg/item/backpack.h b/src/dusk/rpg/item/backpack.h index 6d7e1d31..8e47d3ab 100644 --- a/src/dusk/rpg/item/backpack.h +++ b/src/dusk/rpg/item/backpack.h @@ -8,10 +8,8 @@ #pragma once #include "inventory.h" -#define ITEM_TYPE_SLOT_COUNT_MAX 64 - typedef struct { - inventorystack_t storage[ITEM_TYPE_COUNT_MAX][ITEM_TYPE_SLOT_COUNT_MAX]; + inventorystack_t storage[ITEM_TYPE_COUNT_MAX][INVENTORY_CAPACITY_MAX]; inventory_t inventories[ITEM_TYPE_COUNT_MAX]; } backpack_t; diff --git a/src/dusk/rpg/item/inventory.h b/src/dusk/rpg/item/inventory.h index ba559dfe..1e8f9b0c 100644 --- a/src/dusk/rpg/item/inventory.h +++ b/src/dusk/rpg/item/inventory.h @@ -9,6 +9,7 @@ #include "rpg/item/item.h" #define ITEM_STACK_QUANTITY_MAX 99 +#define INVENTORY_CAPACITY_MAX 250 typedef enum { INVENTORY_SORT_BY_ID, diff --git a/src/dusk/rpg/overworld/chunk.c b/src/dusk/rpg/overworld/chunk.c index 18a5316e..93466700 100644 --- a/src/dusk/rpg/overworld/chunk.c +++ b/src/dusk/rpg/overworld/chunk.c @@ -1,11 +1,29 @@ /** * Copyright (c) 2025 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "chunk.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "asset/asset.h" +#include "asset/loader/assetloader.h" +#include "asset/loader/assetentry.h" +#include "console/console.h" +#include "event/event.h" +#include "rpg/entity/entity.h" +#include "rpg/overworld/map.h" + +chunk_t CHUNKS[MAP_CHUNK_COUNT]; +chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT]; + +static chunkpos_t CHUNK_POSITION; +static chunk_t *CHUNK_LOAD_QUEUE[MAP_CHUNK_COUNT]; +static uint32_t CHUNK_LOAD_QUEUE_COUNT; +static chunk_t *CHUNK_LOADING; uint32_t chunkGetTileIndex(const chunkpos_t position) { return (position.y * CHUNK_WIDTH) + position.x; @@ -13,4 +31,367 @@ uint32_t chunkGetTileIndex(const chunkpos_t position) { bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) { return (a.x == b.x) && (a.y == b.y) && (a.z == b.z); -} \ No newline at end of file +} + +errorret_t chunksLoadGrid(void) { + CHUNK_POSITION = (chunkpos_t){ 0, 0, 0 }; + + chunkindex_t 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 = &CHUNKS[i++]; + chunk->position = (chunkpos_t){ + (chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z + }; + errorChain(chunkLoad(chunk)); + } + } + } + + chunkRebuildOrder(); + errorOk(); +} + +void chunksUnloadAll(void) { + for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { + chunkUnload(&CHUNKS[i]); + } +} + +errorret_t chunkPositionSet(const chunkpos_t newPos) { + if(!mapIsLoaded()) errorThrow("No map loaded"); + if(chunkPositionIsEqual(newPos, CHUNK_POSITION)) errorOk(); + + // Separate loaded chunks into "keep" and "free" buckets. + chunkindex_t chunksFreed[MAP_CHUNK_COUNT]; + uint32_t freedCount = 0; + + // Use a boolean grid so the inner load loop can check O(1). + bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH]; + memoryZero(posLoaded, sizeof(posLoaded)); + + for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { + chunk_t *chunk = &CHUNKS[i]; + chunkunit_t rx = chunk->position.x - newPos.x; + chunkunit_t ry = chunk->position.y - newPos.y; + chunkunit_t rz = chunk->position.z - newPos.z; + if( + rx >= 0 && rx < MAP_CHUNK_WIDTH && + ry >= 0 && ry < MAP_CHUNK_HEIGHT && + rz >= 0 && rz < MAP_CHUNK_DEPTH + ) { + posLoaded[rx][ry][rz] = true; + } else { + chunkUnload(chunk); + chunksFreed[freedCount++] = i; + } + } + + 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++) { + if(posLoaded[x][y][z]) continue; + assertTrue(freedCount > 0, "No free chunk slot available."); + chunk_t *chunk = &CHUNKS[chunksFreed[--freedCount]]; + chunk->position = (chunkpos_t){ + newPos.x + (chunkunit_t)x, + newPos.y + (chunkunit_t)y, + newPos.z + (chunkunit_t)z + }; + errorChain(chunkLoad(chunk)); + } + } + } + + CHUNK_POSITION = newPos; + chunkRebuildOrder(); + errorOk(); +} + +void chunkUnload(chunk_t *chunk) { + chunkLoadQueueRemove(chunk); + if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL; + + for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { + if(chunk->entities[i] == 0xFF) continue; + entity_t *entity = &ENTITIES[chunk->entities[i]]; + if(!entityCanUnload(entity)) { + entitySetChunk(entity, 0xFF); + } else { + entity->type = ENTITY_TYPE_NULL; + } + } + + memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); + + if(chunk->dcfEntry != NULL) { + eventUnsubscribe(&chunk->dcfEntry->onLoaded, chunkLoaded); + eventUnsubscribe(&chunk->dcfEntry->onError, chunkLoadError); + assetUnlockEntry(chunk->dcfEntry); + chunk->dcfEntry = NULL; + } + + // modelEntries are borrowed pointers, not independently locked - the + // chunk asset entry (released above) is what actually holds the ref on + // each model, so nothing to unlock here, just drop our own copies. + for(uint8_t m = 0; m < chunk->meshCount; m++) { + chunk->modelEntries[m] = NULL; + } + chunk->meshCount = 0; +} + +errorret_t chunkLoad(chunk_t *chunk) { + if(!mapIsLoaded()) errorThrow("No map loaded"); + + chunkLoadQueueRemove(chunk); + if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL; + + if(chunk->dcfEntry != NULL) { + eventUnsubscribe(&chunk->dcfEntry->onLoaded, chunkLoaded); + eventUnsubscribe(&chunk->dcfEntry->onError, chunkLoadError); + assetUnlockEntry(chunk->dcfEntry); + chunk->dcfEntry = NULL; + } + + memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); + chunk->meshCount = 0; + + char_t path[MAP_FILE_PATH_MAX + 64]; + stringFormat( + path, sizeof(path), + "map/%s/chunks/%d_%d_%d.dcf", + MAP.name, + (int32_t)chunk->position.x, + (int32_t)chunk->position.y, + (int32_t)chunk->position.z + ); + + if(!assetFileExists(path)) { + for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) { + chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND }; + } + errorOk(); + } + + assertTrue( + CHUNK_LOAD_QUEUE_COUNT < MAP_CHUNK_COUNT, + "Chunk load queue overflow" + ); + CHUNK_LOAD_QUEUE[CHUNK_LOAD_QUEUE_COUNT++] = chunk; + chunkLoadNext(); + errorOk(); +} + +void chunkLoadNext(void) { + if(CHUNK_LOADING != NULL) return; + if(CHUNK_LOAD_QUEUE_COUNT == 0) return; + + chunk_t *chunk = CHUNK_LOAD_QUEUE[0]; + for(uint32_t i = 1; i < CHUNK_LOAD_QUEUE_COUNT; i++) { + CHUNK_LOAD_QUEUE[i - 1] = CHUNK_LOAD_QUEUE[i]; + } + CHUNK_LOAD_QUEUE_COUNT--; + CHUNK_LOADING = chunk; + + char_t path[MAP_FILE_PATH_MAX + 64]; + stringFormat( + path, sizeof(path), + "map/%s/chunks/%d_%d_%d.dcf", + MAP.name, + (int32_t)chunk->position.x, + (int32_t)chunk->position.y, + (int32_t)chunk->position.z + ); + + assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CHUNK, NULL); + assertNotNull(entry, "Failed to get chunk asset entry"); + chunk->dcfEntry = entry; + + // The entry may already be resident from an earlier load that hasn't been + // reaped yet - in that case onLoaded/onError already fired once and never + // will again, so handle the terminal state directly instead of waiting on + // a subscription that would never trigger. + if(entry->state == ASSET_ENTRY_STATE_LOADED) { + chunkLoaded(entry, chunk); + return; + } + if(entry->state == ASSET_ENTRY_STATE_ERROR) { + chunkLoadError(entry, chunk); + return; + } + + eventSubscribe(&entry->onLoaded, chunkLoaded, chunk); + eventSubscribe(&entry->onError, chunkLoadError, chunk); +} + +void chunkLoadQueueRemove(chunk_t *chunk) { + for(uint32_t i = 0; i < CHUNK_LOAD_QUEUE_COUNT; i++) { + if(CHUNK_LOAD_QUEUE[i] != chunk) continue; + for(uint32_t j = i + 1; j < CHUNK_LOAD_QUEUE_COUNT; j++) { + CHUNK_LOAD_QUEUE[j - 1] = CHUNK_LOAD_QUEUE[j]; + } + CHUNK_LOAD_QUEUE_COUNT--; + return; + } +} + +chunkindex_t chunkGetIndexAt(const chunkpos_t position) { + if(!mapIsLoaded()) return -1; + + chunkpos_t relPos = { + position.x - CHUNK_POSITION.x, + position.y - CHUNK_POSITION.y, + position.z - CHUNK_POSITION.z + }; + + if( + relPos.x < 0 || relPos.y < 0 || relPos.z < 0 || + relPos.x >= MAP_CHUNK_WIDTH || + relPos.y >= MAP_CHUNK_HEIGHT || + relPos.z >= MAP_CHUNK_DEPTH + ) { + return -1; + } + + return chunkPosToIndex(&relPos); +} + +chunk_t *chunkGet(const uint8_t index) { + if(index >= MAP_CHUNK_COUNT) return NULL; + if(!mapIsLoaded()) return NULL; + return CHUNK_ORDER[index]; +} + +tile_t chunkGetTile(const worldpos_t position) { + if(!mapIsLoaded()) return TILE_NULL; + + chunkpos_t chunkPos; + worldPosToChunkPos(&position, &chunkPos); + chunkindex_t chunkIndex = chunkGetIndexAt(chunkPos); + if(chunkIndex == -1) return TILE_NULL; + + chunk_t *chunk = chunkGet(chunkIndex); + assertNotNull(chunk, "Chunk pointer cannot be NULL"); + chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); + tile_t tile = chunk->tiles[tileIndex]; + if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL; + return tile; +} + +bool_t chunkGetWalkableZNear( + const worldunit_t x, + const worldunit_t y, + const worldunit_t nearZ, + worldunit_t *outZ +) { + assertNotNull(outZ, "Output Z pointer cannot be NULL"); + + const worldunit_t candidates[] = { + nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1) + }; + for(uint8_t i = 0; i < 3; i++) { + const worldpos_t pos = { x, y, candidates[i] }; + if(!tileShapeIsWalkable(chunkGetTile(pos).shape)) continue; + *outZ = candidates[i]; + return true; + } + + return false; +} + +void chunkRebuildOrder(void) { + memoryZero(CHUNK_ORDER, sizeof(CHUNK_ORDER)); + for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { + chunk_t *chunk = &CHUNKS[i]; + const chunkpos_t rel = { + chunk->position.x - CHUNK_POSITION.x, + chunk->position.y - CHUNK_POSITION.y, + chunk->position.z - CHUNK_POSITION.z + }; + if( + rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH || + rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT || + rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH + ) continue; + CHUNK_ORDER[chunkPosToIndex(&rel)] = chunk; + } +} + +void chunkLoadError(void *params, void *user) { + assertNotNull(params, "chunkLoadError: params cannot be NULL"); + assertNotNull(user, "chunkLoadError: user cannot be NULL"); + assetentry_t *entry = (assetentry_t *)params; + chunk_t *chunk = (chunk_t *)user; + if(chunk->dcfEntry != entry) return; + consolePrint( + "Chunk load error: %d %d %d", + (int32_t)chunk->position.x, + (int32_t)chunk->position.y, + (int32_t)chunk->position.z + ); + eventUnsubscribe(&entry->onLoaded, chunkLoaded); + eventUnsubscribe(&entry->onError, chunkLoadError); + assetUnlockEntry(chunk->dcfEntry); + chunk->dcfEntry = NULL; + memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); + + if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL; + chunkLoadNext(); +} + +void chunkLoaded(void *params, void *user) { + assertNotNull(params, "chunkLoaded: params cannot be NULL"); + assertNotNull(user, "chunkLoaded: user cannot be NULL"); + assetentry_t *entry = (assetentry_t *)params; + chunk_t *chunk = (chunk_t *)user; + if(chunk->dcfEntry != entry) return; + + uint8_t meshCount = entry->data.chunk.meshCount; + memoryCopy( + chunk->tiles, + entry->data.chunk.tiles, + sizeof(chunk->tiles) + ); + worldpos_t wp; + chunkPosToWorldPos(&chunk->position, &wp); + vec3 wpf = { + (float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT + }; + for(uint8_t m = 0; m < meshCount; m++) { + stringCopy( + chunk->modelNames[m], + entry->data.chunk.modelNames[m], + CHUNK_MESH_NAME_MAX + ); + glm_vec3_copy( + entry->data.chunk.meshOffsets[m], + chunk->meshOffsets[m] + ); + vec3 scaledOffset = { + chunk->meshOffsets[m][0], + chunk->meshOffsets[m][1], + chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT + }; + vec3 pos; + glm_vec3_add(wpf, scaledOffset, pos); + glm_translate_make(chunk->meshModels[m], pos); + // Borrow the pointer rather than stealing it - the chunk asset entry + // keeps its own lock on each model (taken once while it loaded) and we + // keep the chunk asset entry itself locked (see below), so the models + // stay valid for as long as this chunk_t is using them. The entry may + // now be reused by a later chunkLoad for a different chunk_t once we + // eventually unlock it in chunkUnload, at which point its modelEntries + // must still be intact for that next reuse to copy from. + chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; + } + eventUnsubscribe(&entry->onLoaded, chunkLoaded); + eventUnsubscribe(&entry->onError, chunkLoadError); + // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the + // chunk asset entry (and therefore its model locks) alive for as long as + // this chunk_t is displaying it. Released in chunkUnload instead. + chunk->meshCount = meshCount; + + if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL; + chunkLoadNext(); +} diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h index 38cc9abe..98107efd 100644 --- a/src/dusk/rpg/overworld/chunk.h +++ b/src/dusk/rpg/overworld/chunk.h @@ -30,6 +30,16 @@ typedef struct chunk_s { uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; } chunk_t; +/** Every chunk slot for the currently loaded map. */ +extern chunk_t CHUNKS[MAP_CHUNK_COUNT]; + +/** + * Chunk pointers arranged by position relative to the currently loaded + * window, indexed via chunkPosToIndex(). NULL where no chunk occupies + * that slot. Rebuilt by chunkRebuildOrder() whenever the window moves. + */ +extern chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT]; + /** * Gets the tile index for a tile position within a chunk. * @@ -46,3 +56,126 @@ uint32_t chunkGetTileIndex(const chunkpos_t position); * @return true if equal, false otherwise. */ bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b); + +/** + * Resets and starts loading the initial MAP_CHUNK_WIDTH x HEIGHT x DEPTH + * grid of chunks (async, see chunkLoad), anchored at chunk position + * (0,0,0). Does not unload chunks already loaded - call chunksUnloadAll() + * first when switching to a different map. + * + * @return Any error that occurs. + */ +errorret_t chunksLoadGrid(void); + +/** + * Unloads every chunk slot in CHUNKS. + */ +void chunksUnloadAll(void); + +/** + * Moves the loaded chunk window to be centered around newPos, unloading + * chunks that fall outside the new window and loading any newly exposed + * ones. No-op if newPos matches the currently loaded window. + * + * @param newPos The new chunk position. + * @return An error code. + */ +errorret_t chunkPositionSet(const chunkpos_t newPos); + +/** + * Unloads a chunk. + * + * @param chunk The chunk to unload. + */ +void chunkUnload(chunk_t *chunk); + +/** + * Loads a chunk. Starts async loading without blocking. + * + * @param chunk The chunk to load. + * @return An error code. + */ +errorret_t chunkLoad(chunk_t *chunk); + +/** + * Starts loading the next queued chunk, if no chunk is currently mid-load. + * Called after chunkLoad enqueues a chunk, and again after the + * currently-loading chunk finishes (or is unloaded) to advance the queue. + */ +void chunkLoadNext(void); + +/** + * Removes a chunk from the load queue if present. Used when a chunk is + * re-queued or unloaded before its turn to load has come up. + * + * @param chunk The chunk to remove from the load queue. + */ +void chunkLoadQueueRemove(chunk_t *chunk); + +/** + * Callback invoked when a chunk DCF asset fails to load. Fills the + * chunk tiles with TILE_SHAPE_GROUND as a fallback. + * Always invoked on the main thread. + * + * @param params The failed assetentry_t. + * @param user The chunk_t that owns the entry. + */ +void chunkLoadError(void *params, void *user); + +/** + * Callback invoked when a chunk DCF asset finishes loading. + * Always invoked on the main thread. + * + * @param params The loaded assetentry_t. + * @param user The chunk_t that owns the entry. + */ +void chunkLoaded(void *params, void *user); + +/** + * Rebuilds CHUNK_ORDER from the loaded chunks that fall within the + * current render window. Called whenever the chunk position changes. + */ +void chunkRebuildOrder(void); + +/** + * Gets the index of a chunk, within the currently loaded window, at the + * given position. + * + * @param position The chunk position. + * @return The index of the chunk, or -1 if out of bounds. + */ +chunkindex_t chunkGetIndexAt(const chunkpos_t position); + +/** + * Gets a chunk by its index in CHUNK_ORDER. + * + * @param index The index of the chunk. + * @return A pointer to the chunk. + */ +chunk_t *chunkGet(const uint8_t index); + +/** + * Gets the tile at the given world position. + * + * @param position The world position. + * @return The tile at that position, or TILE_NULL if the chunk is unloaded. + */ +tile_t chunkGetTile(const worldpos_t position); + +/** + * Finds the closest walkable Z layer to nearZ at the given X/Y. Checks + * nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever + * change height by one Z layer between adjacent tiles. + * + * @param x The world X coordinate to check. + * @param y The world Y coordinate to check. + * @param nearZ The reference Z layer to search outward from. + * @param outZ Output pointer, set to the resolved Z layer on success. + * @return true if a walkable tile was found, false otherwise. + */ +bool_t chunkGetWalkableZNear( + const worldunit_t x, + const worldunit_t y, + const worldunit_t nearZ, + worldunit_t *outZ +); diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index 20ad8de1..133f914a 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -10,31 +10,65 @@ #include "assert/assert.h" #include "asset/asset.h" #include "asset/loader/assetloader.h" +#include "asset/loader/assetentry.h" +#include "asset/loader/json/assetjsonloader.h" #include "console/console.h" #include "event/event.h" #include "util/string.h" +#include "rpg/overworld/chunk.h" +#include "rpg/entity/entity.h" #include "rpg/entity/global/entityglobal.h" +#include "yyjson.h" map_t MAP; -errorret_t mapInit() { - memoryZero(&MAP, sizeof(map_t)); - MAP.loaded = true; +errorret_t mapInit(const char_t *name) { + errorChain(mapSetMap(name)); + errorOk(); +} - chunkindex_t 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 = (chunkpos_t){ - (chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z - }; - errorChain(mapChunkLoad(chunk)); - } +errorret_t mapSetMap(const char_t *name) { + assertNotNull(name, "Map name cannot be NULL"); + assertStrLenMin(name, 1, "Map name cannot be empty"); + assertStrLenMax(name, MAP_FILE_PATH_MAX, "Map name too long"); + + if(mapIsLoaded() && stringEquals(MAP.name, name)) errorOk(); + + if(mapIsLoaded()) { + chunksUnloadAll(); + if(MAP.defEntry != NULL) { + eventUnsubscribe(&MAP.defEntry->onLoaded, mapDefLoaded); + eventUnsubscribe(&MAP.defEntry->onError, mapDefLoadError); + assetUnlockEntry(MAP.defEntry); + MAP.defEntry = NULL; } } - mapRebuildChunkOrder(); + memoryZero(&MAP, sizeof(map_t)); + stringCopy(MAP.name, name, MAP_FILE_PATH_MAX); + MAP.loaded = true; + + char_t defPath[MAP_FILE_PATH_MAX + 16]; + stringFormat(defPath, sizeof(defPath), "map/%s/map.json", MAP.name); + + assetentry_t *defEntry = assetLock(defPath, ASSET_LOADER_TYPE_JSON, NULL); + assertNotNull(defEntry, "Failed to get map def asset entry"); + MAP.defEntry = defEntry; + + // The entry may already be resident from an earlier load that hasn't been + // reaped yet - in that case onLoaded/onError already fired once and never + // will again, so handle the terminal state directly instead of waiting on + // a subscription that would never trigger. + if(defEntry->state == ASSET_ENTRY_STATE_LOADED) { + mapDefLoaded(defEntry, NULL); + } else if(defEntry->state == ASSET_ENTRY_STATE_ERROR) { + mapDefLoadError(defEntry, NULL); + } else { + eventSubscribe(&defEntry->onLoaded, mapDefLoaded, NULL); + eventSubscribe(&defEntry->onError, mapDefLoadError, NULL); + } + + errorChain(chunksLoadGrid()); errorOk(); } @@ -42,258 +76,23 @@ bool_t mapIsLoaded() { return MAP.loaded; } -errorret_t mapPositionSet(const chunkpos_t newPos) { - if(!mapIsLoaded()) errorThrow("No map loaded"); - if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk(); - - // Separate loaded chunks into "keep" and "free" buckets. - chunkindex_t chunksFreed[MAP_CHUNK_COUNT]; - uint32_t freedCount = 0; - - // Use a boolean grid so the inner load loop can check O(1). - bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH]; - memoryZero(posLoaded, sizeof(posLoaded)); - - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = &MAP.chunks[i]; - chunkunit_t rx = chunk->position.x - newPos.x; - chunkunit_t ry = chunk->position.y - newPos.y; - chunkunit_t rz = chunk->position.z - newPos.z; - if( - rx >= 0 && rx < MAP_CHUNK_WIDTH && - ry >= 0 && ry < MAP_CHUNK_HEIGHT && - rz >= 0 && rz < MAP_CHUNK_DEPTH - ) { - posLoaded[rx][ry][rz] = true; - } else { - mapChunkUnload(chunk); - chunksFreed[freedCount++] = i; - } - } - - 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++) { - if(posLoaded[x][y][z]) continue; - assertTrue(freedCount > 0, "No free chunk slot available."); - chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]]; - chunk->position = (chunkpos_t){ - newPos.x + (chunkunit_t)x, - newPos.y + (chunkunit_t)y, - newPos.z + (chunkunit_t)z - }; - errorChain(mapChunkLoad(chunk)); - } - } - } - - MAP.chunkPosition = newPos; - mapRebuildChunkOrder(); - errorOk(); -} - errorret_t mapUpdate() { errorOk(); } errorret_t mapDispose() { - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - mapChunkUnload(&MAP.chunks[i]); + chunksUnloadAll(); + + if(MAP.defEntry != NULL) { + eventUnsubscribe(&MAP.defEntry->onLoaded, mapDefLoaded); + eventUnsubscribe(&MAP.defEntry->onError, mapDefLoadError); + assetUnlockEntry(MAP.defEntry); + MAP.defEntry = NULL; } + errorOk(); } -void mapChunkUnload(chunk_t *chunk) { - mapChunkLoadQueueRemove(chunk); - if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL; - - for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { - if(chunk->entities[i] == 0xFF) continue; - entity_t *entity = &ENTITIES[chunk->entities[i]]; - if(!entityCanUnload(entity)) { - entitySetChunk(entity, 0xFF); - } else { - entity->type = ENTITY_TYPE_NULL; - } - } - - memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); - - if(chunk->dcfEntry != NULL) { - eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); - eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; - } - - // modelEntries are borrowed pointers, not independently locked - the - // chunk asset entry (released above) is what actually holds the ref on - // each model, so nothing to unlock here, just drop our own copies. - for(uint8_t m = 0; m < chunk->meshCount; m++) { - chunk->modelEntries[m] = NULL; - } - chunk->meshCount = 0; -} - -errorret_t mapChunkLoad(chunk_t *chunk) { - if(!mapIsLoaded()) errorThrow("No map loaded"); - - mapChunkLoadQueueRemove(chunk); - if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL; - - if(chunk->dcfEntry != NULL) { - eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded); - eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError); - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; - } - - memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); - chunk->meshCount = 0; - - char_t name[64]; - stringFormat( - name, sizeof(name), - "chunks/%d_%d_%d.dcf", - (int32_t)chunk->position.x, - (int32_t)chunk->position.y, - (int32_t)chunk->position.z - ); - - if(!assetFileExists(name)) { - 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(); - } - - assertTrue( - MAP.loadQueueCount < MAP_CHUNK_COUNT, - "Chunk load queue overflow" - ); - MAP.loadQueue[MAP.loadQueueCount++] = chunk; - mapChunkLoadNext(); - errorOk(); -} - -void mapChunkLoadNext() { - if(MAP.loadingChunk != NULL) return; - if(MAP.loadQueueCount == 0) return; - - chunk_t *chunk = MAP.loadQueue[0]; - for(uint32_t i = 1; i < MAP.loadQueueCount; i++) { - MAP.loadQueue[i - 1] = MAP.loadQueue[i]; - } - MAP.loadQueueCount--; - MAP.loadingChunk = chunk; - - char_t name[64]; - stringFormat( - name, sizeof(name), - "chunks/%d_%d_%d.dcf", - (int32_t)chunk->position.x, - (int32_t)chunk->position.y, - (int32_t)chunk->position.z - ); - - assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL); - assertNotNull(entry, "Failed to get chunk asset entry"); - chunk->dcfEntry = entry; - - // The entry may already be resident from an earlier load that hasn't been - // reaped yet - in that case onLoaded/onError already fired once and never - // will again, so handle the terminal state directly instead of waiting on - // a subscription that would never trigger. - if(entry->state == ASSET_ENTRY_STATE_LOADED) { - mapChunkLoaded(entry, chunk); - return; - } - if(entry->state == ASSET_ENTRY_STATE_ERROR) { - mapChunkLoadError(entry, chunk); - return; - } - - eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk); - eventSubscribe(&entry->onError, mapChunkLoadError, chunk); -} - -void mapChunkLoadQueueRemove(chunk_t *chunk) { - for(uint32_t i = 0; i < MAP.loadQueueCount; i++) { - if(MAP.loadQueue[i] != chunk) continue; - for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) { - MAP.loadQueue[j - 1] = MAP.loadQueue[j]; - } - MAP.loadQueueCount--; - return; - } -} - - -chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) { - if(!mapIsLoaded()) return -1; - - chunkpos_t relPos = { - position.x - MAP.chunkPosition.x, - position.y - MAP.chunkPosition.y, - position.z - MAP.chunkPosition.z - }; - - if( - relPos.x < 0 || relPos.y < 0 || relPos.z < 0 || - relPos.x >= MAP_CHUNK_WIDTH || - relPos.y >= MAP_CHUNK_HEIGHT || - relPos.z >= MAP_CHUNK_DEPTH - ) { - return -1; - } - - return chunkPosToIndex(&relPos); -} - -chunk_t *mapGetChunk(const uint8_t index) { - if(index >= MAP_CHUNK_COUNT) return NULL; - if(!mapIsLoaded()) return NULL; - return MAP.chunkOrder[index]; -} - -tile_t mapGetTile(const worldpos_t position) { - if(!mapIsLoaded()) return TILE_NULL; - - chunkpos_t chunkPos; - worldPosToChunkPos(&position, &chunkPos); - chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos); - if(chunkIndex == -1) return TILE_NULL; - - chunk_t *chunk = mapGetChunk(chunkIndex); - assertNotNull(chunk, "Chunk pointer cannot be NULL"); - chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); - tile_t tile = chunk->tiles[tileIndex]; - if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL; - return tile; -} - -bool_t mapGetWalkableZNear( - const worldunit_t x, - const worldunit_t y, - const worldunit_t nearZ, - worldunit_t *outZ -) { - assertNotNull(outZ, "Output Z pointer cannot be NULL"); - - const worldunit_t candidates[] = { - nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1) - }; - for(uint8_t i = 0; i < 3; i++) { - const worldpos_t pos = { x, y, candidates[i] }; - if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue; - *outZ = candidates[i]; - return true; - } - - return false; -} - entity_t * mapSpawnEntity( const entityglobalid_t globalId, const worldpos_t position @@ -320,7 +119,7 @@ entity_t * mapSpawnEntity( // Get available entity. uint8_t index = entityGetAvailable(); assertTrue(index != 0xFF, "No available entity slots for mapSpawnEntity"); - + // Get the pointer and do the init. entity_t *entity = &ENTITIES[index]; entityInit(entity, def->type); @@ -336,103 +135,44 @@ entity_t * mapSpawnEntity( return entity; } -void mapRebuildChunkOrder() { - memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder)); - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = &MAP.chunks[i]; - const chunkpos_t rel = { - chunk->position.x - MAP.chunkPosition.x, - chunk->position.y - MAP.chunkPosition.y, - chunk->position.z - MAP.chunkPosition.z - }; - if( - rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH || - rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT || - rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH - ) continue; - MAP.chunkOrder[chunkPosToIndex(&rel)] = chunk; - } -} - -void mapChunkLoadError(void *params, void *user) { - assertNotNull(params, "mapChunkLoadError: params cannot be NULL"); - assertNotNull(user, "mapChunkLoadError: user cannot be NULL"); +void mapDefLoadError(void *params, void *user) { + assertNotNull(params, "mapDefLoadError: params cannot be NULL"); assetentry_t *entry = (assetentry_t *)params; - chunk_t *chunk = (chunk_t *)user; - if(chunk->dcfEntry != entry) return; - consolePrint( - "Chunk load error: %d %d %d", - (int32_t)chunk->position.x, - (int32_t)chunk->position.y, - (int32_t)chunk->position.z - ); - eventUnsubscribe(&entry->onLoaded, mapChunkLoaded); - eventUnsubscribe(&entry->onError, mapChunkLoadError); - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; - memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); - - if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL; - mapChunkLoadNext(); + if(MAP.defEntry != entry) return; + consolePrint("Failed to load map.json for '%s'", MAP.name); + eventUnsubscribe(&entry->onLoaded, mapDefLoaded); + eventUnsubscribe(&entry->onError, mapDefLoadError); } -void mapChunkLoaded(void *params, void *user) { - assertNotNull(params, "mapChunkLoaded: params cannot be NULL"); - assertNotNull(user, "mapChunkLoaded: user cannot be NULL"); +void mapDefLoaded(void *params, void *user) { + assertNotNull(params, "mapDefLoaded: params cannot be NULL"); assetentry_t *entry = (assetentry_t *)params; - chunk_t *chunk = (chunk_t *)user; - if(chunk->dcfEntry != entry) return; - // consolePrint( - // "Chunk loaded: %d %d %d", - // (int32_t)chunk->position.x, - // (int32_t)chunk->position.y, - // (int32_t)chunk->position.z - // ); - uint8_t meshCount = entry->data.chunk.meshCount; - memoryCopy( - chunk->tiles, - entry->data.chunk.tiles, - sizeof(chunk->tiles) - ); - worldpos_t wp; - chunkPosToWorldPos(&chunk->position, &wp); - vec3 wpf = { - (float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT - }; - for(uint8_t m = 0; m < meshCount; m++) { - stringCopy( - chunk->modelNames[m], - entry->data.chunk.modelNames[m], - CHUNK_MESH_NAME_MAX - ); - glm_vec3_copy( - entry->data.chunk.meshOffsets[m], - chunk->meshOffsets[m] - ); - vec3 scaledOffset = { - chunk->meshOffsets[m][0], - chunk->meshOffsets[m][1], - chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT - }; - vec3 pos; - glm_vec3_add(wpf, scaledOffset, pos); - glm_translate_make(chunk->meshModels[m], pos); - // Borrow the pointer rather than stealing it - the chunk asset entry - // keeps its own lock on each model (taken once while it loaded) and we - // keep the chunk asset entry itself locked (see below), so the models - // stay valid for as long as this chunk_t is using them. The entry may - // now be reused by a later mapChunkLoad for a different chunk_t once we - // eventually unlock it in mapChunkUnload, at which point its - // modelEntries must still be intact for that next reuse to copy from. - chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; - } - eventUnsubscribe(&entry->onLoaded, mapChunkLoaded); - eventUnsubscribe(&entry->onError, mapChunkLoadError); - // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the - // chunk asset entry (and therefore its model locks) alive for as long as - // this chunk_t is displaying it. Released in mapChunkUnload instead. - chunk->meshCount = meshCount; + if(MAP.defEntry != entry) return; - if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL; - mapChunkLoadNext(); + yyjson_val *root = yyjson_doc_get_root(entry->data.json); + yyjson_val *nameVal = yyjson_obj_get(root, "name"); + if(!nameVal || !yyjson_is_str(nameVal)) { + consolePrint("map.json for '%s' missing 'name' string", MAP.name); + } else { + const char_t *nameStr = yyjson_get_str(nameVal); + size_t nameLen = yyjson_get_len(nameVal); + if(nameLen >= MAP_DISPLAY_NAME_MAX) { + consolePrint("Map display name '%s' exceeds max length", nameStr); + } else { + memoryCopy(MAP.displayName, nameStr, nameLen + 1); + } + } + + yyjson_val *entitiesVal = yyjson_obj_get(root, "entities"); + if(entitiesVal && yyjson_is_arr(entitiesVal)) { + size_t entIdx, entMax; + yyjson_val *entObj; + yyjson_arr_foreach(entitiesVal, entIdx, entMax, entObj) { + entity_t *spawned = NULL; + errorCatch(errorPrint(entityCreateFromJson(entObj, &spawned))); + } + } + + eventUnsubscribe(&entry->onLoaded, mapDefLoaded); + eventUnsubscribe(&entry->onError, mapDefLoadError); } diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h index f8b09bf8..7e6f684e 100644 --- a/src/dusk/rpg/overworld/map.h +++ b/src/dusk/rpg/overworld/map.h @@ -1,43 +1,59 @@ /** * Copyright (c) 2025 Dominic Masters - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #pragma once #include "error/error.h" -#include "rpg/overworld/chunk.h" #include "rpg/entity/entity.h" -#define MAP_FILE_PATH_MAX 128 +#define MAP_FILE_PATH_MAX 32 +#define MAP_DISPLAY_NAME_MAX 64 + +typedef struct assetentry_s assetentry_t; typedef struct map_s { + char_t name[MAP_FILE_PATH_MAX]; + char_t displayName[MAP_DISPLAY_NAME_MAX]; bool_t loaded; - chunk_t chunks[MAP_CHUNK_COUNT]; - chunk_t *chunkOrder[MAP_CHUNK_COUNT]; - chunkpos_t chunkPosition; - - // Only one chunk may be mid-load (asset locked & awaiting onLoaded/ - // onError) at any given time - everything else waits here in FIFO order. - chunk_t *loadQueue[MAP_CHUNK_COUNT]; - uint32_t loadQueueCount; - chunk_t *loadingChunk; + // Asset lock for the map's map.json, held while its async load is + // pending and for as long as the map stays loaded. + assetentry_t *defEntry; } map_t; extern map_t MAP; /** - * Initializes the map. - * + * Initializes the map, loading its chunks from beneath the given name's + * asset directory (e.g. "testmap" -> assets/map/testmap/chunks/X_Y_Z.dcf). + * + * @param name The map's directory name, under assets/map/. * @return An error code. */ -errorret_t mapInit(); +errorret_t mapInit(const char_t *name); + +/** + * Switches to a different map, unloading every currently loaded chunk and + * starting an async load of the initial chunk grid (at chunk position + * 0,0,0) plus map.json (-> MAP.displayName), both from beneath the new + * name's asset directory. Once map.json loads, its optional "entities" + * array (see entityCreateFromJson) is spawned; entries that fail to parse + * are logged and skipped rather than failing the whole map load. Returns + * before either finishes loading - callers must not assume + * MAP.displayName, spawned entities, or chunk data is populated yet. + * No-op if name matches the currently loaded map. + * + * @param name The map's directory name, under assets/map/. + * @return An error code. + */ +errorret_t mapSetMap(const char_t *name); /** * Checks if a map is loaded. - * + * * @return true if a map is loaded, false otherwise. */ bool_t mapIsLoaded(); @@ -51,115 +67,31 @@ errorret_t mapUpdate(); /** * Disposes of the map. - * + * * @return An error code. */ errorret_t mapDispose(); /** - * Sets the map position and updates chunks accordingly. - * - * @param newPos The new chunk position. - * @return An error code. - */ -errorret_t mapPositionSet(const chunkpos_t newPos); - -/** - * Unloads a chunk. - * - * @param chunk The chunk to unload. - */ -void mapChunkUnload(chunk_t* chunk); - -/** - * Loads a chunk. Starts async loading without blocking. - * - * @param chunk The chunk to load. - * @return An error code. - */ -errorret_t mapChunkLoad(chunk_t* chunk); - -/** - * Starts loading the next queued chunk, if no chunk is currently mid-load. - * Called after mapChunkLoad enqueues a chunk, and again after the - * currently-loading chunk finishes (or is unloaded) to advance the queue. - */ -void mapChunkLoadNext(); - -/** - * Removes a chunk from the load queue if present. Used when a chunk is - * re-queued or unloaded before its turn to load has come up. - * - * @param chunk The chunk to remove from the load queue. - */ -void mapChunkLoadQueueRemove(chunk_t *chunk); - -/** - * Callback invoked when a chunk DCF asset fails to load. Fills the - * chunk tiles with TILE_SHAPE_GROUND as a fallback. + * Callback invoked when a map's map.json asset fails to load. Leaves + * MAP.displayName empty and logs a console message. * Always invoked on the main thread. * * @param params The failed assetentry_t. - * @param user The chunk_t that owns the entry. + * @param user Unused. */ -void mapChunkLoadError(void *params, void *user); +void mapDefLoadError(void *params, void *user); /** - * Callback invoked when a chunk DCF asset finishes loading. + * Callback invoked when a map's map.json asset finishes loading. Parses + * out the "name" string into MAP.displayName, and spawns each entry of + * the optional "entities" array via entityCreateFromJson. * Always invoked on the main thread. * * @param params The loaded assetentry_t. - * @param user The chunk_t that owns the entry. + * @param user Unused. */ -void mapChunkLoaded(void *params, void *user); - -/** - * Rebuilds chunkOrder from the loaded chunks that fall within the - * current render window. Called whenever chunkPosition changes. - */ -void mapRebuildChunkOrder(); - -/** - * Gets the index of a chunk, within the world, at the given position. - * - * @param position The chunk position. - * @return The index of the chunk, or -1 if out of bounds. - */ -chunkindex_t mapGetChunkIndexAt(const chunkpos_t position); - -/** - * Gets a chunk by its index. - * - * @param chunkIndex The index of the chunk. - * @return A pointer to the chunk. - */ -chunk_t * mapGetChunk(const uint8_t chunkIndex); - -/** - * Gets the tile at the given world position. - * - * @param position The world position. - * @return The tile at that position, or TILE_NULL if the chunk is unloaded. - */ -tile_t mapGetTile(const worldpos_t position); - -/** - * Finds the closest walkable Z layer to nearZ at the given X/Y. Checks - * nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever - * change height by one Z layer between adjacent tiles. - * - * @param x The world X coordinate to check. - * @param y The world Y coordinate to check. - * @param nearZ The reference Z layer to search outward from. - * @param outZ Output pointer, set to the resolved Z layer on success. - * @return true if a walkable tile was found, false otherwise. - */ -bool_t mapGetWalkableZNear( - const worldunit_t x, - const worldunit_t y, - const worldunit_t nearZ, - worldunit_t *outZ -); +void mapDefLoaded(void *params, void *user); /** * Spawns a global (persistent) entity into the world at the given position. @@ -174,4 +106,4 @@ bool_t mapGetWalkableZNear( entity_t * mapSpawnEntity( const entityglobalid_t globalId, const worldpos_t position -); \ No newline at end of file +); diff --git a/src/dusk/rpg/overworld/maparea.c b/src/dusk/rpg/overworld/maparea.c index 95efe628..398a8bdb 100644 --- a/src/dusk/rpg/overworld/maparea.c +++ b/src/dusk/rpg/overworld/maparea.c @@ -9,7 +9,7 @@ #include "assert/assert.h" #include "util/math.h" #include "util/memory.h" -#include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX]; @@ -74,7 +74,7 @@ bool_t mapAreaCanUnload(const maparea_t *area) { assertNotNull(area, "Map area pointer cannot be NULL"); for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false; + if(mapAreaIsChunkOverlappingOrInside(area, &CHUNKS[i])) return false; } return true; diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index ad42e3e1..85896907 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -8,8 +8,8 @@ #include "rpg.h" #include "entity/entity.h" #include "rpg/entity/npc/npcpath.h" -#include "rpg/entity/item/entityitem.h" #include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" #include "rpg/overworld/maparea.h" #include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/scene/testcutscene.h" @@ -37,31 +37,16 @@ errorret_t rpgInit(void) { partyInit(); cutsceneSystemInit(); - errorChain(mapInit()); + errorChain(mapInit("testmap")); rpgCameraInit(); // Init world - errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 })); - - // TEST: Create some entities. - uint8_t entIndex = entityGetAvailable(); - assertTrue(entIndex != 0xFF, "No available entity slots!."); - entity_t *ent = &ENTITIES[entIndex]; - entityInit(ent, ENTITY_TYPE_PLAYER); - entityPositionSet(ent, (worldpos_t){ 10, 2, 0 });// Also assigns the chunk. - RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; - RPG_CAMERA.followEntity.followEntityId = ent->id; + errorChain(chunkPositionSet((chunkpos_t){ 0, 0, 0 })); + // Player is defined in the map's JSON (see entityCreateFromJson) and + // spawns asynchronously; rpgCameraUpdate picks it up once it appears. mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 }); - // TEST: Place an item entity. - uint8_t itemEntIndex = entityGetAvailable(); - assertTrue(itemEntIndex != 0xFF, "No available entity slots!."); - entity_t *itemEnt = &ENTITIES[itemEntIndex]; - entityInit(itemEnt, ENTITY_TYPE_ITEM); - entityItemSet(itemEnt, itemGetIdByName("POTION"), 1); - entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 }); - // TEST: Give the player a starting assortment of items. backpackAdd(itemGetIdByName("POTION"), 5); backpackAdd(itemGetIdByName("POTATO"), 3); diff --git a/src/dusk/rpg/rpgcamera.c b/src/dusk/rpg/rpgcamera.c index d3742d88..6d6fd5b1 100644 --- a/src/dusk/rpg/rpgcamera.c +++ b/src/dusk/rpg/rpgcamera.c @@ -10,6 +10,7 @@ #include "util/random.h" #include "rpg/entity/entity.h" #include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" #include "assert/assert.h" #include "time/time.h" @@ -114,6 +115,17 @@ errorret_t rpgCameraUpdate(void) { RPG_CAMERA.shakeTime += TIME.delta; } + // The player entity may spawn asynchronously (e.g. via map.json), so + // start following it as soon as it shows up rather than requiring + // whoever creates it to also wire up the camera. + if(RPG_CAMERA.mode == RPG_CAMERA_MODE_FREE) { + entity_t *player = entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER); + if(player != NULL) { + RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; + RPG_CAMERA.followEntity.followEntityId = player->id; + } + } + if(!mapIsLoaded()) errorOk(); vec3 pos; @@ -125,7 +137,7 @@ errorret_t rpgCameraUpdate(void) { .z = (chunkunit_t)floorf(pos[2] / WORLD_LAYER_HEIGHT / CHUNK_DEPTH) }; - errorChain(mapPositionSet((chunkpos_t){ + errorChain(chunkPositionSet((chunkpos_t){ .x = chunkPos.x - (MAP_CHUNK_WIDTH / 2), .y = chunkPos.y - (MAP_CHUNK_HEIGHT / 2), .z = chunkPos.z - (MAP_CHUNK_DEPTH / 2) diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c index 4e42a149..b375677d 100644 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ b/src/dusk/scene/overworld/sceneoverworld.c @@ -17,7 +17,7 @@ #include "display/spritebatch/spritebatch.h" #include "display/texture/texture.h" -#include "rpg/overworld/map.h" +#include "rpg/overworld/chunk.h" #include "rpg/entity/entity.h" #include "rpg/rpgcamera.h" @@ -183,7 +183,7 @@ errorret_t sceneOverworldDrawEntity( errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = MAP.chunkOrder[i]; + chunk_t *chunk = CHUNK_ORDER[i]; if(chunk == NULL) continue; if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue; if(chunk->meshCount == 0) continue; @@ -220,7 +220,7 @@ errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) { errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) { for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = MAP.chunkOrder[i]; + chunk_t *chunk = CHUNK_ORDER[i]; if(chunk == NULL) continue; if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue; diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index bfdb2203..bdd31c06 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -12,9 +12,12 @@ #include "util/memory.h" #include "display/spritebatch/spritebatch.h" #include "display/screen/screen.h" +#include "display/text/text.h" +#include "display/color.h" #include "assert/assert.h" #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" +#include "rpg/overworld/map.h" #define UI_GAME_MENU_INDEX_CHARACTERS 0 #define UI_GAME_MENU_INDEX_ITEMS 1 @@ -77,12 +80,25 @@ errorret_t uiGameMenuDraw(void) { const float_t y = (float_t)SCREEN.scanY; errorChain(uiFrameDraw(x, y, width, height)); + + const float_t contentX = x + UI_FRAME_START_X; + const float_t contentY = y + UI_FRAME_START_Y; + const float_t contentWidth = width - (UI_FRAME_START_X * 2); + const float_t contentHeight = height - (UI_FRAME_START_Y * 2); + + // Map display name header - stopgap placement until this gets a proper + // HUD element of its own. + const float_t nameRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; + errorChain(textDraw( + contentX, contentY, MAP.displayName, COLOR_WHITE, &FONT_DEFAULT + )); + errorChain(uiMenuDraw( &UI_GAME_MENU.menu, - x + UI_FRAME_START_X, - y + UI_FRAME_START_Y, - width - (UI_FRAME_START_X * 2), - height - (UI_FRAME_START_Y * 2) + contentX, + contentY + nameRowHeight + UI_FRAME_PADDING_Y, + contentWidth, + contentHeight - nameRowHeight - UI_FRAME_PADDING_Y )); errorChain(spriteBatchFlush()); diff --git a/src/dusk/ui/widget/uiitemlist.c b/src/dusk/ui/widget/uiitemlist.c index 2ab2f62a..f765fd9e 100644 --- a/src/dusk/ui/widget/uiitemlist.c +++ b/src/dusk/ui/widget/uiitemlist.c @@ -44,7 +44,7 @@ void uiItemListSetItems( ) { assertNotNull(list, "Item list cannot be NULL"); assertTrue( - itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list" + itemCount <= INVENTORY_CAPACITY_MAX, "Too many items for list" ); memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount); @@ -58,7 +58,7 @@ errorret_t uiItemListSetItemStacks( ) { assertNotNull(list, "Item list cannot be NULL"); assertTrue( - stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list" + stackCount <= INVENTORY_CAPACITY_MAX, "Too many items for list" ); for(uint8_t i = 0; i < stackCount; i++) { diff --git a/src/dusk/ui/widget/uiitemlist.h b/src/dusk/ui/widget/uiitemlist.h index 26b3e350..deed14ea 100644 --- a/src/dusk/ui/widget/uiitemlist.h +++ b/src/dusk/ui/widget/uiitemlist.h @@ -12,8 +12,6 @@ #include "ui/focus/uifocus.h" #include "rpg/item/inventory.h" -#define UI_ITEM_LIST_CAPACITY_MAX 40 - typedef struct uiitemlist_s uiitemlist_t; typedef void (*uiitemlistselectedcallback_t)( @@ -51,7 +49,7 @@ typedef errorret_t (*uiitemlistcolumncallback_t)( ); struct uiitemlist_s { - uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX]; + uiitem_t items[INVENTORY_CAPACITY_MAX]; uint8_t itemCount; // Grid layout: how many item slots wide/tall the list displays. @@ -105,7 +103,7 @@ void uiItemListInit( * @param list The item list to update. * @param items The items to display. * @param itemCount Number of entries in items. Must be <= - * UI_ITEM_LIST_CAPACITY_MAX. + * INVENTORY_CAPACITY_MAX. */ void uiItemListSetItems( uiitemlist_t *list, @@ -120,7 +118,7 @@ void uiItemListSetItems( * @param list The item list to update. * @param stacks The item stacks to display. * @param stackCount Number of entries in stacks. Must be <= - * UI_ITEM_LIST_CAPACITY_MAX. + * INVENTORY_CAPACITY_MAX. * @return Any error that occurs. */ errorret_t uiItemListSetItemStacks( diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py index 60ccec06..d34508ed 100644 --- a/tools/asset/chunk/__main__.py +++ b/tools/asset/chunk/__main__.py @@ -7,7 +7,8 @@ Generates DCF + companion DMF files from raw chunk JSON files, or upgrades 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, one subdirectory per +map - e.g. assetsraw/overworld/chunks/): { "tiles": [ { "pos": [x, y, z], "type": , "tile": } @@ -25,8 +26,8 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json): 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 + Output DCF is derived automatically, preserving the map subdirectory: + assetsraw//chunks/chunk_X_Y_Z.json -> assets//chunks/X_Y_Z.dcf Version 4 DCF format (after 8-byte header): tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column) @@ -45,7 +46,7 @@ DMF format: 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 # process all assetsraw/*/chunks/*.json python3 -m tools.asset.chunk # process one JSON file python3 -m tools.asset.chunk # upgrade legacy (v1/v2) DCF in-place """ @@ -143,7 +144,7 @@ def write_model_json(path, mesh_rel, texture_rel, color): def derive_dcf_path(json_path): - """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""" base = os.path.splitext(os.path.basename(json_path))[0] if base.startswith('chunk_'): base = base[len('chunk_'):] @@ -432,17 +433,15 @@ 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) + os.path.join(dirpath, f) + for dirpath, _, filenames in os.walk(ASSETSRAW_DIR) + if os.path.basename(dirpath) == 'chunks' + for f in filenames if f.endswith('.json') ) if not json_files: - print(f"No JSON files found in {chunks_dir}") + print(f"No chunk JSON files found under {ASSETSRAW_DIR}") sys.exit(0) for p in json_files: process_json(p)