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:
2026-08-04 07:37:48 -05:00
parent a84137b5ff
commit 4b0388a0e1
23 changed files with 510 additions and 85 deletions
@@ -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;
+28 -1
View File
@@ -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;
/**
+13 -1
View File
@@ -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
);
}
}
}
}
+4 -1
View File
@@ -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.
+2
View File
@@ -14,3 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
tileshape.c
)
add_subdirectory(global)
+9
View File
@@ -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
View File
@@ -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();
}
+8 -6
View File
@@ -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();
+18
View File
@@ -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
);
}
+26 -1
View File
@@ -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
View File
@@ -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();
}