Author SHA1 Message Date
YourWishesandClaude Sonnet 5 efd760fb2e Store chunk tile data as a dense minified grid instead of per-tile objects
Cuts the six overworld chunk JSON files from 146KB to 10KB by replacing
{"pos":[x,y,z],"type":t,"tile":u} entries with a fixed 256-slot array
(index x + y*16, value null or [type, z]) and dropping pretty-printing.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-09 15:28:22 -05:00
YourWishesandClaude Sonnet 5 f303d24f4a Add a perf timing/stack module and start using it to diagnose PSP hitching
New src/dusk/perf module: a simple push/pop tick-stack (perfPush/perfPop/
perfReport) for measuring how long a labeled span of code takes, wired
into engine.c's init/dispose. perfGetTick() is a placeholder - real time
on DUSK_LINUX (clock_gettime) and DUSK_PSP (sceKernelGetSystemTimeWide),
0 elsewhere until a proper cross-platform answer is settled on.

Used it this session to chase a reported PSP hitch when crossing a chunk
boundary. Confirmed mapPositionSet itself (unload/load sweeps, chunk
order rebuild, entity chunkIndex recompute) is consistently under ~3ms -
not the cause. Along the way, found and fixed a real, unrelated bug:
logDebug()/logError() on PSP did a fopen+write+fclose to the Memory
Stick on every single call, which was massively inflating any perf
measurement that logged from inside another measurement (the actual
source of the ~29-128ms numbers first seen). That's now gated behind a
new DUSK_PSP_LOG_FILE CMake option (default off).

Also, as a real test based on this session's findings:
- MAP_CHUNK_LOAD_CONCURRENCY dropped from 2 to 1 (8 and 4 both crashed/
  were untested further on real PSP hardware; 1 is confirmed safe).
- MAP_CHUNK_LOAD_DELAY_TEST: an intentionally exaggerated (now 50ms)
  artificial delay between starting successive chunk loads, gated in
  mapChunkLoadNext/mapUpdate, to observe the effect on framerate.

All perf instrumentation call sites added during this investigation
(assetChunkLoaderSync, assetMeshLoaderSync, mapPositionSet, the map area
callback invocation, engine.c's frame boundary) were removed again once
they'd served their purpose - only the reusable perf module itself and
the two real fixes above remain.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-09 15:16:48 -05:00
YourWishesandClaude Sonnet 5 9688e88a7c 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]>
2026-09-09 14:05:19 -05:00
YourWishesandClaude Sonnet 5 c8a6325615 Collapse dusk.dsk from two zip archives into one, all uncompressed
Bulk assets (mp3, png) are already compressed at the file level, so a
DEFLATE pass on top bought negligible space for real CPU cost. Packs
everything into a single ZIP_STORED archive instead, so every entry
(not just locale/*) gets the reliable repeated seeking/re-opening that
libzip only supports for uncompressed entries. Header shrinks from 32
to 16 bytes; version bumped to 2 so a stale pre-change dusk.dsk is
rejected and rebuilt rather than misread.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-09 10:17:34 -05:00
YourWishes 798db14bb5 Recover from a corrupt or version-mismatched save file instead of crashing
saveLoadSettings()/saveLoadSlot() previously let a read failure (e.g. a
version mismatch from the new schema check) propagate all the way up
through saveUpdate()/engineUpdate() to main(), which just prints the
error and exits - taking down the whole game over one bad file. Both
now catch the failure, log it, and fall back to init defaults instead,
matching the existing "no file yet" convention.
2026-09-08 12:49:20 -05:00
YourWishes 178d78ef72 Persist the current map name in the save slot
mapSetMap() now keeps SAVE.slot.cachedData.mapName in sync with the
active map, so whichever map is loaded gets written out next time the
slot is actually saved.
2026-09-08 12:49:13 -05:00
YourWishes 8139a0ea54 Add versioned save slot/settings schemas with strict version checks
Splits saveslot_t/savesettings_t into a stable, version-independent
in-memory struct (save/slot/, save/settings/) and a separate frozen
per-schema-version struct (saveslotver1_t/savesettingsver1_t) used only
for JSON serialization, converted between manually. Every write now
stamps a "version" field (SAVE_SLOT_CURRENT/SAVE_SETTINGS_CURRENT), and
every read requires it to match exactly, rejecting anything else -
laying the groundwork for a real migration path instead of silently
misreading old/incompatible save data.
2026-09-08 12:49:06 -05:00
YourWishes 327c2bec70 Show map name in the debug player position overlay
Useful now that multiple maps can be loaded, to tell which map the
printed coordinates belong to.
2026-09-07 22:34:02 -05:00
YourWishes fc0195d9f7 Bump ASSET_FILE_NAME_MAX to 128
Per-map chunk model paths (maps/<map>/models/chunks/chunk_X_Y_Z_0.json)
were hitting the old 48-char limit, crashing assetEntryInit's length
assert during initial chunk/cutscene loading.
2026-09-07 22:33:57 -05:00
YourWishes db42bca44b Reintroduce multiple maps: per-map chunk directories + map_t name/mapSetMap
Move chunk assets from a single flat assetsraw/chunks (and assets/chunks)
into per-map assetsraw/maps/<map>/chunks (assets/maps/<map>/chunks), so
terrain mesh/model output no longer collides across maps sharing chunk
coordinates. map_t gains a name[MAP_NAME_MAX] field plus mapSetMap()/
mapIsLoaded() (checks name[0] == '\0') in place of the old bool loaded
flag; mapInit() now just resets state and mapSetMap() does the chunk
grid load. Updated the chunk asset tool and the map editor's dev
server/client to match.
2026-09-07 21:28:07 -05:00
220 changed files with 2066 additions and 14144 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
{"meshes":[{"file":"house_4_4.dmf","pos":[2,1,0]},{"file":"house_6_3.dmf","pos":[1,9,0]},{"file":"house_2_2.dmf","pos":[12,6,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
@@ -0,0 +1 @@
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[6,0],[2,0],[2,0],[2,0],[2,0],[2,0],[2,0],[7,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[1,1],[1,1],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[4,0],[12,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[3,0],[1,1],[1,1],[5,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[8,0],[4,0],[4,0],[9,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[{"file":"house_5_3.dmf","pos":[1,1,0]}],"entities":[{"type":"global","globalId":3,"pos":[8,8,1]},{"type":"item","itemId":1,"quantity":1,"pos":[12,2,0]}],"areas":[{"min":[11,3,0],"max":[16,9,10],"callbackId":1,"notify":3,"trigger":6}]}
@@ -0,0 +1 @@
{"meshes":[{"file":"house_8_4.dmf","pos":[1,1,0]},{"file":"house_3_3.dmf","pos":[11,2,0]},{"file":"house_2_2.dmf","pos":[3,11,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,null,null,null,null,null,null,[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
@@ -0,0 +1 @@
{"meshes":[{"file":"house_3_2.dmf","pos":[1,1,0]},{"file":"house_2_3.dmf","pos":[7,2,0]},{"file":"house_4_2.dmf","pos":[1,8,0]},{"file":"house_1_1.dmf","pos":[11,10,0]},{"file":"house_5_2.dmf","pos":[13,6,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
@@ -0,0 +1 @@
{"meshes":[{"file":"house_5_5.dmf","pos":[3,2,0]},{"file":"house_2_3.dmf","pos":[10,3,0]},{"file":"house_3_1.dmf","pos":[1,12,0]}],"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],null,null,null,[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]]}
@@ -0,0 +1 @@
{"tiles":[[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],null,null,[1,0],[1,0],[1,0],[1,0],[1,3],[1,3],[1,3],[1,3],[1,3],[1,3],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,1],[4,1],[4,1],[4,1],[4,1],[4,1],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0],[1,0]],"meshes":[]}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
{
"mesh": "meshes/chunks/chunk_-1_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
-10
View File
@@ -1,10 +0,0 @@
{
"mesh": "meshes/chunks/chunk_0_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
-10
View File
@@ -1,10 +0,0 @@
{
"mesh": "meshes/chunks/chunk_0_1_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
-10
View File
@@ -1,10 +0,0 @@
{
"mesh": "meshes/chunks/chunk_1_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
-10
View File
@@ -1,10 +0,0 @@
{
"mesh": "meshes/chunks/chunk_1_1_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
-10
View File
@@ -1,10 +0,0 @@
{
"mesh": "meshes/chunks/chunk_2_0_0_0.dmf",
"color": [
255,
255,
255,
255
],
"texture": "tiles.png"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13
View File
@@ -60,6 +60,19 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_DISPLAY_OVERSCAN=6 DUSK_DISPLAY_OVERSCAN=6
) )
# logDebug()/logError() additionally write every call to a file on the
# Memory Stick (ms0:/PSP/GAME/Dusk/*.log) - a real, measured source of
# per-call hitching (fopen+write+fclose every log line), not just a
# theoretical cost. Default off so a normal build doesn't pay it; opt in
# with -DDUSK_PSP_LOG_FILE=ON when the log file itself is actually needed
# (e.g. no way to see stdout on real hardware).
set(DUSK_PSP_LOG_FILE OFF CACHE BOOL "Also write PSP debug/error logs to ms0:/PSP/GAME/Dusk/*.log (slow - Memory Stick I/O per call).")
if(DUSK_PSP_LOG_FILE)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_PSP_LOG_FILE
)
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_ASSERTIONS_FAKED DUSK_ASSERTIONS_FAKED
+7 -5
View File
@@ -345,12 +345,14 @@
return promise; return promise;
} }
const MAP_NAME = "overworld";
function chunkPaths(x, y, z) { function chunkPaths(x, y, z) {
return { return {
raw: `assetsraw/chunks/chunk_${x}_${y}_${z}.json`, raw: `assetsraw/maps/${MAP_NAME}/chunks/chunk_${x}_${y}_${z}.json`,
dcf: `assets/chunks/${x}_${y}_${z}.dcf`, dcf: `assets/maps/${MAP_NAME}/chunks/${x}_${y}_${z}.dcf`,
terrainDmf: `assets/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`, terrainDmf: `assets/maps/${MAP_NAME}/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`,
terrainModel: `assets/models/chunks/chunk_${x}_${y}_${z}_0.json`, terrainModel: `assets/maps/${MAP_NAME}/models/chunks/chunk_${x}_${y}_${z}_0.json`,
}; };
} }
@@ -767,7 +769,7 @@
async function buildChunkBrowseList() { async function buildChunkBrowseList() {
const list = document.getElementById("chunkBrowseList"); const list = document.getElementById("chunkBrowseList");
list.innerHTML = ""; list.innerHTML = "";
const res = await apiLs("assetsraw/chunks"); const res = await apiLs(`assetsraw/maps/${MAP_NAME}/chunks`);
for(const entry of res.entries || []) { for(const entry of res.entries || []) {
const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/); const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/);
if(!m) continue; if(!m) continue;
+4 -1
View File
@@ -209,7 +209,10 @@ class Handler(BaseHTTPRequestHandler):
self.send_json({"error": "invalid chunk coordinates"}, 400) self.send_json({"error": "invalid chunk coordinates"}, 400)
return return
json_path = PROJECT_ROOT / "assetsraw" / "chunks" / f"chunk_{x}_{y}_{z}.json" json_path = (
PROJECT_ROOT / "assetsraw" / "maps" / "overworld" / "chunks"
/ f"chunk_{x}_{y}_{z}.json"
)
if not json_path.is_file(): if not json_path.is_file():
self.send_json({"error": "chunk json not found"}, 404) self.send_json({"error": "chunk json not found"}, 404)
return return
+1
View File
@@ -74,6 +74,7 @@ add_subdirectory(engine)
add_subdirectory(error) add_subdirectory(error)
add_subdirectory(input) add_subdirectory(input)
add_subdirectory(locale) add_subdirectory(locale)
add_subdirectory(perf)
add_subdirectory(rpg) add_subdirectory(rpg)
add_subdirectory(scene) add_subdirectory(scene)
add_subdirectory(system) add_subdirectory(system)
+9 -11
View File
@@ -1,9 +1,7 @@
/** // Copyright (c) 2026 Dominic Masters
* Copyright (c) 2026 Dominic Masters //
* // This software is released under the MIT License.
* This software is released under the MIT License. // https://opensource.org/licenses/MIT
* https://opensource.org/licenses/MIT
*/
#include "animation.h" #include "animation.h"
#include "assert/assert.h" #include "assert/assert.h"
@@ -16,9 +14,9 @@ void animationInit(
uint16_t *keyframeCounts, uint16_t *keyframeCounts,
const uint16_t layerCount const uint16_t layerCount
) { ) {
assertNotNull(anim, "Animation pointer cannot be NULL."); assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be NULL."); assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be NULL."); assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
assertTrue(layerCount > 0, "Layer count must be greater than zero."); assertTrue(layerCount > 0, "Layer count must be greater than zero.");
memoryZero(anim, sizeof(animation_t)); memoryZero(anim, sizeof(animation_t));
@@ -51,7 +49,7 @@ void animationInit(
} }
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) { float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
assertNotNull(anim, "Animation pointer cannot be NULL."); assertNotNull(anim, "Animation pointer cannot be null.");
assertTrue(layer < anim->layerCount, "Layer index out of bounds."); assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
uint16_t keyframeCount = anim->keyframeCounts[layer]; uint16_t keyframeCount = anim->keyframeCounts[layer];
@@ -63,7 +61,7 @@ void animationUpdate(
animation_t *anim, animation_t *anim,
const float_t deltaTime const float_t deltaTime
) { ) {
assertNotNull(anim, "Animation pointer cannot be NULL."); assertNotNull(anim, "Animation pointer cannot be null.");
assertTrue(deltaTime >= 0, "Delta time must be non-negative."); assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
bool_t justCompleted = false; bool_t justCompleted = false;
+4 -6
View File
@@ -1,9 +1,7 @@
/** // Copyright (c) 2026 Dominic Masters
* Copyright (c) 2026 Dominic Masters //
* // This software is released under the MIT License.
* This software is released under the MIT License. // https://opensource.org/licenses/MIT
* https://opensource.org/licenses/MIT
*/
#pragma once #pragma once
#include "keyframe.h" #include "keyframe.h"
+4 -6
View File
@@ -1,9 +1,7 @@
/** // Copyright (c) 2026 Dominic Masters
* Copyright (c) 2026 Dominic Masters //
* // This software is released under the MIT License.
* This software is released under the MIT License. // https://opensource.org/licenses/MIT
* https://opensource.org/licenses/MIT
*/
#include "easing.h" #include "easing.h"
#include "assert/assert.h" #include "assert/assert.h"
+4 -123
View File
@@ -1,9 +1,7 @@
/** // Copyright (c) 2026 Dominic Masters
* Copyright (c) 2026 Dominic Masters //
* // This software is released under the MIT License.
* This software is released under the MIT License. // https://opensource.org/licenses/MIT
* https://opensource.org/licenses/MIT
*/
#pragma once #pragma once
#include "dusk.h" #include "dusk.h"
@@ -47,136 +45,19 @@ extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT];
*/ */
float_t easingApply(const easingtype_t type, const float_t t); float_t easingApply(const easingtype_t type, const float_t t);
/**
* No easing - returns t unchanged.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, equal to t.
*/
float_t easingLinear(const float_t t); float_t easingLinear(const float_t t);
/**
* Sinusoidal easing in: starts slow, accelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInSine(const float_t t); float_t easingInSine(const float_t t);
/**
* Sinusoidal easing out: starts fast, decelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingOutSine(const float_t t); float_t easingOutSine(const float_t t);
/**
* Sinusoidal easing in and out: slow at both ends, fastest in
* the middle.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInOutSine(const float_t t); float_t easingInOutSine(const float_t t);
/**
* Quadratic easing in: starts slow, accelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInQuad(const float_t t); float_t easingInQuad(const float_t t);
/**
* Quadratic easing out: starts fast, decelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingOutQuad(const float_t t); float_t easingOutQuad(const float_t t);
/**
* Quadratic easing in and out: slow at both ends, fastest in the
* middle.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInOutQuad(const float_t t); float_t easingInOutQuad(const float_t t);
/**
* Cubic easing in: starts slow, accelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInCubic(const float_t t); float_t easingInCubic(const float_t t);
/**
* Cubic easing out: starts fast, decelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingOutCubic(const float_t t); float_t easingOutCubic(const float_t t);
/**
* Cubic easing in and out: slow at both ends, fastest in the
* middle.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInOutCubic(const float_t t); float_t easingInOutCubic(const float_t t);
/**
* Quartic easing in: starts slow, accelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInQuart(const float_t t); float_t easingInQuart(const float_t t);
/**
* Quartic easing out: starts fast, decelerates to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingOutQuart(const float_t t); float_t easingOutQuart(const float_t t);
/**
* Quartic easing in and out: slow at both ends, fastest in the
* middle.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingInOutQuart(const float_t t); float_t easingInOutQuart(const float_t t);
/**
* Back easing in: overshoots backwards before moving to the end.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, which may fall below 0.
*/
float_t easingInBack(const float_t t); float_t easingInBack(const float_t t);
/**
* Back easing out: overshoots past the end before settling on it.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, which may rise above 1.
*/
float_t easingOutBack(const float_t t); float_t easingOutBack(const float_t t);
/**
* Back easing in and out: overshoots backwards at the start and
* past the end before settling on it.
*
* @param t The input time, in the range [0, 1].
* @return The eased value, which may fall below 0 or rise above 1.
*/
float_t easingInOutBack(const float_t t); float_t easingInOutBack(const float_t t);
+1 -1
View File
@@ -14,7 +14,7 @@ float_t keyframeGetValue(
const uint32_t keyframeCount, const uint32_t keyframeCount,
const float_t time const float_t time
) { ) {
assertNotNull(keyframes, "Keyframes pointer cannot be NULL."); assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0."); assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
assertTrue(time >= 0, "Time must be non-negative."); assertTrue(time >= 0, "Time must be non-negative.");
#ifdef DUSK_ASSERTIONS #ifdef DUSK_ASSERTIONS
+4 -6
View File
@@ -1,9 +1,7 @@
/** // Copyright (c) 2026 Dominic Masters
* Copyright (c) 2026 Dominic Masters //
* // This software is released under the MIT License.
* This software is released under the MIT License. // https://opensource.org/licenses/MIT
* https://opensource.org/licenses/MIT
*/
#pragma once #pragma once
#include "easing.h" #include "easing.h"
+5 -15
View File
@@ -25,11 +25,9 @@ errorret_t assetInit(void) {
threadMutexInit(&ASSET.loading[i].mutex); threadMutexInit(&ASSET.loading[i].mutex);
} }
// assetInitPlatform must either define both ASSET.zip/ASSET.zipStored or // assetInitPlatform must either define ASSET.zip or throw an error.
// throw an error.
errorChain(assetInitPlatform()); errorChain(assetInitPlatform());
assertNotNull(ASSET.zip, "Asset zip NULL without error."); assertNotNull(ASSET.zip, "Asset zip null without error.");
assertNotNull(ASSET.zipStored, "Asset stored zip NULL without error.");
threadInit(&ASSET.loadThread, assetUpdateAsync); threadInit(&ASSET.loadThread, assetUpdateAsync);
threadStart(&ASSET.loadThread); threadStart(&ASSET.loadThread);
@@ -40,9 +38,7 @@ bool_t assetFileExists(const char_t *filename) {
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long."); assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
threadMutexLock(&ASSET.zipLock); threadMutexLock(&ASSET.zipLock);
bool_t found = bool_t found = zip_name_locate(ASSET.zip, filename, 0) >= 0;
zip_name_locate(ASSET.zip, filename, 0) >= 0 ||
zip_name_locate(ASSET.zipStored, filename, 0) >= 0;
threadMutexUnlock(&ASSET.zipLock); threadMutexUnlock(&ASSET.zipLock);
return found; return found;
} }
@@ -445,19 +441,13 @@ errorret_t assetDispose(void) {
errorChain(assetReapUnused()); errorChain(assetReapUnused());
// Cleanup zip files. // Cleanup zip file.
if(ASSET.zip != NULL) { if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) { if(zip_close(ASSET.zip) != 0) {
errorThrow("Failed to close compressed asset zip archive."); errorThrow("Failed to close asset zip archive.");
} }
ASSET.zip = NULL; ASSET.zip = NULL;
} }
if(ASSET.zipStored != NULL) {
if(zip_close(ASSET.zipStored) != 0) {
errorThrow("Failed to close stored asset zip archive.");
}
ASSET.zipStored = NULL;
}
errorChain(assetDisposePlatform()); errorChain(assetDisposePlatform());
threadMutexDispose(&ASSET.zipLock); threadMutexDispose(&ASSET.zipLock);
+5 -9
View File
@@ -29,16 +29,12 @@
#define ASSET_BASE_DIRECTORY_MAX 256 #define ASSET_BASE_DIRECTORY_MAX 256
typedef struct asset_s { typedef struct asset_s {
// Compressed (DEFLATE) archive - expected to hold the bulk of a game's // The dusk.dsk archive - a single uncompressed (ZIP_STORED) zip, so
// binary assets. Looked up first by assetFileInit(). // every entry gets the reliable repeated seeking/re-opening that
// libzip only supports reliably for uncompressed entries (locale
// strings, MP3 loop restarts, etc.).
zip_t *zip; zip_t *zip;
// Stored (uncompressed) archive - expected to hold small files that need
// reliable repeated seeking/re-opening (e.g. locale strings), which
// libzip only supports reliably for uncompressed entries. Looked up by
// assetFileInit() only if the name isn't found in `zip`.
zip_t *zipStored;
// Directory dusk.dsk was actually found/opened in (on PSP, the directory // Directory dusk.dsk was actually found/opened in (on PSP, the directory
// EBOOT.PBP lives in, since dusk.dsk is embedded inside it rather than a // EBOOT.PBP lives in, since dusk.dsk is embedded inside it rather than a
// loose file) - populated by each platform's own assetInitXxx() once it // loose file) - populated by each platform's own assetInitXxx() once it
@@ -52,7 +48,7 @@ typedef struct asset_s {
assetplatform_t platform; assetplatform_t platform;
// Guards every libzip call against ASSET.zip/ASSET.zipStored (zip_fopen, // Guards every libzip call against ASSET.zip (zip_fopen,
// zip_fread, zip_fclose, zip_fseek, zip_stat, zip_name_locate - see // zip_fread, zip_fclose, zip_fseek, zip_stat, zip_name_locate - see
// assetfile.c/assetFileExists). libzip is documented as not thread-safe, // assetfile.c/assetFileExists). libzip is documented as not thread-safe,
// and this asset system genuinely calls it from multiple real threads at // and this asset system genuinely calls it from multiple real threads at
+39 -163
View File
@@ -30,9 +30,9 @@ errorret_t assetDskParseHeader(
// Every field is a little-endian uint32_t regardless of host - convert // Every field is a little-endian uint32_t regardless of host - convert
// to host order (a no-op on little-endian hosts, a real byteswap on // to host order (a no-op on little-endian hosts, a real byteswap on
// Dolphin's big-endian PowerPC). // Dolphin's big-endian PowerPC).
uint32_t fields[7]; uint32_t fields[3];
memoryCopy(fields, bytes + ASSET_DSK_MAGIC_SIZE, sizeof(fields)); memoryCopy(fields, bytes + ASSET_DSK_MAGIC_SIZE, sizeof(fields));
for(uint8_t i = 0; i < 7; i++) { for(uint8_t i = 0; i < 3; i++) {
fields[i] = endianLittleToHost32(fields[i]); fields[i] = endianLittleToHost32(fields[i]);
} }
@@ -41,24 +41,17 @@ errorret_t assetDskParseHeader(
errorThrow("dusk.dsk has an unsupported version: %u", version); errorThrow("dusk.dsk has an unsupported version: %u", version);
} }
outHeader->compressedOffset = fields[1]; outHeader->size = fields[1];
outHeader->compressedSize = fields[2]; outHeader->checksum = fields[2];
outHeader->compressedChecksum = fields[3];
outHeader->storedOffset = fields[4];
outHeader->storedSize = fields[5];
outHeader->storedChecksum = fields[6];
errorOk(); errorOk();
} }
errorret_t assetDskOpenFromPath( errorret_t assetDskOpenFromPath(
const char_t *path, const char_t *path,
zip_t **outCompressed, zip_t **outZip
zip_t **outStored
) { ) {
errorChain(assetDskOpenFromPathRange( errorChain(assetDskOpenFromPathRange(path, 0, SIZE_MAX, outZip));
path, 0, SIZE_MAX, outCompressed, outStored
));
errorOk(); errorOk();
} }
@@ -66,15 +59,12 @@ errorret_t assetDskOpenFromPathRange(
const char_t *path, const char_t *path,
const size_t baseOffset, const size_t baseOffset,
const size_t baseSize, const size_t baseSize,
zip_t **outCompressed, zip_t **outZip
zip_t **outStored
) { ) {
assertNotNull(path, "Path cannot be NULL."); assertNotNull(path, "Path cannot be NULL.");
assertNotNull(outCompressed, "Out compressed cannot be NULL."); assertNotNull(outZip, "Out zip cannot be NULL.");
assertNotNull(outStored, "Out stored cannot be NULL.");
*outCompressed = NULL; *outZip = NULL;
*outStored = NULL;
FILE *headerFile = fopen(path, "rb"); FILE *headerFile = fopen(path, "rb");
if(headerFile == NULL) { if(headerFile == NULL) {
@@ -96,104 +86,30 @@ errorret_t assetDskOpenFromPathRange(
assetdskheader_t header; assetdskheader_t header;
errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &header)); errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &header));
if( if((size_t) ASSET_DSK_HEADER_SIZE + header.size > baseSize) {
(size_t) header.compressedOffset + header.compressedSize > baseSize || errorThrow("dusk.dsk header describes a range beyond its containing file.");
(size_t) header.storedOffset + header.storedSize > baseSize
) {
errorThrow("dusk.dsk header describes ranges beyond its containing file.");
} }
zip_error_t zipError; zip_error_t zipError;
zip_error_init(&zipError); zip_error_init(&zipError);
zip_source_t *compressedSource = zip_source_file_create( zip_source_t *source = zip_source_file_create(
path, path,
(zip_uint64_t) (baseOffset + header.compressedOffset), (zip_uint64_t) (baseOffset + ASSET_DSK_HEADER_SIZE),
(zip_int64_t) header.compressedSize, (zip_int64_t) header.size,
&zipError &zipError
); );
if(compressedSource == NULL) { if(source == NULL) {
errorThrow( errorThrow(
"Failed to create compressed dusk.dsk source: %s", "Failed to create dusk.dsk source: %s", zip_error_strerror(&zipError)
zip_error_strerror(&zipError)
); );
} }
*outCompressed = *outZip = zip_open_from_source(source, ZIP_RDONLY, &zipError);
zip_open_from_source(compressedSource, ZIP_RDONLY, &zipError); if(*outZip == NULL) {
if(*outCompressed == NULL) { zip_source_free(source);
zip_source_free(compressedSource);
errorThrow( errorThrow(
"Failed to open compressed dusk.dsk archive: %s", "Failed to open dusk.dsk archive: %s", zip_error_strerror(&zipError)
zip_error_strerror(&zipError)
);
}
zip_source_t *storedSource = zip_source_file_create(
path,
(zip_uint64_t) (baseOffset + header.storedOffset),
(zip_int64_t) header.storedSize,
&zipError
);
if(storedSource == NULL) {
zip_close(*outCompressed);
*outCompressed = NULL;
errorThrow(
"Failed to create stored dusk.dsk source: %s",
zip_error_strerror(&zipError)
);
}
*outStored = zip_open_from_source(storedSource, ZIP_RDONLY, &zipError);
if(*outStored == NULL) {
zip_source_free(storedSource);
zip_close(*outCompressed);
*outCompressed = NULL;
errorThrow(
"Failed to open stored dusk.dsk archive: %s",
zip_error_strerror(&zipError)
);
}
// The stored archive is small by convention, so verifying its checksum
// here (one extra small read) is cheap; the compressed archive isn't
// checked since it's meant to be read lazily/on-demand from here on.
uint8_t *storedBytes = (uint8_t *) memoryAllocate(header.storedSize);
FILE *storedFile = fopen(path, "rb");
if(storedFile == NULL) {
memoryFree(storedBytes);
zip_close(*outStored);
zip_close(*outCompressed);
*outStored = NULL;
*outCompressed = NULL;
errorThrow(
"Failed to re-open dusk.dsk to verify stored checksum: %s", path
);
}
fseek(storedFile, (long) (baseOffset + header.storedOffset), SEEK_SET);
size_t storedRead = fread(storedBytes, 1, header.storedSize, storedFile);
fclose(storedFile);
if(storedRead != header.storedSize) {
memoryFree(storedBytes);
zip_close(*outStored);
zip_close(*outCompressed);
*outStored = NULL;
*outCompressed = NULL;
errorThrow(
"Failed to read dusk.dsk stored archive to verify checksum: %s", path
);
}
uint32_t checksum =
(uint32_t) crc32(0L, storedBytes, (uInt) header.storedSize);
memoryFree(storedBytes);
if(checksum != header.storedChecksum) {
zip_close(*outStored);
zip_close(*outCompressed);
*outStored = NULL;
*outCompressed = NULL;
errorThrow(
"dusk.dsk stored archive failed checksum verification: %s", path
); );
} }
@@ -203,85 +119,45 @@ errorret_t assetDskOpenFromPathRange(
errorret_t assetDskOpenFromBuffer( errorret_t assetDskOpenFromBuffer(
uint8_t *buffer, uint8_t *buffer,
const size_t bufferSize, const size_t bufferSize,
zip_t **outCompressed, zip_t **outZip
zip_t **outStored
) { ) {
assertNotNull(buffer, "Buffer cannot be NULL."); assertNotNull(buffer, "Buffer cannot be NULL.");
assertNotNull(outCompressed, "Out compressed cannot be NULL."); assertNotNull(outZip, "Out zip cannot be NULL.");
assertNotNull(outStored, "Out stored cannot be NULL.");
*outCompressed = NULL; *outZip = NULL;
*outStored = NULL;
assetdskheader_t header; assetdskheader_t header;
errorChain(assetDskParseHeader(buffer, bufferSize, &header)); errorChain(assetDskParseHeader(buffer, bufferSize, &header));
if( if((size_t) ASSET_DSK_HEADER_SIZE + header.size > bufferSize) {
(size_t) header.compressedOffset + header.compressedSize > bufferSize || errorThrow("dusk.dsk header describes a range beyond the buffer.");
(size_t) header.storedOffset + header.storedSize > bufferSize
) {
errorThrow("dusk.dsk header describes ranges beyond the buffer.");
} }
uint32_t compressedChecksum = (uint32_t) crc32( uint8_t *data = buffer + ASSET_DSK_HEADER_SIZE;
0L, buffer + header.compressedOffset, (uInt) header.compressedSize uint32_t checksum = (uint32_t) crc32(0L, data, (uInt) header.size);
); if(checksum != header.checksum) {
if(compressedChecksum != header.compressedChecksum) { errorThrow("dusk.dsk archive failed checksum verification.");
errorThrow("dusk.dsk compressed archive failed checksum verification.");
}
uint32_t storedChecksum = (uint32_t) crc32(
0L, buffer + header.storedOffset, (uInt) header.storedSize
);
if(storedChecksum != header.storedChecksum) {
errorThrow("dusk.dsk stored archive failed checksum verification.");
} }
zip_error_t zipError; zip_error_t zipError;
zip_error_init(&zipError); zip_error_init(&zipError);
// freep=0 for both - they're non-owning windows into the same caller-owned // freep=0 - this is a non-owning window into the same caller-owned
// buffer, not independent allocations libzip should free. // buffer, not an independent allocation libzip should free.
zip_source_t *compressedSource = zip_source_buffer_create( zip_source_t *source = zip_source_buffer_create(
buffer + header.compressedOffset, header.compressedSize, 0, &zipError data, header.size, 0, &zipError
); );
if(compressedSource == NULL) { if(source == NULL) {
errorThrow( errorThrow(
"Failed to create compressed dusk.dsk source: %s", "Failed to create dusk.dsk source: %s", zip_error_strerror(&zipError)
zip_error_strerror(&zipError)
); );
} }
*outCompressed = *outZip = zip_open_from_source(source, ZIP_RDONLY, &zipError);
zip_open_from_source(compressedSource, ZIP_RDONLY, &zipError); if(*outZip == NULL) {
if(*outCompressed == NULL) { zip_source_free(source);
zip_source_free(compressedSource);
errorThrow( errorThrow(
"Failed to open compressed dusk.dsk archive: %s", "Failed to open dusk.dsk archive: %s", zip_error_strerror(&zipError)
zip_error_strerror(&zipError)
);
}
zip_source_t *storedSource = zip_source_buffer_create(
buffer + header.storedOffset, header.storedSize, 0, &zipError
);
if(storedSource == NULL) {
zip_close(*outCompressed);
*outCompressed = NULL;
errorThrow(
"Failed to create stored dusk.dsk source: %s",
zip_error_strerror(&zipError)
);
}
*outStored = zip_open_from_source(storedSource, ZIP_RDONLY, &zipError);
if(*outStored == NULL) {
zip_source_free(storedSource);
zip_close(*outCompressed);
*outCompressed = NULL;
errorThrow(
"Failed to open stored dusk.dsk archive: %s",
zip_error_strerror(&zipError)
); );
} }
+36 -47
View File
@@ -13,30 +13,26 @@
// never gets misread as a valid dusk.dsk. // never gets misread as a valid dusk.dsk.
#define ASSET_DSK_MAGIC_SIZE 4 #define ASSET_DSK_MAGIC_SIZE 4
#define ASSET_DSK_MAGIC "DSK2" #define ASSET_DSK_MAGIC "DSK2"
#define ASSET_DSK_VERSION 1 #define ASSET_DSK_VERSION 2
// magic(4) + version(4) + compressedOffset(4) + compressedSize(4) + // magic(4) + version(4) + size(4) + checksum(4), all little-endian
// compressedChecksum(4) + storedOffset(4) + storedSize(4) + // regardless of host - see assetDskParseHeader. The zip archive itself
// storedChecksum(4), all little-endian regardless of host - see // starts immediately after, at ASSET_DSK_HEADER_SIZE.
// assetDskParseHeader. #define ASSET_DSK_HEADER_SIZE 16
#define ASSET_DSK_HEADER_SIZE 32
/** /**
* Parsed dusk.dsk (DSK2 format) header. dusk.dsk is two independent, back * Parsed dusk.dsk (DSK2 format) header. dusk.dsk is a single uncompressed
* to back zip archives (see tools/asset/pack) rather than a single plain * (ZIP_STORED) zip archive (see tools/asset/pack) rather than DEFLATEd -
* zip: a "compressed" one (DEFLATE, expected to hold the bulk of a game's * the bulk of what's packed (audio, images) is already compressed at the
* binary assets, opened lazily/on-demand) and a "stored" one (uncompressed, * file level, so DEFLATE-ing the whole archive on top buys negligible
* expected to hold small files - locale strings, config - that need * space for real CPU cost, and every entry gets the reliable repeated
* reliable repeated seeking/re-opening, which libzip only supports for * seeking/re-opening that libzip only supports for uncompressed entries
* uncompressed entries). * (locale line-by-line reads, MP3 loop restarts, etc.) instead of just a
* hand-picked subset of files.
*/ */
typedef struct { typedef struct {
uint32_t compressedOffset; uint32_t size;
uint32_t compressedSize; uint32_t checksum;
uint32_t compressedChecksum;
uint32_t storedOffset;
uint32_t storedSize;
uint32_t storedChecksum;
} assetdskheader_t; } assetdskheader_t;
/** /**
@@ -56,27 +52,24 @@ errorret_t assetDskParseHeader(
); );
/** /**
* Opens both archives of a dusk.dsk file given a filesystem path, using * Opens the dusk.dsk archive given a filesystem path, using a lazy/
* lazy/windowed file-backed zip sources - no more memory used than the * windowed file-backed zip source - no more memory used than the
* existing per-platform small read buffers, matching the memory * existing per-platform small read buffers, matching the memory
* characteristics of a plain zip_open() on the whole file. Verifies the * characteristics of a plain zip_open() on the whole file. The checksum
* (small, by convention) stored archive's checksum; the compressed * is intentionally not verified here since doing so would require
* archive's checksum is intentionally not verified here since doing so * reading the whole (potentially large) archive just to compute it.
* would require reading the bulk of the game's assets just to compute it.
* *
* See assetDskOpenFromPathRange() for the case where the DSK2 blob isn't * See assetDskOpenFromPathRange() for the case where the DSK2 blob isn't
* the whole file (e.g. embedded inside another container). * the whole file (e.g. embedded inside another container).
* *
* @param path Filesystem path to the dusk.dsk file. * @param path Filesystem path to the dusk.dsk file.
* @param outCompressed Set to the opened compressed archive on success. * @param outZip Set to the opened archive on success.
* @param outStored Set to the opened stored archive on success.
* @return OK on success, error if the file is missing, too short, has a * @return OK on success, error if the file is missing, too short, has a
* bad header, or either archive fails to open/verify. * bad header, or the archive fails to open.
*/ */
errorret_t assetDskOpenFromPath( errorret_t assetDskOpenFromPath(
const char_t *path, const char_t *path,
zip_t **outCompressed, zip_t **outZip
zip_t **outStored
); );
/** /**
@@ -90,39 +83,35 @@ errorret_t assetDskOpenFromPath(
* @param path Filesystem path to the container file. * @param path Filesystem path to the container file.
* @param baseOffset Byte offset within `path` where the DSK2 blob starts. * @param baseOffset Byte offset within `path` where the DSK2 blob starts.
* @param baseSize Number of bytes available at `baseOffset`. * @param baseSize Number of bytes available at `baseOffset`.
* @param outCompressed Set to the opened compressed archive on success. * @param outZip Set to the opened archive on success.
* @param outStored Set to the opened stored archive on success.
* @return OK on success, error if the file is missing, too short, has a * @return OK on success, error if the file is missing, too short, has a
* bad header, or either archive fails to open/verify. * bad header, or the archive fails to open.
*/ */
errorret_t assetDskOpenFromPathRange( errorret_t assetDskOpenFromPathRange(
const char_t *path, const char_t *path,
const size_t baseOffset, const size_t baseOffset,
const size_t baseSize, const size_t baseSize,
zip_t **outCompressed, zip_t **outZip
zip_t **outStored
); );
/** /**
* Opens both archives of a dusk.dsk file already fully resident in memory * Opens the dusk.dsk archive already fully resident in memory (e.g. a
* (e.g. a PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already * PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already read via
* read via DVD_ReadAbs - platforms that already buffer the whole file for * DVD_ReadAbs - platforms that already buffer the whole file for reasons
* reasons unrelated to this format). Neither archive takes ownership of * unrelated to this format). The returned archive doesn't take ownership
* `buffer` (both are opened as non-owning sub-ranges of it) - the caller * of `buffer` (it's opened as a non-owning sub-range of it) - the caller
* remains responsible for freeing it, and must keep it alive for as long * remains responsible for freeing it, and must keep it alive for as long
* as either archive stays open. Verifies both archives' checksums, since * as the archive stays open. Verifies the checksum, since the bytes are
* the bytes are already resident. * already resident.
* *
* @param buffer The whole dusk.dsk file's bytes. * @param buffer The whole dusk.dsk file's bytes.
* @param bufferSize Number of bytes at `buffer`. * @param bufferSize Number of bytes at `buffer`.
* @param outCompressed Set to the opened compressed archive on success. * @param outZip Set to the opened archive on success.
* @param outStored Set to the opened stored archive on success.
* @return OK on success, error if too short, has a bad header/checksum, or * @return OK on success, error if too short, has a bad header/checksum, or
* either archive fails to open. * the archive fails to open.
*/ */
errorret_t assetDskOpenFromBuffer( errorret_t assetDskOpenFromBuffer(
uint8_t *buffer, uint8_t *buffer,
const size_t bufferSize, const size_t bufferSize,
zip_t **outCompressed, zip_t **outZip
zip_t **outStored
); );
+16 -22
View File
@@ -24,20 +24,11 @@ errorret_t assetFileInit(
file->params = params; file->params = params;
file->output = output; file->output = output;
// Stat the file, trying the compressed archive first and falling back to
// the stored one - remember which matched so assetFileOpen opens it from
// the right archive.
zip_stat_init(&file->stat); zip_stat_init(&file->stat);
threadMutexLock(&ASSET.zipLock); threadMutexLock(&ASSET.zipLock);
if(zip_stat(ASSET.zip, filename, 0, &file->stat) == 0) { int statResult = zip_stat(ASSET.zip, filename, 0, &file->stat);
file->sourceZip = ASSET.zip;
} else if(zip_stat(ASSET.zipStored, filename, 0, &file->stat) == 0) {
file->sourceZip = ASSET.zipStored;
} else {
file->sourceZip = NULL;
}
threadMutexUnlock(&ASSET.zipLock); threadMutexUnlock(&ASSET.zipLock);
if(file->sourceZip == NULL) { if(statResult != 0) {
errorThrow("Failed to stat asset file: %s", filename); errorThrow("Failed to stat asset file: %s", filename);
} }
@@ -85,11 +76,10 @@ errorret_t assetFileRewind(assetfile_t *file) {
errorret_t assetFileOpen(assetfile_t *file) { errorret_t assetFileOpen(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->filename, "Asset file filename cannot be NULL."); assertNotNull(file->filename, "Asset file filename cannot be NULL.");
assertNotNull(file->sourceZip, "Asset file must be inited before opening.");
assertNull(file->zipFile, "Asset file already open."); assertNull(file->zipFile, "Asset file already open.");
threadMutexLock(&ASSET.zipLock); threadMutexLock(&ASSET.zipLock);
file->zipFile = zip_fopen(file->sourceZip, file->filename, 0); file->zipFile = zip_fopen(ASSET.zip, file->filename, 0);
threadMutexUnlock(&ASSET.zipLock); threadMutexUnlock(&ASSET.zipLock);
if(file->zipFile == NULL) { if(file->zipFile == NULL) {
errorThrow("Failed to open asset file: %s", file->filename); errorThrow("Failed to open asset file: %s", file->filename);
@@ -279,7 +269,7 @@ errorret_t assetFileLineReaderAppend(
errorOk(); errorOk();
} }
// reserve room for optional NUL terminator /* reserve room for optional NUL terminator */
if(reader->lineLength + srcLength >= reader->outBufferSize) { if(reader->lineLength + srcLength >= reader->outBufferSize) {
errorThrow("Line length exceeds output buffer size."); errorThrow("Line length exceeds output buffer size.");
} }
@@ -328,7 +318,7 @@ errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) {
size_t unreadBytes = assetFileLineReaderUnreadBytes(reader); size_t unreadBytes = assetFileLineReaderUnreadBytes(reader);
// If buffer is fully consumed, refill from start. /* If buffer is fully consumed, refill from start. */
if(unreadBytes == 0) { if(unreadBytes == 0) {
reader->bufferStart = 0; reader->bufferStart = 0;
reader->bufferEnd = 0; reader->bufferEnd = 0;
@@ -349,14 +339,18 @@ errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) {
errorOk(); errorOk();
} }
// There are unread bytes left but no newline in them. /*
// If bufferStart > 0, slide unread bytes to front so we can read more. * There are unread bytes left but no newline in them.
// This only happens when necessary to make space. * If bufferStart > 0, slide unread bytes to front so we can read more.
* This only happens when necessary to make space.
*/
if(reader->bufferEnd == reader->readBufferSize) { if(reader->bufferEnd == reader->readBufferSize) {
if(reader->bufferStart == 0) { if(reader->bufferStart == 0) {
// Entire read buffer is unread and contains no newline. /*
// Caller must have a large enough outBuffer to accumulate across fills, * Entire read buffer is unread and contains no newline.
// so we consume these bytes into outBuffer before refilling. * Caller must have a large enough outBuffer to accumulate across fills,
* so we consume these bytes into outBuffer before refilling.
*/
errorOk(); errorOk();
} }
@@ -399,7 +393,7 @@ errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
size_t chunkLength = (size_t)newlineIndex - reader->bufferStart; size_t chunkLength = (size_t)newlineIndex - reader->bufferStart;
errorret_t ret; errorret_t ret;
// strip CR in CRLF /* strip CR in CRLF */
if( if(
chunkLength > 0 && chunkLength > 0 &&
reader->readBuffer[(size_t)newlineIndex - 1] == '\r' reader->readBuffer[(size_t)newlineIndex - 1] == '\r'
+1 -6
View File
@@ -9,7 +9,7 @@
#include "error/error.h" #include "error/error.h"
#include <zip.h> #include <zip.h>
#define ASSET_FILE_NAME_MAX 48 #define ASSET_FILE_NAME_MAX 128
// Max bytes requested per zip_fread() call in assetFileRead(). Some // Max bytes requested per zip_fread() call in assetFileRead(). Some
// zip_fread() implementations (seen on PSP) reject a single call asking // zip_fread() implementations (seen on PSP) reject a single call asking
@@ -32,11 +32,6 @@ typedef struct assetfile_s {
zip_int64_t position; zip_int64_t position;
zip_int64_t lastRead; zip_int64_t lastRead;
zip_file_t *zipFile; zip_file_t *zipFile;
// The archive this file was found in (ASSET.zip or ASSET.zipStored),
// set by assetFileInit and used by assetFileOpen so lookups fall back
// correctly between the two dusk.dsk archives.
zip_t *sourceZip;
} assetfile_t; } assetfile_t;
/** /**
+369 -123
View File
@@ -8,41 +8,284 @@
#include "assetchunkloader.h" #include "assetchunkloader.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.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/assetloading.h"
#include "asset/loader/assetentry.h" #include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.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 "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) { errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread."); assertNotMainThread("Should be called from an async thread.");
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
errorOk(); errorOk();
} }
assertNull(loading->loading.chunk.data, "Data already defined?"); 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;
}
assetfile_t *file = &loading->loading.chunk.file; // Reads the "tiles" array - a dense, fixed-size CHUNK_TILE_COUNT grid,
assetLoaderErrorChain(loading, // one slot per (x, y) column at index x + y * CHUNK_WIDTH (position is
assetFileInit(file, loading->entry->name, NULL, NULL) // implicit in the index, not stored). Each slot is either `null` (empty
// column, already zeroed = TILE_SHAPE_NULL) or a 2-element [type, z]
// array.
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) {
if(idx >= CHUNK_TILE_COUNT) break;
if(!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) < 2) continue;
int32_t type = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 0));
int32_t z = (int32_t)yyjson_get_int(yyjson_arr_get(tileVal, 1));
tiles[idx] = (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
); );
uint8_t *data = memoryAllocate(file->size); vec3 offset = {0.0f, 0.0f, 0.0f};
assetLoaderErrorChain(loading, assetFileOpen(file)); yyjson_val *posArr = yyjson_obj_get(meshVal, "pos");
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size)); if(yyjson_is_arr(posArr) && yyjson_arr_size(posArr) >= 3) {
assetLoaderErrorChain(loading, assetFileClose(file)); offset[0] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 0));
assetLoaderErrorChain(loading, assetFileDispose(file)); offset[1] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 1));
assertTrue( offset[2] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 2));
file->lastRead == file->size, }
"Failed to read entire chunk file." 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
);
}
}
assertTrue(vertCount > 0, "Terrain build called with no terrain tiles.");
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;
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(); errorOk();
} }
@@ -55,8 +298,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
switch(loading->loading.chunk.state) { switch(loading->loading.chunk.state) {
case ASSET_CHUNK_LOADING_STATE_INITIAL: case ASSET_CHUNK_LOADING_STATE_INITIAL:
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE; loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
case ASSET_CHUNK_LOADING_STATE_PARSE: case ASSET_CHUNK_LOADING_STATE_PARSE:
@@ -69,9 +312,7 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
out->modelEntries[m] = assetLock( out->modelEntries[m] = assetLock(
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
); );
assertNotNull( assertNotNull(out->modelEntries[m], "Failed to lock model.");
out->modelEntries[m], "Failed to lock model."
);
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
} }
@@ -93,118 +334,95 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
errorOk(); errorOk();
} }
uint8_t *data = loading->loading.chunk.data; // --- PARSE: lock+require the chunk's own JSON as a sub-asset, exactly
assertNotNull(data, "Chunk data should have been loaded by now."); // like assetmodelloader.c's ASSET_MODEL_LOADING_STATE_LOCK_ASSETS does
// for its model descriptor, then read everything out of it before
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') { // unlocking (the yyjson_doc/vals only stay valid while jsonEntry does).
memoryFree(data); char_t jsonKey[ASSET_FILE_NAME_MAX];
assetLoaderErrorThrow(loading, "Invalid chunk file header"); stringFormat(jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name);
} assetloaderinput_t jsonInput;
memoryZero(&jsonInput, sizeof(jsonInput));
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4)); stringCopy(
if(version != ASSET_CHUNK_FILE_VERSION) { jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
memoryFree(data); );
assetentry_t *jsonEntry =
assetLock(jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput);
errorret_t jsonRet = assetRequireLoaded(jsonEntry);
if(errorIsNotOk(jsonRet)) {
assetUnlockEntry(jsonEntry);
assetLoaderErrorThrow( 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); tile_t tiles[CHUNK_TILE_COUNT];
out->tiles = memoryAllocate(tileSize); assetChunkParseTiles(root, tiles);
memoryCopy(out->tiles, data + offset, tileSize); out->hasTerrain = assetChunkTilesHaveTerrain(tiles);
offset += tileSize;
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) { uint8_t meshIndex = out->hasTerrain ? 1 : 0;
uint32_t *shape = (uint32_t *)&out->tiles[t].shape; assetChunkParseMeshes(root, out, &meshIndex);
*shape = endianLittleToHost32(*shape); out->meshCount = meshIndex;
}
out->meshCount = data[offset]; assetChunkParseEntities(root, out);
offset += sizeof(uint8_t); assetChunkParseAreas(root, out);
assertTrue(
out->meshCount <= CHUNK_MESH_COUNT_MAX, assetUnlockEntry(jsonEntry);
"Chunk mesh count exceeds maximum."
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
memoryCopy(out->tiles, tiles, CHUNK_TILE_COUNT * sizeof(tile_t));
// 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
); );
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;
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->entitySpawnCount = data[offset]; assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
offset += sizeof(uint8_t); assetentry_t *texEntry = assetLock(
assertTrue( ASSET_CHUNK_TERRAIN_TEXTURE, ASSET_LOADER_TYPE_TEXTURE, &texInput
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
"Chunk entity spawn count exceeds maximum."
); );
errorret_t texRet = assetRequireLoaded(texEntry);
for(uint8_t s = 0; s < out->entitySpawnCount; s++) { if(errorIsNotOk(texRet)) {
chunkentityspawn_t *spawn = &out->entitySpawns[s]; assetUnlockEntry(texEntry);
spawn->kind = (chunkentityspawnkind_t)data[offset]; meshDispose(&terrainMesh.mesh);
offset += sizeof(uint8_t); memoryFree(out->tiles);
out->tiles = NULL;
uint16_t a; assetLoaderErrorThrow(
memoryCopy(&a, data + offset, sizeof(uint16_t)); loading, "Failed to load chunk terrain texture: %s",
a = endianLittleToHost16(a); loading->entry->name
offset += sizeof(uint16_t);
uint8_t b = data[offset];
offset += sizeof(uint8_t);
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
spawn->globalId = 0;
spawn->itemId = a;
spawn->itemQuantity = b;
} else {
spawn->globalId = a;
spawn->itemId = 0;
spawn->itemQuantity = 0;
}
spawn->position = worldPosReadLE(data, &offset);
}
out->areaSpawnCount = data[offset];
offset += sizeof(uint8_t);
assertTrue(
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
"Chunk area spawn count exceeds maximum."
); );
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
chunkareaspawn_t *area = &out->areaSpawns[s];
area->min = 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);
} }
out->terrainTexEntry = texEntry;
memoryFree(data); out->terrainMeshEntry = memoryAllocate(sizeof(assetentry_t));
loading->loading.chunk.data = NULL; 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;
}
if(out->meshCount == 0) { if(out->meshCount == 0) {
loading->entry->state = ASSET_ENTRY_STATE_LOADED; loading->entry->state = ASSET_ENTRY_STATE_LOADED;
@@ -212,7 +430,7 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
} }
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS; 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; loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
} }
@@ -231,8 +449,36 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
for(uint8_t m = 0; m < out->meshCount; m++) { for(uint8_t m = 0; m < out->meshCount; m++) {
if(out->modelEntries[m] == NULL) continue; if(out->modelEntries[m] == NULL) continue;
if(out->hasTerrain && m == 0) continue; // synthetic - freed below.
assetUnlockEntry(out->modelEntries[m]); assetUnlockEntry(out->modelEntries[m]);
out->modelEntries[m] = NULL; 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(); errorOk();
} }
+34 -11
View File
@@ -9,8 +9,6 @@
#include "asset/assetfile.h" #include "asset/assetfile.h"
#include "rpg/overworld/chunk.h" #include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 5
typedef struct assetloading_s assetloading_t; typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t; typedef struct assetentry_s assetentry_t;
@@ -20,16 +18,17 @@ typedef struct {
typedef enum { typedef enum {
ASSET_CHUNK_LOADING_STATE_INITIAL, ASSET_CHUNK_LOADING_STATE_INITIAL,
ASSET_CHUNK_LOADING_STATE_READ_FILE,
ASSET_CHUNK_LOADING_STATE_PARSE, ASSET_CHUNK_LOADING_STATE_PARSE,
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS, ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
ASSET_CHUNK_LOADING_STATE_DONE ASSET_CHUNK_LOADING_STATE_DONE
} assetchunkloadingstate_t; } assetchunkloadingstate_t;
typedef struct { typedef struct {
assetfile_t file;
assetchunkloadingstate_t state; 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; uint8_t modelIndex;
} assetchunkloaderloading_t; } assetchunkloaderloading_t;
@@ -61,6 +60,27 @@ typedef struct {
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
assetentry_t *modelEntries[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; uint8_t entitySpawnCount;
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX]; chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
@@ -69,9 +89,10 @@ typedef struct {
} assetchunkoutput_t; } assetchunkoutput_t;
/** /**
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into * Async loader stub for chunk assets. Chunk loading is performed entirely
* the loading buffer so the sync phase can parse without blocking the * on the main thread (it delegates to the JSON asset loader and to
* main thread on I/O. * meshInit/meshFlush, both main-thread-only); this callback is never
* reached.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.
@@ -79,9 +100,11 @@ typedef struct {
errorret_t assetChunkLoaderAsync(assetloading_t *loading); errorret_t assetChunkLoaderAsync(assetloading_t *loading);
/** /**
* Synchronous loader for chunk assets. Validates the DCF binary previously * Synchronous loader for chunk assets. Locks the chunk's JSON descriptor
* read by the async phase and populates the output assetchunkoutput_t with * as a sub-asset, parses tiles/external mesh references/entity spawns/
* tile data and model paths. * 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. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation. * @return Error code indicating success or failure of the load operation.
+1 -1
View File
@@ -62,7 +62,7 @@ errorret_t assetModelLoaderSync(assetloading_t *loading) {
// Parse required mesh path. // Parse required mesh path.
yyjson_val *meshVal = yyjson_obj_get(root, "mesh"); yyjson_val *meshVal = yyjson_obj_get(root, "mesh");
if(meshVal == NULL || !yyjson_is_str(meshVal)) { if(!meshVal || !yyjson_is_str(meshVal)) {
assetUnlockEntry(jsonEntry); assetUnlockEntry(jsonEntry);
loading->entry->state = ASSET_ENTRY_STATE_ERROR; loading->entry->state = ASSET_ENTRY_STATE_ERROR;
errorThrow("Model JSON missing 'mesh' string"); errorThrow("Model JSON missing 'mesh' string");
+3 -3
View File
@@ -32,7 +32,7 @@ typedef struct {
* on the main thread; this callback is never reached. * on the main thread; this callback is never reached.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Always success. * @returns Always success.
*/ */
errorret_t assetModelLoaderAsync(assetloading_t *loading); errorret_t assetModelLoaderAsync(assetloading_t *loading);
@@ -44,7 +44,7 @@ errorret_t assetModelLoaderAsync(assetloading_t *loading);
* within ASSET_LOADING_COUNT_MAX slot limits. * within ASSET_LOADING_COUNT_MAX slot limits.
* *
* @param loading Loading information for the asset being loaded. * @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure. * @returns Error code indicating success or failure.
*/ */
errorret_t assetModelLoaderSync(assetloading_t *loading); errorret_t assetModelLoaderSync(assetloading_t *loading);
@@ -53,6 +53,6 @@ errorret_t assetModelLoaderSync(assetloading_t *loading);
* sub-asset entries. * sub-asset entries.
* *
* @param entry Asset entry containing the model to dispose. * @param entry Asset entry containing the model to dispose.
* @return Error code indicating success or failure. * @returns Error code indicating success or failure.
*/ */
errorret_t assetModelDispose(assetentry_t *entry); errorret_t assetModelDispose(assetentry_t *entry);
+1 -1
View File
@@ -77,7 +77,7 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) {
memoryFree(buffer); memoryFree(buffer);
loading->loading.json.buffer = NULL; loading->loading.json.buffer = NULL;
if(loading->entry->data.json == NULL) { if(!loading->entry->data.json) {
assetLoaderErrorThrow(loading, "Failed to parse JSON"); assetLoaderErrorThrow(loading, "Failed to parse JSON");
} }
@@ -41,32 +41,6 @@ typedef struct {
typedef yyjson_doc * assetjsonoutput_t; typedef yyjson_doc * assetjsonoutput_t;
/**
* Asynchronous loader callback. Opens the JSON file and reads the whole of
* it into a heap buffer, so the main thread is not blocked on I/O. The
* buffer is parsed later, on the main thread, by assetJsonLoaderSync. Sets
* the entry state to `ASSET_ENTRY_STATE_PENDING_SYNC` on success.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetJsonLoaderAsync(assetloading_t *loading); errorret_t assetJsonLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader callback. Drives the load state machine: on the first
* call it queues the async read, and once that has completed it parses the
* buffer into a `yyjson_doc` (comments and trailing commas allowed), frees
* the buffer and marks the entry `ASSET_ENTRY_STATE_LOADED`.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetJsonLoaderSync(assetloading_t *loading); errorret_t assetJsonLoaderSync(assetloading_t *loading);
/**
* Dispose callback. Frees the parsed `yyjson_doc` held by the entry.
*
* @param entry The asset entry to dispose.
* @return OK on success, error otherwise.
*/
errorret_t assetJsonDispose(assetentry_t *entry); errorret_t assetJsonDispose(assetentry_t *entry);
@@ -93,7 +93,7 @@ errorret_t assetLocaleParseHeader(
// Find "Plural-Forms: " line and parse out plural form info // Find "Plural-Forms: " line and parse out plural form info
char_t *pluralFormsLine = strstr(headerBuffer, "Plural-Forms:"); char_t *pluralFormsLine = strstr(headerBuffer, "Plural-Forms:");
if(pluralFormsLine == NULL) { if(!pluralFormsLine) {
errorOk(); errorOk();
} }
@@ -101,7 +101,7 @@ errorret_t assetLocaleParseHeader(
// Expect nplurals // Expect nplurals
char_t *npluralsStr = strstr(pluralFormsLine, "nplurals="); char_t *npluralsStr = strstr(pluralFormsLine, "nplurals=");
if(npluralsStr == NULL) { if(!npluralsStr) {
errorThrow("Failed to find nplurals in Plural-Forms header."); errorThrow("Failed to find nplurals in Plural-Forms header.");
} }
npluralsStr += strlen("nplurals="); npluralsStr += strlen("nplurals=");
@@ -120,7 +120,7 @@ errorret_t assetLocaleParseHeader(
// Expect plural= // Expect plural=
char_t *pluralStr = strstr(pluralFormsLine, "plural="); char_t *pluralStr = strstr(pluralFormsLine, "plural=");
if(pluralStr == NULL) { if(!pluralStr) {
errorThrow("Failed to find plural in Plural-Forms header."); errorThrow("Failed to find plural in Plural-Forms header.");
} }
pluralStr += strlen("plural="); pluralStr += strlen("plural=");
@@ -128,7 +128,7 @@ errorret_t assetLocaleParseHeader(
// Expect ( [expressions] ) // Expect ( [expressions] )
char_t *openParen = strchr(pluralStr, '('); char_t *openParen = strchr(pluralStr, '(');
char_t *closeParen = strrchr(pluralStr, ')'); char_t *closeParen = strrchr(pluralStr, ')');
if(openParen == NULL || closeParen == NULL || closeParen < openParen) { if(!openParen || !closeParen || closeParen < openParen) {
errorThrow("Failed to find plural expression in Plural-Forms header."); errorThrow("Failed to find plural expression in Plural-Forms header.");
} }
@@ -399,12 +399,12 @@ errorret_t assetLocaleLineUnbuffer(
// At the point this funciton is called, we are looking for an opening quote. // At the point this funciton is called, we are looking for an opening quote.
char_t *start = strchr((char_t *)lineBuffer, '"'); char_t *start = strchr((char_t *)lineBuffer, '"');
if(start == NULL) { if(!start) {
errorThrow("Expected open (0) \""); errorThrow("Expected open (0) \"");
} }
char *end = strchr(start + 1, '"'); char *end = strchr(start + 1, '"');
if(end == NULL) { if(!end) {
errorThrow("Expected close (0) \""); errorThrow("Expected close (0) \"");
} }
*end = '\0'; *end = '\0';
@@ -690,7 +690,7 @@ errorret_t assetLocaleGetStringLookup(
// Parse until ] // Parse until ]
char *end = strchr(ptr, ']'); char *end = strchr(ptr, ']');
if(end == NULL) { if(!end) {
errorThrow("Expected ] for plural form, found: %s", lineBuffer); errorThrow("Expected ] for plural form, found: %s", lineBuffer);
} }
*end = '\0'; *end = '\0';
@@ -854,7 +854,7 @@ errorret_t assetLocaleGetStringWithArgs(
inIndex++; inIndex++;
// Escaped percent /* Escaped percent */
if(format[inIndex] == '%') { if(format[inIndex] == '%') {
if(outIndex + 1 >= bufferSize) { if(outIndex + 1 >= bufferSize) {
memoryFree(format); memoryFree(format);
@@ -880,7 +880,7 @@ errorret_t assetLocaleGetStringWithArgs(
specBuffer[specLength++] = '%'; specBuffer[specLength++] = '%';
// Allow flags / width / precision /* Allow flags / width / precision */
while(format[inIndex] != '\0') { while(format[inIndex] != '\0') {
char_t ch = format[inIndex]; char_t ch = format[inIndex];
+4 -4
View File
@@ -87,10 +87,10 @@ errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame) {
const size_t currentByte = (size_t) stream->pcm.file.position; const size_t currentByte = (size_t) stream->pcm.file.position;
if(targetByte < currentByte) { if(targetByte < currentByte) {
// Only a full rewind can move a read cursor earlier once it's already // A rewind is needed to move the read cursor earlier once it's
// advanced past a point - a compressed archive entry can't be decoded // already advanced past a point - assetFileRewind() takes the cheap
// backward. See this function's own doc comment for the performance // in-place seek path since every dusk.dsk entry is stored
// implications of a deep loopTo. // uncompressed.
errorChain(assetFileRewind(&stream->pcm.file)); errorChain(assetFileRewind(&stream->pcm.file));
errorChain(assetFileRead(&stream->pcm.file, NULL, targetByte)); errorChain(assetFileRead(&stream->pcm.file, NULL, targetByte));
} else if(targetByte > currentByte) { } else if(targetByte > currentByte) {
+2 -5
View File
@@ -63,11 +63,8 @@ size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream);
* (relative to the start of the PCM data - the same units as * (relative to the start of the PCM data - the same units as
* audiostream_t's startFrame/loopStart/loopTo, once converted from * audiostream_t's startFrame/loopStart/loopTo, once converted from
* seconds). Seeking backward re-reads from the start of the underlying * seconds). Seeking backward re-reads from the start of the underlying
* asset file (see assetFileRewind()) since a compressed archive entry can * asset file (see assetFileRewind()), which is a cheap in-place seek
* only be decoded forward - this makes a loop with a deep loopTo more * since every dusk.dsk entry is stored uncompressed.
* expensive to restart than one near the start, which is a real
* performance caveat, not just a theoretical one, for anything backed by
* a compressed (not stored) asset archive entry.
* *
* @param stream The audio stream to seek. Must be AUDIO_STREAM_TYPE_PCM. * @param stream The audio stream to seek. Must be AUDIO_STREAM_TYPE_PCM.
* @param frame Frame offset to seek to, relative to the start of the PCM * @param frame Frame offset to seek to, relative to the start of the PCM
+5 -5
View File
@@ -55,8 +55,8 @@ void capsuleBuffer(
vertices[vi].uv[1] = (v); \ vertices[vi].uv[1] = (v); \
vi++; vi++;
// ---- Top hemisphere ---- /* ---- Top hemisphere ---- */
// phi ranges from PI/2 (top pole) down to 0 (equator). /* phi ranges from PI/2 (top pole) down to 0 (equator). */
const float_t capStep = (float_t)GLM_PI_2 / (float_t)capRings; const float_t capStep = (float_t)GLM_PI_2 / (float_t)capRings;
for(int32_t i = 0; i < capRings; i++) { for(int32_t i = 0; i < capRings; i++) {
const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * capStep; const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * capStep;
@@ -67,8 +67,8 @@ void capsuleBuffer(
const float_t lxz1 = radius * cosf(phi1); const float_t lxz1 = radius * cosf(phi1);
const float_t lxz2 = radius * cosf(phi2); const float_t lxz2 = radius * cosf(phi2);
// UV: top cap occupies v in [0.5 + halfHeightFrac .. 1.0]: we use a /* UV: top cap occupies v in [0.5 + halfHeightFrac .. 1.0]: we use a
// simple per-band normalisation against the full height. * simple per-band normalisation against the full height. */
const float_t v1 = 1.0f - (float_t)i / (float_t)(2 * capRings + 1); const float_t v1 = 1.0f - (float_t)i / (float_t)(2 * capRings + 1);
const float_t v2 = 1.0f - (float_t)(i + 1) / (float_t)(2 * capRings + 1); const float_t v2 = 1.0f - (float_t)(i + 1) / (float_t)(2 * capRings + 1);
@@ -97,7 +97,7 @@ void capsuleBuffer(
} }
} }
// ---- Cylindrical body ---- /* ---- Cylindrical body ---- */
{ {
const float_t yTop = cy + halfHeight; const float_t yTop = cy + halfHeight;
const float_t yBot = cy - halfHeight; const float_t yBot = cy - halfHeight;
+4 -6
View File
@@ -1,9 +1,7 @@
/** // Copyright (c) 2026 Dominic Masters
* Copyright (c) 2026 Dominic Masters //
* // This software is released under the MIT License.
* This software is released under the MIT License. // https://opensource.org/licenses/MIT
* https://opensource.org/licenses/MIT
*/
#pragma once #pragma once
#include "display/display.h" #include "display/display.h"
+3 -3
View File
@@ -59,7 +59,7 @@ void planeBuffer(
switch(axis) { switch(axis) {
case PLANE_AXIS_XY: { case PLANE_AXIS_XY: {
// Flat in XY at z = min[2]; spans X and Y. /* Flat in XY at z = min[2]; spans X and Y. */
const float_t z = min[2]; const float_t z = min[2];
PLANE_VERT(0, min[0], min[1], z, u0, v0) PLANE_VERT(0, min[0], min[1], z, u0, v0)
PLANE_VERT(1, max[0], min[1], z, u1, v0) PLANE_VERT(1, max[0], min[1], z, u1, v0)
@@ -71,7 +71,7 @@ void planeBuffer(
} }
case PLANE_AXIS_XZ: { case PLANE_AXIS_XZ: {
// Flat in XZ at y = min[1]; spans X and Z. /* Flat in XZ at y = min[1]; spans X and Z. */
const float_t y = min[1]; const float_t y = min[1];
PLANE_VERT(0, min[0], y, min[2], u0, v0) PLANE_VERT(0, min[0], y, min[2], u0, v0)
PLANE_VERT(1, max[0], y, min[2], u1, v0) PLANE_VERT(1, max[0], y, min[2], u1, v0)
@@ -83,7 +83,7 @@ void planeBuffer(
} }
case PLANE_AXIS_YZ: { case PLANE_AXIS_YZ: {
// Flat in YZ at x = min[0]; spans Y and Z. /* Flat in YZ at x = min[0]; spans Y and Z. */
const float_t x = min[0]; const float_t x = min[0];
PLANE_VERT(0, x, min[1], min[2], u0, v0) PLANE_VERT(0, x, min[1], min[2], u0, v0)
PLANE_VERT(1, x, max[1], min[2], u1, v0) PLANE_VERT(1, x, max[1], min[2], u1, v0)
+1 -1
View File
@@ -44,7 +44,7 @@ void sphereBuffer(
int32_t vi = 0; int32_t vi = 0;
for(int32_t i = 0; i < stacks; i++) { for(int32_t i = 0; i < stacks; i++) {
// Latitude angles: top of band -> bottom of band /* Latitude angles: top of band -> bottom of band */
const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * stackStep; const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * stackStep;
const float_t phi2 = (float_t)GLM_PI_2 - (float_t)(i + 1) * stackStep; const float_t phi2 = (float_t)GLM_PI_2 - (float_t)(i + 1) * stackStep;
+5 -5
View File
@@ -47,17 +47,17 @@ void triPrismBuffer(
vertices[vi].uv[1] = (v); \ vertices[vi].uv[1] = (v); \
vi++; vi++;
// --- Front face (z = maxZ), CCW from +Z --- /* --- Front face (z = maxZ), CCW from +Z --- */
PRISM_VERT(x0, y0, maxZ, 0.0f, 0.0f) PRISM_VERT(x0, y0, maxZ, 0.0f, 0.0f)
PRISM_VERT(x1, y1, maxZ, 1.0f, 0.0f) PRISM_VERT(x1, y1, maxZ, 1.0f, 0.0f)
PRISM_VERT(x2, y2, maxZ, 0.5f, 1.0f) PRISM_VERT(x2, y2, maxZ, 0.5f, 1.0f)
// --- Back face (z = minZ), CCW from -Z (reverse winding) --- /* --- Back face (z = minZ), CCW from -Z (reverse winding) --- */
PRISM_VERT(x2, y2, minZ, 0.5f, 1.0f) PRISM_VERT(x2, y2, minZ, 0.5f, 1.0f)
PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f) PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f)
PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f) PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f)
// --- Side face 0: edge p0->p1 --- /* --- Side face 0: edge p0->p1 --- */
PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f) PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f)
PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f) PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f)
PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f) PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f)
@@ -66,7 +66,7 @@ void triPrismBuffer(
PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f) PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f)
PRISM_VERT(x0, y0, maxZ, 0.0f, 1.0f) PRISM_VERT(x0, y0, maxZ, 0.0f, 1.0f)
// --- Side face 1: edge p1->p2 --- /* --- Side face 1: edge p1->p2 --- */
PRISM_VERT(x1, y1, minZ, 0.0f, 0.0f) PRISM_VERT(x1, y1, minZ, 0.0f, 0.0f)
PRISM_VERT(x2, y2, minZ, 1.0f, 0.0f) PRISM_VERT(x2, y2, minZ, 1.0f, 0.0f)
PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f) PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f)
@@ -75,7 +75,7 @@ void triPrismBuffer(
PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f) PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f)
PRISM_VERT(x1, y1, maxZ, 0.0f, 1.0f) PRISM_VERT(x1, y1, maxZ, 0.0f, 1.0f)
// --- Side face 2: edge p2->p0 --- /* --- Side face 2: edge p2->p0 --- */
PRISM_VERT(x2, y2, minZ, 0.0f, 0.0f) PRISM_VERT(x2, y2, minZ, 0.0f, 0.0f)
PRISM_VERT(x0, y0, minZ, 1.0f, 0.0f) PRISM_VERT(x0, y0, minZ, 1.0f, 0.0f)
PRISM_VERT(x0, y0, maxZ, 1.0f, 1.0f) PRISM_VERT(x0, y0, maxZ, 1.0f, 1.0f)
+10 -10
View File
@@ -13,14 +13,14 @@
shader_t *bound = NULL; shader_t *bound = NULL;
errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) { errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
errorChain(shaderInitPlatform(shader, def)); errorChain(shaderInitPlatform(shader, def));
bound = NULL; bound = NULL;
errorOk(); errorOk();
} }
errorret_t shaderBind(shader_t *shader) { errorret_t shaderBind(shader_t *shader) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
errorChain(shaderBindPlatform(shader)); errorChain(shaderBindPlatform(shader));
bound = shader; bound = shader;
errorOk(); errorOk();
@@ -31,9 +31,9 @@ errorret_t shaderSetMatrix(
const char_t *name, const char_t *name,
mat4 matrix mat4 matrix
) { ) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
assertStrLenMin(name, 1, "Uniform name cannot be empty"); assertStrLenMin(name, 1, "Uniform name cannot be empty");
assertNotNull(matrix, "Matrix cannot be NULL"); assertNotNull(matrix, "Matrix cannot be null");
assertTrue(bound == shader, "Shader must be bound."); assertTrue(bound == shader, "Shader must be bound.");
errorChain(shaderSetMatrixPlatform(shader, name, matrix)); errorChain(shaderSetMatrixPlatform(shader, name, matrix));
errorOk(); errorOk();
@@ -44,7 +44,7 @@ errorret_t shaderSetTexture(
const char_t *name, const char_t *name,
texture_t *texture texture_t *texture
) { ) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
assertStrLenMin(name, 1, "Uniform name cannot be empty"); assertStrLenMin(name, 1, "Uniform name cannot be empty");
assertTrue(bound == shader, "Shader must be bound."); assertTrue(bound == shader, "Shader must be bound.");
errorChain(shaderSetTexturePlatform(shader, name, texture)); errorChain(shaderSetTexturePlatform(shader, name, texture));
@@ -56,7 +56,7 @@ errorret_t shaderSetColor(
const char_t *name, const char_t *name,
color_t color color_t color
) { ) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
assertStrLenMin(name, 1, "Uniform name cannot be empty"); assertStrLenMin(name, 1, "Uniform name cannot be empty");
assertTrue(bound == shader, "Shader must be bound."); assertTrue(bound == shader, "Shader must be bound.");
errorChain(shaderSetColorPlatform(shader, name, color)); errorChain(shaderSetColorPlatform(shader, name, color));
@@ -67,16 +67,16 @@ errorret_t shaderSetMaterial(
shader_t *shader, shader_t *shader,
const shadermaterial_t *material const shadermaterial_t *material
) { ) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
assertNotNull(material, "Material cannot be NULL"); assertNotNull(material, "Material cannot be null");
assertTrue(bound == shader, "Shader must be bound."); assertTrue(bound == shader, "Shader must be bound.");
assertNotNull(shader->definition, "Shader definition cannot be NULL"); assertNotNull(shader->definition, "Shader definition cannot be null");
assertNotNull(shader->definition->setMaterial, "Def lacks setMaterial"); assertNotNull(shader->definition->setMaterial, "Def lacks setMaterial");
return shader->definition->setMaterial(shader, material); return shader->definition->setMaterial(shader, material);
} }
errorret_t shaderDispose(shader_t *shader) { errorret_t shaderDispose(shader_t *shader) {
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
bound = NULL; bound = NULL;
errorChain(shaderDisposePlatform(shader)); errorChain(shaderDisposePlatform(shader));
errorOk(); errorOk();
+3 -3
View File
@@ -41,10 +41,10 @@ errorret_t shaderListInit() {
} }
assertNotNull( assertNotNull(
SHADER_LIST_DEFS[i].shader, "Shader cannot be NULL" SHADER_LIST_DEFS[i].shader, "Shader cannot be null"
); );
assertNotNull( assertNotNull(
SHADER_LIST_DEFS[i].definition, "Shader definition cannot be NULL" SHADER_LIST_DEFS[i].definition, "Shader definition cannot be null"
); );
errorChain(shaderInit( errorChain(shaderInit(
@@ -78,7 +78,7 @@ errorret_t shaderListDispose(void) {
} }
assertNotNull( assertNotNull(
SHADER_LIST_DEFS[i].shader, "Shader cannot be NULL" SHADER_LIST_DEFS[i].shader, "Shader cannot be null"
); );
errorChain(shaderDispose(SHADER_LIST_DEFS[i].shader)); errorChain(shaderDispose(SHADER_LIST_DEFS[i].shader));
+4 -4
View File
@@ -31,9 +31,9 @@ errorret_t spriteBatchBuffer(
shader_t *shader, shader_t *shader,
const shadermaterial_t material const shadermaterial_t material
) { ) {
assertNotNull(sprites, "Sprites cannot be NULL"); assertNotNull(sprites, "Sprites cannot be null");
assertTrue(count > 0, "Count must be greater than zero"); assertTrue(count > 0, "Count must be greater than zero");
assertNotNull(shader, "Shader cannot be NULL"); assertNotNull(shader, "Shader cannot be null");
// Did the shader or material data change? // Did the shader or material data change?
if(shader != SPRITEBATCH.shader) { if(shader != SPRITEBATCH.shader) {
@@ -92,9 +92,9 @@ void spriteBatchBufferToMesh(
meshvertex_t *vertices, meshvertex_t *vertices,
const uint32_t verticesSize const uint32_t verticesSize
) { ) {
assertNotNull(sprites, "Sprites cannot be NULL"); assertNotNull(sprites, "Sprites cannot be null");
assertTrue(count > 0, "Count must be greater than zero"); assertTrue(count > 0, "Count must be greater than zero");
assertNotNull(vertices, "Vertices cannot be NULL"); assertNotNull(vertices, "Vertices cannot be null");
assertTrue( assertTrue(
verticesSize >= count * QUAD_VERTEX_COUNT, "Vertices array too small" verticesSize >= count * QUAD_VERTEX_COUNT, "Vertices array too small"
); );
@@ -27,7 +27,7 @@ typedef struct {
* @param y Screen y position of the sprite's top-left corner. * @param y Screen y position of the sprite's top-left corner.
* @param width Width of the sprite in screen space. * @param width Width of the sprite in screen space.
* @param height Height of the sprite in screen space. * @param height Height of the sprite in screen space.
* @return The populated sprite. * @returns The populated sprite.
*/ */
spritebatchsprite_t spriteBatchSpriteTilesetPosition( spritebatchsprite_t spriteBatchSpriteTilesetPosition(
const tileset_t *tileset, const tileset_t *tileset,
+3
View File
@@ -7,6 +7,7 @@
#include "engine.h" #include "engine.h"
#include "util/memory.h" #include "util/memory.h"
#include "perf/perf.h"
#include "time/time.h" #include "time/time.h"
#include "input/input.h" #include "input/input.h"
#include "locale/localemanager.h" #include "locale/localemanager.h"
@@ -36,6 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
ENGINE.version = DUSK_VERSION; ENGINE.version = DUSK_VERSION;
// Init systems. Order is important. // Init systems. Order is important.
perfInit();
timeInit(); timeInit();
consoleInit(); consoleInit();
errorChain(systemInit()); errorChain(systemInit());
@@ -104,6 +106,7 @@ errorret_t engineDispose(void) {
errorChain(displayDispose()); errorChain(displayDispose());
errorChain(saveDispose()); errorChain(saveDispose());
errorChain(assetDispose()); errorChain(assetDispose());
perfDispose();
errorOk(); errorOk();
} }
+4 -4
View File
@@ -152,7 +152,7 @@ float_t inputGetLastValue(const inputaction_t action) {
const inputaction_t negY, const inputaction_t posY, const inputaction_t negY, const inputaction_t posY,
vec2 result vec2 result
) { ) {
assertNotNull(result, "Result vector cannot be NULL"); assertNotNull(result, "Result vector cannot be null");
result[0] = inputAxisDynamic(negX, posX); result[0] = inputAxisDynamic(negX, posX);
result[1] = inputAxisDynamic(negY, posY); result[1] = inputAxisDynamic(negY, posY);
} }
@@ -162,7 +162,7 @@ float_t inputGetLastValue(const inputaction_t action) {
const inputaction_t negY, const inputaction_t posY, const inputaction_t negY, const inputaction_t posY,
vec2 result vec2 result
) { ) {
assertNotNull(result, "Result vector cannot be NULL"); assertNotNull(result, "Result vector cannot be null");
float_t x = inputAxisDynamic(negX, posX); float_t x = inputAxisDynamic(negX, posX);
float_t y = inputAxisDynamic(negY, posY); float_t y = inputAxisDynamic(negY, posY);
float_t mag = sqrtf(x * x + y * y); float_t mag = sqrtf(x * x + y * y);
@@ -208,7 +208,7 @@ void inputAxis2D(
const inputaction_t negY, const inputaction_t posY, const inputaction_t negY, const inputaction_t posY,
vec2 result vec2 result
) { ) {
assertNotNull(result, "Result vector cannot be NULL"); assertNotNull(result, "Result vector cannot be null");
result[0] = inputAxis(negX, posX); result[0] = inputAxis(negX, posX);
result[1] = inputAxis(negY, posY); result[1] = inputAxis(negY, posY);
} }
@@ -218,7 +218,7 @@ void inputAngle2D(
const inputaction_t negY, const inputaction_t posY, const inputaction_t negY, const inputaction_t posY,
vec2 result vec2 result
) { ) {
assertNotNull(result, "Result vector cannot be NULL"); assertNotNull(result, "Result vector cannot be null");
float_t x = inputAxis(negX, posX); float_t x = inputAxis(negX, posX);
float_t y = inputAxis(negY, posY); float_t y = inputAxis(negY, posY);
float_t mag = sqrtf(x * x + y * y); float_t mag = sqrtf(x * x + y * y);
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
perf.c
)
+88
View File
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "perf.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "console/console.h"
#if defined(DUSK_LINUX)
#include <time.h>
#elif defined(DUSK_PSP)
#include <pspthreadman.h>
#endif
perf_t PERF;
void perfInit(void) {
assertIsMainThread("Must be called from the main thread.");
memoryZero(&PERF, sizeof(perf_t));
}
void perfDispose(void) {
assertIsMainThread("Must be called from the main thread.");
}
uint64_t perfGetTick(void) {
assertIsMainThread("Must be called from the main thread.");
#if defined(DUSK_LINUX)
// TEST - CLOCK_MONOTONIC via clock_gettime(), Linux desktop only for
// now. Not a cross-platform answer yet (see perfGetTick's doc
// comment in perf.h) - just something real to look at while that
// gets settled. Returns nanoseconds since some unspecified starting
// point, so only differences between two calls are meaningful.
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
#elif defined(DUSK_PSP)
// TEST - sceKernelGetSystemTimeWide(), microseconds since boot at
// ~1us resolution (the standard PSP homebrew idiom for elapsed-time
// measurement - psprtc.h's sceRtcGetCurrentTick is for wall-clock
// date/time instead, already used by timepsp.c for that purpose).
// Scaled to nanoseconds to match every other platform's tick unit.
return (uint64_t)sceKernelGetSystemTimeWide() * 1000ull;
#else
return 0;
#endif
}
void perfPushItem(
const char_t *label, const char_t *file, const int32_t line
) {
assertIsMainThread("Must be called from the main thread.");
assertNotNull(label, "Perf item label cannot be NULL.");
assertNotNull(file, "Perf item file cannot be NULL.");
assertTrue(PERF.stackSize < PERF_STACK_SIZE, "Perf stack overflow.");
PERF.stack[PERF.stackSize++] = (perfitem_t){
.label = label,
.file = file,
.line = line,
.tick = perfGetTick()
};
}
void perfPopItem(void) {
assertIsMainThread("Must be called from the main thread.");
assertTrue(PERF.stackSize > 0, "Perf stack underflow.");
PERF.stackSize--;
}
void perfReport(void) {
assertIsMainThread("Must be called from the main thread.");
if(PERF.stackSize == 0) return;
const perfitem_t *item = &PERF.stack[PERF.stackSize - 1];
uint64_t elapsedNs = perfGetTick() - item->tick;
double_t elapsedMs = (double_t)elapsedNs / 1000000.0;
consolePrint(
"perf: %s (%s:%d, %.4fms)",
item->label, item->file, item->line, elapsedMs
);
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "perfitem.h"
#define PERF_STACK_SIZE 256
typedef struct {
perfitem_t stack[PERF_STACK_SIZE];
uint32_t stackSize;
} perf_t;
extern perf_t PERF;
/**
* Initializes the perf system.
*/
void perfInit(void);
/**
* Disposes/cleans up the perf system. Currently a no-op - here so
* engine.c has a symmetric call to make once this needs real cleanup.
*/
void perfDispose(void);
/**
* Returns a tick value representing "now", for measuring the duration
* between a perfPush/perfPushItem and its matching perfPopItem. Always
* nanoseconds since some unspecified starting point, on every platform
* that implements it, so perfReport's ms conversion stays correct
* everywhere without per-platform special-casing.
*
* PLACEHOLDER - currently only reads a real value on DUSK_LINUX desktop
* builds (via clock_gettime(CLOCK_MONOTONIC)) and DUSK_PSP (via
* sceKernelGetSystemTimeWide(), scaled from microseconds), as a test;
* every other platform still gets a meaningless 0. A common,
* cross-platform way of reading CPU cycles/time hasn't been settled on
* yet - don't treat results as comparable across platforms until this is
* replaced with a real per-platform implementation. Only the difference
* between two calls is meaningful, never the raw value itself.
*
* @return A tick value, in nanoseconds. 0 on unimplemented platforms.
*/
uint64_t perfGetTick(void);
/**
* Pushes a new item onto the perf stack, capturing the current tick (see
* perfGetTick) as its start time. Prefer the perfPush() macro, which
* supplies file/line automatically.
*
* @param label Short caller-supplied memo of what this item represents
* (e.g. "frame start") - stored by pointer, not copied, so pass a
* string literal or something that otherwise outlives the push.
* @param file Source file the push originated from (e.g. __FILE__).
* @param line Source line the push originated from (e.g. __LINE__).
*/
void perfPushItem(
const char_t *label, const char_t *file, const int32_t line
);
/**
* Pops the most recently pushed perf item off the stack.
*/
void perfPopItem(void);
/**
* Reports on the item currently at the top of the perf stack (the most
* recently pushed one still awaiting its perfPopItem), if any.
*/
void perfReport(void);
/**
* Pushes a new perf item onto the stack, tagged with the call site this
* macro is expanded at.
*
* @param label Short memo of what this item represents (e.g. "frame
* start") - see perfPushItem.
*/
#define perfPush(label) perfPushItem(label, __FILE__, __LINE__)
/**
* Pops the most recently pushed perf item off the stack.
*/
#define perfPop() perfPopItem()
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct {
const char_t *label;
int32_t line;
const char_t *file;
uint64_t tick;
} perfitem_t;
+1 -1
View File
@@ -82,6 +82,6 @@ void battleFighterInit(
* is not dead). * is not dead).
* *
* @param fighter Pointer to the fighter to check. * @param fighter Pointer to the fighter to check.
* @return True if the fighter can act. * @returns True if the fighter can act.
*/ */
bool_t battleFighterIsAlive(const battlefighter_t *fighter); bool_t battleFighterIsAlive(const battlefighter_t *fighter);
+1 -1
View File
@@ -136,7 +136,7 @@ void cutsceneSystemNext() {
// Start item. // Start item.
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem(); const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data)); memset(&CUTSCENE_SYSTEM.data, 0, sizeof(CUTSCENE_SYSTEM.data));
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data); cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
} }
+4 -4
View File
@@ -146,7 +146,7 @@ void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete);
* Asserts the resolved entity is within bounds. * Asserts the resolved entity is within bounds.
* *
* @param entityIndex Raw entity index or sentinel value. * @param entityIndex Raw entity index or sentinel value.
* @return Pointer to the resolved entity. * @returns Pointer to the resolved entity.
*/ */
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex); entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
@@ -155,7 +155,7 @@ entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
* a concrete map area ID. * a concrete map area ID.
* *
* @param areaId Raw map area ID or sentinel value. * @param areaId Raw map area ID or sentinel value.
* @return The resolved map area ID. * @returns The resolved map area ID.
*/ */
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId); uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
@@ -164,7 +164,7 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index. * sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
* *
* @param index Raw slot index or sentinel value. * @param index Raw slot index or sentinel value.
* @return The resolved slot index. * @returns The resolved slot index.
*/ */
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index); uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
@@ -174,7 +174,7 @@ uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
* once its keyboard closes). Empty ("") if nothing has been cached yet * once its keyboard closes). Empty ("") if nothing has been cached yet
* for the running cutscene. * for the running cutscene.
* *
* @return The cached text. * @returns The cached text.
*/ */
const char_t * cutsceneSystemGetTextCache(void); const char_t * cutsceneSystemGetTextCache(void);
@@ -36,7 +36,7 @@ void cutsceneAudioFadeStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioFadeUpdate( bool_t cutsceneAudioFadeUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -16,7 +16,7 @@
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true once the channel is no longer fading. * @returns true once the channel is no longer fading.
*/ */
bool_t cutsceneAudioFadeWaitUpdate( bool_t cutsceneAudioFadeWaitUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -25,7 +25,7 @@ void cutsceneAudioPauseStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioPauseUpdate( bool_t cutsceneAudioPauseUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -38,7 +38,7 @@ void cutsceneAudioPlayStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioPlayUpdate( bool_t cutsceneAudioPlayUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -25,7 +25,7 @@ void cutsceneAudioResumeStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioResumeUpdate( bool_t cutsceneAudioResumeUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -37,7 +37,7 @@ void cutsceneAudioSetStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioSetUpdate( bool_t cutsceneAudioSetUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -34,7 +34,7 @@ void cutsceneAudioSetLoopStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioSetLoopUpdate( bool_t cutsceneAudioSetLoopUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -31,7 +31,7 @@ void cutsceneAudioSetPanStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioSetPanUpdate( bool_t cutsceneAudioSetPanUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -25,7 +25,7 @@ void cutsceneAudioStopStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneAudioStopUpdate( bool_t cutsceneAudioStopUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -32,7 +32,7 @@ void cutsceneBattleForceActionStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneBattleForceActionUpdate( bool_t cutsceneBattleForceActionUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -20,7 +20,7 @@ typedef struct {
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true once BATTLE.state equals the watched state. * @returns true once BATTLE.state equals the watched state.
*/ */
bool_t cutsceneBattleWaitStateUpdate( bool_t cutsceneBattleWaitStateUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -43,7 +43,7 @@ void cutsceneStartBattleStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true once the battle has ended. * @returns true once the battle has ended.
*/ */
bool_t cutsceneStartBattleUpdate( bool_t cutsceneStartBattleUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -52,7 +52,7 @@ void cutsceneConcurrentStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true once every child has completed. * @returns true once every child has completed.
*/ */
bool_t cutsceneConcurrentUpdate( bool_t cutsceneConcurrentUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -15,7 +15,7 @@
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return false always. * @returns false always.
*/ */
bool_t cutsceneIdleUpdate( bool_t cutsceneIdleUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -29,7 +29,7 @@ void cutsceneMarkerStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneMarkerUpdate( bool_t cutsceneMarkerUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -27,7 +27,7 @@ void cutsceneQuitGameStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return false always. * @returns false always.
*/ */
bool_t cutsceneQuitGameUpdate( bool_t cutsceneQuitGameUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -27,7 +27,7 @@ void cutsceneRestartStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return false always. * @returns false always.
*/ */
bool_t cutsceneRestartUpdate( bool_t cutsceneRestartUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -33,7 +33,7 @@ void cutsceneSceneStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneSceneUpdate( bool_t cutsceneSceneUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -25,7 +25,7 @@ void cutsceneSetPauseStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneSetPauseUpdate( bool_t cutsceneSetPauseUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -27,7 +27,7 @@ void cutsceneWaitStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true when the wait has elapsed. * @returns true when the wait has elapsed.
*/ */
bool_t cutsceneWaitUpdate( bool_t cutsceneWaitUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -26,7 +26,7 @@ void cutsceneCallbackStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneCallbackUpdate( bool_t cutsceneCallbackUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
+2 -2
View File
@@ -213,7 +213,7 @@ void cutsceneItemStart(
* *
* @param item The cutscene item to tick. * @param item The cutscene item to tick.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true if the item is complete and the cutscene should advance. * @returns true if the item is complete and the cutscene should advance.
*/ */
bool_t cutsceneItemUpdate( bool_t cutsceneItemUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -252,7 +252,7 @@ void cutsceneCutsceneStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return false always. * @returns false always.
*/ */
bool_t cutsceneCutsceneUpdate( bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
+1 -1
View File
@@ -30,7 +30,7 @@ void cutscenePrintStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutscenePrintUpdate( bool_t cutscenePrintUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -31,7 +31,7 @@ void cutsceneEntityAddStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneEntityAddUpdate( bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -28,7 +28,7 @@ void cutsceneEntityRemoveStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneEntityRemoveUpdate( bool_t cutsceneEntityRemoveUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -30,7 +30,7 @@ void cutsceneEntityTeleportStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true always. * @returns true always.
*/ */
bool_t cutsceneEntityTeleportUpdate( bool_t cutsceneEntityTeleportUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,
@@ -32,7 +32,7 @@ void cutsceneEntityTurnStart(
* *
* @param item The cutscene item. * @param item The cutscene item.
* @param data Runtime data storage. * @param data Runtime data storage.
* @return true once the entity is idle and facing the target direction. * @returns true once the entity is idle and facing the target direction.
*/ */
bool_t cutsceneEntityTurnUpdate( bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item, const cutsceneitem_t *item,

Some files were not shown because too many files have changed in this diff Show More