diff --git a/assets/maps/overworld/chunks/-1_0_0.dcf b/assets/maps/overworld/chunks/-1_0_0.dcf deleted file mode 100644 index 51344b70..00000000 Binary files a/assets/maps/overworld/chunks/-1_0_0.dcf and /dev/null differ diff --git a/assets/maps/overworld/chunks/0_0_0.dcf b/assets/maps/overworld/chunks/0_0_0.dcf deleted file mode 100644 index adbc2840..00000000 Binary files a/assets/maps/overworld/chunks/0_0_0.dcf and /dev/null differ diff --git a/assets/maps/overworld/chunks/0_1_0.dcf b/assets/maps/overworld/chunks/0_1_0.dcf deleted file mode 100644 index 5048d3e4..00000000 Binary files a/assets/maps/overworld/chunks/0_1_0.dcf and /dev/null differ diff --git a/assets/maps/overworld/chunks/1_0_0.dcf b/assets/maps/overworld/chunks/1_0_0.dcf deleted file mode 100644 index ed0d0ba7..00000000 Binary files a/assets/maps/overworld/chunks/1_0_0.dcf and /dev/null differ diff --git a/assets/maps/overworld/chunks/1_1_0.dcf b/assets/maps/overworld/chunks/1_1_0.dcf deleted file mode 100644 index aca3ec24..00000000 Binary files a/assets/maps/overworld/chunks/1_1_0.dcf and /dev/null differ diff --git a/assets/maps/overworld/chunks/2_0_0.dcf b/assets/maps/overworld/chunks/2_0_0.dcf deleted file mode 100644 index 61c16946..00000000 Binary files a/assets/maps/overworld/chunks/2_0_0.dcf and /dev/null differ diff --git a/assetsraw/maps/overworld/chunks/chunk_-1_0_0.json b/assets/maps/overworld/chunks/chunk_-1_0_0.json similarity index 100% rename from assetsraw/maps/overworld/chunks/chunk_-1_0_0.json rename to assets/maps/overworld/chunks/chunk_-1_0_0.json diff --git a/assetsraw/maps/overworld/chunks/chunk_0_0_0.json b/assets/maps/overworld/chunks/chunk_0_0_0.json similarity index 100% rename from assetsraw/maps/overworld/chunks/chunk_0_0_0.json rename to assets/maps/overworld/chunks/chunk_0_0_0.json diff --git a/assetsraw/maps/overworld/chunks/chunk_0_1_0.json b/assets/maps/overworld/chunks/chunk_0_1_0.json similarity index 100% rename from assetsraw/maps/overworld/chunks/chunk_0_1_0.json rename to assets/maps/overworld/chunks/chunk_0_1_0.json diff --git a/assetsraw/maps/overworld/chunks/chunk_1_0_0.json b/assets/maps/overworld/chunks/chunk_1_0_0.json similarity index 100% rename from assetsraw/maps/overworld/chunks/chunk_1_0_0.json rename to assets/maps/overworld/chunks/chunk_1_0_0.json diff --git a/assetsraw/maps/overworld/chunks/chunk_1_1_0.json b/assets/maps/overworld/chunks/chunk_1_1_0.json similarity index 100% rename from assetsraw/maps/overworld/chunks/chunk_1_1_0.json rename to assets/maps/overworld/chunks/chunk_1_1_0.json diff --git a/assetsraw/maps/overworld/chunks/chunk_2_0_0.json b/assets/maps/overworld/chunks/chunk_2_0_0.json similarity index 100% rename from assetsraw/maps/overworld/chunks/chunk_2_0_0.json rename to assets/maps/overworld/chunks/chunk_2_0_0.json diff --git a/assets/maps/overworld/meshes/chunks/chunk_-1_0_0_0.dmf b/assets/maps/overworld/meshes/chunks/chunk_-1_0_0_0.dmf deleted file mode 100644 index 484beafe..00000000 Binary files a/assets/maps/overworld/meshes/chunks/chunk_-1_0_0_0.dmf and /dev/null differ diff --git a/assets/maps/overworld/meshes/chunks/chunk_0_0_0_0.dmf b/assets/maps/overworld/meshes/chunks/chunk_0_0_0_0.dmf deleted file mode 100644 index 8e70f8fd..00000000 Binary files a/assets/maps/overworld/meshes/chunks/chunk_0_0_0_0.dmf and /dev/null differ diff --git a/assets/maps/overworld/meshes/chunks/chunk_0_1_0_0.dmf b/assets/maps/overworld/meshes/chunks/chunk_0_1_0_0.dmf deleted file mode 100644 index 5d16a93a..00000000 Binary files a/assets/maps/overworld/meshes/chunks/chunk_0_1_0_0.dmf and /dev/null differ diff --git a/assets/maps/overworld/meshes/chunks/chunk_1_0_0_0.dmf b/assets/maps/overworld/meshes/chunks/chunk_1_0_0_0.dmf deleted file mode 100644 index 6f5d1be2..00000000 Binary files a/assets/maps/overworld/meshes/chunks/chunk_1_0_0_0.dmf and /dev/null differ diff --git a/assets/maps/overworld/meshes/chunks/chunk_1_1_0_0.dmf b/assets/maps/overworld/meshes/chunks/chunk_1_1_0_0.dmf deleted file mode 100644 index e9e5aace..00000000 Binary files a/assets/maps/overworld/meshes/chunks/chunk_1_1_0_0.dmf and /dev/null differ diff --git a/assets/maps/overworld/meshes/chunks/chunk_2_0_0_0.dmf b/assets/maps/overworld/meshes/chunks/chunk_2_0_0_0.dmf deleted file mode 100644 index 7d390ce7..00000000 Binary files a/assets/maps/overworld/meshes/chunks/chunk_2_0_0_0.dmf and /dev/null differ diff --git a/assets/maps/overworld/models/chunks/chunk_-1_0_0_0.json b/assets/maps/overworld/models/chunks/chunk_-1_0_0_0.json deleted file mode 100644 index 7862dbc9..00000000 --- a/assets/maps/overworld/models/chunks/chunk_-1_0_0_0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mesh": "maps/overworld/meshes/chunks/chunk_-1_0_0_0.dmf", - "color": [ - 255, - 255, - 255, - 255 - ], - "texture": "tiles.png" -} \ No newline at end of file diff --git a/assets/maps/overworld/models/chunks/chunk_0_0_0_0.json b/assets/maps/overworld/models/chunks/chunk_0_0_0_0.json deleted file mode 100644 index 6630678c..00000000 --- a/assets/maps/overworld/models/chunks/chunk_0_0_0_0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mesh": "maps/overworld/meshes/chunks/chunk_0_0_0_0.dmf", - "color": [ - 255, - 255, - 255, - 255 - ], - "texture": "tiles.png" -} \ No newline at end of file diff --git a/assets/maps/overworld/models/chunks/chunk_0_1_0_0.json b/assets/maps/overworld/models/chunks/chunk_0_1_0_0.json deleted file mode 100644 index bb3f33f7..00000000 --- a/assets/maps/overworld/models/chunks/chunk_0_1_0_0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mesh": "maps/overworld/meshes/chunks/chunk_0_1_0_0.dmf", - "color": [ - 255, - 255, - 255, - 255 - ], - "texture": "tiles.png" -} \ No newline at end of file diff --git a/assets/maps/overworld/models/chunks/chunk_1_0_0_0.json b/assets/maps/overworld/models/chunks/chunk_1_0_0_0.json deleted file mode 100644 index 1e309aee..00000000 --- a/assets/maps/overworld/models/chunks/chunk_1_0_0_0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mesh": "maps/overworld/meshes/chunks/chunk_1_0_0_0.dmf", - "color": [ - 255, - 255, - 255, - 255 - ], - "texture": "tiles.png" -} \ No newline at end of file diff --git a/assets/maps/overworld/models/chunks/chunk_1_1_0_0.json b/assets/maps/overworld/models/chunks/chunk_1_1_0_0.json deleted file mode 100644 index 1996649d..00000000 --- a/assets/maps/overworld/models/chunks/chunk_1_1_0_0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mesh": "maps/overworld/meshes/chunks/chunk_1_1_0_0.dmf", - "color": [ - 255, - 255, - 255, - 255 - ], - "texture": "tiles.png" -} \ No newline at end of file diff --git a/assets/maps/overworld/models/chunks/chunk_2_0_0_0.json b/assets/maps/overworld/models/chunks/chunk_2_0_0_0.json deleted file mode 100644 index 0658252b..00000000 --- a/assets/maps/overworld/models/chunks/chunk_2_0_0_0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "mesh": "maps/overworld/meshes/chunks/chunk_2_0_0_0.dmf", - "color": [ - 255, - 255, - 255, - 255 - ], - "texture": "tiles.png" -} \ No newline at end of file diff --git a/assetsraw/maps/overworld/map.json b/assetsraw/maps/overworld/map.json deleted file mode 100644 index 0967ef42..00000000 --- a/assetsraw/maps/overworld/map.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c index b1fb4d77..39034b62 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.c +++ b/src/dusk/asset/loader/chunk/assetchunkloader.c @@ -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/.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(); } diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h index ef871ab6..38e76032 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.h +++ b/src/dusk/asset/loader/chunk/assetchunkloader.h @@ -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. diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index a0b16ca6..c22dfc8d 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -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, diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h index 2a907ff0..d2eecc97 100644 --- a/src/dusk/rpg/overworld/map.h +++ b/src/dusk/rpg/overworld/map.h @@ -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