Chunk streaming concurrency, entity slot fix, and map-data-driven spawns
- Allow 2 chunks to be mid-load concurrently instead of 1 (MAP_CHUNK_LOAD_CONCURRENCY). - Fix entitySetChunk silently losing track of an entity when its target chunk's entity slots are full - it now stays detached (and retries later) instead of claiming a chunk that never actually registered it. - DCF format bumped to v5: chunks can now declare entity spawns (global/NPC via the existing entityglobal registry, or one-shot item pickups) and map area triggers, resolved via a new callback-ID registry (mapareagloballist.h) mirroring the entity one. rpg.c's hardcoded TEST entity/item/area spawns are gone - chunk_0_0_0.json now carries that data instead. The player is still bootstrapped in code since it isn't map-authored content.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2179,5 +2179,27 @@
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
"type": "global",
|
||||
"globalId": 3,
|
||||
"pos": [8, 8, 1]
|
||||
},
|
||||
{
|
||||
"type": "item",
|
||||
"itemId": 1,
|
||||
"quantity": 1,
|
||||
"pos": [12, 2, 0]
|
||||
}
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"min": [11, 3, 0],
|
||||
"max": [16, 9, 10],
|
||||
"callbackId": 1,
|
||||
"notify": 3,
|
||||
"trigger": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,26 @@
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
// Reads a little-endian int16 from a potentially-unaligned offset into a
|
||||
// worldunit_t, advancing *offset past it.
|
||||
static worldunit_t assetChunkReadWorldUnit(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
int16_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(int16_t));
|
||||
*offset += sizeof(int16_t);
|
||||
return (worldunit_t)endianLittleToHost16((uint16_t)value);
|
||||
}
|
||||
|
||||
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
|
||||
worldpos_t pos;
|
||||
pos.x = assetChunkReadWorldUnit(data, offset);
|
||||
pos.y = assetChunkReadWorldUnit(data, offset);
|
||||
pos.z = assetChunkReadWorldUnit(data, offset);
|
||||
return pos;
|
||||
}
|
||||
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
@@ -146,6 +166,62 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||
}
|
||||
|
||||
out->entitySpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
||||
"Chunk entity spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
||||
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
uint16_t a;
|
||||
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
||||
a = endianLittleToHost16(a);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
uint8_t b = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = a;
|
||||
spawn->itemQuantity = b;
|
||||
} else {
|
||||
spawn->globalId = a;
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
}
|
||||
|
||||
spawn->position = assetChunkReadWorldPos(data, &offset);
|
||||
}
|
||||
|
||||
out->areaSpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
||||
"Chunk area spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||
area->min = assetChunkReadWorldPos(data, &offset);
|
||||
area->max = assetChunkReadWorldPos(data, &offset);
|
||||
|
||||
uint16_t callbackId;
|
||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||
area->callbackId = endianLittleToHost16(callbackId);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
area->notify = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
area->trigger = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 4
|
||||
#define ASSET_CHUNK_FILE_VERSION 5
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
@@ -33,12 +33,39 @@ typedef struct {
|
||||
uint8_t modelIndex;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
typedef enum {
|
||||
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
|
||||
CHUNK_ENTITY_SPAWN_KIND_ITEM
|
||||
} chunkentityspawnkind_t;
|
||||
|
||||
typedef struct {
|
||||
chunkentityspawnkind_t kind;
|
||||
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
|
||||
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||
worldpos_t position;
|
||||
} chunkentityspawn_t;
|
||||
|
||||
typedef struct {
|
||||
worldpos_t min;
|
||||
worldpos_t max;
|
||||
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
|
||||
uint8_t notify;
|
||||
uint8_t trigger;
|
||||
} chunkareaspawn_t;
|
||||
|
||||
typedef struct {
|
||||
tile_t *tiles;
|
||||
uint8_t meshCount;
|
||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
uint8_t entitySpawnCount;
|
||||
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
||||
|
||||
uint8_t areaSpawnCount;
|
||||
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "util/memory.h"
|
||||
#include "time/time.h"
|
||||
#include "util/math.h"
|
||||
#include "console/console.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
@@ -292,7 +293,10 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
entity->chunkIndex = chunkIndex;
|
||||
// Only claim the new chunk once actually inserted into one of its slots -
|
||||
// otherwise entity->chunkIndex would point at a chunk that doesn't know
|
||||
// about this entity, so it would never be torn down on unload.
|
||||
entity->chunkIndex = 0xFF;
|
||||
|
||||
if(chunkIndex != 0xFF) {
|
||||
chunk_t *next = mapGetChunk(chunkIndex);
|
||||
@@ -300,8 +304,16 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(next->entities[i] != 0xFF) continue;
|
||||
next->entities[i] = entity->id;
|
||||
entity->chunkIndex = chunkIndex;
|
||||
break;
|
||||
}
|
||||
if(entity->chunkIndex != chunkIndex) {
|
||||
consolePrint(
|
||||
"entitySetChunk: chunk %u has no free entity slots, entity %u "
|
||||
"left untracked",
|
||||
chunkIndex, entity->id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,10 @@ uint8_t entityGetAvailable();
|
||||
|
||||
/**
|
||||
* Assigns an entity to a chunk, removing it from its current chunk first.
|
||||
* Pass 0xFF as chunkIndex to detach the entity from any chunk.
|
||||
* Pass 0xFF as chunkIndex to detach the entity from any chunk. If the
|
||||
* target chunk has no free entity slots, the entity is left detached
|
||||
* (chunkIndex 0xFF) rather than assigned to a chunk that isn't actually
|
||||
* tracking it - entityUpdateChunk will keep retrying on subsequent moves.
|
||||
*
|
||||
* @param entity Pointer to the entity.
|
||||
* @param chunkIndex Index of the chunk to assign to, or 0xFF for none.
|
||||
|
||||
@@ -14,3 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
tileshape.c
|
||||
)
|
||||
|
||||
add_subdirectory(global)
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define CHUNK_MESH_COUNT_MAX 10
|
||||
#define CHUNK_MESH_NAME_MAX 64
|
||||
#define CHUNK_ENTITY_COUNT_MAX 10
|
||||
#define CHUNK_ENTITY_SPAWN_COUNT_MAX 8
|
||||
#define CHUNK_AREA_COUNT_MAX 4
|
||||
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
@@ -28,6 +30,13 @@ typedef struct chunk_s {
|
||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
||||
|
||||
// Map area IDs (into MAP_AREAS) spawned from this chunk's file data.
|
||||
// Removed via mapAreaRemove when this chunk unloads, and re-added if it
|
||||
// streams back in - unlike entities (tracked by current position via
|
||||
// entities[] above), areas have no position-based ownership mechanism of
|
||||
// their own, so the owning chunk must track and tear them down directly.
|
||||
uint8_t areas[CHUNK_AREA_COUNT_MAX];
|
||||
} chunk_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "mapareaglobaldefs.h"
|
||||
#include "mapareagloballist.h"
|
||||
|
||||
#define MAP_AREA_CALLBACK_LIST_COUNT ( \
|
||||
sizeof(MAP_AREA_CALLBACK_LIST) / \
|
||||
sizeof(MAP_AREA_CALLBACK_LIST[0]) \
|
||||
)
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/overworld/maparea.h"
|
||||
|
||||
#define MAP_AREA_CALLBACK(id) \
|
||||
static void MAP_AREA_CALLBACK_##id(entity_t *entity, const uint8_t trigger)
|
||||
|
||||
#define MAP_AREA_CALLBACK_REF(id) \
|
||||
MAP_AREA_CALLBACK_##id
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "mapareaglobaldefs.h"
|
||||
#include "console/console.h"
|
||||
|
||||
MAP_AREA_CALLBACK(1) {
|
||||
consolePrint("mapAreaGlobalCallback 1: trigger=%u", trigger);
|
||||
}
|
||||
|
||||
// Index 0 is reserved (not a valid callback ID) - see mapAreaAddGlobal.
|
||||
static const mapareacallback_t MAP_AREA_CALLBACK_LIST[] = {
|
||||
NULL,
|
||||
MAP_AREA_CALLBACK_REF(1),
|
||||
};
|
||||
|
||||
//EOF
|
||||
@@ -14,9 +14,20 @@
|
||||
#include "event/event.h"
|
||||
#include "util/string.h"
|
||||
#include "rpg/entity/global/entityglobal.h"
|
||||
#include "rpg/entity/item/entityitem.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
|
||||
map_t MAP;
|
||||
|
||||
// Clears chunk's mid-load slot, if it currently holds one.
|
||||
static void mapChunkLoadingSlotClear(chunk_t *chunk) {
|
||||
for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) {
|
||||
if(MAP.loadingChunks[i] != chunk) continue;
|
||||
MAP.loadingChunks[i] = NULL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t mapInit() {
|
||||
memoryZero(&MAP, sizeof(map_t));
|
||||
MAP.loaded = true;
|
||||
@@ -105,7 +116,7 @@ errorret_t mapDispose() {
|
||||
|
||||
void mapChunkUnload(chunk_t *chunk) {
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(chunk->entities[i] == 0xFF) continue;
|
||||
@@ -119,6 +130,12 @@ void mapChunkUnload(chunk_t *chunk) {
|
||||
|
||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||
|
||||
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
|
||||
if(chunk->areas[i] == 0xFF) continue;
|
||||
mapAreaRemove(chunk->areas[i]);
|
||||
}
|
||||
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||
@@ -139,7 +156,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||
|
||||
mapChunkLoadQueueRemove(chunk);
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
@@ -149,6 +166,16 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
}
|
||||
|
||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||
|
||||
// Normally already empty (mapChunkUnload clears these before a chunk is
|
||||
// handed back for reuse), but cleared defensively here too so a reload
|
||||
// never leaks a MAP_AREAS slot referenced by a stale owned area ID.
|
||||
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
|
||||
if(chunk->areas[i] == 0xFF) continue;
|
||||
mapAreaRemove(chunk->areas[i]);
|
||||
}
|
||||
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
||||
|
||||
chunk->meshCount = 0;
|
||||
|
||||
char_t name[64];
|
||||
@@ -178,7 +205,8 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
}
|
||||
|
||||
void mapChunkLoadNext() {
|
||||
if(MAP.loadingChunk != NULL) return;
|
||||
for(uint32_t slot = 0; slot < MAP_CHUNK_LOAD_CONCURRENCY; slot++) {
|
||||
if(MAP.loadingChunks[slot] != NULL) continue;
|
||||
if(MAP.loadQueueCount == 0) return;
|
||||
|
||||
chunk_t *chunk = MAP.loadQueue[0];
|
||||
@@ -186,7 +214,7 @@ void mapChunkLoadNext() {
|
||||
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
||||
}
|
||||
MAP.loadQueueCount--;
|
||||
MAP.loadingChunk = chunk;
|
||||
MAP.loadingChunks[slot] = chunk;
|
||||
|
||||
char_t name[64];
|
||||
stringFormat(
|
||||
@@ -201,21 +229,24 @@ void mapChunkLoadNext() {
|
||||
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.
|
||||
// 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. Both of these
|
||||
// recurse back into mapChunkLoadNext once they clear this slot, so the
|
||||
// outer loop just continues on to try filling the next one.
|
||||
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
mapChunkLoaded(entry, chunk);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
mapChunkLoadError(entry, chunk);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
void mapChunkLoadQueueRemove(chunk_t *chunk) {
|
||||
@@ -372,7 +403,7 @@ void mapChunkLoadError(void *params, void *user) {
|
||||
chunk->dcfEntry = NULL;
|
||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
@@ -433,6 +464,47 @@ void mapChunkLoaded(void *params, void *user) {
|
||||
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
||||
chunk->meshCount = meshCount;
|
||||
|
||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
||||
// Spawn entities declared by this chunk's file. Global entities are
|
||||
// deduped by mapSpawnEntity itself (a persistent NPC that streams back
|
||||
// in won't be duplicated); item entities have no persistent identity, so
|
||||
// each reload spawns a fresh one - picking an item up and then leaving
|
||||
// and re-entering its chunk will currently respawn it, since nothing
|
||||
// tracks "already collected" across a chunk unload/reload yet.
|
||||
for(uint8_t s = 0; s < entry->data.chunk.entitySpawnCount; s++) {
|
||||
chunkentityspawn_t *spawn = &entry->data.chunk.entitySpawns[s];
|
||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL) {
|
||||
mapSpawnEntity((entityglobalid_t)spawn->globalId, spawn->position);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t index = entityGetAvailable();
|
||||
assertTrue(index != 0xFF, "No available entity slots for chunk spawn");
|
||||
entity_t *itemEntity = &ENTITIES[index];
|
||||
entityInit(itemEntity, ENTITY_TYPE_ITEM);
|
||||
entityItemSet(
|
||||
itemEntity, (itemid_t)spawn->itemId, spawn->itemQuantity
|
||||
);
|
||||
entityPositionSet(itemEntity, spawn->position);
|
||||
}
|
||||
|
||||
// Spawn map areas declared by this chunk's file, tracked as owned by
|
||||
// this chunk so mapChunkUnload can tear them down again.
|
||||
for(uint8_t s = 0; s < entry->data.chunk.areaSpawnCount; s++) {
|
||||
chunkareaspawn_t *area = &entry->data.chunk.areaSpawns[s];
|
||||
uint8_t areaId = mapAreaAddGlobal(
|
||||
area->min, area->max, area->callbackId, area->notify, area->trigger
|
||||
);
|
||||
|
||||
uint8_t slot = 0xFF;
|
||||
for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) {
|
||||
if(chunk->areas[i] != 0xFF) continue;
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
assertTrue(slot != 0xFF, "Chunk has no free owned-area slots");
|
||||
chunk->areas[slot] = areaId;
|
||||
}
|
||||
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
#define MAP_FILE_PATH_MAX 128
|
||||
|
||||
// Number of chunks that may be mid-load (asset locked & awaiting onLoaded/
|
||||
// onError) at the same time - everything past this waits in loadQueue.
|
||||
#define MAP_CHUNK_LOAD_CONCURRENCY 2
|
||||
|
||||
typedef struct map_s {
|
||||
bool_t loaded;
|
||||
|
||||
@@ -19,11 +23,9 @@ typedef struct map_s {
|
||||
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;
|
||||
chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY];
|
||||
} map_t;
|
||||
|
||||
extern map_t MAP;
|
||||
@@ -80,9 +82,9 @@ void mapChunkUnload(chunk_t* chunk);
|
||||
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.
|
||||
* Starts loading queued chunks until MAP_CHUNK_LOAD_CONCURRENCY chunks are
|
||||
* mid-load. Called after mapChunkLoad enqueues a chunk, and again after a
|
||||
* mid-load chunk finishes (or is unloaded) to advance the queue.
|
||||
*/
|
||||
void mapChunkLoadNext();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "util/math.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/global/mapareaglobal.h"
|
||||
|
||||
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
|
||||
|
||||
@@ -148,3 +149,20 @@ void mapAreaCheckEntity(entity_t *entity) {
|
||||
|
||||
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) {
|
||||
}
|
||||
|
||||
uint8_t mapAreaAddGlobal(
|
||||
const worldpos_t min,
|
||||
const worldpos_t max,
|
||||
const uint16_t callbackId,
|
||||
const uint8_t notify,
|
||||
const uint8_t trigger
|
||||
) {
|
||||
assertTrue(callbackId > 0, "Map area callback ID 0 is reserved");
|
||||
assertTrue(
|
||||
callbackId < MAP_AREA_CALLBACK_LIST_COUNT,
|
||||
"Map area callback ID is out of range"
|
||||
);
|
||||
return mapAreaAdd(
|
||||
min, max, MAP_AREA_CALLBACK_LIST[callbackId], notify, trigger
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,3 +159,28 @@ void mapAreaCheckEntity(entity_t *entity);
|
||||
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
|
||||
*/
|
||||
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
|
||||
|
||||
/**
|
||||
* Adds a map area using a compiled-in callback referenced by ID (see
|
||||
* MAP_AREA_CALLBACK_LIST in rpg/overworld/global/mapareagloballist.h),
|
||||
* rather than a direct function pointer. This is what lets chunk file
|
||||
* data - which can only reference compiled code by a small integer ID,
|
||||
* not a function pointer - declare map areas.
|
||||
*
|
||||
* @param min The minimum world position of the area.
|
||||
* @param max The maximum world position of the area.
|
||||
* @param callbackId Index into MAP_AREA_CALLBACK_LIST. Must be greater
|
||||
* than 0 (0 is reserved) and within range.
|
||||
* @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types
|
||||
* should trigger the callback.
|
||||
* @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should
|
||||
* invoke the callback.
|
||||
* @returns The ID of the newly added map area.
|
||||
*/
|
||||
uint8_t mapAreaAddGlobal(
|
||||
const worldpos_t min,
|
||||
const worldpos_t max,
|
||||
const uint16_t callbackId,
|
||||
const uint8_t notify,
|
||||
const uint8_t trigger
|
||||
);
|
||||
+4
-30
@@ -7,12 +7,9 @@
|
||||
|
||||
#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/maparea.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/scene/testcutscene.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
@@ -21,14 +18,9 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
|
||||
void rpgTestAreaCallback(entity_t *entity, const uint8_t trigger) {
|
||||
consolePrint("rpgTestAreaCallback: trigger=%u", trigger);
|
||||
}
|
||||
|
||||
errorret_t rpgInit(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
@@ -43,7 +35,9 @@ errorret_t rpgInit(void) {
|
||||
// Init world
|
||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||
|
||||
// TEST: Create some entities.
|
||||
// The player is the one entity that isn't sourced from map/chunk data -
|
||||
// every other entity (NPCs, items) and map area comes from the loaded
|
||||
// chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded).
|
||||
uint8_t entIndex = entityGetAvailable();
|
||||
assertTrue(entIndex != 0xFF, "No available entity slots!.");
|
||||
entity_t *ent = &ENTITIES[entIndex];
|
||||
@@ -52,31 +46,11 @@ errorret_t rpgInit(void) {
|
||||
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
||||
RPG_CAMERA.followEntity.followEntityId = ent->id;
|
||||
|
||||
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, ITEM_ID_POTION, 1);
|
||||
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 });
|
||||
|
||||
// TEST: Give the player a starting assortment of items.
|
||||
// Starting inventory.
|
||||
backpackAdd(ITEM_ID_POTION, 5);
|
||||
backpackAdd(ITEM_ID_POTATO, 3);
|
||||
backpackAdd(ITEM_ID_APPLE, 8);
|
||||
|
||||
// TEST: Create a test map area.
|
||||
uint8_t areaIndex = mapAreaAdd(
|
||||
(worldpos_t){ 11, 3, 0 },
|
||||
(worldpos_t){ 16, 9, 10 },
|
||||
rpgTestAreaCallback,
|
||||
MAP_AREA_NOTIFY_ALL,
|
||||
MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT
|
||||
);
|
||||
assertTrue(areaIndex != 0xFF, "No available map area slots!.");
|
||||
|
||||
// All Good!
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,16 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
||||
],
|
||||
"meshes": [
|
||||
{ "file": "house_5_3.dmf", "pos": [x, y, z] }
|
||||
],
|
||||
"entities": [
|
||||
{ "type": "global", "globalId": <int>, "pos": [x, y, z] },
|
||||
{ "type": "item", "itemId": <int>, "quantity": <int>, "pos": [x, y, z] }
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"min": [x, y, z], "max": [x, y, z],
|
||||
"callbackId": <int>, "notify": <int>, "trigger": <int>
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,10 +35,27 @@ 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.
|
||||
|
||||
"entities" spawns things into the world when this chunk loads. A "global"
|
||||
entity is spawned via mapSpawnEntity() - globalId indexes
|
||||
ENTITY_GLOBAL_LIST (src/dusk/rpg/entity/global/entitygloballist.h) and is
|
||||
deduped automatically if already spawned, so it's safe to declare on a
|
||||
chunk that streams in more than once. An "item" entity has no persistent
|
||||
identity - it respawns fresh every time this chunk (re)loads, including
|
||||
after being picked up, since nothing tracks "already collected" yet.
|
||||
itemId is a raw ITEM_ID_* value (see src/dusk/rpg/item/item.json for the
|
||||
name -> id mapping, same convention as the tile "type" ints above).
|
||||
|
||||
"areas" declares map trigger regions (see rpg/overworld/maparea.h) owned
|
||||
by this chunk - they're removed when the chunk unloads and re-added if it
|
||||
streams back in. callbackId indexes MAP_AREA_CALLBACK_LIST
|
||||
(src/dusk/rpg/overworld/global/mapareagloballist.h; 0 is reserved and
|
||||
invalid). notify is bitwise MAP_AREA_NOTIFY_PLAYER(1)|NOTIFY_NPC(2).
|
||||
trigger is bitwise MAP_TRIGGER_STEP(1)|ENTER(2)|EXIT(4).
|
||||
|
||||
Output DCF is derived automatically:
|
||||
assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf
|
||||
|
||||
Version 4 DCF format (after 8-byte header):
|
||||
Version 5 DCF format (after 8-byte header):
|
||||
tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column)
|
||||
each tile: uint32_t shape, uint8_t z, 3 padding bytes (8 bytes total,
|
||||
matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; })
|
||||
@@ -36,6 +63,19 @@ Version 4 DCF format (after 8-byte header):
|
||||
for each model:
|
||||
null-terminated string (relative asset path to .json model)
|
||||
float32[3] (x, y, z offset)
|
||||
uint8_t entitySpawnCount
|
||||
for each entity spawn:
|
||||
uint8_t kind (0 = global entity, 1 = item entity)
|
||||
uint16_t a (globalId if kind 0, itemId if kind 1)
|
||||
uint8_t b (unused if kind 0, quantity if kind 1)
|
||||
int16_t x, y, z (world position, 3 fields)
|
||||
uint8_t areaSpawnCount
|
||||
for each area spawn:
|
||||
int16_t minX, minY, minZ (3 fields)
|
||||
int16_t maxX, maxY, maxZ (3 fields)
|
||||
uint16_t callbackId
|
||||
uint8_t notify
|
||||
uint8_t trigger
|
||||
|
||||
DMF format:
|
||||
Bytes 0-3: DMF\\x00
|
||||
@@ -74,6 +114,11 @@ WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2)
|
||||
|
||||
CHUNK_MESH_COUNT_MAX = 10
|
||||
CHUNK_MESH_NAME_MAX = 64
|
||||
CHUNK_ENTITY_SPAWN_COUNT_MAX = 8
|
||||
CHUNK_AREA_COUNT_MAX = 4
|
||||
|
||||
ENTITY_SPAWN_KIND_GLOBAL = 0
|
||||
ENTITY_SPAWN_KIND_ITEM = 1
|
||||
|
||||
# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded
|
||||
# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment).
|
||||
@@ -106,7 +151,7 @@ TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
|
||||
|
||||
FILE_MAGIC = b'DCF'
|
||||
DMF_MAGIC = b'DMF\x00'
|
||||
VERSION_OUT = 4
|
||||
VERSION_OUT = 5
|
||||
DMF_VERSION = 1
|
||||
|
||||
|
||||
@@ -163,11 +208,29 @@ def write_dmf(path, vertex_bytes):
|
||||
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
||||
|
||||
|
||||
def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
||||
def write_dcf(
|
||||
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
||||
entity_spawns=None, area_spawns=None
|
||||
):
|
||||
"""Write a current-version DCF referencing the given DMF asset paths."""
|
||||
mesh_count = len(mesh_names)
|
||||
if mesh_offsets is None:
|
||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||
if entity_spawns is None:
|
||||
entity_spawns = []
|
||||
if area_spawns is None:
|
||||
area_spawns = []
|
||||
|
||||
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
||||
f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}"
|
||||
)
|
||||
if len(area_spawns) > CHUNK_AREA_COUNT_MAX:
|
||||
raise ValueError(
|
||||
f"Too many area spawns ({len(area_spawns)}) - max "
|
||||
f"{CHUNK_AREA_COUNT_MAX}"
|
||||
)
|
||||
|
||||
buf = bytearray()
|
||||
buf += FILE_MAGIC
|
||||
@@ -183,11 +246,34 @@ def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
||||
)
|
||||
buf += encoded + b'\x00'
|
||||
buf += struct.pack('<3f', offset[0], offset[1], offset[2])
|
||||
|
||||
buf += struct.pack('<B', len(entity_spawns))
|
||||
for spawn in entity_spawns:
|
||||
kind = spawn['kind']
|
||||
x, y, z = spawn['pos']
|
||||
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
||||
a, b = spawn['globalId'], 0
|
||||
else:
|
||||
a, b = spawn['itemId'], spawn['quantity']
|
||||
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
||||
|
||||
buf += struct.pack('<B', len(area_spawns))
|
||||
for area in area_spawns:
|
||||
minX, minY, minZ = area['min']
|
||||
maxX, maxY, maxZ = area['max']
|
||||
buf += struct.pack(
|
||||
'<6hHBB',
|
||||
minX, minY, minZ, maxX, maxY, maxZ,
|
||||
area['callbackId'], area['notify'], area['trigger']
|
||||
)
|
||||
|
||||
with open(dcf_path, 'wb') as f:
|
||||
f.write(buf)
|
||||
print(
|
||||
f' Wrote DCF {dcf_path}: '
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes'
|
||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
||||
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
||||
f'area(s), {len(buf)} bytes'
|
||||
)
|
||||
|
||||
|
||||
@@ -323,7 +409,39 @@ def from_json(json_path, dcf_path):
|
||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||
print(f' Resolved {filename} -> {rel}')
|
||||
|
||||
write_dcf(dcf_path, bytes(tiles), model_names, mesh_offsets)
|
||||
entity_spawns = []
|
||||
for spawn in data.get('entities', []):
|
||||
pos = tuple(int(v) for v in spawn['pos'])
|
||||
if spawn['type'] == 'global':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_GLOBAL,
|
||||
'globalId': int(spawn['globalId']),
|
||||
'pos': pos,
|
||||
})
|
||||
elif spawn['type'] == 'item':
|
||||
entity_spawns.append({
|
||||
'kind': ENTITY_SPAWN_KIND_ITEM,
|
||||
'itemId': int(spawn['itemId']),
|
||||
'quantity': int(spawn['quantity']),
|
||||
'pos': pos,
|
||||
})
|
||||
else:
|
||||
raise ValueError(f"Unknown entity spawn type: {spawn['type']}")
|
||||
|
||||
area_spawns = []
|
||||
for area in data.get('areas', []):
|
||||
area_spawns.append({
|
||||
'min': tuple(int(v) for v in area['min']),
|
||||
'max': tuple(int(v) for v in area['max']),
|
||||
'callbackId': int(area['callbackId']),
|
||||
'notify': int(area['notify']),
|
||||
'trigger': int(area['trigger']),
|
||||
})
|
||||
|
||||
write_dcf(
|
||||
dcf_path, bytes(tiles), model_names, mesh_offsets,
|
||||
entity_spawns, area_spawns
|
||||
)
|
||||
|
||||
|
||||
def process_json(json_path):
|
||||
|
||||
Reference in New Issue
Block a user