Load raw chunk JSON at runtime instead of baked DCF/DMF
Experimental: skip the offline tools/asset/chunk bake step entirely. The chunk asset loader now parses the same JSON schema tools/asset/chunk already consumed (tiles/meshes/entities/areas) directly at load time, generating the terrain mesh in C instead of reading pre-baked .dcf/.dmf bytes. The raw assetsraw/maps/*/chunks/*.json files are now the shipped source directly under assets/, replacing the old baked chunk/mesh/model output. Also drops MAP_CHUNK_LOAD_CONCURRENCY to 1 for now, per PSP hardware testing (8 crashed, 4 untested further - 1 is the known-safe value). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
c8a6325615
commit
9688e88a7c
29 files changed
+412
-200
No files matched your search
@@ -8,41 +8,288 @@
|
||||
#include "assetchunkloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/endian.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "display/color.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#define ASSET_CHUNK_TERRAIN_TEXTURE "tiles.png"
|
||||
|
||||
// Per-shape corner Z offsets (sw, se, ne, nw) as offsets from a tile's own
|
||||
// base Z, ported 1:1 from tools/asset/chunk/__main__.py's _RAMP_CORNERS.
|
||||
// TILE_SHAPE_NULL's row is never read - assetChunkTilesHaveTerrain/
|
||||
// assetChunkBuildTerrain skip those tiles outright.
|
||||
static const float_t CHUNK_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = {
|
||||
[TILE_SHAPE_GROUND] = {0.0f, 0.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_NORTH] = {0.0f, 0.0f, 1.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTH] = {1.0f, 1.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_EAST] = {0.0f, 1.0f, 1.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_WEST] = {1.0f, 0.0f, 0.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHEAST] = {0.0f, 0.0f, 1.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHWEST] = {0.0f, 0.0f, 0.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST] = {0.0f, 1.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST] = {1.0f, 0.0f, 0.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHEAST_INNER] = {0.0f, 1.0f, 1.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_NORTHWEST_INNER] = {1.0f, 0.0f, 1.0f, 1.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST_INNER] = {1.0f, 1.0f, 1.0f, 0.0f},
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST_INNER] = {1.0f, 1.0f, 0.0f, 1.0f},
|
||||
};
|
||||
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
static bool_t assetChunkTilesHaveTerrain(const tile_t *tiles) {
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
if(tiles[i].shape != TILE_SHAPE_NULL) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reads the "tiles" array - one quad's worth of column data per entry,
|
||||
// last one in the array wins per (x, y) column - matching
|
||||
// tools/asset/chunk/__main__.py:390-403.
|
||||
static void assetChunkParseTiles(yyjson_val *root, tile_t *tiles) {
|
||||
memoryZero(tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
|
||||
yyjson_val *tilesArr = yyjson_obj_get(root, "tiles");
|
||||
if(!yyjson_is_arr(tilesArr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *tileVal;
|
||||
yyjson_arr_foreach(tilesArr, idx, len, tileVal) {
|
||||
yyjson_val *posArr = yyjson_obj_get(tileVal, "pos");
|
||||
if(!yyjson_is_arr(posArr) || yyjson_arr_size(posArr) < 3) continue;
|
||||
|
||||
int32_t x = (int32_t)yyjson_get_int(yyjson_arr_get(posArr, 0));
|
||||
int32_t y = (int32_t)yyjson_get_int(yyjson_arr_get(posArr, 1));
|
||||
int32_t z = (int32_t)yyjson_get_int(yyjson_arr_get(posArr, 2));
|
||||
if(x < 0 || x >= CHUNK_WIDTH || y < 0 || y >= CHUNK_HEIGHT) continue;
|
||||
|
||||
int32_t type = (int32_t)yyjson_get_int(yyjson_obj_get(tileVal, "type"));
|
||||
tiles[x + y * CHUNK_WIDTH] = (tile_t){
|
||||
.shape = (tileshape_t)type, .z = (uint8_t)z
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves "meshes" (external building references, by bare filename) into
|
||||
// out->modelNames/meshOffsets starting at *ioIndex, advancing it as
|
||||
// entries are written. Filenames are resolved as
|
||||
// "models/buildings/<basename>.json" - a hardcoded stand-in for
|
||||
// tools/asset/chunk/__main__.py's find_model() recursive search, valid
|
||||
// as long as every building model lives flat under that one directory
|
||||
// (true today - see the raw-json-maps plan notes for the tradeoff).
|
||||
static void assetChunkParseMeshes(
|
||||
yyjson_val *root, assetchunkoutput_t *out, uint8_t *ioIndex
|
||||
) {
|
||||
yyjson_val *meshesArr = yyjson_obj_get(root, "meshes");
|
||||
if(!yyjson_is_arr(meshesArr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *meshVal;
|
||||
yyjson_arr_foreach(meshesArr, idx, len, meshVal) {
|
||||
if(*ioIndex >= CHUNK_MESH_COUNT_MAX) break;
|
||||
|
||||
yyjson_val *fileVal = yyjson_obj_get(meshVal, "file");
|
||||
if(!fileVal || !yyjson_is_str(fileVal)) continue;
|
||||
|
||||
char_t baseName[CHUNK_MESH_NAME_MAX];
|
||||
stringCopy(baseName, yyjson_get_str(fileVal), sizeof(baseName));
|
||||
char_t *dot = stringFindLastChar(baseName, '.');
|
||||
if(dot != NULL) *dot = '\0';
|
||||
|
||||
uint8_t m = *ioIndex;
|
||||
stringFormat(
|
||||
out->modelNames[m], CHUNK_MESH_NAME_MAX,
|
||||
"models/buildings/%s.json", baseName
|
||||
);
|
||||
|
||||
vec3 offset = {0.0f, 0.0f, 0.0f};
|
||||
yyjson_val *posArr = yyjson_obj_get(meshVal, "pos");
|
||||
if(yyjson_is_arr(posArr) && yyjson_arr_size(posArr) >= 3) {
|
||||
offset[0] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 0));
|
||||
offset[1] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 1));
|
||||
offset[2] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 2));
|
||||
}
|
||||
memoryCopy(out->meshOffsets[m], offset, sizeof(vec3));
|
||||
|
||||
(*ioIndex)++;
|
||||
}
|
||||
}
|
||||
|
||||
static void assetChunkParseEntities(yyjson_val *root, assetchunkoutput_t *out) {
|
||||
out->entitySpawnCount = 0;
|
||||
|
||||
yyjson_val *arr = yyjson_obj_get(root, "entities");
|
||||
if(!yyjson_is_arr(arr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *val;
|
||||
yyjson_arr_foreach(arr, idx, len, val) {
|
||||
if(out->entitySpawnCount >= CHUNK_ENTITY_SPAWN_COUNT_MAX) break;
|
||||
|
||||
const char_t *type = yyjson_get_str(yyjson_obj_get(val, "type"));
|
||||
yyjson_val *posArr = yyjson_obj_get(val, "pos");
|
||||
if(type == NULL || !yyjson_is_arr(posArr) || yyjson_arr_size(posArr) < 3) {
|
||||
continue;
|
||||
}
|
||||
worldpos_t pos = {
|
||||
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 0)),
|
||||
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 1)),
|
||||
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 2)),
|
||||
};
|
||||
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[out->entitySpawnCount];
|
||||
if(stringEquals(type, "global")) {
|
||||
spawn->kind = CHUNK_ENTITY_SPAWN_KIND_GLOBAL;
|
||||
spawn->globalId =
|
||||
(uint16_t)yyjson_get_int(yyjson_obj_get(val, "globalId"));
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
} else if(stringEquals(type, "item")) {
|
||||
spawn->kind = CHUNK_ENTITY_SPAWN_KIND_ITEM;
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = (uint16_t)yyjson_get_int(yyjson_obj_get(val, "itemId"));
|
||||
spawn->itemQuantity =
|
||||
(uint8_t)yyjson_get_int(yyjson_obj_get(val, "quantity"));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
spawn->position = pos;
|
||||
out->entitySpawnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
static void assetChunkParseAreas(yyjson_val *root, assetchunkoutput_t *out) {
|
||||
out->areaSpawnCount = 0;
|
||||
|
||||
yyjson_val *arr = yyjson_obj_get(root, "areas");
|
||||
if(!yyjson_is_arr(arr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *val;
|
||||
yyjson_arr_foreach(arr, idx, len, val) {
|
||||
if(out->areaSpawnCount >= CHUNK_AREA_COUNT_MAX) break;
|
||||
|
||||
yyjson_val *minArr = yyjson_obj_get(val, "min");
|
||||
yyjson_val *maxArr = yyjson_obj_get(val, "max");
|
||||
if(
|
||||
!yyjson_is_arr(minArr) || yyjson_arr_size(minArr) < 3 ||
|
||||
!yyjson_is_arr(maxArr) || yyjson_arr_size(maxArr) < 3
|
||||
) continue;
|
||||
|
||||
chunkareaspawn_t *area = &out->areaSpawns[out->areaSpawnCount];
|
||||
area->min = (worldpos_t){
|
||||
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 0)),
|
||||
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 1)),
|
||||
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 2)),
|
||||
};
|
||||
area->max = (worldpos_t){
|
||||
.x = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 0)),
|
||||
.y = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 1)),
|
||||
.z = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 2)),
|
||||
};
|
||||
area->callbackId =
|
||||
(uint16_t)yyjson_get_int(yyjson_obj_get(val, "callbackId"));
|
||||
area->notify = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "notify"));
|
||||
area->trigger = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "trigger"));
|
||||
out->areaSpawnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Six vertices (2 CCW triangles) for one tile's quad, matching
|
||||
// tools/asset/chunk/__main__.py's _tile_quad. Corner order: SW=(x,y),
|
||||
// SE=(x+1,y), NE=(x+1,y+1), NW=(x,y+1). NORTH=+Y, EAST=+X.
|
||||
static void assetChunkAppendTileQuad(
|
||||
meshvertex_t *verts, uint32_t *vertCount,
|
||||
const uint32_t x, const uint32_t y,
|
||||
const float_t sw, const float_t se, const float_t ne, const float_t nw
|
||||
) {
|
||||
const float_t u0 = (float_t)x / (float_t)CHUNK_WIDTH;
|
||||
const float_t u1 = (float_t)(x + 1) / (float_t)CHUNK_WIDTH;
|
||||
const float_t v0 = (float_t)y / (float_t)CHUNK_HEIGHT;
|
||||
const float_t v1 = (float_t)(y + 1) / (float_t)CHUNK_HEIGHT;
|
||||
const float_t fx = (float_t)x;
|
||||
const float_t fy = (float_t)y;
|
||||
|
||||
meshvertex_t *v = verts + *vertCount;
|
||||
v[0] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
|
||||
v[1] = (meshvertex_t){ .uv = {u1, v0}, .pos = {fx + 1, fy, se} };
|
||||
v[2] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
|
||||
v[3] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} };
|
||||
v[4] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} };
|
||||
v[5] = (meshvertex_t){ .uv = {u0, v1}, .pos = {fx, fy + 1, nw} };
|
||||
*vertCount += 6;
|
||||
}
|
||||
|
||||
// Builds a mesh_t straight from the tile grid - one quad per non-null
|
||||
// tile column, shaped by CHUNK_RAMP_CORNERS - the runtime equivalent of
|
||||
// tools/asset/chunk/__main__.py's build_terrain_verts. Only called once
|
||||
// assetChunkTilesHaveTerrain has confirmed at least one real tile exists.
|
||||
// Mirrors assetMeshLoaderSync's meshInit/meshFlush/free-after-upload
|
||||
// sequence (assetmeshloader.c) since this produces the same
|
||||
// assetmeshoutput_t shape, just from generated verts instead of a file.
|
||||
static errorret_t assetChunkBuildTerrain(
|
||||
const tile_t *tiles, assetmeshoutput_t *out
|
||||
) {
|
||||
meshvertex_t *verts = memoryAllocate(
|
||||
CHUNK_TILE_COUNT * 6 * sizeof(meshvertex_t)
|
||||
);
|
||||
uint32_t vertCount = 0;
|
||||
|
||||
for(uint32_t y = 0; y < CHUNK_HEIGHT; y++) {
|
||||
for(uint32_t x = 0; x < CHUNK_WIDTH; x++) {
|
||||
const tile_t *tile = &tiles[x + y * CHUNK_WIDTH];
|
||||
if(tile->shape == TILE_SHAPE_NULL) continue;
|
||||
|
||||
const float_t *corners = CHUNK_RAMP_CORNERS[tile->shape];
|
||||
const float_t fz = (float_t)tile->z;
|
||||
assetChunkAppendTileQuad(
|
||||
verts, &vertCount, x, y,
|
||||
(fz + corners[0]) * WORLD_LAYER_HEIGHT,
|
||||
(fz + corners[1]) * WORLD_LAYER_HEIGHT,
|
||||
(fz + corners[2]) * WORLD_LAYER_HEIGHT,
|
||||
(fz + corners[3]) * WORLD_LAYER_HEIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
||||
assertTrue(vertCount > 0, "Terrain build called with no terrain tiles.");
|
||||
|
||||
assetfile_t *file = &loading->loading.chunk.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
out->vertices = verts;
|
||||
errorret_t ret = meshInit(
|
||||
&out->mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, (int32_t)vertCount, verts
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
out->meshInitialized = true;
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire chunk file."
|
||||
);
|
||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||
if(errorIsNotOk(ret)) {
|
||||
meshDispose(&out->mesh);
|
||||
out->meshInitialized = false;
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
#endif
|
||||
|
||||
loading->loading.chunk.data = data;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -55,8 +302,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
|
||||
switch(loading->loading.chunk.state) {
|
||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
||||
@@ -69,9 +316,7 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
out->modelEntries[m] = assetLock(
|
||||
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
|
||||
);
|
||||
assertNotNull(
|
||||
out->modelEntries[m], "Failed to lock model."
|
||||
);
|
||||
assertNotNull(out->modelEntries[m], "Failed to lock model.");
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
@@ -93,126 +338,103 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.chunk.data;
|
||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
// --- PARSE: lock+require the chunk's own JSON as a sub-asset, exactly
|
||||
// like assetmodelloader.c's ASSET_MODEL_LOADING_STATE_LOCK_ASSETS does
|
||||
// for its model descriptor, then read everything out of it before
|
||||
// unlocking (the yyjson_doc/vals only stay valid while jsonEntry does).
|
||||
char_t jsonKey[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name);
|
||||
assetloaderinput_t jsonInput;
|
||||
memoryZero(&jsonInput, sizeof(jsonInput));
|
||||
stringCopy(
|
||||
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
|
||||
);
|
||||
assetentry_t *jsonEntry =
|
||||
assetLock(jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput);
|
||||
errorret_t jsonRet = assetRequireLoaded(jsonEntry);
|
||||
if(errorIsNotOk(jsonRet)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported chunk file version %u", version
|
||||
loading, "Failed to load chunk JSON: %s", loading->entry->name
|
||||
);
|
||||
}
|
||||
|
||||
size_t offset = 8;
|
||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
out->tiles = memoryAllocate(tileSize);
|
||||
memoryCopy(out->tiles, data + offset, tileSize);
|
||||
offset += tileSize;
|
||||
tile_t tiles[CHUNK_TILE_COUNT];
|
||||
assetChunkParseTiles(root, tiles);
|
||||
out->hasTerrain = assetChunkTilesHaveTerrain(tiles);
|
||||
|
||||
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
|
||||
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
|
||||
*shape = endianLittleToHost32(*shape);
|
||||
}
|
||||
uint8_t meshIndex = out->hasTerrain ? 1 : 0;
|
||||
assetChunkParseMeshes(root, out, &meshIndex);
|
||||
out->meshCount = meshIndex;
|
||||
|
||||
out->meshCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
||||
"Chunk mesh count exceeds maximum."
|
||||
);
|
||||
assetChunkParseEntities(root, out);
|
||||
assetChunkParseAreas(root, out);
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
uint8_t nameLen = 0;
|
||||
while(
|
||||
data[offset + nameLen] != '\0' &&
|
||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
||||
) {
|
||||
nameLen++;
|
||||
}
|
||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
||||
out->modelNames[m][nameLen] = '\0';
|
||||
offset += nameLen + 1;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
|
||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
||||
offset += sizeof(vec3);
|
||||
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
|
||||
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
|
||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||
}
|
||||
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
memoryCopy(out->tiles, tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
|
||||
out->entitySpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
||||
"Chunk entity spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
||||
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
uint16_t a;
|
||||
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
||||
a = endianLittleToHost16(a);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
uint8_t b = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = a;
|
||||
spawn->itemQuantity = b;
|
||||
} else {
|
||||
spawn->globalId = a;
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
// Terrain: build the mesh, then wrap it in synthetic (non-cache,
|
||||
// non-refcounted) mesh/model entries so modelEntries[0] behaves exactly
|
||||
// like a real file-backed model entry for every downstream consumer -
|
||||
// see the header's doc comment on hasTerrain/terrainMeshEntry.
|
||||
if(out->hasTerrain) {
|
||||
assetmeshoutput_t terrainMesh;
|
||||
memoryZero(&terrainMesh, sizeof(terrainMesh));
|
||||
errorret_t terrainRet = assetChunkBuildTerrain(out->tiles, &terrainMesh);
|
||||
if(errorIsNotOk(terrainRet)) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Failed to build chunk terrain mesh: %s",
|
||||
loading->entry->name
|
||||
);
|
||||
}
|
||||
|
||||
spawn->position = worldPosReadLE(data, &offset);
|
||||
assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
|
||||
assetentry_t *texEntry = assetLock(
|
||||
ASSET_CHUNK_TERRAIN_TEXTURE, ASSET_LOADER_TYPE_TEXTURE, &texInput
|
||||
);
|
||||
errorret_t texRet = assetRequireLoaded(texEntry);
|
||||
if(errorIsNotOk(texRet)) {
|
||||
assetUnlockEntry(texEntry);
|
||||
meshDispose(&terrainMesh.mesh);
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Failed to load chunk terrain texture: %s",
|
||||
loading->entry->name
|
||||
);
|
||||
}
|
||||
out->terrainTexEntry = texEntry;
|
||||
|
||||
out->terrainMeshEntry = memoryAllocate(sizeof(assetentry_t));
|
||||
memoryZero(out->terrainMeshEntry, sizeof(assetentry_t));
|
||||
out->terrainMeshEntry->type = ASSET_LOADER_TYPE_MESH;
|
||||
out->terrainMeshEntry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
out->terrainMeshEntry->data.mesh = terrainMesh;
|
||||
|
||||
out->terrainModelEntry = memoryAllocate(sizeof(assetentry_t));
|
||||
memoryZero(out->terrainModelEntry, sizeof(assetentry_t));
|
||||
out->terrainModelEntry->type = ASSET_LOADER_TYPE_MODEL;
|
||||
out->terrainModelEntry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
out->terrainModelEntry->data.model.meshEntry = out->terrainMeshEntry;
|
||||
out->terrainModelEntry->data.model.texEntry = out->terrainTexEntry;
|
||||
out->terrainModelEntry->data.model.color = COLOR_WHITE;
|
||||
|
||||
out->modelEntries[0] = out->terrainModelEntry;
|
||||
}
|
||||
|
||||
out->areaSpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
||||
"Chunk area spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||
area->min = worldPosReadLE(data, &offset);
|
||||
area->max = worldPosReadLE(data, &offset);
|
||||
|
||||
uint16_t callbackId;
|
||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||
area->callbackId = endianLittleToHost16(callbackId);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
area->notify = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
area->trigger = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->loading.chunk.modelIndex = out->hasTerrain ? 1 : 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
@@ -231,8 +453,36 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
if(out->modelEntries[m] == NULL) continue;
|
||||
if(out->hasTerrain && m == 0) continue; // synthetic - freed below.
|
||||
assetUnlockEntry(out->modelEntries[m]);
|
||||
out->modelEntries[m] = NULL;
|
||||
}
|
||||
|
||||
if(out->hasTerrain) {
|
||||
if(out->terrainMeshEntry != NULL) {
|
||||
assetmeshoutput_t *meshOut = &out->terrainMeshEntry->data.mesh;
|
||||
if(meshOut->meshInitialized) {
|
||||
meshDispose(&meshOut->mesh);
|
||||
meshOut->meshInitialized = false;
|
||||
}
|
||||
if(meshOut->vertices != NULL) {
|
||||
memoryFree(meshOut->vertices);
|
||||
meshOut->vertices = NULL;
|
||||
}
|
||||
memoryFree(out->terrainMeshEntry);
|
||||
out->terrainMeshEntry = NULL;
|
||||
}
|
||||
if(out->terrainModelEntry != NULL) {
|
||||
memoryFree(out->terrainModelEntry);
|
||||
out->terrainModelEntry = NULL;
|
||||
}
|
||||
if(out->terrainTexEntry != NULL) {
|
||||
assetUnlockEntry(out->terrainTexEntry);
|
||||
out->terrainTexEntry = NULL;
|
||||
}
|
||||
out->modelEntries[0] = NULL;
|
||||
out->hasTerrain = false;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 5
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
@@ -20,16 +18,17 @@ typedef struct {
|
||||
|
||||
typedef enum {
|
||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
||||
ASSET_CHUNK_LOADING_STATE_DONE
|
||||
} assetchunkloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetchunkloadingstate_t state;
|
||||
uint8_t *data;
|
||||
|
||||
// Index into assetchunkoutput_t.modelNames/modelEntries the external
|
||||
// (non-terrain) model polling loop has reached - see
|
||||
// ASSET_CHUNK_LOADING_STATE_LOAD_MODELS.
|
||||
uint8_t modelIndex;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
@@ -61,6 +60,27 @@ typedef struct {
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
// Terrain mesh generated at load time from the tile grid (no baked
|
||||
// .dmf/.json - see assetChunkBuildTerrain in assetchunkloader.c). NULL
|
||||
// (with hasTerrain false) for a chunk with no non-null tiles, in which
|
||||
// case modelEntries[0] (if meshCount > 0) is the first external mesh
|
||||
// instead - matches the old baked pipeline's "only write a terrain
|
||||
// model if it has vertices" behavior.
|
||||
//
|
||||
// terrainMeshEntry/terrainModelEntry are heap-allocated, plain
|
||||
// assetentry_t values that are never passed to assetLock/assetGetEntry/
|
||||
// assetUnlockEntry (only a real, name-keyed, ref-counted entry may be)
|
||||
// - they exist purely so modelEntries[0] can point at something with
|
||||
// the same ->state/->data.model/->data.mesh.mesh shape every other
|
||||
// model/mesh entry has, so chunk.c/map.c/sceneoverworld.c don't need to
|
||||
// know terrain isn't file-backed. Freed directly (not assetUnlockEntry)
|
||||
// in assetChunkDispose. terrainTexEntry is a real, ref-counted texture
|
||||
// entry (the shared "tiles.png") and must be assetUnlockEntry'd there.
|
||||
bool_t hasTerrain;
|
||||
assetentry_t *terrainMeshEntry;
|
||||
assetentry_t *terrainModelEntry;
|
||||
assetentry_t *terrainTexEntry;
|
||||
|
||||
uint8_t entitySpawnCount;
|
||||
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
||||
|
||||
@@ -69,9 +89,10 @@ typedef struct {
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
||||
* the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
* Async loader stub for chunk assets. Chunk loading is performed entirely
|
||||
* on the main thread (it delegates to the JSON asset loader and to
|
||||
* meshInit/meshFlush, both main-thread-only); this callback is never
|
||||
* reached.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
@@ -79,9 +100,11 @@ typedef struct {
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||
* read by the async phase and populates the output assetchunkoutput_t with
|
||||
* tile data and model paths.
|
||||
* Synchronous loader for chunk assets. Locks the chunk's JSON descriptor
|
||||
* as a sub-asset, parses tiles/external mesh references/entity spawns/
|
||||
* area spawns, generates the terrain mesh directly from the tile grid,
|
||||
* and locks/waits for every referenced model (and the terrain's shared
|
||||
* tileset texture) to finish loading.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
|
||||
@@ -226,7 +226,7 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
char_t name[MAP_FILE_PATH_MAX];
|
||||
stringFormat(
|
||||
name, sizeof(name),
|
||||
"maps/%s/chunks/%d_%d_%d.dcf",
|
||||
"maps/%s/chunks/chunk_%d_%d_%d.json",
|
||||
MAP.name,
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
@@ -265,7 +265,7 @@ void mapChunkLoadNext() {
|
||||
char_t name[MAP_FILE_PATH_MAX];
|
||||
stringFormat(
|
||||
name, sizeof(name),
|
||||
"maps/%s/chunks/%d_%d_%d.dcf",
|
||||
"maps/%s/chunks/chunk_%d_%d_%d.json",
|
||||
MAP.name,
|
||||
(int32_t)chunk->position.x,
|
||||
(int32_t)chunk->position.y,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
// Number of chunks that may be mid-load (asset locked & awaiting onLoaded/
|
||||
// onError) at the same time - everything past this waits in loadQueue.
|
||||
#define MAP_CHUNK_LOAD_CONCURRENCY 2
|
||||
#define MAP_CHUNK_LOAD_CONCURRENCY 1
|
||||
|
||||
typedef struct map_s {
|
||||
// Name of the currently loaded map, or an empty string if none is
|
||||
|
||||
Reference in new issue
Block a user