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:
@@ -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
|
||||
+112
-40
@@ -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,44 +205,48 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
}
|
||||
|
||||
void mapChunkLoadNext() {
|
||||
if(MAP.loadingChunk != NULL) return;
|
||||
if(MAP.loadQueueCount == 0) 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];
|
||||
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
|
||||
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
||||
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.loadingChunks[slot] = 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. 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);
|
||||
continue;
|
||||
}
|
||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
mapChunkLoadError(entry, chunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
||||
}
|
||||
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) {
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,4 +158,29 @@ void mapAreaCheckEntity(entity_t *entity);
|
||||
* @param entity Pointer to the entity associated with the callback.
|
||||
* @param trigger Which MAP_TRIGGER_* condition invoked the callback.
|
||||
*/
|
||||
void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger);
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user