Map as a file

This commit is contained in:
2026-07-11 17:07:03 -05:00
parent b08ad308e4
commit 60dfb89b53
32 changed files with 859 additions and 531 deletions
+7
View File
@@ -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 }
]
}
@@ -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);
}
+2 -2
View File
@@ -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) {
+106 -8
View File
@@ -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();
}
+29 -1
View File
@@ -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);
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);
+1 -1
View File
@@ -15,7 +15,7 @@ void backpackInit() {
inventoryInit(
&BACKPACK.inventories[i],
BACKPACK.storage[i],
ITEM_TYPE_SLOT_COUNT_MAX
INVENTORY_CAPACITY_MAX
);
}
}
+1 -3
View File
@@ -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;
+1
View File
@@ -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,
+383 -2
View File
@@ -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);
}
}
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();
}
+133
View File
@@ -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
);
+92 -352
View File
@@ -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);
}
+43 -111
View File
@@ -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
);
);
+2 -2
View File
@@ -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;
+5 -20
View File
@@ -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);
+13 -1
View File
@@ -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)
+3 -3
View File
@@ -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;
+20 -4
View File
@@ -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());
+2 -2
View File
@@ -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++) {
+3 -5
View File
@@ -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(
+11 -12
View File
@@ -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/<map>/chunks/chunk_X_Y_Z.json, one subdirectory per
map - e.g. assetsraw/overworld/chunks/):
{
"tiles": [
{ "pos": [x, y, z], "type": <tile_shape_int>, "tile": <uv_tile_int> }
@@ -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/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/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 <file.json> # process one JSON file
python3 -m tools.asset.chunk <file.dcf> # 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/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/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)