Map as a file
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -15,7 +15,7 @@ void backpackInit() {
|
||||
inventoryInit(
|
||||
&BACKPACK.inventories[i],
|
||||
BACKPACK.storage[i],
|
||||
ITEM_TYPE_SLOT_COUNT_MAX
|
||||
INVENTORY_CAPACITY_MAX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
);
|
||||
);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user