From 3710924503440579b0e0d6c8e7c1578d65007eae Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 7 Nov 2026 16:33:26 -0600 Subject: [PATCH] Remove all existing code for fresh rewrite on new branch Co-Authored-By: Claude Sonnet 5.5 --- .github/actions/setup-devkitpro/action.yml | 49 - .github/actions/setup-pspdev/action.yml | 20 - .github/workflows/build.yml | 247 ----- .github/workflows/test.yml | 35 - README.md | 26 - assets/audio/audiotest.mp3 | Bin 17179 -> 0 bytes assets/audio/audiotest.wav | Bin 88244 -> 0 bytes assets/battle/moves.jsonc | 21 - assets/cutscenes/battle/moves/fire.jsonc | 21 - assets/cutscenes/battle/moves/slash.jsonc | 21 - assets/cutscenes/battletest.jsonc | 18 - assets/cutscenes/initial.jsonc | 68 -- assets/cutscenes/main_menu.jsonc | 140 --- assets/cutscenes/new_game.jsonc | 11 - assets/items/items.jsonc | 7 - assets/locale/en_US.po | 289 ----- .../maps/overworld/chunks/chunk_-1_0_0.json | 1 - assets/maps/overworld/chunks/chunk_0_0_0.json | 1 - assets/maps/overworld/chunks/chunk_0_1_0.json | 1 - assets/maps/overworld/chunks/chunk_1_0_0.json | 1 - assets/maps/overworld/chunks/chunk_1_1_0.json | 1 - assets/maps/overworld/chunks/chunk_2_0_0.json | 1 - assets/meshes/buildings/house_1_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_1_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_2_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_3_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_4_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_5_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_6_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_7_8.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_1.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_2.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_3.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_4.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_5.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_6.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_7.dmf | Bin 1332 -> 0 bytes assets/meshes/buildings/house_8_8.dmf | Bin 1332 -> 0 bytes assets/models/buildings/house_1_1.json | 4 - assets/models/buildings/house_1_2.json | 4 - assets/models/buildings/house_1_3.json | 4 - assets/models/buildings/house_1_4.json | 4 - assets/models/buildings/house_1_5.json | 4 - assets/models/buildings/house_1_6.json | 4 - assets/models/buildings/house_1_7.json | 4 - assets/models/buildings/house_1_8.json | 4 - assets/models/buildings/house_2_1.json | 4 - assets/models/buildings/house_2_2.json | 4 - assets/models/buildings/house_2_3.json | 4 - assets/models/buildings/house_2_4.json | 4 - assets/models/buildings/house_2_5.json | 4 - assets/models/buildings/house_2_6.json | 4 - assets/models/buildings/house_2_7.json | 4 - assets/models/buildings/house_2_8.json | 4 - assets/models/buildings/house_3_1.json | 4 - assets/models/buildings/house_3_2.json | 4 - assets/models/buildings/house_3_3.json | 4 - assets/models/buildings/house_3_4.json | 4 - assets/models/buildings/house_3_5.json | 4 - assets/models/buildings/house_3_6.json | 4 - assets/models/buildings/house_3_7.json | 4 - assets/models/buildings/house_3_8.json | 4 - assets/models/buildings/house_4_1.json | 4 - assets/models/buildings/house_4_2.json | 4 - assets/models/buildings/house_4_3.json | 4 - assets/models/buildings/house_4_4.json | 4 - assets/models/buildings/house_4_5.json | 4 - assets/models/buildings/house_4_6.json | 4 - assets/models/buildings/house_4_7.json | 4 - assets/models/buildings/house_4_8.json | 4 - assets/models/buildings/house_5_1.json | 4 - assets/models/buildings/house_5_2.json | 4 - assets/models/buildings/house_5_3.json | 4 - assets/models/buildings/house_5_4.json | 4 - assets/models/buildings/house_5_5.json | 4 - assets/models/buildings/house_5_6.json | 4 - assets/models/buildings/house_5_7.json | 4 - assets/models/buildings/house_5_8.json | 4 - assets/models/buildings/house_6_1.json | 4 - assets/models/buildings/house_6_2.json | 4 - assets/models/buildings/house_6_3.json | 4 - assets/models/buildings/house_6_4.json | 4 - assets/models/buildings/house_6_5.json | 4 - assets/models/buildings/house_6_6.json | 4 - assets/models/buildings/house_6_7.json | 4 - assets/models/buildings/house_6_8.json | 4 - assets/models/buildings/house_7_1.json | 4 - assets/models/buildings/house_7_2.json | 4 - assets/models/buildings/house_7_3.json | 4 - assets/models/buildings/house_7_4.json | 4 - assets/models/buildings/house_7_5.json | 4 - assets/models/buildings/house_7_6.json | 4 - assets/models/buildings/house_7_7.json | 4 - assets/models/buildings/house_7_8.json | 4 - assets/models/buildings/house_8_1.json | 4 - assets/models/buildings/house_8_2.json | 4 - assets/models/buildings/house_8_3.json | 4 - assets/models/buildings/house_8_4.json | 4 - assets/models/buildings/house_8_5.json | 4 - assets/models/buildings/house_8_6.json | 4 - assets/models/buildings/house_8_7.json | 4 - assets/models/buildings/house_8_8.json | 4 - assets/party.jsonc | 22 - assets/story/storyflag.jsonc | 10 - assets/tiles.png | Bin 110 -> 0 bytes cmake/modules/Findcglm.cmake | 15 - cmake/modules/Findcmocka.cmake | 14 - cmake/modules/Findlibmad.cmake | 32 - cmake/modules/Findlibzip.cmake | 61 -- cmake/modules/Findstb.cmake | 31 - cmake/modules/Findyyjson.cmake | 18 - cmake/modules/dusktest.cmake | 32 - cmake/targets/dolphin.cmake | 81 -- cmake/targets/gamecube.cmake | 24 - cmake/targets/knulli.cmake | 46 - cmake/targets/linux.cmake | 49 - cmake/targets/psp.cmake | 137 --- cmake/targets/vita.cmake | 88 -- cmake/targets/wii.cmake | 38 - cmake/toolchains/aarch64-linux-gnu.cmake | 29 - docker/dolphin/Dockerfile | 7 - docker/dolphin/meta.xml.in | 10 - docker/knulli/Dockerfile | 35 - docker/linux/Dockerfile | 24 - docker/psp/Dockerfile | 7 - docker/vita/Dockerfile | 13 - editor/client/public/common/chunkterrain.js | 159 --- editor/client/public/common/dmf.js | 64 -- editor/client/public/index.html | 6 - editor/client/public/map/index.html | 92 -- editor/client/public/map/map.css | 65 -- editor/client/public/map/map.js | 920 ---------------- editor/client/public/map/renderer.js | 298 ------ editor/client/public/template.html | 23 - editor/server/server.py | 298 ------ editor/start.sh | 4 - scripts/build-gamecube-docker.sh | 3 - scripts/build-gamecube-iso-docker.sh | 3 - scripts/build-gamecube-iso.sh | 13 - scripts/build-gamecube.sh | 14 - scripts/build-knulli-docker.sh | 3 - scripts/build-knulli.sh | 24 - scripts/build-linux-docker.sh | 3 - scripts/build-linux.sh | 3 - scripts/build-psp-docker.sh | 3 - scripts/build-psp.sh | 12 - scripts/build-vita-docker.sh | 3 - scripts/build-vita.sh | 13 - scripts/build-wii-docker.sh | 3 - scripts/build-wii-iso-docker.sh | 3 - scripts/build-wii-iso.sh | 13 - scripts/build-wii.sh | 14 - scripts/test-linux-docker.sh | 7 - scripts/test-linux.sh | 6 - src/CMakeLists.txt | 35 - src/dusk/CMakeLists.txt | 91 -- src/dusk/animation/CMakeLists.txt | 11 - src/dusk/animation/animation.c | 132 --- src/dusk/animation/animation.h | 94 -- src/dusk/animation/easing.c | 111 -- src/dusk/animation/easing.h | 63 -- src/dusk/animation/keyframe.c | 45 - src/dusk/animation/keyframe.h | 27 - src/dusk/assert/CMakeLists.txt | 10 - src/dusk/assert/assert.c | 170 --- src/dusk/assert/assert.h | 283 ----- src/dusk/asset/CMakeLists.txt | 15 - src/dusk/asset/asset.c | 455 -------- src/dusk/asset/asset.h | 193 ---- src/dusk/asset/assetdsk.c | 165 --- src/dusk/asset/assetdsk.h | 120 --- src/dusk/asset/assetfile.c | 436 -------- src/dusk/asset/assetfile.h | 219 ---- src/dusk/asset/loader/CMakeLists.txt | 21 - src/dusk/asset/loader/assetentry.c | 88 -- src/dusk/asset/loader/assetentry.h | 114 -- src/dusk/asset/loader/assetloader.c | 66 -- src/dusk/asset/loader/assetloader.h | 109 -- src/dusk/asset/loader/assetloading.h | 26 - src/dusk/asset/loader/chunk/CMakeLists.txt | 10 - .../asset/loader/chunk/assetchunkloader.c | 484 --------- .../asset/loader/chunk/assetchunkloader.h | 120 --- src/dusk/asset/loader/display/CMakeLists.txt | 11 - .../asset/loader/display/assettextureloader.c | 171 --- .../asset/loader/display/assettextureloader.h | 81 -- .../asset/loader/display/assettilesetloader.c | 124 --- .../asset/loader/display/assettilesetloader.h | 60 -- src/dusk/asset/loader/dmf/CMakeLists.txt | 10 - src/dusk/asset/loader/dmf/assetmeshloader.c | 157 --- src/dusk/asset/loader/dmf/assetmeshloader.h | 63 -- src/dusk/asset/loader/dmf/assetmodelloader.c | 172 --- src/dusk/asset/loader/dmf/assetmodelloader.h | 58 - src/dusk/asset/loader/json/CMakeLists.txt | 10 - src/dusk/asset/loader/json/assetjsonloader.c | 97 -- src/dusk/asset/loader/json/assetjsonloader.h | 46 - src/dusk/asset/loader/locale/CMakeLists.txt | 10 - .../asset/loader/locale/assetlocaleloader.c | 996 ------------------ .../asset/loader/locale/assetlocaleloader.h | 425 -------- src/dusk/asset/loader/mp3/CMakeLists.txt | 9 - src/dusk/asset/loader/mp3/assetmp3loader.c | 255 ----- src/dusk/asset/loader/mp3/assetmp3loader.h | 113 -- src/dusk/asset/loader/wav/CMakeLists.txt | 10 - src/dusk/asset/loader/wav/assetwavloader.c | 181 ---- src/dusk/asset/loader/wav/assetwavloader.h | 117 -- src/dusk/audio/CMakeLists.txt | 14 - src/dusk/audio/audio.c | 73 -- src/dusk/audio/audio.h | 77 -- src/dusk/audio/mixer/CMakeLists.txt | 11 - src/dusk/audio/mixer/audiomixer.c | 199 ---- src/dusk/audio/mixer/audiomixer.h | 204 ---- src/dusk/audio/mixer/audiomixerchannel.c | 223 ---- src/dusk/audio/mixer/audiomixerchannel.h | 241 ----- src/dusk/audio/stream/CMakeLists.txt | 13 - src/dusk/audio/stream/audiostream.c | 312 ------ src/dusk/audio/stream/audiostream.h | 360 ------- src/dusk/audio/stream/audiostreammp3.c | 167 --- src/dusk/audio/stream/audiostreammp3.h | 140 --- src/dusk/audio/stream/audiostreampcm.c | 179 ---- src/dusk/audio/stream/audiostreampcm.h | 96 -- src/dusk/audio/stream/audiostreamtype.c | 50 - src/dusk/audio/stream/audiostreamtype.h | 71 -- src/dusk/console/CMakeLists.txt | 9 - src/dusk/console/console.c | 57 - src/dusk/console/console.h | 45 - src/dusk/display/CMakeLists.txt | 28 - src/dusk/display/color.csv | 23 - src/dusk/display/display.c | 120 --- src/dusk/display/display.h | 73 -- src/dusk/display/displaystate.h | 17 - src/dusk/display/framebuffer/CMakeLists.txt | 10 - src/dusk/display/framebuffer/framebuffer.c | 77 -- src/dusk/display/framebuffer/framebuffer.h | 118 --- src/dusk/display/mesh/CMakeLists.txt | 16 - src/dusk/display/mesh/capsule.c | 174 --- src/dusk/display/mesh/capsule.h | 48 - src/dusk/display/mesh/cube.c | 95 -- src/dusk/display/mesh/cube.h | 39 - src/dusk/display/mesh/mesh.c | 116 -- src/dusk/display/mesh/mesh.h | 100 -- src/dusk/display/mesh/meshvertex.h | 17 - src/dusk/display/mesh/plane.c | 99 -- src/dusk/display/mesh/plane.h | 57 - src/dusk/display/mesh/quad.c | 100 -- src/dusk/display/mesh/quad.h | 65 -- src/dusk/display/mesh/sphere.c | 107 -- src/dusk/display/mesh/sphere.h | 43 - src/dusk/display/mesh/triprism.c | 88 -- src/dusk/display/mesh/triprism.h | 46 - src/dusk/display/screen/CMakeLists.txt | 10 - src/dusk/display/screen/screen.c | 465 -------- src/dusk/display/screen/screen.h | 159 --- src/dusk/display/shader/CMakeLists.txt | 12 - src/dusk/display/shader/shader.c | 83 -- src/dusk/display/shader/shader.h | 108 -- src/dusk/display/shader/shaderlist.c | 87 -- src/dusk/display/shader/shaderlist.h | 40 - src/dusk/display/shader/shadermaterial.h | 13 - src/dusk/display/shader/shaderunlit.c | 32 - src/dusk/display/shader/shaderunlit.h | 35 - src/dusk/display/spritebatch/CMakeLists.txt | 11 - src/dusk/display/spritebatch/spritebatch.c | 180 ---- src/dusk/display/spritebatch/spritebatch.h | 101 -- .../display/spritebatch/spritebatchsprite.c | 41 - .../display/spritebatch/spritebatchsprite.h | 40 - src/dusk/display/text/CMakeLists.txt | 11 - src/dusk/display/text/font.c | 205 ---- src/dusk/display/text/font.h | 99 -- src/dusk/display/text/text.c | 223 ---- src/dusk/display/text/text.h | 120 --- src/dusk/display/texture/CMakeLists.txt | 12 - src/dusk/display/texture/palette.c | 10 - src/dusk/display/texture/palette.h | 19 - src/dusk/display/texture/texture.c | 68 -- src/dusk/display/texture/texture.h | 61 -- src/dusk/display/texture/tileset.c | 36 - src/dusk/display/texture/tileset.h | 46 - src/dusk/dusk.h | 32 - src/dusk/engine/CMakeLists.txt | 10 - src/dusk/engine/engine.c | 114 -- src/dusk/engine/engine.h | 40 - src/dusk/error/CMakeLists.txt | 10 - src/dusk/error/error.c | 138 --- src/dusk/error/error.h | 153 --- src/dusk/input/CMakeLists.txt | 21 - src/dusk/input/input.c | 256 ----- src/dusk/input/input.csv | 12 - src/dusk/input/input.h | 242 ----- src/dusk/input/inputaction.c | 22 - src/dusk/input/inputaction.h | 31 - src/dusk/input/inputbutton.c | 42 - src/dusk/input/inputbutton.h | 107 -- src/dusk/locale/CMakeLists.txt | 10 - src/dusk/locale/localeinfo.h | 43 - src/dusk/locale/localemanager.c | 55 - src/dusk/locale/localemanager.h | 113 -- src/dusk/log/CMakeLists.txt | 9 - src/dusk/log/log.h | 26 - src/dusk/main.c | 39 - src/dusk/null.c | 11 - src/dusk/perf/CMakeLists.txt | 10 - src/dusk/perf/perf.c | 88 -- src/dusk/perf/perf.h | 89 -- src/dusk/perf/perfitem.h | 16 - src/dusk/rpg/CMakeLists.txt | 20 - src/dusk/rpg/battle/CMakeLists.txt | 15 - src/dusk/rpg/battle/battle.c | 79 -- src/dusk/rpg/battle/battle.h | 75 -- src/dusk/rpg/battle/fighter/CMakeLists.txt | 10 - src/dusk/rpg/battle/fighter/battlefighter.c | 230 ---- src/dusk/rpg/battle/fighter/battlefighter.h | 127 --- .../battle/fighter/battlefightercontroller.h | 18 - .../rpg/battle/fighter/battlefighterpos.h | 35 - .../rpg/battle/fighter/battlefighterspecies.h | 22 - .../rpg/battle/fighter/battlefighterstats.h | 22 - .../rpg/battle/fighter/battlefighterstatus.h | 20 - .../rpg/battle/fighter/battlefighterteam.h | 16 - src/dusk/rpg/battle/move/CMakeLists.txt | 10 - src/dusk/rpg/battle/move/battlemove.c | 139 --- src/dusk/rpg/battle/move/battlemove.h | 42 - src/dusk/rpg/battle/move/battlemoveaction.h | 18 - src/dusk/rpg/battle/move/battletarget.h | 16 - src/dusk/rpg/battle/party.c | 188 ---- src/dusk/rpg/battle/party.h | 126 --- src/dusk/rpg/battle/state/CMakeLists.txt | 19 - src/dusk/rpg/battle/state/battlestate.c | 66 -- src/dusk/rpg/battle/state/battlestate.h | 64 -- src/dusk/rpg/battle/state/battlestateending.c | 28 - src/dusk/rpg/battle/state/battlestateending.h | 29 - .../rpg/battle/state/battlestateexecuting.c | 116 -- .../rpg/battle/state/battlestateexecuting.h | 50 - src/dusk/rpg/battle/state/battlestatefled.c | 32 - src/dusk/rpg/battle/state/battlestatefled.h | 30 - src/dusk/rpg/battle/state/battlestatelost.c | 32 - src/dusk/rpg/battle/state/battlestatelost.h | 30 - .../rpg/battle/state/battlestateopening.c | 28 - .../rpg/battle/state/battlestateopening.h | 28 - .../rpg/battle/state/battlestatepostround.c | 20 - .../rpg/battle/state/battlestatepostround.h | 28 - .../rpg/battle/state/battlestatepreround.c | 41 - .../rpg/battle/state/battlestatepreround.h | 28 - .../rpg/battle/state/battlestateselection.c | 146 --- .../rpg/battle/state/battlestateselection.h | 120 --- src/dusk/rpg/battle/state/battlestatewon.c | 32 - src/dusk/rpg/battle/state/battlestatewon.h | 30 - src/dusk/rpg/cutscene/CMakeLists.txt | 15 - src/dusk/rpg/cutscene/cutscene.c | 178 ---- src/dusk/rpg/cutscene/cutscene.h | 154 --- src/dusk/rpg/cutscene/cutscenepause.h | 26 - src/dusk/rpg/cutscene/cutsceneref.c | 24 - src/dusk/rpg/cutscene/cutsceneref.h | 36 - src/dusk/rpg/cutscene/cutscenesystem.c | 358 ------- src/dusk/rpg/cutscene/cutscenesystem.h | 241 ----- src/dusk/rpg/cutscene/item/CMakeLists.txt | 24 - .../rpg/cutscene/item/audio/CMakeLists.txt | 17 - .../cutscene/item/audio/cutsceneaudiofade.c | 59 -- .../cutscene/item/audio/cutsceneaudiofade.h | 56 - .../item/audio/cutsceneaudiofadewait.c | 24 - .../item/audio/cutsceneaudiofadewait.h | 36 - .../cutscene/item/audio/cutsceneaudiopause.c | 31 - .../cutscene/item/audio/cutsceneaudiopause.h | 45 - .../cutscene/item/audio/cutsceneaudioplay.c | 62 -- .../cutscene/item/audio/cutsceneaudioplay.h | 58 - .../cutscene/item/audio/cutsceneaudioresume.c | 31 - .../cutscene/item/audio/cutsceneaudioresume.h | 45 - .../cutscene/item/audio/cutsceneaudioset.c | 54 - .../cutscene/item/audio/cutsceneaudioset.h | 57 - .../item/audio/cutsceneaudiosetloop.c | 52 - .../item/audio/cutsceneaudiosetloop.h | 54 - .../cutscene/item/audio/cutsceneaudiosetpan.c | 41 - .../cutscene/item/audio/cutsceneaudiosetpan.h | 51 - .../cutscene/item/audio/cutsceneaudiostop.c | 31 - .../cutscene/item/audio/cutsceneaudiostop.h | 45 - .../rpg/cutscene/item/battle/CMakeLists.txt | 14 - .../item/battle/cutscenebattlefighterdamage.c | 68 -- .../item/battle/cutscenebattlefighterdamage.h | 55 - .../item/battle/cutscenebattlefighterheal.c | 68 -- .../item/battle/cutscenebattlefighterheal.h | 55 - .../item/battle/cutscenebattlepostmove.c | 60 -- .../item/battle/cutscenebattlepostmove.h | 37 - .../item/battle/cutscenebattlesetstate.c | 41 - .../item/battle/cutscenebattlesetstate.h | 50 - .../item/battle/cutscenebattlestart.c | 95 -- .../item/battle/cutscenebattlestart.h | 64 -- .../item/battle/cutscenebattlewaitselection.c | 29 - .../item/battle/cutscenebattlewaitselection.h | 27 - .../rpg/cutscene/item/control/CMakeLists.txt | 16 - .../item/control/cutsceneconcurrent.c | 46 - .../item/control/cutsceneconcurrent.h | 60 -- .../rpg/cutscene/item/control/cutsceneidle.c | 15 - .../rpg/cutscene/item/control/cutsceneidle.h | 26 - .../cutscene/item/control/cutscenemarker.c | 35 - .../cutscene/item/control/cutscenemarker.h | 54 - .../cutscene/item/control/cutscenequitgame.c | 23 - .../cutscene/item/control/cutscenequitgame.h | 38 - .../cutscene/item/control/cutscenerestart.c | 23 - .../cutscene/item/control/cutscenerestart.h | 38 - .../rpg/cutscene/item/control/cutscenescene.c | 39 - .../rpg/cutscene/item/control/cutscenescene.h | 53 - .../cutscene/item/control/cutscenesetpause.c | 41 - .../cutscene/item/control/cutscenesetpause.h | 45 - .../rpg/cutscene/item/control/cutscenewait.c | 34 - .../rpg/cutscene/item/control/cutscenewait.h | 47 - src/dusk/rpg/cutscene/item/cutscenecallback.c | 23 - src/dusk/rpg/cutscene/item/cutscenecallback.h | 34 - src/dusk/rpg/cutscene/item/cutsceneitem.c | 562 ---------- src/dusk/rpg/cutscene/item/cutsceneitem.h | 367 ------- src/dusk/rpg/cutscene/item/cutsceneitembase.c | 15 - src/dusk/rpg/cutscene/item/cutsceneitembase.h | 110 -- src/dusk/rpg/cutscene/item/cutsceneprint.c | 37 - src/dusk/rpg/cutscene/item/cutsceneprint.h | 50 - .../rpg/cutscene/item/entity/CMakeLists.txt | 14 - .../cutscene/item/entity/cutsceneentityadd.c | 57 - .../cutscene/item/entity/cutsceneentityadd.h | 51 - .../item/entity/cutsceneentityremove.c | 41 - .../item/entity/cutsceneentityremove.h | 48 - .../item/entity/cutsceneentityteleport.c | 50 - .../item/entity/cutsceneentityteleport.h | 50 - .../cutscene/item/entity/cutsceneentityturn.c | 52 - .../cutscene/item/entity/cutsceneentityturn.h | 52 - .../item/entity/cutsceneentitywalkto.c | 85 -- .../item/entity/cutsceneentitywalkto.h | 63 -- .../item/entity/cutsceneentitywalktoentity.c | 65 -- .../item/entity/cutsceneentitywalktoentity.h | 56 - .../rpg/cutscene/item/item/CMakeLists.txt | 9 - .../rpg/cutscene/item/item/cutsceneitemgive.c | 36 - .../rpg/cutscene/item/item/cutsceneitemgive.h | 50 - .../rpg/cutscene/item/json/CMakeLists.txt | 22 - .../cutscene/item/json/cutscenejsonareaid.c | 23 - .../cutscene/item/json/cutscenejsonareaid.h | 25 - .../item/json/cutscenejsonaudiochannel.c | 49 - .../item/json/cutscenejsonaudiochannel.h | 40 - .../item/json/cutscenejsonbattlestate.c | 38 - .../item/json/cutscenejsonbattlestate.h | 21 - .../cutscene/item/json/cutscenejsoneasing.c | 42 - .../cutscene/item/json/cutscenejsoneasing.h | 19 - .../cutscene/item/json/cutscenejsonemoji.c | 29 - .../cutscene/item/json/cutscenejsonemoji.h | 19 - .../item/json/cutscenejsonentitydir.c | 34 - .../item/json/cutscenejsonentitydir.h | 19 - .../item/json/cutscenejsonentityindex.c | 27 - .../item/json/cutscenejsonentityindex.h | 27 - .../item/json/cutscenejsonentitytype.c | 30 - .../item/json/cutscenejsonentitytype.h | 19 - .../rpg/cutscene/item/json/cutscenejsonenum.c | 41 - .../rpg/cutscene/item/json/cutscenejsonenum.h | 51 - .../cutscene/item/json/cutscenejsonfield.c | 70 -- .../cutscene/item/json/cutscenejsonfield.h | 88 -- .../item/json/cutscenejsonpauseflags.c | 38 - .../item/json/cutscenejsonpauseflags.h | 24 - .../item/json/cutscenejsonscenetype.c | 30 - .../item/json/cutscenejsonscenetype.h | 19 - .../item/json/cutscenejsonstoryflag.c | 18 - .../item/json/cutscenejsonstoryflag.h | 21 - .../item/json/cutscenejsonuiscreentype.c | 31 - .../item/json/cutscenejsonuiscreentype.h | 22 - .../rpg/cutscene/item/maparea/CMakeLists.txt | 11 - .../item/maparea/cutscenemapareaadd.c | 29 - .../item/maparea/cutscenemapareaadd.h | 44 - .../item/maparea/cutscenemaparearemove.c | 38 - .../item/maparea/cutscenemaparearemove.h | 49 - .../item/maparea/cutscenemapareawait.c | 72 -- .../item/maparea/cutscenemapareawait.h | 59 -- .../rpg/cutscene/item/party/CMakeLists.txt | 12 - .../item/party/cutscenepartyaddmember.c | 42 - .../item/party/cutscenepartyaddmember.h | 54 - .../item/party/cutscenepartyhealall.c | 23 - .../item/party/cutscenepartyhealall.h | 33 - .../item/party/cutscenepartyhealmember.c | 42 - .../item/party/cutscenepartyhealmember.h | 53 - .../item/party/cutscenepartyremovemember.c | 44 - .../item/party/cutscenepartyremovemember.h | 54 - .../rpg/cutscene/item/save/CMakeLists.txt | 10 - .../item/save/cutscenesavedevicecheck.c | 57 - .../item/save/cutscenesavedevicecheck.h | 55 - .../item/save/cutscenesaveloadallslots.c | 55 - .../item/save/cutscenesaveloadallslots.h | 54 - .../rpg/cutscene/item/story/CMakeLists.txt | 10 - .../item/story/cutscenestorysetflag.c | 39 - .../item/story/cutscenestorysetflag.h | 51 - .../item/story/cutscenestorywaitflag.c | 32 - .../item/story/cutscenestorywaitflag.h | 40 - src/dusk/rpg/cutscene/item/ui/CMakeLists.txt | 18 - src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c | 49 - src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h | 52 - src/dusk/rpg/cutscene/item/ui/cutscenefade.c | 66 -- src/dusk/rpg/cutscene/item/ui/cutscenefade.h | 53 - .../rpg/cutscene/item/ui/cutscenekeyboard.c | 27 - .../rpg/cutscene/item/ui/cutscenekeyboard.h | 44 - src/dusk/rpg/cutscene/item/ui/cutscenemodal.c | 79 -- src/dusk/rpg/cutscene/item/ui/cutscenemodal.h | 129 --- .../item/ui/cutscenemodaloptionsmarkers.c | 112 -- .../item/ui/cutscenemodaloptionsmarkers.h | 71 -- src/dusk/rpg/cutscene/item/ui/cutsceneshake.c | 34 - src/dusk/rpg/cutscene/item/ui/cutsceneshake.h | 50 - src/dusk/rpg/cutscene/item/ui/cutscenetext.c | 37 - src/dusk/rpg/cutscene/item/ui/cutscenetext.h | 50 - .../rpg/cutscene/item/ui/cutscenetextmini.c | 58 - .../rpg/cutscene/item/ui/cutscenetextmini.h | 53 - .../cutscene/item/ui/cutscenetextminihide.c | 35 - .../cutscene/item/ui/cutscenetextminihide.h | 48 - .../rpg/cutscene/item/ui/cutsceneuishow.c | 43 - .../rpg/cutscene/item/ui/cutsceneuishow.h | 57 - src/dusk/rpg/entity/CMakeLists.txt | 19 - src/dusk/rpg/entity/anim/CMakeLists.txt | 13 - src/dusk/rpg/entity/anim/entityanim.c | 44 - src/dusk/rpg/entity/anim/entityanim.h | 46 - src/dusk/rpg/entity/anim/entityanimidle.c | 17 - src/dusk/rpg/entity/anim/entityanimidle.h | 18 - src/dusk/rpg/entity/anim/entityanimrun.c | 24 - src/dusk/rpg/entity/anim/entityanimrun.h | 21 - src/dusk/rpg/entity/anim/entityanimturn.c | 17 - src/dusk/rpg/entity/anim/entityanimturn.h | 21 - src/dusk/rpg/entity/anim/entityanimwalk.c | 24 - src/dusk/rpg/entity/anim/entityanimwalk.h | 22 - src/dusk/rpg/entity/entity.c | 333 ------ src/dusk/rpg/entity/entity.h | 215 ---- src/dusk/rpg/entity/entitydir.c | 51 - src/dusk/rpg/entity/entitydir.h | 54 - src/dusk/rpg/entity/entitypathstep.c | 59 -- src/dusk/rpg/entity/entitypathstep.h | 26 - src/dusk/rpg/entity/entitytype.h | 62 -- src/dusk/rpg/entity/global/CMakeLists.txt | 9 - src/dusk/rpg/entity/global/entityglobal.h | 17 - src/dusk/rpg/entity/global/entityglobaldefs.h | 44 - src/dusk/rpg/entity/global/entitygloballist.h | 33 - src/dusk/rpg/entity/interact/CMakeLists.txt | 9 - src/dusk/rpg/entity/interact/entityinteract.c | 67 -- src/dusk/rpg/entity/interact/entityinteract.h | 66 -- src/dusk/rpg/entity/item/CMakeLists.txt | 10 - src/dusk/rpg/entity/item/entityitem.c | 45 - src/dusk/rpg/entity/item/entityitem.h | 56 - src/dusk/rpg/entity/npc/CMakeLists.txt | 13 - src/dusk/rpg/entity/npc/npc.c | 58 - src/dusk/rpg/entity/npc/npc.h | 81 -- src/dusk/rpg/entity/npc/npcpath.c | 43 - src/dusk/rpg/entity/npc/npcpath.h | 44 - src/dusk/rpg/entity/npc/npcturn.c | 27 - src/dusk/rpg/entity/npc/npcturn.h | 36 - src/dusk/rpg/entity/npc/npcwalk.c | 52 - src/dusk/rpg/entity/npc/npcwalk.h | 54 - src/dusk/rpg/entity/player.c | 101 -- src/dusk/rpg/entity/player.h | 52 - src/dusk/rpg/item/CMakeLists.txt | 13 - src/dusk/rpg/item/backpack.c | 67 -- src/dusk/rpg/item/backpack.h | 97 -- src/dusk/rpg/item/inventory.c | 250 ----- src/dusk/rpg/item/inventory.h | 123 --- src/dusk/rpg/item/item.c | 98 -- src/dusk/rpg/item/item.h | 91 -- src/dusk/rpg/item/itemgive.c | 32 - src/dusk/rpg/item/itemgive.h | 20 - src/dusk/rpg/overworld/CMakeLists.txt | 18 - src/dusk/rpg/overworld/chunk.c | 16 - src/dusk/rpg/overworld/chunk.h | 57 - src/dusk/rpg/overworld/global/CMakeLists.txt | 9 - src/dusk/rpg/overworld/global/mapareaglobal.h | 17 - .../rpg/overworld/global/mapareaglobaldefs.h | 17 - .../rpg/overworld/global/mapareagloballist.h | 22 - src/dusk/rpg/overworld/map.c | 579 ---------- src/dusk/rpg/overworld/map.h | 214 ---- src/dusk/rpg/overworld/maparea.c | 168 --- src/dusk/rpg/overworld/maparea.h | 187 ---- src/dusk/rpg/overworld/tile.c | 8 - src/dusk/rpg/overworld/tile.h | 20 - src/dusk/rpg/overworld/tileshape.c | 39 - src/dusk/rpg/overworld/tileshape.h | 45 - src/dusk/rpg/overworld/worldpos.c | 121 --- src/dusk/rpg/overworld/worldpos.h | 132 --- src/dusk/rpg/rpg.c | 93 -- src/dusk/rpg/rpg.h | 34 - src/dusk/rpg/rpgcamera.c | 134 --- src/dusk/rpg/rpgcamera.h | 94 -- src/dusk/rpg/story/CMakeLists.txt | 10 - src/dusk/rpg/story/story.c | 87 -- src/dusk/rpg/story/story.h | 76 -- src/dusk/save/CMakeLists.txt | 15 - src/dusk/save/save.c | 278 ----- src/dusk/save/save.h | 120 --- src/dusk/save/savedevice.c | 546 ---------- src/dusk/save/savedevice.h | 345 ------ src/dusk/save/savejson.h | 600 ----------- src/dusk/save/saverror.h | 16 - src/dusk/save/settings/CMakeLists.txt | 12 - src/dusk/save/settings/savesettings.c | 38 - src/dusk/save/settings/savesettings.h | 58 - src/dusk/save/settings/savesettingscurrent.c | 66 -- src/dusk/save/settings/savesettingscurrent.h | 40 - src/dusk/save/settings/savesettingsver.h | 15 - src/dusk/save/settings/savesettingsver1.c | 47 - src/dusk/save/settings/savesettingsver1.h | 55 - src/dusk/save/slot/CMakeLists.txt | 12 - src/dusk/save/slot/saveslot.c | 92 -- src/dusk/save/slot/saveslot.h | 117 -- src/dusk/save/slot/saveslotcurrent.c | 79 -- src/dusk/save/slot/saveslotcurrent.h | 37 - src/dusk/save/slot/saveslotver.h | 17 - src/dusk/save/slot/saveslotver1.c | 105 -- src/dusk/save/slot/saveslotver1.h | 86 -- src/dusk/scene/CMakeLists.txt | 15 - src/dusk/scene/battle/CMakeLists.txt | 9 - src/dusk/scene/battle/scenebattle.c | 163 --- src/dusk/scene/battle/scenebattle.h | 110 -- src/dusk/scene/initial/CMakeLists.txt | 9 - src/dusk/scene/initial/sceneinitial.c | 39 - src/dusk/scene/initial/sceneinitial.h | 50 - src/dusk/scene/overworld/CMakeLists.txt | 9 - src/dusk/scene/overworld/sceneoverworld.c | 264 ----- src/dusk/scene/overworld/sceneoverworld.h | 149 --- src/dusk/scene/scene.c | 120 --- src/dusk/scene/scene.h | 57 - src/dusk/scene/scenebase.h | 11 - src/dusk/scene/scenetype.c | 34 - src/dusk/scene/scenetype.h | 39 - src/dusk/system/CMakeLists.txt | 9 - src/dusk/system/system.c | 47 - src/dusk/system/system.h | 58 - src/dusk/system/systemplatformlist.h | 23 - src/dusk/thread/CMakeLists.txt | 11 - src/dusk/thread/thread.c | 117 -- src/dusk/thread/thread.h | 97 -- src/dusk/thread/threadlocal.h | 17 - src/dusk/thread/threadmutex.c | 52 - src/dusk/thread/threadmutex.h | 73 -- src/dusk/time/CMakeLists.txt | 11 - src/dusk/time/time.c | 61 -- src/dusk/time/time.h | 72 -- src/dusk/time/timeepoch.c | 198 ---- src/dusk/time/timeepoch.h | 145 --- src/dusk/ui/CMakeLists.txt | 22 - src/dusk/ui/debug/CMakeLists.txt | 12 - src/dusk/ui/debug/uiconsole.c | 41 - src/dusk/ui/debug/uiconsole.h | 33 - src/dusk/ui/debug/uicutscene.c | 73 -- src/dusk/ui/debug/uicutscene.h | 42 - src/dusk/ui/debug/uifps.c | 86 -- src/dusk/ui/debug/uifps.h | 45 - src/dusk/ui/debug/uiplayerpos.c | 64 -- src/dusk/ui/debug/uiplayerpos.h | 37 - src/dusk/ui/dialog/CMakeLists.txt | 12 - src/dusk/ui/dialog/keyboard/CMakeLists.txt | 9 - src/dusk/ui/dialog/keyboard/uikeyboard.c | 610 ----------- src/dusk/ui/dialog/keyboard/uikeyboard.h | 490 --------- src/dusk/ui/dialog/save/CMakeLists.txt | 10 - src/dusk/ui/dialog/save/uisaveslot.c | 119 --- src/dusk/ui/dialog/save/uisaveslot.h | 132 --- src/dusk/ui/dialog/save/uiselectsave.c | 416 -------- src/dusk/ui/dialog/save/uiselectsave.h | 134 --- src/dusk/ui/dialog/uiconfirm.c | 64 -- src/dusk/ui/dialog/uiconfirm.h | 77 -- src/dusk/ui/focus/CMakeLists.txt | 9 - src/dusk/ui/focus/uifocus.c | 204 ---- src/dusk/ui/focus/uifocus.h | 133 --- src/dusk/ui/focus/uifocusitem.h | 71 -- src/dusk/ui/overlay/CMakeLists.txt | 12 - src/dusk/ui/overlay/uicrop.c | 89 -- src/dusk/ui/overlay/uicrop.h | 32 - src/dusk/ui/overlay/uifatalerror.c | 115 -- src/dusk/ui/overlay/uifatalerror.h | 80 -- src/dusk/ui/overlay/uifullbox.c | 118 --- src/dusk/ui/overlay/uifullbox.h | 126 --- src/dusk/ui/overlay/uiloading.c | 105 -- src/dusk/ui/overlay/uiloading.h | 89 -- src/dusk/ui/rpg/CMakeLists.txt | 12 - src/dusk/ui/rpg/textbox/CMakeLists.txt | 12 - src/dusk/ui/rpg/textbox/uitextbox.c | 244 ----- src/dusk/ui/rpg/textbox/uitextbox.h | 138 --- src/dusk/ui/rpg/textbox/uitextboxmain.c | 122 --- src/dusk/ui/rpg/textbox/uitextboxmain.h | 94 -- src/dusk/ui/rpg/textbox/uitextboxmini.c | 122 --- src/dusk/ui/rpg/textbox/uitextboxmini.h | 116 -- src/dusk/ui/rpg/textbox/uitextboxminilist.c | 40 - src/dusk/ui/rpg/textbox/uitextboxminilist.h | 44 - src/dusk/ui/rpg/uiemoji.c | 119 --- src/dusk/ui/rpg/uiemoji.h | 66 -- src/dusk/ui/screen/CMakeLists.txt | 9 - src/dusk/ui/screen/backpack/CMakeLists.txt | 9 - src/dusk/ui/screen/backpack/uibackpack.c | 126 --- src/dusk/ui/screen/backpack/uibackpack.h | 67 -- src/dusk/ui/screen/battle/CMakeLists.txt | 10 - src/dusk/ui/screen/battle/uibattlehud.c | 88 -- src/dusk/ui/screen/battle/uibattlehud.h | 33 - src/dusk/ui/screen/battle/uibattlemenu.c | 178 ---- src/dusk/ui/screen/battle/uibattlemenu.h | 63 -- src/dusk/ui/screen/game/CMakeLists.txt | 9 - src/dusk/ui/screen/game/uigamemenu.c | 152 --- src/dusk/ui/screen/game/uigamemenu.h | 71 -- src/dusk/ui/screen/mainmenu/CMakeLists.txt | 9 - src/dusk/ui/screen/mainmenu/uimainmenu.c | 200 ---- src/dusk/ui/screen/mainmenu/uimainmenu.h | 97 -- src/dusk/ui/transition/CMakeLists.txt | 10 - src/dusk/ui/transition/uitransition.c | 89 -- src/dusk/ui/transition/uitransition.h | 74 -- src/dusk/ui/transition/uitransitiondata.h | 23 - src/dusk/ui/transition/uitransitionfade.c | 56 - src/dusk/ui/transition/uitransitionfade.h | 29 - src/dusk/ui/ui.c | 76 -- src/dusk/ui/ui.h | 49 - src/dusk/ui/uielement.c | 53 - src/dusk/ui/uielement.h | 68 -- src/dusk/ui/uielementlist.c | 166 --- src/dusk/ui/uielementlist.h | 11 - src/dusk/ui/widget/CMakeLists.txt | 20 - src/dusk/ui/widget/uibutton.c | 58 - src/dusk/ui/widget/uibutton.h | 79 -- src/dusk/ui/widget/uicheckbox.c | 59 -- src/dusk/ui/widget/uicheckbox.h | 79 -- src/dusk/ui/widget/uidropdown.c | 100 -- src/dusk/ui/widget/uidropdown.h | 113 -- src/dusk/ui/widget/uiframe.c | 121 --- src/dusk/ui/widget/uiframe.h | 64 -- src/dusk/ui/widget/uiitem.c | 77 -- src/dusk/ui/widget/uiitem.h | 89 -- src/dusk/ui/widget/uiitemlist.c | 179 ---- src/dusk/ui/widget/uiitemlist.h | 200 ---- src/dusk/ui/widget/uilabel.c | 121 --- src/dusk/ui/widget/uilabel.h | 130 --- src/dusk/ui/widget/uimenu.c | 444 -------- src/dusk/ui/widget/uimenu.h | 386 ------- src/dusk/ui/widget/uimodal.c | 264 ----- src/dusk/ui/widget/uimodal.h | 176 ---- src/dusk/ui/widget/uiscrolling.c | 34 - src/dusk/ui/widget/uiscrolling.h | 37 - src/dusk/ui/widget/uislider.c | 244 ----- src/dusk/ui/widget/uislider.h | 185 ---- src/dusk/ui/widget/uitab.c | 62 -- src/dusk/ui/widget/uitab.h | 53 - src/dusk/util/CMakeLists.txt | 18 - src/dusk/util/array.c | 39 - src/dusk/util/array.h | 22 - src/dusk/util/crypt.c | 36 - src/dusk/util/crypt.h | 50 - src/dusk/util/endian.c | 100 -- src/dusk/util/endian.h | 53 - src/dusk/util/math.c | 31 - src/dusk/util/math.h | 84 -- src/dusk/util/memory.c | 255 ----- src/dusk/util/memory.h | 191 ---- src/dusk/util/random.c | 17 - src/dusk/util/random.h | 27 - src/dusk/util/ref.c | 52 - src/dusk/util/ref.h | 55 - src/dusk/util/sort.c | 71 -- src/dusk/util/sort.h | 65 -- src/dusk/util/string.c | 248 ----- src/dusk/util/string.h | 181 ---- src/duskdolphin/CMakeLists.txt | 28 - src/duskdolphin/asset/CMakeLists.txt | 16 - src/duskdolphin/asset/assetdolphindvd.c | 127 --- src/duskdolphin/asset/assetdolphindvd.h | 70 -- src/duskdolphin/asset/assetdolphinfat.c | 65 -- src/duskdolphin/asset/assetdolphinfat.h | 56 - src/duskdolphin/asset/assetplatform.h | 21 - src/duskdolphin/audio/CMakeLists.txt | 17 - src/duskdolphin/audio/audiodolphin.c | 28 - src/duskdolphin/audio/audiodolphin.h | 31 - src/duskdolphin/audio/audioplatform.h | 13 - src/duskdolphin/audio/audiostreamdolphin.c | 242 ----- src/duskdolphin/audio/audiostreamdolphin.h | 145 --- src/duskdolphin/audio/audiostreammp3decoder.h | 19 - src/duskdolphin/audio/audiostreamplatform.h | 16 - src/duskdolphin/display/CMakeLists.txt | 17 - src/duskdolphin/display/displaydolphin.c | 138 --- src/duskdolphin/display/displaydolphin.h | 39 - src/duskdolphin/display/displayplatform.h | 16 - .../display/framebuffer/CMakeLists.txt | 11 - .../display/framebuffer/framebufferdolphin.c | 68 -- .../display/framebuffer/framebufferdolphin.h | 64 -- .../display/framebuffer/framebufferplatform.h | 18 - src/duskdolphin/display/mesh/CMakeLists.txt | 11 - src/duskdolphin/display/mesh/meshdolphin.c | 80 -- src/duskdolphin/display/mesh/meshdolphin.h | 68 -- src/duskdolphin/display/mesh/meshplatform.h | 16 - src/duskdolphin/display/shader/CMakeLists.txt | 11 - .../display/shader/shaderdolphin.c | 267 ----- .../display/shader/shaderdolphin.h | 134 --- .../display/shader/shaderplatform.h | 19 - .../display/shader/shaderunlitdolphin.c | 12 - .../display/texture/CMakeLists.txt | 11 - .../display/texture/texturedolphin.c | 116 -- .../display/texture/texturedolphin.h | 64 -- .../display/texture/textureplatform.h | 16 - src/duskdolphin/duskplatform.h | 22 - src/duskdolphin/input/CMakeLists.txt | 12 - src/duskdolphin/input/inputdolphin.c | 191 ---- src/duskdolphin/input/inputdolphin.h | 74 -- src/duskdolphin/input/inputplatform.h | 20 - src/duskdolphin/log/CMakeLists.txt | 11 - src/duskdolphin/log/log.c | 132 --- src/duskdolphin/log/logdolphin.h | 18 - src/duskdolphin/network/CMakeLists.txt | 9 - src/duskdolphin/network/networkdolphin.c | 117 -- src/duskdolphin/network/networkdolphin.h | 86 -- src/duskdolphin/network/networkplatform.h | 19 - src/duskdolphin/save/CMakeLists.txt | 26 - src/duskdolphin/save/savedevicedolphincard.c | 281 ----- src/duskdolphin/save/savedevicedolphincard.h | 160 --- src/duskdolphin/save/savedevicedolphinnand.c | 177 ---- src/duskdolphin/save/savedevicedolphinnand.h | 108 -- src/duskdolphin/save/savedevicedolphinsd.c | 147 --- src/duskdolphin/save/savedevicedolphinsd.h | 98 -- src/duskdolphin/save/savedeviceplatform.h | 64 -- .../script/module/moduleplatformdolphin.h | 13 - .../script/module/moduleplatformplatform.h | 11 - src/duskdolphin/system/CMakeLists.txt | 9 - src/duskdolphin/system/systemdolphin.c | 55 - src/duskdolphin/system/systemdolphin.h | 61 -- src/duskdolphin/system/systemplatform.h | 13 - src/duskdolphin/time/CMakeLists.txt | 9 - src/duskdolphin/time/timedolphin.c | 27 - src/duskdolphin/time/timedolphin.h | 23 - src/duskdolphin/time/timeplatform.h | 12 - src/duskgl/CMakeLists.txt | 14 - src/duskgl/assert/assertgl.h | 15 - src/duskgl/display/CMakeLists.txt | 16 - src/duskgl/display/displaygl.c | 26 - src/duskgl/display/displaygl.h | 16 - src/duskgl/display/framebuffer/CMakeLists.txt | 10 - .../display/framebuffer/framebuffergl.c | 149 --- .../display/framebuffer/framebuffergl.h | 78 -- .../display/framebuffer/framebufferplatform.h | 21 - src/duskgl/display/mesh/CMakeLists.txt | 10 - src/duskgl/display/mesh/meshgl.c | 165 --- src/duskgl/display/mesh/meshgl.h | 89 -- src/duskgl/display/mesh/meshplatform.h | 18 - src/duskgl/display/shader/CMakeLists.txt | 11 - src/duskgl/display/shader/shadergl.c | 416 -------- src/duskgl/display/shader/shadergl.h | 149 --- src/duskgl/display/shader/shaderplatform.h | 19 - src/duskgl/display/shader/shaderunlitgl.c | 193 ---- src/duskgl/display/texture/CMakeLists.txt | 10 - src/duskgl/display/texture/texturegl.c | 95 -- src/duskgl/display/texture/texturegl.h | 54 - src/duskgl/display/texture/textureplatform.h | 15 - src/duskgl/duskgl.h | 30 - src/duskgl/error/CMakeLists.txt | 10 - src/duskgl/error/errorgl.c | 17 - src/duskgl/error/errorgl.h | 16 - src/dusklinux/CMakeLists.txt | 23 - src/dusklinux/asset/CMakeLists.txt | 10 - src/dusklinux/asset/assetlinux.c | 104 -- src/dusklinux/asset/assetlinux.h | 39 - src/dusklinux/asset/assetplatform.h | 13 - src/dusklinux/audio/CMakeLists.txt | 23 - src/dusklinux/audio/audiolinux.c | 27 - src/dusklinux/audio/audiolinux.h | 30 - src/dusklinux/audio/audioplatform.h | 13 - src/dusklinux/audio/audiostreamlinux.c | 252 ----- src/dusklinux/audio/audiostreamlinux.h | 113 -- src/dusklinux/audio/audiostreammp3decoder.h | 15 - src/dusklinux/audio/audiostreamplatform.h | 16 - src/dusklinux/display/displayplatform.h | 17 - src/dusklinux/duskplatform.h | 9 - src/dusklinux/input/CMakeLists.txt | 10 - src/dusklinux/input/inputlinux.c | 550 ---------- src/dusklinux/input/inputlinux.h | 17 - src/dusklinux/input/inputplatform.h | 11 - src/dusklinux/log/CMakeLists.txt | 10 - src/dusklinux/log/log.c | 24 - src/dusklinux/network/CMakeLists.txt | 9 - src/dusklinux/network/networklinux.c | 120 --- src/dusklinux/network/networklinux.h | 54 - src/dusklinux/network/networkplatform.h | 17 - src/dusklinux/save/CMakeLists.txt | 9 - src/dusklinux/save/savedevicelinux.c | 241 ----- src/dusklinux/save/savedevicelinux.h | 148 --- src/dusklinux/save/savedeviceplatform.h | 20 - .../script/module/moduleplatformlinux.h | 13 - .../script/module/moduleplatformplatform.h | 11 - src/dusklinux/system/CMakeLists.txt | 9 - src/dusklinux/system/systemlinux.c | 20 - src/dusklinux/system/systemlinux.h | 29 - src/dusklinux/system/systemplatform.h | 13 - src/dusklinux/time/CMakeLists.txt | 9 - src/dusklinux/time/timelinux.c | 29 - src/dusklinux/time/timelinux.h | 23 - src/dusklinux/time/timeplatform.h | 15 - src/dusklinux/util/CMakeLists.txt | 9 - src/dusklinux/util/mkdirp.c | 38 - src/dusklinux/util/mkdirp.h | 20 - src/duskmad/CMakeLists.txt | 33 - src/duskmad/audiostreammp3decodersw.c | 190 ---- src/duskmad/audiostreammp3decodersw.h | 140 --- src/duskmad/audiostreammp3ring.c | 309 ------ src/duskmad/audiostreammp3ring.h | 289 ----- src/dusknetwork/CMakeLists.txt | 13 - src/dusknetwork/network/CMakeLists.txt | 10 - src/dusknetwork/network/network.c | 116 -- src/dusknetwork/network/network.h | 126 --- src/dusknetwork/network/networkinfo.c | 24 - src/dusknetwork/network/networkinfo.h | 51 - src/duskpsp/CMakeLists.txt | 27 - src/duskpsp/asset/CMakeLists.txt | 11 - src/duskpsp/asset/assetpbp.c | 91 -- src/duskpsp/asset/assetpbp.h | 47 - src/duskpsp/asset/assetplatform.h | 13 - src/duskpsp/asset/assetpsp.c | 40 - src/duskpsp/asset/assetpsp.h | 27 - src/duskpsp/audio/CMakeLists.txt | 12 - src/duskpsp/audio/audioplatform.h | 13 - src/duskpsp/audio/audiopsp.c | 23 - src/duskpsp/audio/audiopsp.h | 30 - src/duskpsp/audio/audiostreammp3decoder.c | 264 ----- src/duskpsp/audio/audiostreammp3decoder.h | 127 --- src/duskpsp/audio/audiostreamplatform.h | 16 - src/duskpsp/audio/audiostreampsp.c | 470 --------- src/duskpsp/audio/audiostreampsp.h | 232 ---- src/duskpsp/display/displayplatform.h | 17 - src/duskpsp/duskplatform.h | 14 - src/duskpsp/input/CMakeLists.txt | 10 - src/duskpsp/input/inputplatform.h | 11 - src/duskpsp/input/inputpsp.c | 98 -- src/duskpsp/input/inputpsp.h | 17 - src/duskpsp/log/CMakeLists.txt | 11 - src/duskpsp/log/log.c | 58 - src/duskpsp/network/CMakeLists.txt | 9 - src/duskpsp/network/networkplatform.h | 19 - src/duskpsp/network/networkpsp.c | 265 ----- src/duskpsp/network/networkpsp.h | 110 -- src/duskpsp/save/CMakeLists.txt | 9 - src/duskpsp/save/savedeviceplatform.h | 32 - src/duskpsp/save/savedevicepsp.c | 153 --- src/duskpsp/save/savedevicepsp.h | 94 -- .../script/module/moduleplatformplatform.h | 11 - src/duskpsp/script/module/moduleplatformpsp.h | 13 - src/duskpsp/system/CMakeLists.txt | 9 - src/duskpsp/system/systemplatform.h | 13 - src/duskpsp/system/systempsp.c | 73 -- src/duskpsp/system/systempsp.h | 48 - src/duskpsp/time/CMakeLists.txt | 9 - src/duskpsp/time/timeplatform.h | 15 - src/duskpsp/time/timepsp.c | 40 - src/duskpsp/time/timepsp.h | 23 - src/dusksdl2/CMakeLists.txt | 15 - src/dusksdl2/display/CMakeLists.txt | 12 - src/dusksdl2/display/displaysdl2.c | 151 --- src/dusksdl2/display/displaysdl2.h | 41 - src/dusksdl2/dusksdl2.h | 10 - src/dusksdl2/input/CMakeLists.txt | 12 - src/dusksdl2/input/inputsdl2.c | 107 -- src/dusksdl2/input/inputsdl2.h | 86 -- src/dusksdl2/time/CMakeLists.txt | 12 - src/dusksdl2/time/timeplatform.h | 13 - src/dusksdl2/time/timesdl2.c | 20 - src/dusksdl2/time/timesdl2.h | 24 - src/duskvita/CMakeLists.txt | 21 - src/duskvita/asset/CMakeLists.txt | 10 - src/duskvita/asset/assetplatform.h | 13 - src/duskvita/asset/assetvita.c | 19 - src/duskvita/asset/assetvita.h | 30 - src/duskvita/display/displayplatform.h | 16 - src/duskvita/duskplatform.h | 23 - src/duskvita/input/CMakeLists.txt | 10 - src/duskvita/input/inputvita.c | 87 -- src/duskvita/log/CMakeLists.txt | 10 - src/duskvita/log/log.c | 52 - .../script/module/moduleplatformplatform.h | 11 - .../script/module/moduleplatformvita.h | 13 - story.md | 608 ----------- test/CMakeLists.txt | 17 - test/animation/CMakeLists.txt | 11 - test/animation/test_animation.c | 443 -------- test/animation/test_easing.c | 227 ---- test/animation/test_keyframe.c | 188 ---- test/assert/CMakeLists.txt | 9 - test/assert/test_assert.c | 152 --- test/asset/CMakeLists.txt | 11 - test/asset/test_asset.c | 692 ------------ test/asset/test_assetjsonloader.c | 257 ----- test/asset/test_assetlocale.c | 739 ------------- test/asset/test_assettilesetloader.c | 422 -------- test/console/CMakeLists.txt | 9 - test/console/test_console.c | 168 --- test/display/CMakeLists.txt | 9 - test/display/test_color.c | 89 -- test/dusktest.h | 10 - test/error/CMakeLists.txt | 9 - test/error/test_error.c | 371 ------- test/item/CMakeLists.txt | 9 - test/item/test_inventory.c | 433 -------- test/rpg/CMakeLists.txt | 15 - test/rpg/battle/CMakeLists.txt | 9 - test/rpg/battle/test_battle.c | 271 ----- test/rpg/cutscene/CMakeLists.txt | 12 - test/rpg/cutscene/test_cutscene.c | 84 -- test/rpg/cutscene/test_cutscenecontrol.c | 173 --- test/rpg/cutscene/test_cutscenemaparea.c | 175 --- test/rpg/cutscene/test_cutscenesystem.c | 583 ---------- test/rpg/entity/CMakeLists.txt | 19 - test/rpg/entity/entitytestfixture.c | 31 - test/rpg/entity/entitytestfixture.h | 22 - test/rpg/entity/test_entity.c | 300 ------ test/rpg/entity/test_entityinteract.c | 135 --- test/rpg/entity/test_entityitem.c | 85 -- test/rpg/entity/test_npc.c | 155 --- test/rpg/overworld/CMakeLists.txt | 11 - test/rpg/overworld/test_maparea.c | 217 ---- test/rpg/test_rpg.c | 16 - test/save/CMakeLists.txt | 20 - test/save/savetestfixture.c | 137 --- test/save/savetestfixture.h | 55 - test/save/test_save.c | 620 ----------- test/save/test_savedevice.c | 342 ------ test/save/test_savedevicelinux.c | 455 -------- test/save/test_savesettings.c | 172 --- test/save/test_saveslot.c | 314 ------ test/thread/CMakeLists.txt | 9 - test/thread/test_thread.c | 216 ---- test/time/CMakeLists.txt | 9 - test/time/test_time.c | 134 --- test/util/CMakeLists.txt | 13 - test/util/test_array.c | 68 -- test/util/test_math.c | 239 ----- test/util/test_memory.c | 536 ---------- test/util/test_sort.c | 148 --- test/util/test_string.c | 913 ---------------- tools/CMakeLists.txt | 6 - tools/asset/chunk/__main__.py | 624 ----------- tools/asset/dmf/__init__.py | 4 - tools/asset/dmf/__main__.py | 63 -- tools/asset/pack/__main__.py | 132 --- tools/color.py | 79 -- tools/editor/common/dtf.js | 175 --- tools/editor/common/png.js | 55 - tools/editor/index.html | 42 - .../editor/styles/components/bg-swatches.css | 28 - tools/editor/styles/components/btn.css | 38 - .../editor/styles/components/control-row.css | 13 - .../editor/styles/components/empty-state.css | 16 - tools/editor/styles/components/file-name.css | 8 - tools/editor/styles/components/hero.css | 16 - tools/editor/styles/components/info-table.css | 19 - tools/editor/styles/components/load-area.css | 19 - .../editor/styles/components/panel-inputs.css | 47 - .../styles/components/panel-section.css | 13 - .../styles/components/preview-empty.css | 14 - .../styles/components/preview-scroll.css | 16 - .../editor/styles/components/site-header.css | 43 - tools/editor/styles/components/tool-card.css | 30 - tools/editor/styles/components/tool-panel.css | 12 - .../editor/styles/components/tool-preview.css | 15 - .../editor/styles/components/warning-list.css | 24 - .../styles/components/warnings-section.css | 5 - tools/editor/styles/elements.css | 19 - tools/editor/styles/generic.css | 9 - tools/editor/styles/main.css | 32 - tools/editor/styles/objects/page.css | 7 - tools/editor/styles/objects/section-label.css | 13 - tools/editor/styles/objects/section.css | 4 - tools/editor/styles/objects/tool-grid.css | 7 - .../editor/styles/objects/tool-workspace.css | 7 - tools/editor/styles/settings.css | 17 - tools/editor/texture-padder/index.html | 130 --- tools/editor/texture-padder/texture-padder.js | 275 ----- tools/editor/texture/index.html | 124 --- tools/editor/texture/texture.js | 279 ----- tools/input.py | 58 - tools/makedolphiniso.py | 137 --- tools/run_python/CMakeLists.txt | 14 - tools/server.py | 23 - 1107 files changed, 78347 deletions(-) delete mode 100644 .github/actions/setup-devkitpro/action.yml delete mode 100644 .github/actions/setup-pspdev/action.yml delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/test.yml delete mode 100644 README.md delete mode 100644 assets/audio/audiotest.mp3 delete mode 100644 assets/audio/audiotest.wav delete mode 100644 assets/battle/moves.jsonc delete mode 100644 assets/cutscenes/battle/moves/fire.jsonc delete mode 100644 assets/cutscenes/battle/moves/slash.jsonc delete mode 100644 assets/cutscenes/battletest.jsonc delete mode 100644 assets/cutscenes/initial.jsonc delete mode 100644 assets/cutscenes/main_menu.jsonc delete mode 100644 assets/cutscenes/new_game.jsonc delete mode 100644 assets/items/items.jsonc delete mode 100644 assets/locale/en_US.po delete mode 100644 assets/maps/overworld/chunks/chunk_-1_0_0.json delete mode 100644 assets/maps/overworld/chunks/chunk_0_0_0.json delete mode 100644 assets/maps/overworld/chunks/chunk_0_1_0.json delete mode 100644 assets/maps/overworld/chunks/chunk_1_0_0.json delete mode 100644 assets/maps/overworld/chunks/chunk_1_1_0.json delete mode 100644 assets/maps/overworld/chunks/chunk_2_0_0.json delete mode 100644 assets/meshes/buildings/house_1_1.dmf delete mode 100644 assets/meshes/buildings/house_1_2.dmf delete mode 100644 assets/meshes/buildings/house_1_3.dmf delete mode 100644 assets/meshes/buildings/house_1_4.dmf delete mode 100644 assets/meshes/buildings/house_1_5.dmf delete mode 100644 assets/meshes/buildings/house_1_6.dmf delete mode 100644 assets/meshes/buildings/house_1_7.dmf delete mode 100644 assets/meshes/buildings/house_1_8.dmf delete mode 100644 assets/meshes/buildings/house_2_1.dmf delete mode 100644 assets/meshes/buildings/house_2_2.dmf delete mode 100644 assets/meshes/buildings/house_2_3.dmf delete mode 100644 assets/meshes/buildings/house_2_4.dmf delete mode 100644 assets/meshes/buildings/house_2_5.dmf delete mode 100644 assets/meshes/buildings/house_2_6.dmf delete mode 100644 assets/meshes/buildings/house_2_7.dmf delete mode 100644 assets/meshes/buildings/house_2_8.dmf delete mode 100644 assets/meshes/buildings/house_3_1.dmf delete mode 100644 assets/meshes/buildings/house_3_2.dmf delete mode 100644 assets/meshes/buildings/house_3_3.dmf delete mode 100644 assets/meshes/buildings/house_3_4.dmf delete mode 100644 assets/meshes/buildings/house_3_5.dmf delete mode 100644 assets/meshes/buildings/house_3_6.dmf delete mode 100644 assets/meshes/buildings/house_3_7.dmf delete mode 100644 assets/meshes/buildings/house_3_8.dmf delete mode 100644 assets/meshes/buildings/house_4_1.dmf delete mode 100644 assets/meshes/buildings/house_4_2.dmf delete mode 100644 assets/meshes/buildings/house_4_3.dmf delete mode 100644 assets/meshes/buildings/house_4_4.dmf delete mode 100644 assets/meshes/buildings/house_4_5.dmf delete mode 100644 assets/meshes/buildings/house_4_6.dmf delete mode 100644 assets/meshes/buildings/house_4_7.dmf delete mode 100644 assets/meshes/buildings/house_4_8.dmf delete mode 100644 assets/meshes/buildings/house_5_1.dmf delete mode 100644 assets/meshes/buildings/house_5_2.dmf delete mode 100644 assets/meshes/buildings/house_5_3.dmf delete mode 100644 assets/meshes/buildings/house_5_4.dmf delete mode 100644 assets/meshes/buildings/house_5_5.dmf delete mode 100644 assets/meshes/buildings/house_5_6.dmf delete mode 100644 assets/meshes/buildings/house_5_7.dmf delete mode 100644 assets/meshes/buildings/house_5_8.dmf delete mode 100644 assets/meshes/buildings/house_6_1.dmf delete mode 100644 assets/meshes/buildings/house_6_2.dmf delete mode 100644 assets/meshes/buildings/house_6_3.dmf delete mode 100644 assets/meshes/buildings/house_6_4.dmf delete mode 100644 assets/meshes/buildings/house_6_5.dmf delete mode 100644 assets/meshes/buildings/house_6_6.dmf delete mode 100644 assets/meshes/buildings/house_6_7.dmf delete mode 100644 assets/meshes/buildings/house_6_8.dmf delete mode 100644 assets/meshes/buildings/house_7_1.dmf delete mode 100644 assets/meshes/buildings/house_7_2.dmf delete mode 100644 assets/meshes/buildings/house_7_3.dmf delete mode 100644 assets/meshes/buildings/house_7_4.dmf delete mode 100644 assets/meshes/buildings/house_7_5.dmf delete mode 100644 assets/meshes/buildings/house_7_6.dmf delete mode 100644 assets/meshes/buildings/house_7_7.dmf delete mode 100644 assets/meshes/buildings/house_7_8.dmf delete mode 100644 assets/meshes/buildings/house_8_1.dmf delete mode 100644 assets/meshes/buildings/house_8_2.dmf delete mode 100644 assets/meshes/buildings/house_8_3.dmf delete mode 100644 assets/meshes/buildings/house_8_4.dmf delete mode 100644 assets/meshes/buildings/house_8_5.dmf delete mode 100644 assets/meshes/buildings/house_8_6.dmf delete mode 100644 assets/meshes/buildings/house_8_7.dmf delete mode 100644 assets/meshes/buildings/house_8_8.dmf delete mode 100644 assets/models/buildings/house_1_1.json delete mode 100644 assets/models/buildings/house_1_2.json delete mode 100644 assets/models/buildings/house_1_3.json delete mode 100644 assets/models/buildings/house_1_4.json delete mode 100644 assets/models/buildings/house_1_5.json delete mode 100644 assets/models/buildings/house_1_6.json delete mode 100644 assets/models/buildings/house_1_7.json delete mode 100644 assets/models/buildings/house_1_8.json delete mode 100644 assets/models/buildings/house_2_1.json delete mode 100644 assets/models/buildings/house_2_2.json delete mode 100644 assets/models/buildings/house_2_3.json delete mode 100644 assets/models/buildings/house_2_4.json delete mode 100644 assets/models/buildings/house_2_5.json delete mode 100644 assets/models/buildings/house_2_6.json delete mode 100644 assets/models/buildings/house_2_7.json delete mode 100644 assets/models/buildings/house_2_8.json delete mode 100644 assets/models/buildings/house_3_1.json delete mode 100644 assets/models/buildings/house_3_2.json delete mode 100644 assets/models/buildings/house_3_3.json delete mode 100644 assets/models/buildings/house_3_4.json delete mode 100644 assets/models/buildings/house_3_5.json delete mode 100644 assets/models/buildings/house_3_6.json delete mode 100644 assets/models/buildings/house_3_7.json delete mode 100644 assets/models/buildings/house_3_8.json delete mode 100644 assets/models/buildings/house_4_1.json delete mode 100644 assets/models/buildings/house_4_2.json delete mode 100644 assets/models/buildings/house_4_3.json delete mode 100644 assets/models/buildings/house_4_4.json delete mode 100644 assets/models/buildings/house_4_5.json delete mode 100644 assets/models/buildings/house_4_6.json delete mode 100644 assets/models/buildings/house_4_7.json delete mode 100644 assets/models/buildings/house_4_8.json delete mode 100644 assets/models/buildings/house_5_1.json delete mode 100644 assets/models/buildings/house_5_2.json delete mode 100644 assets/models/buildings/house_5_3.json delete mode 100644 assets/models/buildings/house_5_4.json delete mode 100644 assets/models/buildings/house_5_5.json delete mode 100644 assets/models/buildings/house_5_6.json delete mode 100644 assets/models/buildings/house_5_7.json delete mode 100644 assets/models/buildings/house_5_8.json delete mode 100644 assets/models/buildings/house_6_1.json delete mode 100644 assets/models/buildings/house_6_2.json delete mode 100644 assets/models/buildings/house_6_3.json delete mode 100644 assets/models/buildings/house_6_4.json delete mode 100644 assets/models/buildings/house_6_5.json delete mode 100644 assets/models/buildings/house_6_6.json delete mode 100644 assets/models/buildings/house_6_7.json delete mode 100644 assets/models/buildings/house_6_8.json delete mode 100644 assets/models/buildings/house_7_1.json delete mode 100644 assets/models/buildings/house_7_2.json delete mode 100644 assets/models/buildings/house_7_3.json delete mode 100644 assets/models/buildings/house_7_4.json delete mode 100644 assets/models/buildings/house_7_5.json delete mode 100644 assets/models/buildings/house_7_6.json delete mode 100644 assets/models/buildings/house_7_7.json delete mode 100644 assets/models/buildings/house_7_8.json delete mode 100644 assets/models/buildings/house_8_1.json delete mode 100644 assets/models/buildings/house_8_2.json delete mode 100644 assets/models/buildings/house_8_3.json delete mode 100644 assets/models/buildings/house_8_4.json delete mode 100644 assets/models/buildings/house_8_5.json delete mode 100644 assets/models/buildings/house_8_6.json delete mode 100644 assets/models/buildings/house_8_7.json delete mode 100644 assets/models/buildings/house_8_8.json delete mode 100644 assets/party.jsonc delete mode 100644 assets/story/storyflag.jsonc delete mode 100644 assets/tiles.png delete mode 100644 cmake/modules/Findcglm.cmake delete mode 100644 cmake/modules/Findcmocka.cmake delete mode 100644 cmake/modules/Findlibmad.cmake delete mode 100644 cmake/modules/Findlibzip.cmake delete mode 100644 cmake/modules/Findstb.cmake delete mode 100644 cmake/modules/Findyyjson.cmake delete mode 100644 cmake/modules/dusktest.cmake delete mode 100644 cmake/targets/dolphin.cmake delete mode 100644 cmake/targets/gamecube.cmake delete mode 100644 cmake/targets/knulli.cmake delete mode 100644 cmake/targets/linux.cmake delete mode 100644 cmake/targets/psp.cmake delete mode 100644 cmake/targets/vita.cmake delete mode 100644 cmake/targets/wii.cmake delete mode 100644 cmake/toolchains/aarch64-linux-gnu.cmake delete mode 100644 docker/dolphin/Dockerfile delete mode 100644 docker/dolphin/meta.xml.in delete mode 100644 docker/knulli/Dockerfile delete mode 100644 docker/linux/Dockerfile delete mode 100644 docker/psp/Dockerfile delete mode 100644 docker/vita/Dockerfile delete mode 100644 editor/client/public/common/chunkterrain.js delete mode 100644 editor/client/public/common/dmf.js delete mode 100644 editor/client/public/index.html delete mode 100644 editor/client/public/map/index.html delete mode 100644 editor/client/public/map/map.css delete mode 100644 editor/client/public/map/map.js delete mode 100644 editor/client/public/map/renderer.js delete mode 100644 editor/client/public/template.html delete mode 100644 editor/server/server.py delete mode 100755 editor/start.sh delete mode 100755 scripts/build-gamecube-docker.sh delete mode 100755 scripts/build-gamecube-iso-docker.sh delete mode 100755 scripts/build-gamecube-iso.sh delete mode 100755 scripts/build-gamecube.sh delete mode 100755 scripts/build-knulli-docker.sh delete mode 100755 scripts/build-knulli.sh delete mode 100755 scripts/build-linux-docker.sh delete mode 100755 scripts/build-linux.sh delete mode 100755 scripts/build-psp-docker.sh delete mode 100755 scripts/build-psp.sh delete mode 100755 scripts/build-vita-docker.sh delete mode 100755 scripts/build-vita.sh delete mode 100755 scripts/build-wii-docker.sh delete mode 100755 scripts/build-wii-iso-docker.sh delete mode 100755 scripts/build-wii-iso.sh delete mode 100755 scripts/build-wii.sh delete mode 100755 scripts/test-linux-docker.sh delete mode 100755 scripts/test-linux.sh delete mode 100644 src/CMakeLists.txt delete mode 100644 src/dusk/CMakeLists.txt delete mode 100644 src/dusk/animation/CMakeLists.txt delete mode 100644 src/dusk/animation/animation.c delete mode 100644 src/dusk/animation/animation.h delete mode 100644 src/dusk/animation/easing.c delete mode 100644 src/dusk/animation/easing.h delete mode 100644 src/dusk/animation/keyframe.c delete mode 100644 src/dusk/animation/keyframe.h delete mode 100644 src/dusk/assert/CMakeLists.txt delete mode 100644 src/dusk/assert/assert.c delete mode 100644 src/dusk/assert/assert.h delete mode 100644 src/dusk/asset/CMakeLists.txt delete mode 100644 src/dusk/asset/asset.c delete mode 100644 src/dusk/asset/asset.h delete mode 100644 src/dusk/asset/assetdsk.c delete mode 100644 src/dusk/asset/assetdsk.h delete mode 100644 src/dusk/asset/assetfile.c delete mode 100644 src/dusk/asset/assetfile.h delete mode 100644 src/dusk/asset/loader/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/assetentry.c delete mode 100644 src/dusk/asset/loader/assetentry.h delete mode 100644 src/dusk/asset/loader/assetloader.c delete mode 100644 src/dusk/asset/loader/assetloader.h delete mode 100644 src/dusk/asset/loader/assetloading.h delete mode 100644 src/dusk/asset/loader/chunk/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/chunk/assetchunkloader.c delete mode 100644 src/dusk/asset/loader/chunk/assetchunkloader.h delete mode 100644 src/dusk/asset/loader/display/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/display/assettextureloader.c delete mode 100644 src/dusk/asset/loader/display/assettextureloader.h delete mode 100644 src/dusk/asset/loader/display/assettilesetloader.c delete mode 100644 src/dusk/asset/loader/display/assettilesetloader.h delete mode 100644 src/dusk/asset/loader/dmf/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/dmf/assetmeshloader.c delete mode 100644 src/dusk/asset/loader/dmf/assetmeshloader.h delete mode 100644 src/dusk/asset/loader/dmf/assetmodelloader.c delete mode 100644 src/dusk/asset/loader/dmf/assetmodelloader.h delete mode 100644 src/dusk/asset/loader/json/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/json/assetjsonloader.c delete mode 100644 src/dusk/asset/loader/json/assetjsonloader.h delete mode 100644 src/dusk/asset/loader/locale/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/locale/assetlocaleloader.c delete mode 100644 src/dusk/asset/loader/locale/assetlocaleloader.h delete mode 100644 src/dusk/asset/loader/mp3/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/mp3/assetmp3loader.c delete mode 100644 src/dusk/asset/loader/mp3/assetmp3loader.h delete mode 100644 src/dusk/asset/loader/wav/CMakeLists.txt delete mode 100644 src/dusk/asset/loader/wav/assetwavloader.c delete mode 100644 src/dusk/asset/loader/wav/assetwavloader.h delete mode 100644 src/dusk/audio/CMakeLists.txt delete mode 100644 src/dusk/audio/audio.c delete mode 100644 src/dusk/audio/audio.h delete mode 100644 src/dusk/audio/mixer/CMakeLists.txt delete mode 100644 src/dusk/audio/mixer/audiomixer.c delete mode 100644 src/dusk/audio/mixer/audiomixer.h delete mode 100644 src/dusk/audio/mixer/audiomixerchannel.c delete mode 100644 src/dusk/audio/mixer/audiomixerchannel.h delete mode 100644 src/dusk/audio/stream/CMakeLists.txt delete mode 100644 src/dusk/audio/stream/audiostream.c delete mode 100644 src/dusk/audio/stream/audiostream.h delete mode 100644 src/dusk/audio/stream/audiostreammp3.c delete mode 100644 src/dusk/audio/stream/audiostreammp3.h delete mode 100644 src/dusk/audio/stream/audiostreampcm.c delete mode 100644 src/dusk/audio/stream/audiostreampcm.h delete mode 100644 src/dusk/audio/stream/audiostreamtype.c delete mode 100644 src/dusk/audio/stream/audiostreamtype.h delete mode 100644 src/dusk/console/CMakeLists.txt delete mode 100644 src/dusk/console/console.c delete mode 100644 src/dusk/console/console.h delete mode 100644 src/dusk/display/CMakeLists.txt delete mode 100644 src/dusk/display/color.csv delete mode 100644 src/dusk/display/display.c delete mode 100644 src/dusk/display/display.h delete mode 100644 src/dusk/display/displaystate.h delete mode 100644 src/dusk/display/framebuffer/CMakeLists.txt delete mode 100644 src/dusk/display/framebuffer/framebuffer.c delete mode 100644 src/dusk/display/framebuffer/framebuffer.h delete mode 100644 src/dusk/display/mesh/CMakeLists.txt delete mode 100644 src/dusk/display/mesh/capsule.c delete mode 100644 src/dusk/display/mesh/capsule.h delete mode 100644 src/dusk/display/mesh/cube.c delete mode 100644 src/dusk/display/mesh/cube.h delete mode 100644 src/dusk/display/mesh/mesh.c delete mode 100644 src/dusk/display/mesh/mesh.h delete mode 100644 src/dusk/display/mesh/meshvertex.h delete mode 100644 src/dusk/display/mesh/plane.c delete mode 100644 src/dusk/display/mesh/plane.h delete mode 100644 src/dusk/display/mesh/quad.c delete mode 100644 src/dusk/display/mesh/quad.h delete mode 100644 src/dusk/display/mesh/sphere.c delete mode 100644 src/dusk/display/mesh/sphere.h delete mode 100644 src/dusk/display/mesh/triprism.c delete mode 100644 src/dusk/display/mesh/triprism.h delete mode 100644 src/dusk/display/screen/CMakeLists.txt delete mode 100644 src/dusk/display/screen/screen.c delete mode 100644 src/dusk/display/screen/screen.h delete mode 100644 src/dusk/display/shader/CMakeLists.txt delete mode 100644 src/dusk/display/shader/shader.c delete mode 100644 src/dusk/display/shader/shader.h delete mode 100644 src/dusk/display/shader/shaderlist.c delete mode 100644 src/dusk/display/shader/shaderlist.h delete mode 100644 src/dusk/display/shader/shadermaterial.h delete mode 100644 src/dusk/display/shader/shaderunlit.c delete mode 100644 src/dusk/display/shader/shaderunlit.h delete mode 100644 src/dusk/display/spritebatch/CMakeLists.txt delete mode 100644 src/dusk/display/spritebatch/spritebatch.c delete mode 100644 src/dusk/display/spritebatch/spritebatch.h delete mode 100644 src/dusk/display/spritebatch/spritebatchsprite.c delete mode 100644 src/dusk/display/spritebatch/spritebatchsprite.h delete mode 100644 src/dusk/display/text/CMakeLists.txt delete mode 100644 src/dusk/display/text/font.c delete mode 100644 src/dusk/display/text/font.h delete mode 100644 src/dusk/display/text/text.c delete mode 100644 src/dusk/display/text/text.h delete mode 100644 src/dusk/display/texture/CMakeLists.txt delete mode 100644 src/dusk/display/texture/palette.c delete mode 100644 src/dusk/display/texture/palette.h delete mode 100644 src/dusk/display/texture/texture.c delete mode 100644 src/dusk/display/texture/texture.h delete mode 100644 src/dusk/display/texture/tileset.c delete mode 100644 src/dusk/display/texture/tileset.h delete mode 100644 src/dusk/dusk.h delete mode 100644 src/dusk/engine/CMakeLists.txt delete mode 100644 src/dusk/engine/engine.c delete mode 100644 src/dusk/engine/engine.h delete mode 100644 src/dusk/error/CMakeLists.txt delete mode 100644 src/dusk/error/error.c delete mode 100644 src/dusk/error/error.h delete mode 100644 src/dusk/input/CMakeLists.txt delete mode 100644 src/dusk/input/input.c delete mode 100644 src/dusk/input/input.csv delete mode 100644 src/dusk/input/input.h delete mode 100644 src/dusk/input/inputaction.c delete mode 100644 src/dusk/input/inputaction.h delete mode 100644 src/dusk/input/inputbutton.c delete mode 100644 src/dusk/input/inputbutton.h delete mode 100644 src/dusk/locale/CMakeLists.txt delete mode 100644 src/dusk/locale/localeinfo.h delete mode 100644 src/dusk/locale/localemanager.c delete mode 100644 src/dusk/locale/localemanager.h delete mode 100644 src/dusk/log/CMakeLists.txt delete mode 100644 src/dusk/log/log.h delete mode 100644 src/dusk/main.c delete mode 100644 src/dusk/null.c delete mode 100644 src/dusk/perf/CMakeLists.txt delete mode 100644 src/dusk/perf/perf.c delete mode 100644 src/dusk/perf/perf.h delete mode 100644 src/dusk/perf/perfitem.h delete mode 100644 src/dusk/rpg/CMakeLists.txt delete mode 100644 src/dusk/rpg/battle/CMakeLists.txt delete mode 100644 src/dusk/rpg/battle/battle.c delete mode 100644 src/dusk/rpg/battle/battle.h delete mode 100644 src/dusk/rpg/battle/fighter/CMakeLists.txt delete mode 100644 src/dusk/rpg/battle/fighter/battlefighter.c delete mode 100644 src/dusk/rpg/battle/fighter/battlefighter.h delete mode 100644 src/dusk/rpg/battle/fighter/battlefightercontroller.h delete mode 100644 src/dusk/rpg/battle/fighter/battlefighterpos.h delete mode 100644 src/dusk/rpg/battle/fighter/battlefighterspecies.h delete mode 100644 src/dusk/rpg/battle/fighter/battlefighterstats.h delete mode 100644 src/dusk/rpg/battle/fighter/battlefighterstatus.h delete mode 100644 src/dusk/rpg/battle/fighter/battlefighterteam.h delete mode 100644 src/dusk/rpg/battle/move/CMakeLists.txt delete mode 100644 src/dusk/rpg/battle/move/battlemove.c delete mode 100644 src/dusk/rpg/battle/move/battlemove.h delete mode 100644 src/dusk/rpg/battle/move/battlemoveaction.h delete mode 100644 src/dusk/rpg/battle/move/battletarget.h delete mode 100644 src/dusk/rpg/battle/party.c delete mode 100644 src/dusk/rpg/battle/party.h delete mode 100644 src/dusk/rpg/battle/state/CMakeLists.txt delete mode 100644 src/dusk/rpg/battle/state/battlestate.c delete mode 100644 src/dusk/rpg/battle/state/battlestate.h delete mode 100644 src/dusk/rpg/battle/state/battlestateending.c delete mode 100644 src/dusk/rpg/battle/state/battlestateending.h delete mode 100644 src/dusk/rpg/battle/state/battlestateexecuting.c delete mode 100644 src/dusk/rpg/battle/state/battlestateexecuting.h delete mode 100644 src/dusk/rpg/battle/state/battlestatefled.c delete mode 100644 src/dusk/rpg/battle/state/battlestatefled.h delete mode 100644 src/dusk/rpg/battle/state/battlestatelost.c delete mode 100644 src/dusk/rpg/battle/state/battlestatelost.h delete mode 100644 src/dusk/rpg/battle/state/battlestateopening.c delete mode 100644 src/dusk/rpg/battle/state/battlestateopening.h delete mode 100644 src/dusk/rpg/battle/state/battlestatepostround.c delete mode 100644 src/dusk/rpg/battle/state/battlestatepostround.h delete mode 100644 src/dusk/rpg/battle/state/battlestatepreround.c delete mode 100644 src/dusk/rpg/battle/state/battlestatepreround.h delete mode 100644 src/dusk/rpg/battle/state/battlestateselection.c delete mode 100644 src/dusk/rpg/battle/state/battlestateselection.h delete mode 100644 src/dusk/rpg/battle/state/battlestatewon.c delete mode 100644 src/dusk/rpg/battle/state/battlestatewon.h delete mode 100644 src/dusk/rpg/cutscene/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/cutscene.c delete mode 100644 src/dusk/rpg/cutscene/cutscene.h delete mode 100644 src/dusk/rpg/cutscene/cutscenepause.h delete mode 100644 src/dusk/rpg/cutscene/cutsceneref.c delete mode 100644 src/dusk/rpg/cutscene/cutsceneref.h delete mode 100644 src/dusk/rpg/cutscene/cutscenesystem.c delete mode 100644 src/dusk/rpg/cutscene/cutscenesystem.h delete mode 100755 src/dusk/rpg/cutscene/item/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/audio/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c delete mode 100644 src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c delete mode 100644 src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h delete mode 100644 src/dusk/rpg/cutscene/item/control/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutsceneidle.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutsceneidle.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenemarker.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenemarker.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenequitgame.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenequitgame.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenerestart.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenerestart.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenescene.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenescene.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenesetpause.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenesetpause.h delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenewait.c delete mode 100644 src/dusk/rpg/cutscene/item/control/cutscenewait.h delete mode 100644 src/dusk/rpg/cutscene/item/cutscenecallback.c delete mode 100644 src/dusk/rpg/cutscene/item/cutscenecallback.h delete mode 100644 src/dusk/rpg/cutscene/item/cutsceneitem.c delete mode 100644 src/dusk/rpg/cutscene/item/cutsceneitem.h delete mode 100644 src/dusk/rpg/cutscene/item/cutsceneitembase.c delete mode 100644 src/dusk/rpg/cutscene/item/cutsceneitembase.h delete mode 100644 src/dusk/rpg/cutscene/item/cutsceneprint.c delete mode 100644 src/dusk/rpg/cutscene/item/cutsceneprint.h delete mode 100644 src/dusk/rpg/cutscene/item/entity/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c delete mode 100644 src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h delete mode 100644 src/dusk/rpg/cutscene/item/item/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c delete mode 100644 src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h delete mode 100644 src/dusk/rpg/cutscene/item/json/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c delete mode 100644 src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h delete mode 100644 src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c delete mode 100644 src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h delete mode 100644 src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c delete mode 100644 src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h delete mode 100644 src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c delete mode 100644 src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h delete mode 100644 src/dusk/rpg/cutscene/item/party/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c delete mode 100644 src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h delete mode 100644 src/dusk/rpg/cutscene/item/save/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c delete mode 100644 src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h delete mode 100644 src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c delete mode 100644 src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h delete mode 100644 src/dusk/rpg/cutscene/item/story/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c delete mode 100644 src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h delete mode 100644 src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c delete mode 100644 src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/CMakeLists.txt delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenefade.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenefade.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenemodal.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenemodal.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneshake.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneshake.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenetext.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenetext.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c delete mode 100644 src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h delete mode 100644 src/dusk/rpg/entity/CMakeLists.txt delete mode 100644 src/dusk/rpg/entity/anim/CMakeLists.txt delete mode 100644 src/dusk/rpg/entity/anim/entityanim.c delete mode 100644 src/dusk/rpg/entity/anim/entityanim.h delete mode 100644 src/dusk/rpg/entity/anim/entityanimidle.c delete mode 100644 src/dusk/rpg/entity/anim/entityanimidle.h delete mode 100644 src/dusk/rpg/entity/anim/entityanimrun.c delete mode 100644 src/dusk/rpg/entity/anim/entityanimrun.h delete mode 100644 src/dusk/rpg/entity/anim/entityanimturn.c delete mode 100644 src/dusk/rpg/entity/anim/entityanimturn.h delete mode 100644 src/dusk/rpg/entity/anim/entityanimwalk.c delete mode 100644 src/dusk/rpg/entity/anim/entityanimwalk.h delete mode 100644 src/dusk/rpg/entity/entity.c delete mode 100644 src/dusk/rpg/entity/entity.h delete mode 100644 src/dusk/rpg/entity/entitydir.c delete mode 100644 src/dusk/rpg/entity/entitydir.h delete mode 100644 src/dusk/rpg/entity/entitypathstep.c delete mode 100644 src/dusk/rpg/entity/entitypathstep.h delete mode 100644 src/dusk/rpg/entity/entitytype.h delete mode 100644 src/dusk/rpg/entity/global/CMakeLists.txt delete mode 100644 src/dusk/rpg/entity/global/entityglobal.h delete mode 100644 src/dusk/rpg/entity/global/entityglobaldefs.h delete mode 100644 src/dusk/rpg/entity/global/entitygloballist.h delete mode 100644 src/dusk/rpg/entity/interact/CMakeLists.txt delete mode 100644 src/dusk/rpg/entity/interact/entityinteract.c delete mode 100644 src/dusk/rpg/entity/interact/entityinteract.h delete mode 100644 src/dusk/rpg/entity/item/CMakeLists.txt delete mode 100644 src/dusk/rpg/entity/item/entityitem.c delete mode 100644 src/dusk/rpg/entity/item/entityitem.h delete mode 100644 src/dusk/rpg/entity/npc/CMakeLists.txt delete mode 100644 src/dusk/rpg/entity/npc/npc.c delete mode 100644 src/dusk/rpg/entity/npc/npc.h delete mode 100644 src/dusk/rpg/entity/npc/npcpath.c delete mode 100644 src/dusk/rpg/entity/npc/npcpath.h delete mode 100644 src/dusk/rpg/entity/npc/npcturn.c delete mode 100644 src/dusk/rpg/entity/npc/npcturn.h delete mode 100644 src/dusk/rpg/entity/npc/npcwalk.c delete mode 100644 src/dusk/rpg/entity/npc/npcwalk.h delete mode 100644 src/dusk/rpg/entity/player.c delete mode 100644 src/dusk/rpg/entity/player.h delete mode 100644 src/dusk/rpg/item/CMakeLists.txt delete mode 100644 src/dusk/rpg/item/backpack.c delete mode 100644 src/dusk/rpg/item/backpack.h delete mode 100644 src/dusk/rpg/item/inventory.c delete mode 100644 src/dusk/rpg/item/inventory.h delete mode 100644 src/dusk/rpg/item/item.c delete mode 100644 src/dusk/rpg/item/item.h delete mode 100644 src/dusk/rpg/item/itemgive.c delete mode 100644 src/dusk/rpg/item/itemgive.h delete mode 100644 src/dusk/rpg/overworld/CMakeLists.txt delete mode 100644 src/dusk/rpg/overworld/chunk.c delete mode 100644 src/dusk/rpg/overworld/chunk.h delete mode 100644 src/dusk/rpg/overworld/global/CMakeLists.txt delete mode 100644 src/dusk/rpg/overworld/global/mapareaglobal.h delete mode 100644 src/dusk/rpg/overworld/global/mapareaglobaldefs.h delete mode 100644 src/dusk/rpg/overworld/global/mapareagloballist.h delete mode 100644 src/dusk/rpg/overworld/map.c delete mode 100644 src/dusk/rpg/overworld/map.h delete mode 100644 src/dusk/rpg/overworld/maparea.c delete mode 100644 src/dusk/rpg/overworld/maparea.h delete mode 100644 src/dusk/rpg/overworld/tile.c delete mode 100644 src/dusk/rpg/overworld/tile.h delete mode 100644 src/dusk/rpg/overworld/tileshape.c delete mode 100644 src/dusk/rpg/overworld/tileshape.h delete mode 100644 src/dusk/rpg/overworld/worldpos.c delete mode 100644 src/dusk/rpg/overworld/worldpos.h delete mode 100644 src/dusk/rpg/rpg.c delete mode 100644 src/dusk/rpg/rpg.h delete mode 100644 src/dusk/rpg/rpgcamera.c delete mode 100644 src/dusk/rpg/rpgcamera.h delete mode 100644 src/dusk/rpg/story/CMakeLists.txt delete mode 100644 src/dusk/rpg/story/story.c delete mode 100644 src/dusk/rpg/story/story.h delete mode 100644 src/dusk/save/CMakeLists.txt delete mode 100644 src/dusk/save/save.c delete mode 100644 src/dusk/save/save.h delete mode 100644 src/dusk/save/savedevice.c delete mode 100644 src/dusk/save/savedevice.h delete mode 100644 src/dusk/save/savejson.h delete mode 100644 src/dusk/save/saverror.h delete mode 100644 src/dusk/save/settings/CMakeLists.txt delete mode 100644 src/dusk/save/settings/savesettings.c delete mode 100644 src/dusk/save/settings/savesettings.h delete mode 100644 src/dusk/save/settings/savesettingscurrent.c delete mode 100644 src/dusk/save/settings/savesettingscurrent.h delete mode 100644 src/dusk/save/settings/savesettingsver.h delete mode 100644 src/dusk/save/settings/savesettingsver1.c delete mode 100644 src/dusk/save/settings/savesettingsver1.h delete mode 100644 src/dusk/save/slot/CMakeLists.txt delete mode 100644 src/dusk/save/slot/saveslot.c delete mode 100644 src/dusk/save/slot/saveslot.h delete mode 100644 src/dusk/save/slot/saveslotcurrent.c delete mode 100644 src/dusk/save/slot/saveslotcurrent.h delete mode 100644 src/dusk/save/slot/saveslotver.h delete mode 100644 src/dusk/save/slot/saveslotver1.c delete mode 100644 src/dusk/save/slot/saveslotver1.h delete mode 100644 src/dusk/scene/CMakeLists.txt delete mode 100644 src/dusk/scene/battle/CMakeLists.txt delete mode 100644 src/dusk/scene/battle/scenebattle.c delete mode 100644 src/dusk/scene/battle/scenebattle.h delete mode 100644 src/dusk/scene/initial/CMakeLists.txt delete mode 100644 src/dusk/scene/initial/sceneinitial.c delete mode 100644 src/dusk/scene/initial/sceneinitial.h delete mode 100644 src/dusk/scene/overworld/CMakeLists.txt delete mode 100644 src/dusk/scene/overworld/sceneoverworld.c delete mode 100644 src/dusk/scene/overworld/sceneoverworld.h delete mode 100644 src/dusk/scene/scene.c delete mode 100644 src/dusk/scene/scene.h delete mode 100644 src/dusk/scene/scenebase.h delete mode 100644 src/dusk/scene/scenetype.c delete mode 100644 src/dusk/scene/scenetype.h delete mode 100644 src/dusk/system/CMakeLists.txt delete mode 100644 src/dusk/system/system.c delete mode 100644 src/dusk/system/system.h delete mode 100644 src/dusk/system/systemplatformlist.h delete mode 100644 src/dusk/thread/CMakeLists.txt delete mode 100644 src/dusk/thread/thread.c delete mode 100644 src/dusk/thread/thread.h delete mode 100644 src/dusk/thread/threadlocal.h delete mode 100644 src/dusk/thread/threadmutex.c delete mode 100644 src/dusk/thread/threadmutex.h delete mode 100644 src/dusk/time/CMakeLists.txt delete mode 100644 src/dusk/time/time.c delete mode 100644 src/dusk/time/time.h delete mode 100644 src/dusk/time/timeepoch.c delete mode 100644 src/dusk/time/timeepoch.h delete mode 100644 src/dusk/ui/CMakeLists.txt delete mode 100644 src/dusk/ui/debug/CMakeLists.txt delete mode 100644 src/dusk/ui/debug/uiconsole.c delete mode 100644 src/dusk/ui/debug/uiconsole.h delete mode 100644 src/dusk/ui/debug/uicutscene.c delete mode 100644 src/dusk/ui/debug/uicutscene.h delete mode 100644 src/dusk/ui/debug/uifps.c delete mode 100644 src/dusk/ui/debug/uifps.h delete mode 100644 src/dusk/ui/debug/uiplayerpos.c delete mode 100644 src/dusk/ui/debug/uiplayerpos.h delete mode 100644 src/dusk/ui/dialog/CMakeLists.txt delete mode 100644 src/dusk/ui/dialog/keyboard/CMakeLists.txt delete mode 100644 src/dusk/ui/dialog/keyboard/uikeyboard.c delete mode 100644 src/dusk/ui/dialog/keyboard/uikeyboard.h delete mode 100644 src/dusk/ui/dialog/save/CMakeLists.txt delete mode 100644 src/dusk/ui/dialog/save/uisaveslot.c delete mode 100644 src/dusk/ui/dialog/save/uisaveslot.h delete mode 100644 src/dusk/ui/dialog/save/uiselectsave.c delete mode 100644 src/dusk/ui/dialog/save/uiselectsave.h delete mode 100644 src/dusk/ui/dialog/uiconfirm.c delete mode 100644 src/dusk/ui/dialog/uiconfirm.h delete mode 100644 src/dusk/ui/focus/CMakeLists.txt delete mode 100644 src/dusk/ui/focus/uifocus.c delete mode 100644 src/dusk/ui/focus/uifocus.h delete mode 100644 src/dusk/ui/focus/uifocusitem.h delete mode 100644 src/dusk/ui/overlay/CMakeLists.txt delete mode 100644 src/dusk/ui/overlay/uicrop.c delete mode 100644 src/dusk/ui/overlay/uicrop.h delete mode 100644 src/dusk/ui/overlay/uifatalerror.c delete mode 100644 src/dusk/ui/overlay/uifatalerror.h delete mode 100644 src/dusk/ui/overlay/uifullbox.c delete mode 100644 src/dusk/ui/overlay/uifullbox.h delete mode 100644 src/dusk/ui/overlay/uiloading.c delete mode 100644 src/dusk/ui/overlay/uiloading.h delete mode 100644 src/dusk/ui/rpg/CMakeLists.txt delete mode 100644 src/dusk/ui/rpg/textbox/CMakeLists.txt delete mode 100644 src/dusk/ui/rpg/textbox/uitextbox.c delete mode 100644 src/dusk/ui/rpg/textbox/uitextbox.h delete mode 100644 src/dusk/ui/rpg/textbox/uitextboxmain.c delete mode 100644 src/dusk/ui/rpg/textbox/uitextboxmain.h delete mode 100644 src/dusk/ui/rpg/textbox/uitextboxmini.c delete mode 100644 src/dusk/ui/rpg/textbox/uitextboxmini.h delete mode 100644 src/dusk/ui/rpg/textbox/uitextboxminilist.c delete mode 100644 src/dusk/ui/rpg/textbox/uitextboxminilist.h delete mode 100644 src/dusk/ui/rpg/uiemoji.c delete mode 100644 src/dusk/ui/rpg/uiemoji.h delete mode 100644 src/dusk/ui/screen/CMakeLists.txt delete mode 100644 src/dusk/ui/screen/backpack/CMakeLists.txt delete mode 100644 src/dusk/ui/screen/backpack/uibackpack.c delete mode 100644 src/dusk/ui/screen/backpack/uibackpack.h delete mode 100644 src/dusk/ui/screen/battle/CMakeLists.txt delete mode 100644 src/dusk/ui/screen/battle/uibattlehud.c delete mode 100644 src/dusk/ui/screen/battle/uibattlehud.h delete mode 100644 src/dusk/ui/screen/battle/uibattlemenu.c delete mode 100644 src/dusk/ui/screen/battle/uibattlemenu.h delete mode 100644 src/dusk/ui/screen/game/CMakeLists.txt delete mode 100644 src/dusk/ui/screen/game/uigamemenu.c delete mode 100644 src/dusk/ui/screen/game/uigamemenu.h delete mode 100644 src/dusk/ui/screen/mainmenu/CMakeLists.txt delete mode 100644 src/dusk/ui/screen/mainmenu/uimainmenu.c delete mode 100644 src/dusk/ui/screen/mainmenu/uimainmenu.h delete mode 100644 src/dusk/ui/transition/CMakeLists.txt delete mode 100644 src/dusk/ui/transition/uitransition.c delete mode 100644 src/dusk/ui/transition/uitransition.h delete mode 100644 src/dusk/ui/transition/uitransitiondata.h delete mode 100644 src/dusk/ui/transition/uitransitionfade.c delete mode 100644 src/dusk/ui/transition/uitransitionfade.h delete mode 100644 src/dusk/ui/ui.c delete mode 100644 src/dusk/ui/ui.h delete mode 100644 src/dusk/ui/uielement.c delete mode 100644 src/dusk/ui/uielement.h delete mode 100644 src/dusk/ui/uielementlist.c delete mode 100644 src/dusk/ui/uielementlist.h delete mode 100644 src/dusk/ui/widget/CMakeLists.txt delete mode 100644 src/dusk/ui/widget/uibutton.c delete mode 100644 src/dusk/ui/widget/uibutton.h delete mode 100644 src/dusk/ui/widget/uicheckbox.c delete mode 100644 src/dusk/ui/widget/uicheckbox.h delete mode 100644 src/dusk/ui/widget/uidropdown.c delete mode 100644 src/dusk/ui/widget/uidropdown.h delete mode 100644 src/dusk/ui/widget/uiframe.c delete mode 100644 src/dusk/ui/widget/uiframe.h delete mode 100644 src/dusk/ui/widget/uiitem.c delete mode 100644 src/dusk/ui/widget/uiitem.h delete mode 100644 src/dusk/ui/widget/uiitemlist.c delete mode 100644 src/dusk/ui/widget/uiitemlist.h delete mode 100644 src/dusk/ui/widget/uilabel.c delete mode 100644 src/dusk/ui/widget/uilabel.h delete mode 100644 src/dusk/ui/widget/uimenu.c delete mode 100644 src/dusk/ui/widget/uimenu.h delete mode 100644 src/dusk/ui/widget/uimodal.c delete mode 100644 src/dusk/ui/widget/uimodal.h delete mode 100644 src/dusk/ui/widget/uiscrolling.c delete mode 100644 src/dusk/ui/widget/uiscrolling.h delete mode 100644 src/dusk/ui/widget/uislider.c delete mode 100644 src/dusk/ui/widget/uislider.h delete mode 100644 src/dusk/ui/widget/uitab.c delete mode 100644 src/dusk/ui/widget/uitab.h delete mode 100644 src/dusk/util/CMakeLists.txt delete mode 100644 src/dusk/util/array.c delete mode 100644 src/dusk/util/array.h delete mode 100644 src/dusk/util/crypt.c delete mode 100644 src/dusk/util/crypt.h delete mode 100644 src/dusk/util/endian.c delete mode 100644 src/dusk/util/endian.h delete mode 100644 src/dusk/util/math.c delete mode 100644 src/dusk/util/math.h delete mode 100644 src/dusk/util/memory.c delete mode 100644 src/dusk/util/memory.h delete mode 100644 src/dusk/util/random.c delete mode 100644 src/dusk/util/random.h delete mode 100644 src/dusk/util/ref.c delete mode 100644 src/dusk/util/ref.h delete mode 100644 src/dusk/util/sort.c delete mode 100644 src/dusk/util/sort.h delete mode 100644 src/dusk/util/string.c delete mode 100644 src/dusk/util/string.h delete mode 100644 src/duskdolphin/CMakeLists.txt delete mode 100644 src/duskdolphin/asset/CMakeLists.txt delete mode 100644 src/duskdolphin/asset/assetdolphindvd.c delete mode 100644 src/duskdolphin/asset/assetdolphindvd.h delete mode 100644 src/duskdolphin/asset/assetdolphinfat.c delete mode 100644 src/duskdolphin/asset/assetdolphinfat.h delete mode 100644 src/duskdolphin/asset/assetplatform.h delete mode 100644 src/duskdolphin/audio/CMakeLists.txt delete mode 100644 src/duskdolphin/audio/audiodolphin.c delete mode 100644 src/duskdolphin/audio/audiodolphin.h delete mode 100644 src/duskdolphin/audio/audioplatform.h delete mode 100644 src/duskdolphin/audio/audiostreamdolphin.c delete mode 100644 src/duskdolphin/audio/audiostreamdolphin.h delete mode 100644 src/duskdolphin/audio/audiostreammp3decoder.h delete mode 100644 src/duskdolphin/audio/audiostreamplatform.h delete mode 100644 src/duskdolphin/display/CMakeLists.txt delete mode 100644 src/duskdolphin/display/displaydolphin.c delete mode 100644 src/duskdolphin/display/displaydolphin.h delete mode 100644 src/duskdolphin/display/displayplatform.h delete mode 100644 src/duskdolphin/display/framebuffer/CMakeLists.txt delete mode 100644 src/duskdolphin/display/framebuffer/framebufferdolphin.c delete mode 100644 src/duskdolphin/display/framebuffer/framebufferdolphin.h delete mode 100644 src/duskdolphin/display/framebuffer/framebufferplatform.h delete mode 100644 src/duskdolphin/display/mesh/CMakeLists.txt delete mode 100644 src/duskdolphin/display/mesh/meshdolphin.c delete mode 100644 src/duskdolphin/display/mesh/meshdolphin.h delete mode 100644 src/duskdolphin/display/mesh/meshplatform.h delete mode 100644 src/duskdolphin/display/shader/CMakeLists.txt delete mode 100644 src/duskdolphin/display/shader/shaderdolphin.c delete mode 100644 src/duskdolphin/display/shader/shaderdolphin.h delete mode 100644 src/duskdolphin/display/shader/shaderplatform.h delete mode 100644 src/duskdolphin/display/shader/shaderunlitdolphin.c delete mode 100644 src/duskdolphin/display/texture/CMakeLists.txt delete mode 100644 src/duskdolphin/display/texture/texturedolphin.c delete mode 100644 src/duskdolphin/display/texture/texturedolphin.h delete mode 100644 src/duskdolphin/display/texture/textureplatform.h delete mode 100644 src/duskdolphin/duskplatform.h delete mode 100644 src/duskdolphin/input/CMakeLists.txt delete mode 100644 src/duskdolphin/input/inputdolphin.c delete mode 100644 src/duskdolphin/input/inputdolphin.h delete mode 100644 src/duskdolphin/input/inputplatform.h delete mode 100644 src/duskdolphin/log/CMakeLists.txt delete mode 100644 src/duskdolphin/log/log.c delete mode 100644 src/duskdolphin/log/logdolphin.h delete mode 100644 src/duskdolphin/network/CMakeLists.txt delete mode 100644 src/duskdolphin/network/networkdolphin.c delete mode 100644 src/duskdolphin/network/networkdolphin.h delete mode 100644 src/duskdolphin/network/networkplatform.h delete mode 100644 src/duskdolphin/save/CMakeLists.txt delete mode 100644 src/duskdolphin/save/savedevicedolphincard.c delete mode 100644 src/duskdolphin/save/savedevicedolphincard.h delete mode 100644 src/duskdolphin/save/savedevicedolphinnand.c delete mode 100644 src/duskdolphin/save/savedevicedolphinnand.h delete mode 100644 src/duskdolphin/save/savedevicedolphinsd.c delete mode 100644 src/duskdolphin/save/savedevicedolphinsd.h delete mode 100644 src/duskdolphin/save/savedeviceplatform.h delete mode 100644 src/duskdolphin/script/module/moduleplatformdolphin.h delete mode 100644 src/duskdolphin/script/module/moduleplatformplatform.h delete mode 100644 src/duskdolphin/system/CMakeLists.txt delete mode 100644 src/duskdolphin/system/systemdolphin.c delete mode 100644 src/duskdolphin/system/systemdolphin.h delete mode 100644 src/duskdolphin/system/systemplatform.h delete mode 100644 src/duskdolphin/time/CMakeLists.txt delete mode 100644 src/duskdolphin/time/timedolphin.c delete mode 100644 src/duskdolphin/time/timedolphin.h delete mode 100644 src/duskdolphin/time/timeplatform.h delete mode 100644 src/duskgl/CMakeLists.txt delete mode 100644 src/duskgl/assert/assertgl.h delete mode 100644 src/duskgl/display/CMakeLists.txt delete mode 100644 src/duskgl/display/displaygl.c delete mode 100644 src/duskgl/display/displaygl.h delete mode 100644 src/duskgl/display/framebuffer/CMakeLists.txt delete mode 100644 src/duskgl/display/framebuffer/framebuffergl.c delete mode 100644 src/duskgl/display/framebuffer/framebuffergl.h delete mode 100644 src/duskgl/display/framebuffer/framebufferplatform.h delete mode 100644 src/duskgl/display/mesh/CMakeLists.txt delete mode 100644 src/duskgl/display/mesh/meshgl.c delete mode 100644 src/duskgl/display/mesh/meshgl.h delete mode 100644 src/duskgl/display/mesh/meshplatform.h delete mode 100644 src/duskgl/display/shader/CMakeLists.txt delete mode 100644 src/duskgl/display/shader/shadergl.c delete mode 100644 src/duskgl/display/shader/shadergl.h delete mode 100644 src/duskgl/display/shader/shaderplatform.h delete mode 100644 src/duskgl/display/shader/shaderunlitgl.c delete mode 100644 src/duskgl/display/texture/CMakeLists.txt delete mode 100644 src/duskgl/display/texture/texturegl.c delete mode 100644 src/duskgl/display/texture/texturegl.h delete mode 100644 src/duskgl/display/texture/textureplatform.h delete mode 100644 src/duskgl/duskgl.h delete mode 100644 src/duskgl/error/CMakeLists.txt delete mode 100644 src/duskgl/error/errorgl.c delete mode 100644 src/duskgl/error/errorgl.h delete mode 100644 src/dusklinux/CMakeLists.txt delete mode 100644 src/dusklinux/asset/CMakeLists.txt delete mode 100644 src/dusklinux/asset/assetlinux.c delete mode 100644 src/dusklinux/asset/assetlinux.h delete mode 100644 src/dusklinux/asset/assetplatform.h delete mode 100644 src/dusklinux/audio/CMakeLists.txt delete mode 100644 src/dusklinux/audio/audiolinux.c delete mode 100644 src/dusklinux/audio/audiolinux.h delete mode 100644 src/dusklinux/audio/audioplatform.h delete mode 100644 src/dusklinux/audio/audiostreamlinux.c delete mode 100644 src/dusklinux/audio/audiostreamlinux.h delete mode 100644 src/dusklinux/audio/audiostreammp3decoder.h delete mode 100644 src/dusklinux/audio/audiostreamplatform.h delete mode 100644 src/dusklinux/display/displayplatform.h delete mode 100644 src/dusklinux/duskplatform.h delete mode 100644 src/dusklinux/input/CMakeLists.txt delete mode 100644 src/dusklinux/input/inputlinux.c delete mode 100644 src/dusklinux/input/inputlinux.h delete mode 100644 src/dusklinux/input/inputplatform.h delete mode 100644 src/dusklinux/log/CMakeLists.txt delete mode 100644 src/dusklinux/log/log.c delete mode 100644 src/dusklinux/network/CMakeLists.txt delete mode 100644 src/dusklinux/network/networklinux.c delete mode 100644 src/dusklinux/network/networklinux.h delete mode 100644 src/dusklinux/network/networkplatform.h delete mode 100644 src/dusklinux/save/CMakeLists.txt delete mode 100644 src/dusklinux/save/savedevicelinux.c delete mode 100644 src/dusklinux/save/savedevicelinux.h delete mode 100644 src/dusklinux/save/savedeviceplatform.h delete mode 100644 src/dusklinux/script/module/moduleplatformlinux.h delete mode 100644 src/dusklinux/script/module/moduleplatformplatform.h delete mode 100644 src/dusklinux/system/CMakeLists.txt delete mode 100644 src/dusklinux/system/systemlinux.c delete mode 100644 src/dusklinux/system/systemlinux.h delete mode 100644 src/dusklinux/system/systemplatform.h delete mode 100644 src/dusklinux/time/CMakeLists.txt delete mode 100644 src/dusklinux/time/timelinux.c delete mode 100644 src/dusklinux/time/timelinux.h delete mode 100644 src/dusklinux/time/timeplatform.h delete mode 100644 src/dusklinux/util/CMakeLists.txt delete mode 100644 src/dusklinux/util/mkdirp.c delete mode 100644 src/dusklinux/util/mkdirp.h delete mode 100644 src/duskmad/CMakeLists.txt delete mode 100644 src/duskmad/audiostreammp3decodersw.c delete mode 100644 src/duskmad/audiostreammp3decodersw.h delete mode 100644 src/duskmad/audiostreammp3ring.c delete mode 100644 src/duskmad/audiostreammp3ring.h delete mode 100644 src/dusknetwork/CMakeLists.txt delete mode 100644 src/dusknetwork/network/CMakeLists.txt delete mode 100644 src/dusknetwork/network/network.c delete mode 100644 src/dusknetwork/network/network.h delete mode 100644 src/dusknetwork/network/networkinfo.c delete mode 100644 src/dusknetwork/network/networkinfo.h delete mode 100644 src/duskpsp/CMakeLists.txt delete mode 100644 src/duskpsp/asset/CMakeLists.txt delete mode 100644 src/duskpsp/asset/assetpbp.c delete mode 100644 src/duskpsp/asset/assetpbp.h delete mode 100644 src/duskpsp/asset/assetplatform.h delete mode 100644 src/duskpsp/asset/assetpsp.c delete mode 100644 src/duskpsp/asset/assetpsp.h delete mode 100644 src/duskpsp/audio/CMakeLists.txt delete mode 100644 src/duskpsp/audio/audioplatform.h delete mode 100644 src/duskpsp/audio/audiopsp.c delete mode 100644 src/duskpsp/audio/audiopsp.h delete mode 100644 src/duskpsp/audio/audiostreammp3decoder.c delete mode 100644 src/duskpsp/audio/audiostreammp3decoder.h delete mode 100644 src/duskpsp/audio/audiostreamplatform.h delete mode 100644 src/duskpsp/audio/audiostreampsp.c delete mode 100644 src/duskpsp/audio/audiostreampsp.h delete mode 100644 src/duskpsp/display/displayplatform.h delete mode 100644 src/duskpsp/duskplatform.h delete mode 100644 src/duskpsp/input/CMakeLists.txt delete mode 100644 src/duskpsp/input/inputplatform.h delete mode 100644 src/duskpsp/input/inputpsp.c delete mode 100644 src/duskpsp/input/inputpsp.h delete mode 100644 src/duskpsp/log/CMakeLists.txt delete mode 100644 src/duskpsp/log/log.c delete mode 100644 src/duskpsp/network/CMakeLists.txt delete mode 100644 src/duskpsp/network/networkplatform.h delete mode 100644 src/duskpsp/network/networkpsp.c delete mode 100644 src/duskpsp/network/networkpsp.h delete mode 100644 src/duskpsp/save/CMakeLists.txt delete mode 100644 src/duskpsp/save/savedeviceplatform.h delete mode 100644 src/duskpsp/save/savedevicepsp.c delete mode 100644 src/duskpsp/save/savedevicepsp.h delete mode 100644 src/duskpsp/script/module/moduleplatformplatform.h delete mode 100644 src/duskpsp/script/module/moduleplatformpsp.h delete mode 100644 src/duskpsp/system/CMakeLists.txt delete mode 100644 src/duskpsp/system/systemplatform.h delete mode 100644 src/duskpsp/system/systempsp.c delete mode 100644 src/duskpsp/system/systempsp.h delete mode 100644 src/duskpsp/time/CMakeLists.txt delete mode 100644 src/duskpsp/time/timeplatform.h delete mode 100644 src/duskpsp/time/timepsp.c delete mode 100644 src/duskpsp/time/timepsp.h delete mode 100644 src/dusksdl2/CMakeLists.txt delete mode 100644 src/dusksdl2/display/CMakeLists.txt delete mode 100644 src/dusksdl2/display/displaysdl2.c delete mode 100644 src/dusksdl2/display/displaysdl2.h delete mode 100644 src/dusksdl2/dusksdl2.h delete mode 100644 src/dusksdl2/input/CMakeLists.txt delete mode 100644 src/dusksdl2/input/inputsdl2.c delete mode 100644 src/dusksdl2/input/inputsdl2.h delete mode 100644 src/dusksdl2/time/CMakeLists.txt delete mode 100644 src/dusksdl2/time/timeplatform.h delete mode 100644 src/dusksdl2/time/timesdl2.c delete mode 100644 src/dusksdl2/time/timesdl2.h delete mode 100644 src/duskvita/CMakeLists.txt delete mode 100644 src/duskvita/asset/CMakeLists.txt delete mode 100644 src/duskvita/asset/assetplatform.h delete mode 100644 src/duskvita/asset/assetvita.c delete mode 100644 src/duskvita/asset/assetvita.h delete mode 100644 src/duskvita/display/displayplatform.h delete mode 100644 src/duskvita/duskplatform.h delete mode 100644 src/duskvita/input/CMakeLists.txt delete mode 100644 src/duskvita/input/inputvita.c delete mode 100644 src/duskvita/log/CMakeLists.txt delete mode 100644 src/duskvita/log/log.c delete mode 100644 src/duskvita/script/module/moduleplatformplatform.h delete mode 100644 src/duskvita/script/module/moduleplatformvita.h delete mode 100644 story.md delete mode 100644 test/CMakeLists.txt delete mode 100644 test/animation/CMakeLists.txt delete mode 100644 test/animation/test_animation.c delete mode 100644 test/animation/test_easing.c delete mode 100644 test/animation/test_keyframe.c delete mode 100644 test/assert/CMakeLists.txt delete mode 100644 test/assert/test_assert.c delete mode 100644 test/asset/CMakeLists.txt delete mode 100644 test/asset/test_asset.c delete mode 100644 test/asset/test_assetjsonloader.c delete mode 100644 test/asset/test_assetlocale.c delete mode 100644 test/asset/test_assettilesetloader.c delete mode 100644 test/console/CMakeLists.txt delete mode 100644 test/console/test_console.c delete mode 100644 test/display/CMakeLists.txt delete mode 100644 test/display/test_color.c delete mode 100644 test/dusktest.h delete mode 100644 test/error/CMakeLists.txt delete mode 100644 test/error/test_error.c delete mode 100644 test/item/CMakeLists.txt delete mode 100644 test/item/test_inventory.c delete mode 100644 test/rpg/CMakeLists.txt delete mode 100644 test/rpg/battle/CMakeLists.txt delete mode 100644 test/rpg/battle/test_battle.c delete mode 100644 test/rpg/cutscene/CMakeLists.txt delete mode 100644 test/rpg/cutscene/test_cutscene.c delete mode 100644 test/rpg/cutscene/test_cutscenecontrol.c delete mode 100644 test/rpg/cutscene/test_cutscenemaparea.c delete mode 100644 test/rpg/cutscene/test_cutscenesystem.c delete mode 100644 test/rpg/entity/CMakeLists.txt delete mode 100644 test/rpg/entity/entitytestfixture.c delete mode 100644 test/rpg/entity/entitytestfixture.h delete mode 100644 test/rpg/entity/test_entity.c delete mode 100644 test/rpg/entity/test_entityinteract.c delete mode 100644 test/rpg/entity/test_entityitem.c delete mode 100644 test/rpg/entity/test_npc.c delete mode 100644 test/rpg/overworld/CMakeLists.txt delete mode 100644 test/rpg/overworld/test_maparea.c delete mode 100644 test/rpg/test_rpg.c delete mode 100644 test/save/CMakeLists.txt delete mode 100644 test/save/savetestfixture.c delete mode 100644 test/save/savetestfixture.h delete mode 100644 test/save/test_save.c delete mode 100644 test/save/test_savedevice.c delete mode 100644 test/save/test_savedevicelinux.c delete mode 100644 test/save/test_savesettings.c delete mode 100644 test/save/test_saveslot.c delete mode 100644 test/thread/CMakeLists.txt delete mode 100644 test/thread/test_thread.c delete mode 100644 test/time/CMakeLists.txt delete mode 100644 test/time/test_time.c delete mode 100644 test/util/CMakeLists.txt delete mode 100644 test/util/test_array.c delete mode 100644 test/util/test_math.c delete mode 100644 test/util/test_memory.c delete mode 100644 test/util/test_sort.c delete mode 100644 test/util/test_string.c delete mode 100644 tools/CMakeLists.txt delete mode 100644 tools/asset/chunk/__main__.py delete mode 100644 tools/asset/dmf/__init__.py delete mode 100644 tools/asset/dmf/__main__.py delete mode 100644 tools/asset/pack/__main__.py delete mode 100644 tools/color.py delete mode 100644 tools/editor/common/dtf.js delete mode 100644 tools/editor/common/png.js delete mode 100644 tools/editor/index.html delete mode 100644 tools/editor/styles/components/bg-swatches.css delete mode 100644 tools/editor/styles/components/btn.css delete mode 100644 tools/editor/styles/components/control-row.css delete mode 100644 tools/editor/styles/components/empty-state.css delete mode 100644 tools/editor/styles/components/file-name.css delete mode 100644 tools/editor/styles/components/hero.css delete mode 100644 tools/editor/styles/components/info-table.css delete mode 100644 tools/editor/styles/components/load-area.css delete mode 100644 tools/editor/styles/components/panel-inputs.css delete mode 100644 tools/editor/styles/components/panel-section.css delete mode 100644 tools/editor/styles/components/preview-empty.css delete mode 100644 tools/editor/styles/components/preview-scroll.css delete mode 100644 tools/editor/styles/components/site-header.css delete mode 100644 tools/editor/styles/components/tool-card.css delete mode 100644 tools/editor/styles/components/tool-panel.css delete mode 100644 tools/editor/styles/components/tool-preview.css delete mode 100644 tools/editor/styles/components/warning-list.css delete mode 100644 tools/editor/styles/components/warnings-section.css delete mode 100644 tools/editor/styles/elements.css delete mode 100644 tools/editor/styles/generic.css delete mode 100644 tools/editor/styles/main.css delete mode 100644 tools/editor/styles/objects/page.css delete mode 100644 tools/editor/styles/objects/section-label.css delete mode 100644 tools/editor/styles/objects/section.css delete mode 100644 tools/editor/styles/objects/tool-grid.css delete mode 100644 tools/editor/styles/objects/tool-workspace.css delete mode 100644 tools/editor/styles/settings.css delete mode 100644 tools/editor/texture-padder/index.html delete mode 100644 tools/editor/texture-padder/texture-padder.js delete mode 100644 tools/editor/texture/index.html delete mode 100644 tools/editor/texture/texture.js delete mode 100644 tools/input.py delete mode 100644 tools/makedolphiniso.py delete mode 100644 tools/run_python/CMakeLists.txt delete mode 100755 tools/server.py diff --git a/.github/actions/setup-devkitpro/action.yml b/.github/actions/setup-devkitpro/action.yml deleted file mode 100644 index b65ad15d..00000000 --- a/.github/actions/setup-devkitpro/action.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Setup devkitPro -description: Install devkitPro with GameCube/Wii packages on an Ubuntu runner - -runs: - using: composite - steps: - - name: Install apt dependencies - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y \ - cmake \ - python3 \ - python3-pip \ - python3-polib \ - python3-pil \ - python3-dotenv \ - python3-pyqt5 \ - python3-opengl \ - xorriso - - name: Install devkitPro pacman - shell: bash - run: | - sudo ln -sf /proc/self/mounts /etc/mtab - wget https://apt.devkitpro.org/install-devkitpro-pacman \ - -O /tmp/install-devkitpro-pacman - echo "=== installer script contents ===" - cat /tmp/install-devkitpro-pacman - echo "=================================" - chmod +x /tmp/install-devkitpro-pacman - sudo /tmp/install-devkitpro-pacman - echo "DEVKITPRO=/opt/devkitpro" >> $GITHUB_ENV - echo "DEVKITPPC=/opt/devkitpro/devkitPPC" >> $GITHUB_ENV - echo "/opt/devkitpro/tools/bin" >> $GITHUB_PATH - echo "/opt/devkitpro/devkitPPC/bin" >> $GITHUB_PATH - - name: Install devkitPro GameCube/Wii packages - shell: bash - run: | - sudo dkp-pacman -S --needed --noconfirm \ - gamecube-dev \ - gamecube-sdl2 \ - ppc-liblzma \ - ppc-libzip \ - libogc2 \ - gamecube-tools \ - ppc-libmad \ - ppc-zlib-ng \ - ppc-bzip2 \ - ppc-zstd diff --git a/.github/actions/setup-pspdev/action.yml b/.github/actions/setup-pspdev/action.yml deleted file mode 100644 index 8db48e7e..00000000 --- a/.github/actions/setup-pspdev/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Setup pspdev -description: Install the pspdev PSP toolchain on an Ubuntu runner - -runs: - using: composite - steps: - - name: Install dependencies - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y cmake python3 python3-pip python3-dotenv - - name: Install pspdev toolchain - shell: bash - run: | - wget -q \ - https://github.com/pspdev/pspdev/releases/latest/download/pspdev-ubuntu-latest-x86_64.tar.gz \ - -O /tmp/pspdev.tar.gz - sudo tar -xzf /tmp/pspdev.tar.gz -C /usr/local - echo "PSPDEV=/usr/local/pspdev" >> $GITHUB_ENV - echo "/usr/local/pspdev/bin" >> $GITHUB_PATH diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e39129b4..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,247 +0,0 @@ -name: Build Dusk -on: - push: - tags: - - '*' -jobs: - build-linux: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - cmake \ - python3 \ - python3-pip \ - python3-polib \ - python3-pil \ - libsdl2-dev \ - libgl1-mesa-dev \ - libzip-dev \ - python3-dotenv \ - python3-pyqt5 \ - python3-opengl \ - xz-utils \ - liblzma-dev \ - libbz2-dev \ - zlib1g-dev \ - git \ - libssl-dev - - name: Build Linux - run: ./scripts/build-linux.sh - - name: Upload Linux binary - uses: actions/upload-artifact@v3 - with: - name: dusk-linux - path: build-linux/Dusk - if-no-files-found: error - - build-psp: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup pspdev - uses: ./.github/actions/setup-pspdev - - name: Build PSP - run: ./scripts/build-psp.sh - - name: Move EBOOT.PBP to Dusk subfolder - run: | - mkdir -p ./git-artifcats/Dusk/PSP/GAME/Dusk - cp build-psp/EBOOT.PBP ./git-artifcats/Dusk/PSP/GAME/Dusk/EBOOT.PBP - - name: Upload PSP binary - uses: actions/upload-artifact@v3 - with: - name: dusk-psp - path: ./git-artifcats/Dusk - if-no-files-found: error - - build-knulli: - runs-on: ubuntu-latest - container: - image: debian:trixie - steps: - - name: Install Node.js - run: apt-get update && apt-get install -y nodejs - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install dependencies - run: | - dpkg --add-architecture arm64 - apt-get update - apt-get install -y --no-install-recommends \ - crossbuild-essential-arm64 \ - ca-certificates \ - pkg-config \ - cmake \ - make \ - ninja-build \ - git \ - file \ - python3 \ - python3-pip \ - python3-polib \ - python3-pil \ - python3-dotenv \ - python3-pyqt5 \ - python3-opengl \ - liblua5.4-dev:arm64 \ - xz-utils:arm64 \ - libbz2-dev:arm64 \ - zlib1g-dev:arm64 \ - libzip-dev:arm64 \ - libssl-dev:arm64 \ - libsdl2-dev:arm64 \ - liblzma-dev:arm64 \ - libopengl0:arm64 \ - libgl1:arm64 \ - libegl1:arm64 \ - libgles2:arm64 \ - libgl1-mesa-dev:arm64 - - name: Build Knulli - run: ./scripts/build-knulli.sh - - name: Move output to Dusk subfolder - run: | - mkdir -p ./git-artifcats/Dusk - cp -r build-knulli/dusk ./git-artifcats/Dusk - - name: Upload Knulli binary - uses: actions/upload-artifact@v3 - with: - name: dusk-knulli - path: ./git-artifcats/Dusk - if-no-files-found: error - - build-gamecube: - runs-on: ubuntu-latest - container: - image: ghcr.io/extremscorner/libogc2:latest - steps: - - name: Install Node.js - run: apt-get update && apt-get install -y nodejs - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install additional dependencies - run: | - apt-get install -y \ - python3-pip python3-polib python3-pil \ - python3-dotenv python3-pyqt5 python3-opengl - dkp-pacman -Syu --noconfirm - dkp-pacman -S --needed --noconfirm \ - gamecube-sdl2 ppc-liblzma ppc-libzip \ - gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd - - name: Build GameCube - run: ./scripts/build-gamecube.sh - - name: Copy output files - run: | - mkdir -p ./git-artifcats/Dusk - cp build-gamecube/Dusk.dol ./git-artifcats/Dusk/Dusk.dol - cp build-gamecube/dusk.dsk ./git-artifcats/Dusk/dusk.dsk - - name: Upload GameCube binary - uses: actions/upload-artifact@v3 - with: - name: dusk-gamecube - path: ./git-artifcats/Dusk - if-no-files-found: error - - build-gamecube-iso: - runs-on: ubuntu-latest - container: - image: ghcr.io/extremscorner/libogc2:latest - steps: - - name: Install Node.js - run: apt-get update && apt-get install -y nodejs - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install additional dependencies - run: | - apt-get install -y \ - python3-pip python3-polib python3-pil \ - python3-dotenv python3-pyqt5 python3-opengl xorriso - dkp-pacman -Syu --noconfirm - dkp-pacman -S --needed --noconfirm \ - gamecube-sdl2 ppc-liblzma ppc-libzip \ - gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd - - name: Build GameCube ISO - run: ./scripts/build-gamecube-iso.sh - - name: Copy output files - run: | - mkdir -p ./git-artifcats/Dusk - cp build-gamecube-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso - cp build-gamecube-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso - cp build-gamecube-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso - - name: Upload GameCube ISO - uses: actions/upload-artifact@v3 - with: - name: dusk-gamecube-iso - path: ./git-artifcats/Dusk - if-no-files-found: error - - build-wii: - runs-on: ubuntu-latest - container: - image: ghcr.io/extremscorner/libogc2:latest - steps: - - name: Install Node.js - run: apt-get update && apt-get install -y nodejs - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install additional dependencies - run: | - apt-get install -y \ - python3-pip python3-polib python3-pil \ - python3-dotenv python3-pyqt5 python3-opengl - dkp-pacman -Syu --noconfirm - dkp-pacman -S --needed --noconfirm \ - gamecube-sdl2 ppc-liblzma ppc-libzip \ - gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd - - name: Build Wii - run: ./scripts/build-wii.sh - - name: Copy output files - run: | - mkdir -p ./git-artifcats/Dusk/apps/Dusk - cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol - cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk - cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml - - name: Upload Wii binary - uses: actions/upload-artifact@v3 - with: - name: dusk-wii - path: ./git-artifcats/Dusk - if-no-files-found: error - - build-wii-iso: - runs-on: ubuntu-latest - container: - image: ghcr.io/extremscorner/libogc2:latest - steps: - - name: Install Node.js - run: apt-get update && apt-get install -y nodejs - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install additional dependencies - run: | - apt-get install -y \ - python3-pip python3-polib python3-pil \ - python3-dotenv python3-pyqt5 python3-opengl xorriso - dkp-pacman -Syu --noconfirm - dkp-pacman -S --needed --noconfirm \ - gamecube-sdl2 ppc-liblzma ppc-libzip \ - gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd - - name: Build Wii ISO - run: ./scripts/build-wii-iso.sh - - name: Copy output files - run: | - mkdir -p ./git-artifcats/Dusk - cp build-wii-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso - cp build-wii-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso - cp build-wii-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso - - name: Upload Wii ISO - uses: actions/upload-artifact@v3 - with: - name: dusk-wii-iso - path: ./git-artifcats/Dusk - if-no-files-found: error diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index f2cbf7af..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test Dusk -on: - pull_request: - branches: - - main -jobs: - run-tests: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - cmake \ - python3 \ - python3-pip \ - python3-polib \ - python3-pil \ - libsdl2-dev \ - libgl1-mesa-dev \ - libzip-dev \ - python3-dotenv \ - python3-pyqt5 \ - python3-opengl \ - xz-utils \ - liblzma-dev \ - libbz2-dev \ - zlib1g-dev \ - git \ - libssl-dev - - name: Run tests - run: ./scripts/test-linux.sh diff --git a/README.md b/README.md deleted file mode 100644 index 889197d6..00000000 --- a/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Dusk -RPG Game Project, small and able to run on a PSP. - -# Building -Each build target has different requirements. You can take a look at the git -workflow to see how the builds are done for each target. In addition, for -accessing the editor and building the game on your host system, install the -following packages, depending on your system; - -Fedora; -``` -sudo dnf install git make gcc cmake python python-polib python3-pillow python3-dotenv python3-numpy python-qt5 python3-pyopengl SDL2-devel zlib-devel libzip-devel bzip2-devel openssl-devel lzma-sdk-devel xz xz-devel lua-devel libmad-devel -``` -`libmad-devel` (used for MP3 decoding) is GPL-licensed and ships in the -[RPM Fusion](https://rpmfusion.org/) free repository rather than Fedora's -base repos - make sure RPM Fusion is enabled first. - -Ubuntu; -``` -sudo apt-get install git build-essential gcc python python-polib python3-pillow python3-dotenv python3-numpy python3-pyqt5 python3-opengl libmad0-dev -``` - -Arch Linux; -``` -sudo pacman -S git base-devel gcc python python-polib python-pillow python-dotenv python-numpy python-pyqt5 python-opengl libmad -``` \ No newline at end of file diff --git a/assets/audio/audiotest.mp3 b/assets/audio/audiotest.mp3 deleted file mode 100644 index 5f46ebfd91c1150b40dc45bd8f83e22533c79bb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17179 zcmeHO2~<;8yS)hn2nGm)Gz`j&fT%YCBq$JO5Ch1hC@P~MRAsWxnu%Eu5V4>rlY)R_ zDJl*P$l!zu;(#KJ2u>(P1VMtyc_-4Y_rKqEy}#CiUGLqsNG><`oOAEl+26O%zF!tN zn1~~yP20!Y+Y!FYB8V70XhYasliB3iWR(2z!|@j|mRb8R|Bml2SQi!r_vGN80fN}| zBLsqi0*OS@(9qMmwACR3@_*47RVuCA`0o=cW2S+OD{BqTC2Iy!p8hHcxnaX2X{ zDH$0#IXU_H#l^)(jvPOJyt?}Q`SW#kmoHzwdGpSlI}aZ|e*F0P^S-{m!NJkdQ2<0E z!)pxrc#Wcee%o|$+s1e8`UlEyZT{`>|CM(LPy>P}8D~>{&oHORm_M6hWrN@{6*(;P z``+Q+ICAdWqB^f>_vAah)UiPaQgf4QGP3fdUZ$nwZd%G=?M+7;YW>AE&9J1|;+ids zX12Z9T$)tWJG^O#IpB9?;Hg=^%-F~XkK1VIwzVywpYKHl&6db5L`)8Dhj{&ShnojMnNnbWXfx?8c*-2{vwI z8EG+m^g#lCwI5Zwy!Mu}Sp4fNLWw6Nl8H??U3+J=i2;JTkcu>elMYpUFm^#$7?zZp zfnStRrb6RnaN&>=VWwfykWvH~+ufkZmOeR?ba4Q|kP|{y@!q&_ zmMLQ5BZ*q^Y@#h|o&*vocCCgSYA!*Td4vZ*KWH0fyaGL-4Gar@<_cSw;)OVeIHgqk zIo{sOXNbwH+2HN7zo5#$VXLC-A|tUU&u*&8QR9-kH&uD;r@J7oBMKU6PKIwzp8$P= z0FP4z^FgIEy!tW53(!Wlb#pYGjrxi<6Qx>{+Lval{49)ik{3FkrJ`0_sGZKY$@W;h zWaK52N6p_6Td!WYcIow{OqUDBy%a){s{42FAI@i%GZlT$?8J0cJI7#g;&2*b7l|0|nKrb4MkZCR3P%lXH~ZJEscQ*6|=Ld#BHJ|mxU!9 z8z9PluxA;KBQ$Zt!;w&Xh(#c?2+grMM8;xdD0cQMtu@s%Y4mzkiUTv&Bv0K$kun1{ z)+@m^IHL|zy^%!}CFDD=HK!1p)>~X>NEHaq`&$6q2j7F4pl=v}GeE!v;N1y6V_?5v z8>5BD(#9uu*Krc5y@f$Cvjb^zCxZ&d&IVm4qJiHgOkeY|?gu$ZoKpUgMe=_7hDYpq znq@h6B2Ui;y~0~y2#i30Krduz#pECaIdq_rDEGd!PG>yIO(;TeF!S4K{i{mwE*Nj3 zB_>XHHLkvUjkL+IpInJgg=!($^~HWI0b)bW(r5*~Xqs#o?w zZ2*>lVK{+YAx{7Xc?>YZ0Am2~1iheO>^LwPiZ3LvgR`(f<(WYWr9m>rfivZP2uc|{ z7gR#j55(fcBl1*^yLR#F_aw3I7Hg*_SbNP^%4xrNLZn}M1vG&OAwGZtX>t(S#U?l& zBg*cmWla)76{v;HCe)vaL3h{2K_1Pe*w>%8)>gNoc{GqIZdg;34!t-PFK_gjzCGS_ z0XiS@2%)GT-zlw&DkQ4M-M<6IdBg)yU}giI|K?GjFb4z(&AH&imhCtKJwS`#N@-r` zowlxoPK@bKTQBF5w%t>ZHl;A1eoV>?t^K)+vs?X!&s1f@89O`m&n#b3t|7{E;i6Cq z1KkgkgbYMU>1o!fy?RmK~wTwtp??n*t=aS3R z{8jUP)ZN@??DYsPtUkwpnj0#3H~`$v-~Sb%|6PoJu`@(@2y*D^cpg$1N9c?7{-1P2 zIVx*LOr{=KNLzM+V(2p8Y9ozn{r#1T6e?!}I%e01ZnO(P{jrNs&xWa}v8_ES&73XH zl+I5()}H3k-nUH^Y6NfsyoB(a5k`F|_)zds%5?xC@9Bv?51}IDPn>w+)$|_Q^d^<8 zh}QgUm3ZxW^83xMcO6(apT)T#pO@~}v`KRRnwIG2DP>7T;)k^d-8)YB^`3?z`!y8s z1vJnp?1zyQ{1)Z@lN#HJk{N9flm_l5fYtZon_9<@arg7%awJuDg25S5ow zJ16F%N$kKSAM!w|szTnC^sJI=tp|E7^1a9zI8XNDEp}#SgU!|U`o|f8ve&|X0hdwv z)D%<+Xy98bAy^x4q~~!T865&v0=B#=MI&=cBS^k*z%{Z830^myyDZw81J|+6Y+Q461YxIluKqy(`Ty8hU*~|rz7$d zCOXRf@px#DA>~N1={rXYqg=Ab5XdSK0d#G!8Dt@vMIU^Y5`zj%h3clgy!NL?!K5Dgjnp9Ov8_! zf`WLr?^l8lqGijr?aDZ8Q6MdU-ZUb4n^j%4N4IB9vrSC$Tjjh4cUGw@`A`J@ut(jV z23GuIS8216x8@!}y&ZthEd$*9-~kuT?7Nw*!;EgE#7KIOe52kmNAbHCV#Jf12P2)Y zn8YDl2Z`!h_4!ljuE*zchPoL#zK7Bl;cdgttKud2>Ci&}0$MmUkY2)7SI5W}FcvvE z2*q>oM+`~FA%}FuLvUXSLKO(|1^vF;VTR0b*6qk^Cu4n;gZE}cM+a^hiaD2YC^%7E zLH2HZcuj7vR!R2eoP-AnnpvBc-U(Ha-JAir4ygJ&N)`n-PdqRDl- z>GP5SpWfc-YnsTCi=Nam8&;97OVC^Aw_mHuy~JF9nm$+ce8-#~9solO=H4_Tu4Zq(WL-jBB~vdK?1+V!2)WRh5Q)M9Ay=3o(t~g%3_uHy5ANrJl_#wxheKIx`~|~l zA`b1O7GrpK{60&BeNH10K=FhbD(u5G$*J-=LwHW0Wl_$~POa}(Tc6@QQ;N7&De<$P zy3(^)Tvs^`)9}t_uu<>rxx5FtPy!1xqCh}f8Js}D!Tu#ej2ORoZ1D-va2Hl-s z7gYg5E}tu$0T)&M0PNrbXeOl#On|V!NqllRv>S`hocR%W=^*;WIs}3)kcKfU6@ZSjE#FxaVf-6d~mb&+pCV- z{g-k<2cJ6z1uzXlG^Pf?%ZV`fS|^P|%yERW&lY2VQv2u7;q%+T6%d|Ccb-mACtvmz zlZuGLyx-1|k?gFO^<#0wmKzL)yUS#5q8c1}YR62iBfHs6AH`W7FY!Ia151V(@M{mh z-m@J!lY>wO8&4mP za>>=jm|N|{&!6o-+Y&+cU^3}mnshS?LEEQU>u?$_XqZl>ZKRt9i#H=Sh3O_bW>=Tb ze;=FqX5Nnuo{P$^IS21P|4lW-BuWtQD>?zfTY@l2138H)(TKi+pKO?>jee8Mrj6kZ z_DN+g-zVxXD0E8P8hSj$$Tdv8;YyHxioRrUMIa(!uPIxceq#tLwrdN&0IqppHiK;B zGu|?K7=oWZ<{*(vew+IWG(mqm!()=355YY2D?)iN%1t5nt&w2wsiG4Uox2^gfMm{^P15U{lRKTQN|LiFBViw_ zBrqjBHe~iSJ)iUTAfCo&K-+Z;3b!)g0avH;5E2srcx8b1VGl?L`_F(6fs?cz+QpWN zpP_ynDuq1j3C=^~84BsrN*7{)gq^=i(_mVk>k-B@x8d!DEkM6BG!tqEM)(Y z)R^j1<&Cxtcb;qx9X6Wc4>dwBR0#nhguvLvFkeWdHAa(iH?Sn;m4nkJik5q zO{p_+U6aOYzf##33!hC1bd`{%vP*Hv24R>*xY|sNcUI1M`INC~Cpb_aK#Na68-w!g z26V`?e|uWn@u6B!4lZqk)gqIJ(9v;(_Dr-=_&p5)BOf9`kJ2WrLz{QHr}gAEYst5y zJvbgfcop-AFzs<#b7YfAtz}0TC&|*y(n?Ct@SA-bI=5W*t>(c;)zIr46*)Z&QT2zP z9Z8d^jb8ISdYg0diFtFw<8RLXoL94{%ZT*8Q3g-%ak|p_boI@5>NgEZm)S?^qOG)4 zACcA4ug|K|Lh^#O*INM}KB^698`{2~3m>d+Jkc>Y0x~8Cp+qeH zq~S+Tfr8T(c?y3pZxenP(Ui~X>B?@3*dK{Gu4s|CdmgJ|T1(Kez4K2cS`x4R*y7|u z-S4<9`nAOonP+;4-nZ$qxF?@4qlp%tBE!>}4*_dYBI!;i@xAlj6sc7?17d66# zl!*tZLR8Sm=15(bp(@Hla}soLUk5^nre_XrbMCXQ;%h1AhA_!~mZQ}=Jt{R!tI=dx zV%JUSQoSW=w)_}wR>f^61M$MD73t!f?M`qi8N!09U*5sxXBp@lyT}j}A$S|<&$>&fw!bbdH>ZW2Ngmc$FZ7XYypcb5dh~Porh%}6xw)B0&M&$&Qkr+y+5ouX z`l%qqdLvkz0s9>Wt)O(`5fVKfiic882O6hrSd8_tpmD;v^7{jRg$0e1Bosa=X#D*N zo6w_QX+h(>$>Go^1&tGm#XqW`v3PP2`lO(70ulO$6*T^jvhqLcpM2i=zgy7wd0l;G ztBLSCzgR(I`L7)M%zOQuX5)n<|5niWIUW6#HvhbW#=mm6f3mFzp-&1L|70J3{q%oK zLF1{(;n1#sFKGPhQ~u|A_~#Tf-kThy@JT`A1S*A!ihpE5;{?vocpmzRIK5g)32$oyrOmCEF&mu z5n7b8ULlD6L3toSN@xo6@$$QiU z@iE;kD$zlZI2dDsMIy>Sn*^UO-k(3YCi%lcwdRY(p7#uzy|(U6FttBVoiRo;kW zs2|*J)ekDvt0vRc)o_ZfF}ouJ{SzfKA%tGF?R`OWU{b=Rhic*HiQKaa8_T5L)qA@WQ-lc{O(tsQ1{ zuB9{v(VUvvdUBN)H4+~_JIx|dgV#SI8&t6N@td7kY~j>{{zL2MTq4a}Z6iZl9J4_Q zKU-O)#yaxhi*uE(y4{xMEAkwT7v@F{-hWkatABH|c>%QiKX`*Gf^-0Jl*x_`ZaZ1Mxs|< z+gRKc;{4hN!_BB zWU97A!O^LY%X!y8^q#4I^X>mmJQ5+4B6bJq?Okz(34bwf3SY#3=e++vG+8t7U$D!6 AQvd(} diff --git a/assets/audio/audiotest.wav b/assets/audio/audiotest.wav deleted file mode 100644 index 1a2628c38ba2ca10c051410abed244a8d0ec4f01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88244 zcmeI*ze^io7zgk-QHtUcT->yqpo30MI#d#&2n9PNE)FMBK`n?8M2hB=A{qQKh~grF zP9owUZU+wH;+92m6elO0Bv91$y7y1`kR=JXgd}(GJ@4~;KVLtOjGT;TLg`bn@a_BJ z$6N>@lRok%Aym`%vmqCzqQ&Un^AK8F+H$WQH(lG^oqh4Z@z7p=aCCpHR{Ango7|di zpX-^gm$OUB^6wwX%G>HiG4N1r&s5*N%1nieyA_p_1-r>zC1S? zK8Nqa@5=wf`O10M^~3ety2Sd*I?=k_{fGM~_qFcN)fv=F)UVXx)DzWr)v49n?Hky~ zu^(k$%|567TKmrS&-F9t%g|q<-$&n){ww`(`ug-I>a*1Ms()7>vc7Qr?fUf18<_tv z$6_AGe3Usa^J?bq%t4yxG?!{l*u1m3ZS&{y0OT6TXONd6_d|Y(JQ=w-@_pnK$&HeK zCC5vyntV7pck=q=C(418XDMG)PO7|DxwUeHc`{LB?9hI(4{7NaW&wJd)7|;CH^?e+@pL?x-b&eq% zr#{z+cTBZ&s(HrDLN81{oUWHfx}Dp7;*}%+h;=S~iD|Vi0o9!q>z&+#x^-2{PhD_d zG`b5I_n!Iya$dU!ie)B%z&7*giP@91O_|aeb-E)s*U3Bb|GdYX0bDw19P1nu=50-3 zps^Z{Y$_WV>V0mad;{`X>eSn#CstnSeG6}@PE5Yt%O0}rF6BJ_56v`P^I0$>9GMt6 zeVPF!Jt!`I2f3IU^p16Z;YH-Xw_--pI-xtQ!dg=^G?VznT;lNRt_U_S+FsGse4E6W Nk(5r}ukwg7?+5NxZI1u| diff --git a/assets/meshes/buildings/house_1_2.dmf b/assets/meshes/buildings/house_1_2.dmf deleted file mode 100644 index c308143767f69062fbeedd4c1179f68347c01bd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiwYi;c+XPN z-hXtrK2|I*W$EAq-<bD*D2`sJKF9SIFO9#shH%FS5kO%tIb!dp=7ziTvd3Vb zW8R?$O#^?rD+#*kQR!@s-oSf`8_phQd+7->ocl@gh#TuBt##BDp$hZ}H3Iq$ZV+(J z=91AHiLU9~)xPpl#C{V!H@1CCN7^E?>sovNyo(Q9{44L$G$UNBZ=x5hbME`#K*TTd C7i%a0 diff --git a/assets/meshes/buildings/house_1_3.dmf b/assets/meshes/buildings/house_1_3.dmf deleted file mode 100644 index 2cef82ef397e17c7bed828d9200297a28e52b76d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaix?QH}h3<(_ADR{hTJH*RaUr@Fe?|D}A@>AmuMJ)&m9)u~w- zz2w*8g|TGLgxRm%npMY{Ft@1l$*5UZE}r>MJYo7yFEwF5>p^pmT=z_vI?l#?H$i3@ z{amXTMyza*-3vqes-NDTqh{iD24}+D-)*4UV;t42UKmSOrh2}1Yq(Q~XN#zcyY=0; NnR;}0KiHM)5x>|wcW3|r diff --git a/assets/meshes/buildings/house_1_4.dmf b/assets/meshes/buildings/house_1_4.dmf deleted file mode 100644 index 4c6a9fb1008ff36e6827799770ae31a07acbb3ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)&Q4Yf}2t%`z4NcSjcBdYFHqA>(muf?zfMXlf=lj)et+j`8jxF|iyC1b;#(A5S z!i#pryVBVQZT9T8pAxUE9n;GQv@0GvhjysCZydnT8&i(C-(!rZek-0a2etNR41EeB zzmtwzs>jZn{p)?auQXuw4zyrfh^1t{l#T=g}1`&1YWhN^Z&M& zlBj;o-aX-Yc2E<)z|PuTS-w$vT4CsNhi9c@+8JfeSAB#v?lqI~(#fGH%3pfA61IdbH4OH%v9qTjVes<|tdhs;X+LImk6h#QcK`dG zwK8$uR;SrF-G4sK!K)r!GH-YG!lWbcqESW1tidov-I?x#`MNi4!HA&?fKN5U8&f}6 z>34=-dhSb+0{I}S$*!5mL2S*TIfEaGc?sJri4}RDW9@yq8mD9iDh`N QQS^ov9ktFAyM^!YH-=Yv*Z=?k diff --git a/assets/meshes/buildings/house_1_6.dmf b/assets/meshes/buildings/house_1_6.dmf deleted file mode 100644 index 267305335aa0ac01c3cb2de77b751871121aa0a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaixQBDLw2nF$E5@Y=HP99~CGDnl@?wV$1*GR$u)$hSZKfk}?Eh0WDuW{s@f8KZP z4DQ?28S$U)FCTmK(yJRR=j>e>Izz7-sycU_OjFf6V?J44?+#lrmcjFh1xVD5{h={kCKqshJBw^)q1dxyS%O7|4S^xeJ8gumH&QXVSTO_(}*Kcf=*HX(!-XLl2X!^at!vfv9qTjVes<|tdjflVLxW}8@a~m?f$yW zTA4U+tJCb8?mwUA;8l+T?o9W=eBGP2V8qY`z^9twjj123 z^gF{Zy?LICPYe&WsV2{R=&oY-NVDt|a2<2r{`@!a_Ed(wdc~xz>}np~tUh;2%MNx@Ep#8z8Jg-9Q^KnEluy|;(G4B5#4^0s QD0;(-j#}r59pQWXA8dPu7ytkO diff --git a/assets/meshes/buildings/house_1_8.dmf b/assets/meshes/buildings/house_1_8.dmf deleted file mode 100644 index 9eea5b56e1cbacb3d8abdb13d53bdf45f4d5b94b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)4(GtQi2*WQEJ#TOR(>`sVCSe2%>zv%xHX#XX&)0jAn}|GZwR-G$y$}1qfoHEd zMJ{xwJ_Lmq$JBw-9p~KOpv#)g9lX$aX6WeaT%SSgI+&Esnm5zY!yQhZxswA;Lj{*w!<((kxqO4kTQMtV#SCEY&F+h)+KN1%Z+O!WS2S}` diff --git a/assets/meshes/buildings/house_2_1.dmf b/assets/meshes/buildings/house_2_1.dmf deleted file mode 100644 index 52187c10703c12310072ee5af5361bac7008025e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmah^TaLp(2%|UIn-r~9{p+22^x6ayXOec2B7+|=`FlR%77@Srj`g~4voJD3J+XRl zSM{Gy`nN1K`E-luJ>TTz{?#YV|6{Vb-y(OF6;~Pfr_gIRarH^_w|?T*EN_gLL)whq zYxTLN{@`hn+=0(c5LXCJbH@h6%QpfVX(ui>W^}ya2i(l+g|w>&vc_xK;u2zW^i^g1 zfz(!C%`{SHrR_&w1*C7ywz>m+rvmpWJGbpW4tZolcPX@wt*UmUrVYgr)rVz^qt!TV ykbgY6?;s~VF0!?w{j0BL8lYDRdvtEReS`D5&ttA#W!*dXN#?$Ha^R|nr@LyQ=-G&?Vn%c^fHi;o-yLgqekXT+iaE$#<@yxMP5Xl966jU}BpStEQ%N zoD|=^rVWqwRjAL*? zd+kQ_5YI3!YH~Kv1D>f#ta`PNju|-DE&HUVZj0bjs|s?rcjZ&&_p&Wi4Y37h@<2eRqdK;MgX{*KDx*A|3)`3|LXzztPJi*)t_Rc#o zy3M;+3|=$pRJroP?*+3?J_^B9Sl=;m%vo+mwYFEyF9*SsHlG0e@n*Y^wSFtkp- RP28H?-=+yztJlE?N4tUX`F^!qYwdwG$7Q{~?!i%4<~%f^ z$nBRdd!}O~@{&mB%zCZ4V#B$=pLFWg*avG0)RnC>Sc|z2`V8dq?sV>>7&^s??PjEw zW{Z17&P}oE#Opq3;(%=8)(^z|ZD6XymYcYAd4R-O1!C5ry#6k*wRXP#|Gs$L&vW() z9jZ~2>AlRcsH<~9_rG)Kjx*fYy-IVpK4a&tv!}br3~$q&$xu^Q!|MZM|9|H0`mJ~K z*@u4Bf6~={u3zq4S=I7=tew%rntKVa4L~}cp(f8>WDN6f{~(t)bvoy+e$25h`vA4} BV_5(I diff --git a/assets/meshes/buildings/house_2_5.dmf b/assets/meshes/buildings/house_2_5.dmf deleted file mode 100644 index 76f6f45335c433d089812d6a5723cbc2317d52eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma))(G9{d3`ET)Y(j((f9%8vjL=azJ9)0#rUFYP_RHNRMSZ-Ub}_TNo^36y&+psd znkx%FG((ZgFI{G)V{Py{K^Ls`nR8*QbACT`WSMe;RpXBB&+r)*HtDm}lQ+|a&&ASF zXKZ#xYTew{|D@*m)?VjqUmMNXbhhEGZ@}W+#F&RI7dX1yP!p^ISnCk)Rr5y=v#BR< z#@r3B-eB^=To={WJmY=E(2-?gQCiQ~=^4YC8O*-6o;UMy&fzZjH)y%gMOf#%{xu5aA+C)cI>XnhD9&hTz LqI@M!eb4&>sR(I# diff --git a/assets/meshes/buildings/house_2_6.dmf b/assets/meshes/buildings/house_2_6.dmf deleted file mode 100644 index b200e0b65cb843ba743a90a1f93b52c0de88881c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)&(TxHz3`9+3XCZWd^h7C?LTQYhJg;}j3M_@h&oh%OxBI_c%3K0a>Vh3>rT>$pZ^rmT!5+_llt-%fzy@pRvMxoEmSL diff --git a/assets/meshes/buildings/house_2_7.dmf b/assets/meshes/buildings/house_2_7.dmf deleted file mode 100644 index 5a5b54616b300c36a9a10af0b9bfff924cf54d5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma))(G9{d3`EUjLQ9hJ9)0#rUFYP_RHNRMZG^Cb}_S?o^5?vpI_I( zHCGmVXoez}U%Jdp$J*d^f-YF=Gv~rq=lp)?$TH;wtHvGMpW!nsY|>|`CvT<;pNplV z&e-gX)VjH?|4GgBt-a3KzBZb%>1@MW-+;xti7^jbE^u_Yp(a=bu+|~oMe|z^v#BR< z#@r3B-eB^=To={WJmY=E(2-?gQCiQ~=^4YC8O*-6o;UMy&fzZjXnhD9&hTz LqI@M!eb4&>k1Tb$ diff --git a/assets/meshes/buildings/house_2_8.dmf b/assets/meshes/buildings/house_2_8.dmf deleted file mode 100644 index 38b1707c5b83a946cc4fab9e4dc991b5164f36b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)4K@P$&3@a}qAjFM7?P)v>JBy<>%YaC2Q^$^%YLC}*T*er8tP;2VzFh-1oD=3a zu`Yh_vga2qOUYwD+wbWc7|ftms<+a& w`Ak3hEqd$cUHVY~-XQ`LSM#3Dk$SyB^H%DW-0<)_^J%ukm>8D&A>Y5{19|0Q@&Et; diff --git a/assets/meshes/buildings/house_3_1.dmf b/assets/meshes/buildings/house_3_1.dmf deleted file mode 100644 index a36cffe899cefc502698e95655238bdeebb2745a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)&Q4Yc|3`4_BxCs$L{Bb9aPVB~AmUTd+t&RPhb$z^^b}_SiJo|dzMz7V~Ona<& zpsnf`C;wX(o;a>PdhF*}LaSBz&dHnE671S4D~&PV!Ziz~bLdgvRA*Yxf)~rIZ+)G& z;>fqxZdQ8GcDfHk8rRU&XKXIg#Z4nN@(!90W%_Z3YiR1{ot|h&i?x!4#(PN(9IDO( z-#PT+#%7z(c{GRCv*hc`Ym@uXkXvQ-ZOb#yUSlE)Z|cd(#!AntvT5+}^n3Ydo|t-n xXY+PnkF%_}I*)vChe}PX<^Qhc)hu~r^MC1;Y-J5xR{uB1yen?vmGh`^-Uln+WpDrh diff --git a/assets/meshes/buildings/house_3_2.dmf b/assets/meshes/buildings/house_3_2.dmf deleted file mode 100644 index b78ceb508a8daf699d57c875c28d4ba1c66eb9a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmZ{gQ4Yf}2t%`z4NcSjcBdVEcEA+U1c?9-Lz=$7pYa%Dyx@zO*T1JnV@-#V(I<1% zPxJDm)6PB=Rm`$V9T~js3x>{R=IKtSUG*IrTp6{=u(R#s5ST1=06Sf~H-=7(eD0&p zTrBOZz*MKsWyz%r+=6B+Bx~Hfn~XI)Yj5Y792$LdHpx~q06I*wjC_x3=2LTE=%%*L z)_m@u5LWk%crRJir*0D+oHgGkw$7dz?pe81YQwe~=sqsOTGeY^v;Gjg&j z{^>OHmzO>1a$byDd&PW4wO2aL3eG`#>}=n`S}c9B-0rP(WDM#QSFGA0ZF*z<6Q9T4 zu1o#)IBAX^*@?$!h;`OT)uX$f;?k`SJn-$|I(c(Gw5o$=WODm|V(3h5YM`Z;890mK zltI>!hz;v|D$cwRzid=R}-^^qAjfP>l1K5ojWl<2d&i z1I}wc>#pnB#ZYiaQuRgNX{By5w z^E_(OrOo0WN6#y-YH+2W)ttF#vBzvK9KFo@z~LRWafWiV;_)6t^v*bG3US0ro6UeN zUOT=??5Enu6@GYh%wCh$=~fG9S-CIrT0NfVa7X3*omGn*-px|_`XV>q!_72r)7a=0 zD=(Tjg|zG|X@$Fli!+?)ey_YG{4H)ZW2fqjmh77gFSEz9v=;77;f~;1-8Wf`RtU7! zna8=o+WNtm9Wz$_qF-m3aj{?Fojb*@!ET=O-;k!Wpha(%Piyk6=;hTK#-{ee3un~z HKHd2b!_slN diff --git a/assets/meshes/buildings/house_3_6.dmf b/assets/meshes/buildings/house_3_6.dmf deleted file mode 100644 index bb150cf015061c0ce710584cd8cdddb9a20d5536..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmah`!4bnC409)|uh;9ZJ9U(d!f3Ss5kZ{9cMe#VWCHK+#~x<(ikzc(tAEe7den*m z2bRShkDgar^vJ7z-fGSzrXJZ`^62H^Lmss&w$4CJtZKr8q1ucG3;`bF49jMaTeZI5 ziTa_p@+yD%Oq|{WHsRhH#0u(2)~&mLiIcZ?j`xh7@~Cw;+ak3lFT>!YIJLmwg#^Yx zT_O!BZwY*hRIgx6{|ioySiw7+(OGTADEj6?tF!ei!y>dMZ^Sc?eryUjNC{$RXI|%u xboeRf&QJ9?OEy>iDnEFK=$LZ1z%oD8@_d%u6}<+(&p_?5rV%t9Xx6l!K@$>wc>#pnB#ZYiaQuRgNX{By5w z^E_(OrOo0WN6#y-YH+2W)ttF#vBzvK9KFo@z~LRWafWiV;_)6t^v*bG3US0ro6UeN zUOT=??5Enu6@GYh%wCh$=~fG9S-CIrT0NfVa7X3*omGn*-px|_`XV>q!_72r)7a=0 zD=(Tjg|zG|X@$Fli!+?)ey_YG{4H)ZW2fqjmh77gFSEz9v=;77;f~;1-8Wf`RtU7! zna8=o+WNtm9Wz$_qF-m3aj{?Fojb*@!ET=O-;k!Wpha(%Piyk6=;hTK#-{ee3un~z HKHd2bsrG&O diff --git a/assets/meshes/buildings/house_3_8.dmf b/assets/meshes/buildings/house_3_8.dmf deleted file mode 100644 index 675c68de43860d4f838342de5dfe6c95601c0e28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)3NfH7v2#hZiEz6sKny2|R(?B!8WYkn;q0>v2$Ll#RV~qRkt=+8Nt~QIzIbpyR zv+X+m;L)>+ymBq7-~kh>BiAwTqOEe17QB29>N?glPzR5s^ZeJs+O2A=-ypWM(TKb}qB;D=w) s%lf{>4}!@%G{?y0eNm(1EX0bIZ?JePdIjGzP{WV!N1kH3Ifp~;2b8aCG5`Po diff --git a/assets/meshes/buildings/house_4_1.dmf b/assets/meshes/buildings/house_4_1.dmf deleted file mode 100644 index c3349b074cf9b614855bc6a979a030b13a693241..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmah_TXF+22%|U2P0D04{p+1_^dcFt+O3l?;|25puOGiZ@f8vGI_G$PuYPhyOkEu6 zXJNbeq!a(n3r;%b?47+=n9p!MYTQq{;e?rSbdBx2!YHX33indu?@~J7JD$=Y{QY)b zYFbM}?&s}v)L%Zji^JMGSguN1=EBsEt_$JPIWO)XmJ1oa{_$3r`gzkA7~Il^d4cgw z8-otv^Ps=#&`ZZ6kNKPju(7Kb)G6}Rt)?*gHuux+=sQoZG35m}c*)5 z^U?#0VQ3NLvNL+sox$^6Hr|rId@JqrH>KV`xf*GscW~#Mz0|`CO&x}L8(Arj?)q7+;`U~H34pRd2`ab4H1atoXN_=%gIRDgM! zx1UGgCtiAf3WbE0S@FP>i(g=xyu)UB$4jpPCwP-B-wktpV!9VUM9d<(_KvsgO-=u% z4XeFiHV3KqPFVK7cT)UJJb7(Y<-!RRY{6{~3Zqw6KgV?zE>6C=hG2V`08)`!ZnM^_ zcFqCQ^j3?{2n~K2VCi?o@9n&L2Ry=>ziJQN?tj=3thuL}D86WG_x26SLfU*Y7<>ck zVQR=8rbc;s^S-xB-7B=RTzr?^$t6VkJh7&l*Ne7(p5kj`I)5b^!|K|uQA3qp1mU0dfw`d zwKMWrnx*L9Pda6`qdR#?q)QfSt+`_Dygr|F%9_YYX2vr+&&ajk7a1z|Ug>=9;jwS( zOmmaP+L2#OYtHo&PZqw@{iZ2Qw(;_c<+lRUy3!wz zW2Cry$*2>fj!*t;3!#KDiQ8?f|UoKJIJr*CkW;T?5mGU?c>BGB=% z5@UWov3to_|K&+DWB)&{@-j?6(5Hs8(QAh1r4)OAs+hFyCG$M@>b|4y%RTqHc0{k3 QCol6^WLbPy9@)zK1u&0yi2wiq diff --git a/assets/meshes/buildings/house_4_4.dmf b/assets/meshes/buildings/house_4_4.dmf deleted file mode 100644 index c49548905919438e759ac7dd9860aead0c6ed30c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmZ{iQEtO93`4z>O(=%_?M^-VBwEjAQpZ5zh<>ExqCbDX<78(qwSBXc|U7v56fq{Xc1i?*G69`6h1S>8i^Dmx$6y@%(C3CQWgd*Zw=t|v`? zvU`V>H#KT{O3xQM9J=oprVmQH=rIq_)F(CSwyg~vZXM*uea1R-4)2JmQx^kw(v;Kx zOZOJLe3`-WywUe{&U?rcnbY~=N^I?^Mw1=bAsckJTEivQj(cxtE#8;kQ~@7`jK5<--f7 Lx(@Sxy&P diff --git a/assets/meshes/buildings/house_4_5.dmf b/assets/meshes/buildings/house_4_5.dmf deleted file mode 100644 index a6501df9d6a9b7ee6605ee3d39a95ac3c7a9fa25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy>y5)O41?WGvdIaDqW^9ukI)e^>XEW(GmeW2a3XysCFk+{dzD8iVqU-L)N?v!CNGI}$)eSoD>j|`^GT;(6@8MKab@d_to?b>L*d??&gTw~ zy;EnJn=IOn{9>v(*F!v6_|D8PnvTgXynDs+U4f~d-C8Cto$n`efZTQR9#P60OLavq z-HxHn(7Kmedc~+?keyhCt3NNE4kmiFV$dS98|0$m(Y%lWy!I zIRh6i3$dKJc`xf+vyOh!)R;3q^>aVZveS9L)I)cR&ZIq1>lEp8hSvvK? SZi`TI{Wqkm+UNcDzx)H6w&Lw?BWs@rsCV5z1Lo`<=nrYbeh#LrF!gI%uf(PE^I{HAyH4KBhy$s9}T|K$zQu7DW; diff --git a/assets/meshes/buildings/house_4_7.dmf b/assets/meshes/buildings/house_4_7.dmf deleted file mode 100644 index 160e5e1f2c1e1ba30490d0266eb2d587fee68697..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy+pWVe3`5?O6EpGe#zS>{(pD^HW zU{n0yx#uMH8ps6?ezMO|*D&xRrn*TB9(y*P>pIqJ@SN`}uB?6&4;UiP>I|b^Naua2 zGd)qwP1=b^e*3sK!>rkA6u&mVtetoz-{#Dm`DHC;bLjH^;yq5VGcE4|jV}0kWWR-X z#L~R;N?nMMvzbtvu&za_gp|`hC`MhYi)5~I7!P=a=+1fu<~?H{k!ISlf9r-0oA&0M zLBi-MC%r(Aob?Q?_Y4d{KWqe_7P8Nodnh*f6{XK$n3a14U;RATOS&8I(%;uTKCgLm AasU7T diff --git a/assets/meshes/buildings/house_5_1.dmf b/assets/meshes/buildings/house_5_1.dmf deleted file mode 100644 index e01e4b0f4e8a7b9d7a5440b1424251d012b23da5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)3?F~XP2*gd?L}HBp+{sZKRgQ9$VxLM4N%*ymXct5x+~M=rg^*s-0LnA>!ks~4KC!;FHCb%b><^whdn=Unr4 zI@hta$1Ix02o+!WuU}Eyk>@*^ld6T$+4y9m4Km0;D3nRI@c->Zj&` z>GbGj58m8|j@b8&_}^WJzH~IU_D}D|p6)<0!=mm*4ByF{cT+8NyOp<$@WN9^Ut#LV zJxqfVJ$XfMa0M)DGAH_)XKL{7?6gmc6?VS2?L#fyzX+CEs)1n=_nlyKPMtinu6}q#Ihu(xj!G}QcAh`8evxJ_qFbu z*O`NdTj8HhnZLaBq>DVOwdRV;8LPR{DQoi_>ExZcc?Pw(@OXxhy(=9YKqs7Ws~OUU z*ZP~}JbJS(^wVe1v>x2RTWyFlYG0~rw|~V;*E`2|rk8c{?tX`t^% H`(f?}J@k92 diff --git a/assets/meshes/buildings/house_5_4.dmf b/assets/meshes/buildings/house_5_4.dmf deleted file mode 100644 index 7eaf2865f0698065f45a00c94ac37dfe7429b49f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiwQ4Yf(3`5z;hNfwMyHk(CQD+AXE=#vaX~~P7&_3Uwb$A!<}}p47hIgKIsMq(?Zryp-<}KsSfjA54F~7~A80RrtMH3ku z$GOKCaD;ma=yjIO3!@&?CwsM%^CI*fr88*@G~AudbDQp0l7BU4$$)tZ-bG&N$Rhau G`i>9y?|1nC diff --git a/assets/meshes/buildings/house_5_5.dmf b/assets/meshes/buildings/house_5_5.dmf deleted file mode 100644 index eff99b288a07dec6b931e21f1fa9e348c94fad2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy?TrI52t>I~wv(Xflz-_dC2R>PH9Xs6vz((t@~#=)0QvZRKJ3TL?s2a1_MX3P z{Y{=Vbmg5`Pw6l_PCb`n-Q-0P*EH|B`bk^PwGT|?a<4oCKY7-6UTJM!@QUn(sT?^` z`|q^sg^{1T`O0%YN|i&aabZRg?V@`xNNaOKQ$B3-C0*Ps#HH>?Epzf7Zg$2|4kK=- zMVrZHFDyQ3a0D91IO#VMmuCdA|DL_i{5GTchIig|@$X*EuDi&ZcSaL8>v4H|JUA(& z(SNsdotvz)pBQ@m*Dw3oWj8nXYkY8jVrXKI$o1bqV-IQBi*B6pdE+)Y^+Idm;~g)2 Q@``IYNYvC1?TB;w2kZfU82|tP diff --git a/assets/meshes/buildings/house_5_6.dmf b/assets/meshes/buildings/house_5_6.dmf deleted file mode 100644 index 66cf599382ee5341cb0d26243d6df36a59bd2fd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)4(X9g^45O1Zs-F7SojMAmFk0gTL*T+|Bjq5D?I5_n*R$M8DG$vxnt1)b`&zY* z7;s=&eB;scKP`IXnV)#gxx}JJHkUklRrMi{+8x_xpeAM-^*fAF-xGv6 diff --git a/assets/meshes/buildings/house_5_7.dmf b/assets/meshes/buildings/house_5_7.dmf deleted file mode 100644 index d77782b777683ac16d6f44c64af82353dea70a79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy>kY#o420duimIyrxfgzAuBEjeHZ1Q-%Uv@LI2i6!Vl>Boq z_lVqU)TyoFAE%yIUe?6rexc-=E6w*<%@wC!5q-qTJ8Sa{=F+kUJrv%%;;1pmDR$aw zhS=;4{hj!Jtj%2Jr^~?98nr;vD-CTL z>%C;@l}3(1Enyj#` L&d9<0>6`xm(j|&n diff --git a/assets/meshes/buildings/house_5_8.dmf b/assets/meshes/buildings/house_5_8.dmf deleted file mode 100644 index eabf0f664a380859abce6bcc765d582de8e4cdff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)3TM_~>2&*R(9mhZKv`6tM9!)|C3v7Kb)9n%x($d@GK2BqdtF9K8nQ!O0KSlS1 z0XJ-x>-PtbJ*&tw*CGoZFqwJyItE_E$~S4j%lDwJW8DLJU=RtFwcm;d3}GJgH}2K3 zl?U}Em3<~zi(2OyzkLeFYrqOFvk+LDUu(H*zr07$Bpy9B^?d1KZg*?G8ACsEvlbY5 z#K1!oZF)=c*vohj$lkwWpFAM55a08r7<4Nwz57{0uG*2_h`gxdC(ShQG{1j*LznvT yY&L@*{fb`J_bvS(P~IUNBbWC@j)k)jD_YKAF)Q{8zIz}?KhBRl!%#%+;QI!=-glA! diff --git a/assets/meshes/buildings/house_6_1.dmf b/assets/meshes/buildings/house_6_1.dmf deleted file mode 100644 index 9eb483a1351ca2fa0481b8f0c2699ae4009c43cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)3?F~XP2*gd?L}HBp+{w`#O^$MuVxLM4N%*ymXct5x+~M=rg^*s-0LnA>!ks~4KC!;FHCb%b><^whdn=Unr4 zI@hta$1Ix02o+!WuU}Eyk>@*^ld6T$+4y9m4Km0;D3nRI@c->Zj&` z>GbGj58m8|j@b8&_}^WJzH~IU_D}D|p6)<0!=mm*4ByF{cT+8NyOp<$@WN9^Ut#LV zJxqfVJ$XfMa0M)DGAH_)XKL{7?6gmc6?VS2?L#fyzX+CEs)ax+7p&k_pfEtK3Q{53D`RTK&1Vy`s(> zd2%cK(kb)G%bs+eXRX#;aXDi(S2|@? zCOMD2S?Bucv(rS6+>W=_5NE8hRJZQ$wFZ+3ym5v-y&9}pyosC}CrgtGnZ@qd0^j6-O*tVF#f_cUotNU0Jh820# GwYgtQS%1j@ diff --git a/assets/meshes/buildings/house_6_4.dmf b/assets/meshes/buildings/house_6_4.dmf deleted file mode 100644 index cda4f39bf86d8348373d7f3620a46d2855ea7b67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiwQ4Yf(3`5z;hNfwMyHk(G(Pjq>E=#vaX~~P7&_3Uwb$A!<}}p47hIgKIsMq(?Zryp-<}KsSfjA54F~7~A80RrtMH3ku z$GOKCaD;ma=yjIO3!@&?CwsM%^CI*fr88*@G~AudbDQp0l7BU4$$)tZ-bG&N$Rhau G`i>6kY#o420cHvPn@@^Boq z_k`SQ)TyoFAE%yIUe?6rexc-=E6w*<%@wC!34O%LJ8Sa{=F+kUJrv%%;;1pmDR$aw zhS=;4{hj!Jtj%2Jr^~?98nr%C;@l}3(1En*p$` L&d9<0>6`xm`DTJS diff --git a/assets/meshes/buildings/house_6_6.dmf b/assets/meshes/buildings/house_6_6.dmf deleted file mode 100644 index 75e4d9af7ef65051df4b11422a3fc25c53c60a94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiyVXgu(2!!!u5@Y=5ojgj9vPY9?JMAp8k%VU%z5(L%`F`2W%pQ^FIDFKf`}jny z8g$l!*ng>msW{Huj{Tw+3c0TNs5y_?cJ6z?WKL)44){^4R(YxQeZe!-3np`@5qW;4 zWiJ@|@iyPJ&O!@Hy?iO3c~aZ9yV=XE}~pO_k1kGe%x^VG}xOn%SmRn}r213r4Wv)2Hk$9d2^ GviTS6c!*g5 diff --git a/assets/meshes/buildings/house_6_7.dmf b/assets/meshes/buildings/house_6_7.dmf deleted file mode 100644 index b02e05153b2391a7db49eaa1aeb77697c4e8de0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy>kY#o420duimIyrxfgzAuBEjeHZ1Q-%Uv@LI2i6!Vl>Boq z_lVqU)TyoFAE%yIUe?6rexc-=E6w*<%@wC!5q-qTJ8Sa{=F+kUJrv%%;;1pmDR$aw zhS=;4{hj!Jtj%2Jr^~?98nr;vD-CTL z>%C;@l}3(1Enyj#` L&d9<0>6`xm--?cT diff --git a/assets/meshes/buildings/house_6_8.dmf b/assets/meshes/buildings/house_6_8.dmf deleted file mode 100644 index 637bfa2126e3d2d4c3ac40d810767a8b81a1918e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)4Q5M252&*R(eLnxZ(~joRB+xLxZpwRWOPY|u_IN$VWsGsxr^U_l?dtnmbWa#? z!~D9gKX|NJMKhaPWWfU_o`>(mz>8SP;&9OtKbtooD>E6wbT@R&eo)z}oy;$z6N$J&Gpr=&`9M`eAOpHQ$V(9=TZy%<9Z~ z;30}Oeaoy39uBhCH*La>JV3FKx#vwW=vG=_i2g2)>MHt7Eim$Mjz}}@*uTD^!{KE1 rNA-$c*8Q^AJkz2>qFMyG?DwZHXRw$RYk6f|B~InvAg{dN*LV8>%0qjy diff --git a/assets/meshes/buildings/house_7_1.dmf b/assets/meshes/buildings/house_7_1.dmf deleted file mode 100644 index 0d26b2b4693911a46dcc0a837e63baf99abb3b45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)3?F~XP2*gd?L}HBp+{sZLMUHZmVxLM4N%*ymXct5x+~M=rg^*s-0LnA>!ks~4KC!;FHCb%b><^whdn=Unr4 zI@hta$1Ix02o+!WuU}Eyk>@*^ld6T$+4y9m4Km0;D3nRI@c->Zj&` z>GbGj58m8|j@b8&_}^WJzH~IU_D}D|p6)<0!=mm*4ByF{cT+8NyOp<$@WN9^Ut#LV zJxqfVJ$XfMa0M)DGAH_)XKL{7?6gmc6?VS2?L#fyzX+CEs)@~ diff --git a/assets/meshes/buildings/house_7_3.dmf b/assets/meshes/buildings/house_7_3.dmf deleted file mode 100644 index af0bd704366e6cd0ad94df7be3928f5416d389fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiw(GkKh3`4_Y_&AO~c3>1n>nI%sKPMtinu6}q#Ihu(xj!G}QcAh`8evxJ_qFc3 z*O`NdTj8HhnZLaBq>DVOwdRV;8LPR{DQou}>ExZcc?Pw(@OXxhy(=9YKqs7Ws~OUU z*ZP~}JbJS(^wVe1v>x2RTWyFlYG0~rw|~V;*E`2|rk8c{ZhnWC^%=XLrIRLe H`(f?}SiXbl diff --git a/assets/meshes/buildings/house_7_4.dmf b/assets/meshes/buildings/house_7_4.dmf deleted file mode 100644 index 442b265f788d7bf0da6a5b96aa761c3080ad402f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiwQ4Yf(3`5z;hNfwMyHk(KQDz4WE=#vaX~~P7&_3Uwb$A!<}}p47hIgKIsMq(?Zryp-<}KsSfjA54F~7~A80RrtMH3ku z$GOKCaD;ma=yjIO3!@&?CwsM%^CI*fr88*@G~AudbDQp0l7BU4$$)tZ-bG&N$Rhau G`i>71$bdNj diff --git a/assets/meshes/buildings/house_7_5.dmf b/assets/meshes/buildings/house_7_5.dmf deleted file mode 100644 index 275dc21d31e535269417f1e7605f2cad5370fb23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy>kY#o420cHvPn@@^fgzAuBEjeHZ1Q-%Uv@LI2i6!dl>Boq z_k`SQ)TyoFAE%yIUe?6rexc-=E6w*<%@wC!34O%LJ8Sa{=F+kUJrv%%;;1pmDR$aw zhS=;4{hj!Jtj%2Jr^~?98nr%C;@l}3(1En*p$` L&d9<0>6`xm2HJ*M diff --git a/assets/meshes/buildings/house_7_6.dmf b/assets/meshes/buildings/house_7_6.dmf deleted file mode 100644 index 64a40b7713167298911f7d21e664758022e3089a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaiy(G9{d3`E0ZSP1dQPK?q~GD=3_F3BZM>VhS=@%f!yTAuG$x}}sJ$ThOH`g3ph zg4%1;sYUUR)68F9_QchB)@tpQ=4V8E#c5W-9C7mQ+P;IewCceOh4)4rH3m7wl@{#~ zTfH{liJ!;b&Q*T9EKKiFTXbs;X_+-rb?eTnbaAtDe$VWAPF}N=4y5XWv$#mZn8A84 zS$d_BV^GV;#B|?MoZkUr`Mvbkz@6OHj!v~5qv(epUhUqWWsHC~xo;$H^l`{!m@cGU zJ@dZTS;ssv#paHydEU=wi8w#6^59O_#~OcYl5(p-FSS)Hjn$?!!Blai*t^b_x%6b z-{e_CSKf*Bln%4w)N?u3OIhJuWYg2PcIz z`tNqGbCY%U6GN|G{j#53c5`#T#s~KiLlb*MuKxxadq~S(bmNTA8@I`+7g`G+?|9*p QS6s_MqNaXmN1Ug>0qpORnE(I) diff --git a/assets/meshes/buildings/house_7_8.dmf b/assets/meshes/buildings/house_7_8.dmf deleted file mode 100644 index 9eccf6f73164da4d72478feabdf152285ec9afd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)3TM_~>2&*R(9mhZKv`6zO9z{Y43v7Kb)9n%x($d@GK2BqdtF9K8nQ!O0zeM+h z0XJ-x>-PtbJ*&tw*CGoZFqwJyItE_E$~S4j%lDwJW8DLJU=RtFwcm;d3}GJgH}2K3 zl?U}Em3<~zi(2OyzkLeFYrqOFvk+LDUu(H*zr07$Bpy9B^?d4LZg*?G8ACsEvlbY5 z#K1!oZF)=c*vohj$lkwWpFAM55a08r7<4Nwz57{0uG*2_h`gxdC(ShQG{1j*LznvT yY&L@*{fb`J_bvS(P~IUNBbWC@j)k)jD_YKAF)Q{8zIz}?KhBRl!%#%+;QI#GsDIJ` diff --git a/assets/meshes/buildings/house_8_1.dmf b/assets/meshes/buildings/house_8_1.dmf deleted file mode 100644 index 180481bfebc52296f171aee749170d7068e4a4e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmah|TMhz22*aCr6VVv|ypuW#AEi+v({b$t5Nx#r&&EmYUMY9J*_ZOJe5}c!09~ppqINk!nzi+vk{1Kt1tG zPPso|#GBu;y6>O2`jH>JO-wAgbASKVB_CSt55`4{Hp8>}{l9_H1EccI(=X5a1!-Dc AQ2+n{ diff --git a/assets/meshes/buildings/house_8_2.dmf b/assets/meshes/buildings/house_8_2.dmf deleted file mode 100644 index 611d63ddf2b8de1fee9fbed784d1e06dc12dbc28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmaJ<;c|l^2y;)ithejGzEdB42?8S1<_?jN;HT~H>p8y07!O7RX8rzl{)f46#)~@v zla%J9%b6d%YNZY=7~4GO*jd2bo^&~@+#`*RGi`93xy_S_Tq1NNPP*+}H{CenHBW%8 z>kXVcdDTkZbxIwZ#+%|c9L=$UC)q4fUWyNPQH^j&Q{*uASTc|!pm*wqHP&m6%}B?kY1s? zRj+ZRE5~vIOpMU<-^2JeZyJ4dZ`W<}B&F^joC~co$SE6lJ6AqM=(w$3mJ3+hemCos K|3S{nbN>Ks99rrC diff --git a/assets/meshes/buildings/house_8_3.dmf b/assets/meshes/buildings/house_8_3.dmf deleted file mode 100644 index 6c57db41daf81c020f395c7105cc2c3dad362361..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma));SEAD2!wGm(HQ@^gQGaw8%>V(nk%o3k{IRV4qEEt^|Xtb-QzQdd&@u9&TnSQ zk_TJGFWu<5Fw=_?b;-QtoJ*!H=!K^0*vsS|!&}(OOO~EE%Lws8Q+1g`EW6sV>V<7+ zk+|g252dODt1)SM5p3e#3S`_f^IcEsw9fZ9F>i!Dk2u3S81wN)8wNME>P;4VA%iZ1 zZ{(5jzM@MXgxqQ9b}YGgVVlCItR=zVTX{L>s5@Yr@EX1GlRB31z1_Y4KX=uSXRGg# v{KpQUsXFbK^WuD`=#1J!_EP()ckF<3qo`ihTAz_D?m;hS>i@vlZ`}6_Q$S}b diff --git a/assets/meshes/buildings/house_8_4.dmf b/assets/meshes/buildings/house_8_4.dmf deleted file mode 100644 index 6e31cd48ae62b973fb37594da9b8240dc9e1fb7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)(Q4Yc|3`4`o2ng}VopyBMw2r*26-1i0yxMM?y*_Wdnb|X*a~(eV<39PrOj+_^ ztNNv@%!Qd=EK!%tN6&r9G=g4ex{g^U_b6{;D=%4k;w)9-g{JGWhgfE{W8Di|Y0RA%(hR?nGk7~G|WX6lk*E|bOAD9JP4 zS9FyHhH}YoY`aGqhnJuA_K~;p*51%eUHR}4%y{P6L#LJfh9Uo-eOGyPHkscpPyIOa qN~d|L58b6ED{b_+-zrbNH3Qxki}2wIcB)`i?QY5CpBwYQ9C=?3N@IEe diff --git a/assets/meshes/buildings/house_8_5.dmf b/assets/meshes/buildings/house_8_5.dmf deleted file mode 100644 index 67ec1b554f4f39d56cb4d48b273007c1f1c59a68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmah{+YJIS3=5mE2?Zhk*ohGsfl+bNI&yO@M7q=2v7PJV{(RWU%x-w*`m~N;m%(4n zlnWl*s{ZLRb77{}Ths;DI_6w*8bPmXs*YJE>qu|mR$g-9!M|jQ*ELla*u}C}JFa@& zmRbi*u;h8G>YS@JX-3nziEq6@j{8i!%cM^GJdXp{I>XE}@EZ?tp3#PLH?`^w4*NpR zbx}N{2E_G>j#+s|(CxVGj2MI$yoUnkzLl4A_|L?d0k6@EPwKLdk59wPA1QL47n!nN zmvPRoJZkEBtny@Z-qWhnywJDF)xqGX<^MUZdFtkRjXE^dt6Cc~f|I+!nR*X0FZb^Y D0bFUC diff --git a/assets/meshes/buildings/house_8_6.dmf b/assets/meshes/buildings/house_8_6.dmf deleted file mode 100644 index 00e3661314681bc6ebf7ec34253dc1883570a984..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcmah{TMoi73=1bCAjBVc;wT(tMujrUncNDrEx1EuL;uYRg1qa{C%Q;*#ab<+p=#5Y6svjSRqY;l3Ipk%g>=!c5 z`ISe_d>^Yk8J%l^X4Yw5>V0rE&^c0%&vIm`m~N;=fPjh zlnWl*s{ZLRb77{}Ths;DI_6w*8bPmXs*YJE>qu|mR$g-9!M|jQ*ELla*u}C}JFa@& zmRbi*u;h8G>YS@JX-3nziEq6@j{8i!%cM^GJdXp{I>XE-@EZ?tp3#PLH?`^w4*NpR zbx}N{2E_G>j#+s|(CxVGj2MI$yoUnkzLl4A_|L?d0k6@EPwKLdk59wPA1QL47n!nN zmvPRoJZkEBtny@Z-qWhnywJDF)xqGX<^MUZdFtkRjXE^dt6Cc~f|I+!nR*X0FZb^Y D=W%t~ diff --git a/assets/meshes/buildings/house_8_8.dmf b/assets/meshes/buildings/house_8_8.dmf deleted file mode 100644 index b29554d5fad9e0dce7f5c98cd7e04eabc7316ec8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1332 zcma)(Q4+#12t?z_M91l$ck0nRnk-=vXtZV~ZQ1N&ll1Z2#$}9g*V)Ex*8A0U)JZel z>qegXqUQnpWMCR|pbia?1UY*-yYd+ad+)!{RvE#= zFYQjC`RG3C`CUATE4s{u(6DdSqMpm~4BdUPfRA+M(36ksE@QlOvNo>gqCR1bR}cG@ zOtR@;(pPc;N!v@(zGlFKA@-~f)xfP-W*_##4$2O)`bi&`?ai}g(Y1ebGs=7B-unMA ueRa=lBJ1;(e( diff --git a/assets/models/buildings/house_1_1.json b/assets/models/buildings/house_1_1.json deleted file mode 100644 index f3231bce..00000000 --- a/assets/models/buildings/house_1_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_2.json b/assets/models/buildings/house_1_2.json deleted file mode 100644 index 6962b659..00000000 --- a/assets/models/buildings/house_1_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_3.json b/assets/models/buildings/house_1_3.json deleted file mode 100644 index 3c369e8e..00000000 --- a/assets/models/buildings/house_1_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_4.json b/assets/models/buildings/house_1_4.json deleted file mode 100644 index 81b57861..00000000 --- a/assets/models/buildings/house_1_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_5.json b/assets/models/buildings/house_1_5.json deleted file mode 100644 index 36d93d6c..00000000 --- a/assets/models/buildings/house_1_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_6.json b/assets/models/buildings/house_1_6.json deleted file mode 100644 index c544d562..00000000 --- a/assets/models/buildings/house_1_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_7.json b/assets/models/buildings/house_1_7.json deleted file mode 100644 index 7f419a27..00000000 --- a/assets/models/buildings/house_1_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_1_8.json b/assets/models/buildings/house_1_8.json deleted file mode 100644 index b741c12f..00000000 --- a/assets/models/buildings/house_1_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_1_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_1.json b/assets/models/buildings/house_2_1.json deleted file mode 100644 index 693ee13f..00000000 --- a/assets/models/buildings/house_2_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_2.json b/assets/models/buildings/house_2_2.json deleted file mode 100644 index 83080cd5..00000000 --- a/assets/models/buildings/house_2_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_3.json b/assets/models/buildings/house_2_3.json deleted file mode 100644 index 172b0784..00000000 --- a/assets/models/buildings/house_2_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_4.json b/assets/models/buildings/house_2_4.json deleted file mode 100644 index c0b4a34b..00000000 --- a/assets/models/buildings/house_2_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_5.json b/assets/models/buildings/house_2_5.json deleted file mode 100644 index a450c254..00000000 --- a/assets/models/buildings/house_2_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_6.json b/assets/models/buildings/house_2_6.json deleted file mode 100644 index 6b9e98ab..00000000 --- a/assets/models/buildings/house_2_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_7.json b/assets/models/buildings/house_2_7.json deleted file mode 100644 index b93f7400..00000000 --- a/assets/models/buildings/house_2_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_2_8.json b/assets/models/buildings/house_2_8.json deleted file mode 100644 index a306084d..00000000 --- a/assets/models/buildings/house_2_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_2_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_1.json b/assets/models/buildings/house_3_1.json deleted file mode 100644 index 8a51936f..00000000 --- a/assets/models/buildings/house_3_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_2.json b/assets/models/buildings/house_3_2.json deleted file mode 100644 index 91a189a9..00000000 --- a/assets/models/buildings/house_3_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_3.json b/assets/models/buildings/house_3_3.json deleted file mode 100644 index 98f13cca..00000000 --- a/assets/models/buildings/house_3_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_4.json b/assets/models/buildings/house_3_4.json deleted file mode 100644 index f9413bde..00000000 --- a/assets/models/buildings/house_3_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_5.json b/assets/models/buildings/house_3_5.json deleted file mode 100644 index 71507711..00000000 --- a/assets/models/buildings/house_3_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_6.json b/assets/models/buildings/house_3_6.json deleted file mode 100644 index 5eb7865d..00000000 --- a/assets/models/buildings/house_3_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_7.json b/assets/models/buildings/house_3_7.json deleted file mode 100644 index 7bcc8b5a..00000000 --- a/assets/models/buildings/house_3_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_3_8.json b/assets/models/buildings/house_3_8.json deleted file mode 100644 index 0643ca47..00000000 --- a/assets/models/buildings/house_3_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_3_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_1.json b/assets/models/buildings/house_4_1.json deleted file mode 100644 index 73a911cd..00000000 --- a/assets/models/buildings/house_4_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_2.json b/assets/models/buildings/house_4_2.json deleted file mode 100644 index db055224..00000000 --- a/assets/models/buildings/house_4_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_3.json b/assets/models/buildings/house_4_3.json deleted file mode 100644 index 64e39039..00000000 --- a/assets/models/buildings/house_4_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_4.json b/assets/models/buildings/house_4_4.json deleted file mode 100644 index 8963ba12..00000000 --- a/assets/models/buildings/house_4_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_5.json b/assets/models/buildings/house_4_5.json deleted file mode 100644 index c13ba9e3..00000000 --- a/assets/models/buildings/house_4_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_6.json b/assets/models/buildings/house_4_6.json deleted file mode 100644 index 9fc76393..00000000 --- a/assets/models/buildings/house_4_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_7.json b/assets/models/buildings/house_4_7.json deleted file mode 100644 index c7545cac..00000000 --- a/assets/models/buildings/house_4_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_4_8.json b/assets/models/buildings/house_4_8.json deleted file mode 100644 index a470127f..00000000 --- a/assets/models/buildings/house_4_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_4_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_1.json b/assets/models/buildings/house_5_1.json deleted file mode 100644 index 7966a3e8..00000000 --- a/assets/models/buildings/house_5_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_2.json b/assets/models/buildings/house_5_2.json deleted file mode 100644 index cdbc0a7c..00000000 --- a/assets/models/buildings/house_5_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_3.json b/assets/models/buildings/house_5_3.json deleted file mode 100644 index 657900a1..00000000 --- a/assets/models/buildings/house_5_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_4.json b/assets/models/buildings/house_5_4.json deleted file mode 100644 index ce52eac1..00000000 --- a/assets/models/buildings/house_5_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_5.json b/assets/models/buildings/house_5_5.json deleted file mode 100644 index 0223c690..00000000 --- a/assets/models/buildings/house_5_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_6.json b/assets/models/buildings/house_5_6.json deleted file mode 100644 index 769373be..00000000 --- a/assets/models/buildings/house_5_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_7.json b/assets/models/buildings/house_5_7.json deleted file mode 100644 index c428dbc2..00000000 --- a/assets/models/buildings/house_5_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_5_8.json b/assets/models/buildings/house_5_8.json deleted file mode 100644 index 2efa8d5b..00000000 --- a/assets/models/buildings/house_5_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_5_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_1.json b/assets/models/buildings/house_6_1.json deleted file mode 100644 index d2a4d5be..00000000 --- a/assets/models/buildings/house_6_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_2.json b/assets/models/buildings/house_6_2.json deleted file mode 100644 index c3f0a02f..00000000 --- a/assets/models/buildings/house_6_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_3.json b/assets/models/buildings/house_6_3.json deleted file mode 100644 index f145bf86..00000000 --- a/assets/models/buildings/house_6_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_4.json b/assets/models/buildings/house_6_4.json deleted file mode 100644 index 6c37797c..00000000 --- a/assets/models/buildings/house_6_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_5.json b/assets/models/buildings/house_6_5.json deleted file mode 100644 index b2d252ce..00000000 --- a/assets/models/buildings/house_6_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_6.json b/assets/models/buildings/house_6_6.json deleted file mode 100644 index e50bba0c..00000000 --- a/assets/models/buildings/house_6_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_7.json b/assets/models/buildings/house_6_7.json deleted file mode 100644 index 21ff16fd..00000000 --- a/assets/models/buildings/house_6_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_6_8.json b/assets/models/buildings/house_6_8.json deleted file mode 100644 index a2c66829..00000000 --- a/assets/models/buildings/house_6_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_6_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_1.json b/assets/models/buildings/house_7_1.json deleted file mode 100644 index 0017a2f4..00000000 --- a/assets/models/buildings/house_7_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_2.json b/assets/models/buildings/house_7_2.json deleted file mode 100644 index d62cfc35..00000000 --- a/assets/models/buildings/house_7_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_3.json b/assets/models/buildings/house_7_3.json deleted file mode 100644 index 112826b2..00000000 --- a/assets/models/buildings/house_7_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_4.json b/assets/models/buildings/house_7_4.json deleted file mode 100644 index 3a68ee2e..00000000 --- a/assets/models/buildings/house_7_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_5.json b/assets/models/buildings/house_7_5.json deleted file mode 100644 index 11a4aba9..00000000 --- a/assets/models/buildings/house_7_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_6.json b/assets/models/buildings/house_7_6.json deleted file mode 100644 index ab1f64e2..00000000 --- a/assets/models/buildings/house_7_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_7.json b/assets/models/buildings/house_7_7.json deleted file mode 100644 index 22262621..00000000 --- a/assets/models/buildings/house_7_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_7_8.json b/assets/models/buildings/house_7_8.json deleted file mode 100644 index 8797c640..00000000 --- a/assets/models/buildings/house_7_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_7_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_1.json b/assets/models/buildings/house_8_1.json deleted file mode 100644 index dc1860fa..00000000 --- a/assets/models/buildings/house_8_1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_1.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_2.json b/assets/models/buildings/house_8_2.json deleted file mode 100644 index 4a32f105..00000000 --- a/assets/models/buildings/house_8_2.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_2.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_3.json b/assets/models/buildings/house_8_3.json deleted file mode 100644 index 2b7c73b7..00000000 --- a/assets/models/buildings/house_8_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_3.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_4.json b/assets/models/buildings/house_8_4.json deleted file mode 100644 index 29a2a49c..00000000 --- a/assets/models/buildings/house_8_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_4.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_5.json b/assets/models/buildings/house_8_5.json deleted file mode 100644 index 54788a80..00000000 --- a/assets/models/buildings/house_8_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_5.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_6.json b/assets/models/buildings/house_8_6.json deleted file mode 100644 index 6d2e35b6..00000000 --- a/assets/models/buildings/house_8_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_6.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_7.json b/assets/models/buildings/house_8_7.json deleted file mode 100644 index 41009c69..00000000 --- a/assets/models/buildings/house_8_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_7.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/models/buildings/house_8_8.json b/assets/models/buildings/house_8_8.json deleted file mode 100644 index 800d2758..00000000 --- a/assets/models/buildings/house_8_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mesh": "meshes/buildings/house_8_8.dmf", - "color": [255, 0, 255, 255] -} diff --git a/assets/party.jsonc b/assets/party.jsonc deleted file mode 100644 index 260acbc1..00000000 --- a/assets/party.jsonc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "members": { - "hero": { - "attack": 10, - "defense": 5, - "magic": 0, - "speed": 10, - "luck": 0, - "healthMax": 30, - "mpMax": 10 - }, - "companion": { - "attack": 8, - "defense": 6, - "magic": 4, - "speed": 8, - "luck": 1, - "healthMax": 25, - "mpMax": 15 - } - } -} diff --git a/assets/story/storyflag.jsonc b/assets/story/storyflag.jsonc deleted file mode 100644 index f2738e1b..00000000 --- a/assets/story/storyflag.jsonc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "flags": { - "test": { - "initial": true - }, - "test2": { - "initial": 1 - } - } -} diff --git a/assets/tiles.png b/assets/tiles.png deleted file mode 100644 index bc170af1c5d190c29153c0a4053d6a7735a28977..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`rk*a2Ar*6yQxXz>tUq+%Km+&Y z6%VEbCnh8$Bz{OZz#sC#o1cx@#EJbDD|7yV42hmLMrLM)O_^K)ON6~I0}WvCboFyt I=akR{0DW;G6951J diff --git a/cmake/modules/Findcglm.cmake b/cmake/modules/Findcglm.cmake deleted file mode 100644 index 7f3d7867..00000000 --- a/cmake/modules/Findcglm.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(FetchContent) -FetchContent_Declare( - cglm - # GIT_REPOSITORY https://git.wish.moe/YourWishes/cglm.git - GIT_REPOSITORY https://github.com/recp/cglm.git - GIT_TAG v0.9.6 -) - -FetchContent_MakeAvailable(cglm) -set(cglm_FOUND ON) \ No newline at end of file diff --git a/cmake/modules/Findcmocka.cmake b/cmake/modules/Findcmocka.cmake deleted file mode 100644 index 4297fabd..00000000 --- a/cmake/modules/Findcmocka.cmake +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(FetchContent) -FetchContent_Declare( - cmocka - GIT_REPOSITORY https://gitlab.com/cmocka/cmocka.git - GIT_TAG cmocka-2.0.1 -) - -FetchContent_MakeAvailable(cmocka) -set(cmocka_FOUND ON) \ No newline at end of file diff --git a/cmake/modules/Findlibmad.cmake b/cmake/modules/Findlibmad.cmake deleted file mode 100644 index b0c3d020..00000000 --- a/cmake/modules/Findlibmad.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# NOTE: libmad itself is GPL-licensed (see https://www.underbit.com/products/mad/), -# not MIT like the rest of this project. Any binary linking it must comply -# with the GPL - a deliberate tradeoff accepted for MP3 decode correctness -# (see audiostreammp3decodersw.c's own comment on why minimp3 wasn't enough). - -find_path(LIBMAD_INCLUDE_DIR NAMES mad.h) -mark_as_advanced(LIBMAD_INCLUDE_DIR) - -find_library(LIBMAD_LIBRARY NAMES mad) -mark_as_advanced(LIBMAD_LIBRARY) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - libmad - REQUIRED_VARS - LIBMAD_LIBRARY - LIBMAD_INCLUDE_DIR -) - -if(libmad_FOUND AND NOT TARGET libmad::mad) - add_library(libmad::mad UNKNOWN IMPORTED) - set_target_properties(libmad::mad - PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${LIBMAD_INCLUDE_DIR} - IMPORTED_LOCATION ${LIBMAD_LIBRARY} - ) -endif() diff --git a/cmake/modules/Findlibzip.cmake b/cmake/modules/Findlibzip.cmake deleted file mode 100644 index 2254ae29..00000000 --- a/cmake/modules/Findlibzip.cmake +++ /dev/null @@ -1,61 +0,0 @@ -find_package(ZLIB REQUIRED) - -find_path(LIBZIP_INCLUDE_DIR NAMES zip.h) -mark_as_advanced(LIBZIP_INCLUDE_DIR) - -find_library(LIBZIP_LIBRARY NAMES zip) -mark_as_advanced(LIBZIP_LIBRARY) - -get_filename_component(_libzip_libdir ${LIBZIP_LIBRARY} DIRECTORY) -find_file(_libzip_pkgcfg libzip.pc - HINTS ${_libzip_libdir} ${LIBZIP_INCLUDE_DIR}/.. - PATH_SUFFIXES pkgconfig lib/pkgconfig libdata/pkgconfig - NO_DEFAULT_PATH -) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - LIBZIP - REQUIRED_VARS - LIBZIP_LIBRARY - LIBZIP_INCLUDE_DIR - _libzip_pkgcfg -) - -if(LIBZIP_FOUND) - if(NOT TARGET libzip::zip) - add_library(libzip::zip UNKNOWN IMPORTED) - set_target_properties(libzip::zip - PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${LIBZIP_INCLUDE_DIR} - INTERFACE_LINK_LIBRARIES ZLIB::ZLIB - IMPORTED_LOCATION ${LIBZIP_LIBRARY} - ) - # (Ab)use the (always) installed pkgconfig file to check if BZip2 is required - file(STRINGS ${_libzip_pkgcfg} _have_extra_libs REGEX Libs) - if(_have_extra_libs MATCHES "-lbz2") - find_package(BZip2 REQUIRED) - set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES BZip2::BZip2) - endif() - if(_have_extra_libs MATCHES "-lcrypto") - find_package(OpenSSL REQUIRED) - set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES OpenSSL::Crypto) - endif() - if(_have_extra_libs MATCHES "-lgnutls") - find_package(GnuTLS REQUIRED) - set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES GnuTLS::GnuTLS) - endif() - if(_have_extra_libs MATCHES "-lnettle") - find_package(Nettle REQUIRED) - set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES Nettle::Nettle) - endif() - if(_have_extra_libs MATCHES "-llzma") - find_package(LibLZMA REQUIRED) - set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES LibLZMA::LibLZMA) - endif() - if(_have_extra_libs MATCHES "-lz") - find_package(ZLIB REQUIRED) - set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES ZLIB::ZLIB) - endif() - endif() -endif() diff --git a/cmake/modules/Findstb.cmake b/cmake/modules/Findstb.cmake deleted file mode 100644 index 263088cd..00000000 --- a/cmake/modules/Findstb.cmake +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(FetchContent) -FetchContent_Declare( - stb - GIT_REPOSITORY https://github.com/nothings/stb.git -) - -# Fetch stb if not already done -FetchContent_MakeAvailable(stb) - -# Find the stb_image.h header -set(STB_INCLUDE_DIR "${stb_SOURCE_DIR}") -set(STB_IMAGE_HEADER "${stb_SOURCE_DIR}/stb_image.h") - -if(EXISTS "${STB_IMAGE_HEADER}") - add_library(stb_image INTERFACE) - target_include_directories(stb_image INTERFACE "${STB_INCLUDE_DIR}") - set(STB_IMAGE_FOUND TRUE) -else() - set(STB_IMAGE_FOUND FALSE) -endif() - -# Standard CMake variables -set(STB_IMAGE_INCLUDE_DIRS "${STB_INCLUDE_DIR}") -set(STB_IMAGE_LIBRARIES stb_image) - -mark_as_advanced(STB_IMAGE_INCLUDE_DIRS STB_IMAGE_LIBRARIES STB_IMAGE_FOUND) \ No newline at end of file diff --git a/cmake/modules/Findyyjson.cmake b/cmake/modules/Findyyjson.cmake deleted file mode 100644 index 5acfcd85..00000000 --- a/cmake/modules/Findyyjson.cmake +++ /dev/null @@ -1,18 +0,0 @@ -include(FetchContent) - -if(NOT TARGET yyjson) - FetchContent_Declare( - yyjson - GIT_REPOSITORY https://github.com/ibireme/yyjson.git - GIT_TAG 0.12.0 - ) - FetchContent_MakeAvailable(yyjson) -endif() - -# Provide an imported target if not already available -if(NOT TARGET yyjson::yyjson) - add_library(yyjson::yyjson ALIAS yyjson) -endif() - -# Mark yyjson as found for find_package compatibility -set(yyjson_FOUND TRUE) diff --git a/cmake/modules/dusktest.cmake b/cmake/modules/dusktest.cmake deleted file mode 100644 index 421620ea..00000000 --- a/cmake/modules/dusktest.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -find_package(cmocka REQUIRED) - -# Function to create a test executable from a single .c file -function(dusktest TEST_C_FILE) - # Create target name from file name - get_filename_component(TEST_NAME ${TEST_C_FILE} NAME_WE) - - # Create the executable target - add_executable(${TEST_NAME}) - - # Setup some compiler definitions. - target_compile_definitions(${TEST_NAME} PRIVATE - DUSK_TEST_ASSERT=1 - ) - - # Add the test file - target_sources(${TEST_NAME} PRIVATE ${TEST_C_FILE}) - - # Add sources, both common and test-specific - target_include_directories(${TEST_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR} ${DUSK_TEST_DIR}) - - # Link against DuskCore and cmocka - target_link_libraries(${TEST_NAME} PUBLIC ${DUSK_LIBRARY_TARGET_NAME} cmocka) - - # Add as a CTest - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) -endfunction() \ No newline at end of file diff --git a/cmake/targets/dolphin.cmake b/cmake/targets/dolphin.cmake deleted file mode 100644 index 46f5b2d5..00000000 --- a/cmake/targets/dolphin.cmake +++ /dev/null @@ -1,81 +0,0 @@ -# Build type: DOL (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver) -set(DUSK_DOLPHIN_BUILD_TYPE "DOL" CACHE STRING "Dolphin asset source: DOL (SD/USB) or ISO (DVD disc)") -set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "DOL" "ISO") - -# Numeric tokens so #if DUSK_DOLPHIN_BUILD_TYPE == DOL works in C. -# DUSK_DOLPHIN_BUILD_TYPE is passed without quotes so it expands to the identifier. -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_DOLPHIN - DUSK_INPUT_GAMEPAD - # DUSK_DISPLAY_WIDTH=640 - # DUSK_DISPLAY_HEIGHT=480 - DUSK_DISPLAY_WIDTH=320 - DUSK_DISPLAY_HEIGHT=240 - DUSK_DISPLAY_OVERSCAN=16 - DUSK_THREAD_PTHREAD - DOL=1 - ISO=2 - DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE} - # GameCube/Wii PowerPC is always big-endian; declare it at compile time - # like every other target instead of relying on endian.h's runtime probe. - DUSK_PLATFORM_ENDIAN_BIG -) - -# Custom compiler flags -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions") - -# Disable all warnings -target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w) - -# cglm: fetched at source level via Findcglm.cmake (FetchContent, headers only) -find_package(cglm REQUIRED) - -# Pre-create ZLIB::ZLIB so any downstream cmake module that resolves it -# (FindZLIB, pkg-config IMPORTED_TARGET Requires processing) gets plain -lz -# rather than an unresolvable IMPORTED target that the PPC linker rejects. -if(NOT TARGET ZLIB::ZLIB) - add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL) - set_target_properties(ZLIB::ZLIB PROPERTIES INTERFACE_LINK_LIBRARIES "z") -endif() - -# Mark libzip as found so src/dusk/CMakeLists.txt skips Findlibzip.cmake. -# Findlibzip.cmake calls find_package(ZLIB) which can recreate a broken -# ZLIB::ZLIB IMPORTED target, bypassing the shim above. -set(libzip_FOUND TRUE CACHE BOOL "libzip found (devkitpro portlibs)" FORCE) - -# Locate zip.h in the devkitpro sysroot (respects CMAKE_FIND_ROOT_PATH). -find_path(_dusk_zip_inc NAMES zip.h) -if(_dusk_zip_inc) - target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE "${_dusk_zip_inc}") -endif() - -# Link libraries. -# zip/z/lzma use target_link_options (raw flags) to bypass cmake target -# resolution - pkg-config-generated targets for these carry ZLIB::ZLIB in -# INTERFACE_LINK_LIBRARIES which breaks the PPC link step. -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE - cglm - m - zip - bz2 - zstd - z - lzma - asnd - mad -) - -if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN_BUILD_ISO) -else() - target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE fat) -endif() - -# Postbuild: ELF -> DOL -set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol") -add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD - COMMAND elf2dol - "$" - "${DUSK_BINARY_TARGET_NAME_DOL}" - COMMENT "Generating ${DUSK_BINARY_TARGET_NAME_DOL} from ${DUSK_BINARY_TARGET_NAME}" -) diff --git a/cmake/targets/gamecube.cmake b/cmake/targets/gamecube.cmake deleted file mode 100644 index f8234be9..00000000 --- a/cmake/targets/gamecube.cmake +++ /dev/null @@ -1,24 +0,0 @@ -include(cmake/targets/dolphin.cmake) - -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_GAMECUBE -) - -# Link libraries -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE - # bba -) - -# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images -if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") - add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD - COMMAND ${Python3_EXECUTABLE} - "${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py" - "GCN" - "${DUSK_BINARY_TARGET_NAME_DOL}" - "${DUSK_ASSETS_ZIP}" - "${DUSK_GAME_NAME}" - "${DUSK_BUILD_DIR}" - COMMENT "Building GameCube ISO images (NTSC-J, NTSC-U, PAL)" - ) -endif() diff --git a/cmake/targets/knulli.cmake b/cmake/targets/knulli.cmake deleted file mode 100644 index 140aecae..00000000 --- a/cmake/targets/knulli.cmake +++ /dev/null @@ -1,46 +0,0 @@ -# Find link platform-specific libraries -set(OpenGL_GL_PREFERENCE LEGACY) -find_package(SDL2 REQUIRED) -find_library(EGL_LIB EGL REQUIRED) -find_library(GL_LIB GL REQUIRED) -find_package(OpenGL REQUIRED) - -# Setup endianess at compile time to optimize. -include(TestBigEndian) -test_big_endian(IS_BIG_ENDIAN) -if(IS_BIG_ENDIAN) - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_PLATFORM_ENDIAN_BIG - ) -else() - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_PLATFORM_ENDIAN_LITTLE - ) -endif() - -# Link required libraries. -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE - SDL2 - pthread - OpenGL::GLES2 - ${GL_LIB} - ${EGL_LIB} - m -) - -# Define platform-specific macros. -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_SDL2 - DUSK_OPENGL - DUSK_OPENGL_ES - DUSK_LINUX - DUSK_KNULLI - DUSK_DISPLAY_SIZE_DYNAMIC - DUSK_DISPLAY_WIDTH_DEFAULT=640 - DUSK_DISPLAY_HEIGHT_DEFAULT=480 - DUSK_DISPLAY_SCREEN_HEIGHT=240 - DUSK_INPUT_KEYBOARD - DUSK_INPUT_POINTER - DUSK_INPUT_GAMEPAD - DUSK_TIME_DYNAMIC -) \ No newline at end of file diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake deleted file mode 100644 index ac198623..00000000 --- a/cmake/targets/linux.cmake +++ /dev/null @@ -1,49 +0,0 @@ -# Find link platform-specific libraries -find_package(SDL2 REQUIRED) -find_package(OpenGL REQUIRED) -# find_package(CURL REQUIRED) - -# Setup endianess at compile time to optimize. -include(TestBigEndian) -test_big_endian(IS_BIG_ENDIAN) -if(IS_BIG_ENDIAN) - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_PLATFORM_ENDIAN_BIG - ) -else() - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_PLATFORM_ENDIAN_LITTLE - ) -endif() - -# Link required libraries. -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - SDL2 - pthread - OpenGL::GL - GL - m - # CURL::libcurl -) - -set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.") -set(DUSK_TRACK_MEMORY ON CACHE BOOL "Track memoryAllocate() call sites to help find memory leaks.") - -# Define platform-specific macros. -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_SDL2 - DUSK_OPENGL - DUSK_CONSOLE_POSIX - # DUSK_OPENGL_LEGACY - DUSK_LINUX - DUSK_DISPLAY_SIZE_DYNAMIC - DUSK_DISPLAY_WIDTH_DEFAULT=854 - DUSK_DISPLAY_HEIGHT_DEFAULT=480 - DUSK_DISPLAY_SCREEN_HEIGHT=240 - DUSK_INPUT_KEYBOARD - DUSK_INPUT_POINTER - DUSK_INPUT_GAMEPAD - DUSK_TIME_DYNAMIC - DUSK_NETWORK_IPV6 - DUSK_THREAD_PTHREAD -) \ No newline at end of file diff --git a/cmake/targets/psp.cmake b/cmake/targets/psp.cmake deleted file mode 100644 index 68f5fec5..00000000 --- a/cmake/targets/psp.cmake +++ /dev/null @@ -1,137 +0,0 @@ -set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE) -set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE) -set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE) -set(CMAKE_C_COMPILER_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE) -set(CMAKE_C_ARCHIVE_CREATE "$ENV{PSPDEV}/bin/psp-ar qc ") -set(CMAKE_C_ARCHIVE_APPEND "$ENV{PSPDEV}/bin/psp-ar q ") -set(CMAKE_C_ARCHIVE_FINISH "$ENV{PSPDEV}/bin/psp-ranlib ") -set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) -set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE) - -find_package(SDL2 REQUIRED) -find_package(OpenGL REQUIRED) -target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC - ${SDL2_LIBRARIES} - SDL2 - pthread - OpenGL::GL - zip - bz2 - z - mbedtls - mbedcrypto - lzma - m - - pspdebug - pspdisplay - pspge - pspctrl - pspgu - pspaudio - pspaudiolib - pspmp3 - psputility - pspvfpu - pspvram - psphprm - pspnet - pspnet_inet - pspnet_apctl - psphttp - pspssl -) - -target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE - ${SDL2_INCLUDE_DIRS} -) - -target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC - DUSK_SDL2 - DUSK_OPENGL - DUSK_PSP - DUSK_INPUT_GAMEPAD - DUSK_PLATFORM_ENDIAN_LITTLE - DUSK_OPENGL_LEGACY - DUSK_DISPLAY_WIDTH=480 - DUSK_DISPLAY_HEIGHT=272 - DUSK_THREAD_PTHREAD - DUSK_TIME_DYNAMIC - 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") - target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC - DUSK_ASSERTIONS_FAKED - ) -endif() - -# Generate PARAM.SFO as a normal tracked build output (instead of letting -# create_pbp_file() auto-generate + delete it) so it can be reused below by -# a properly dependency-tracked EBOOT.PBP repack step. -set(DUSK_PSP_PARAM_SFO "${DUSK_BUILD_DIR}/PARAM.SFO") -add_custom_command( - OUTPUT "${DUSK_PSP_PARAM_SFO}" - COMMAND "$ENV{PSPDEV}/bin/mksfoex" "-d" "MEMSIZE=1" "-s" "APP_VER=01.00" - "${DUSK_BINARY_TARGET_NAME}" "${DUSK_PSP_PARAM_SFO}" - COMMENT "Generating PARAM.SFO for ${DUSK_BINARY_TARGET_NAME}" - VERBATIM -) -add_custom_target(DuskPspParamSfo DEPENDS "${DUSK_PSP_PARAM_SFO}") - -# create_pbp_file()'s own POST_BUILD chain (below) also consumes -# DUSK_PSP_PARAM_SFO, so make sure it exists before that chain runs. -add_dependencies(${DUSK_BINARY_TARGET_NAME} DuskPspParamSfo) - -# Postbuild, create .pbp file for PSP. -create_pbp_file( - TARGET "${DUSK_BINARY_TARGET_NAME}" - ICON_PATH NULL - BACKGROUND_PATH NULL - PREVIEW_PATH NULL - TITLE "${DUSK_BINARY_TARGET_NAME}" - PSAR_PATH ${DUSK_ASSETS_ZIP} - VERSION 01.00 - SFO_PATH "${DUSK_PSP_PARAM_SFO}" - OUTPUT_DIR "${DUSK_BUILD_DIR}" -) - -# CreatePBP.cmake's pack-pbp step is a POST_BUILD command tied to the -# executable target, so it only reruns when the ELF itself relinks. That -# means regenerating dusk.dsk (assets) alone, without touching any C -# source, silently leaves EBOOT.PBP embedding a stale asset pak. Repack it -# here as a normal file-tracked custom command depending on both the -# executable and the asset zip, so EBOOT.PBP always reflects the current -# assets even when nothing else about the build changed. -set(DUSK_PSP_EBOOT "${DUSK_BUILD_DIR}/EBOOT.PBP") -if(BUILD_PRX) - set(DUSK_PSP_EXECUTABLE "$.prx") -else() - set(DUSK_PSP_EXECUTABLE "$") -endif() -add_custom_command( - OUTPUT "${DUSK_PSP_EBOOT}" - COMMAND "$ENV{PSPDEV}/bin/pack-pbp" "${DUSK_PSP_EBOOT}" "${DUSK_PSP_PARAM_SFO}" - "NULL" "NULL" "NULL" "NULL" "NULL" - "${DUSK_PSP_EXECUTABLE}" "${DUSK_ASSETS_ZIP}" - DEPENDS - "$" - "${DUSK_PSP_PARAM_SFO}" - "${DUSK_ASSETS_ZIP}" - COMMENT "Repacking EBOOT.PBP (tracks executable + asset pak freshness)" - VERBATIM -) -add_custom_target(DuskPspEbootRepack ALL DEPENDS "${DUSK_PSP_EBOOT}") \ No newline at end of file diff --git a/cmake/targets/vita.cmake b/cmake/targets/vita.cmake deleted file mode 100644 index eadf73e1..00000000 --- a/cmake/targets/vita.cmake +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -if(NOT DEFINED ENV{VITASDK}) - message(FATAL_ERROR "VITASDK environment variable is not set.") -endif() - -include("$ENV{VITASDK}/share/vita.cmake" REQUIRED) - -set(VITA_APP_NAME "Dusk") -set(VITA_TITLEID "DUSK00001") -set(VITA_VERSION "01.00") - -find_package(SDL2 REQUIRED) - -# Custom flags for cglm -set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE) -set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE) -find_package(cglm REQUIRED) - -# Link libraries -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - ${SDL2_LIBRARIES} - cglm - SDL2 - SDL2main - zip - bz2 - z - zstd - crypto - lzma - m - pthread - stdc++ - vitaGL - mathneon - vitashark - kubridge_stub - SceAppMgr_stub - SceAudio_stub - SceCtrl_stub - SceCommonDialog_stub - SceDisplay_stub - SceKernelDmacMgr_stub - SceGxm_stub - SceShaccCg_stub - SceSysmodule_stub - ScePower_stub - SceTouch_stub - SceVshBridge_stub - SceIofilemgr_stub - SceShaccCgExt - libtaihen_stub.a - - - # SceKernel_stub - SceAppUtil_stub - SceHid_stub - SceRtc_stub -) - -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE - ${SDL2_INCLUDE_DIRS} -) - -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_SDL2 - DUSK_OPENGL - DUSK_VITA - DUSK_INPUT_GAMEPAD - DUSK_PLATFORM_ENDIAN_LITTLE - DUSK_OPENGL_LEGACY - DUSK_DISPLAY_WIDTH=960 - DUSK_DISPLAY_HEIGHT=544 -) - -# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing) -vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE) - -# Post-build: package SELF + assets into a .vpk installable on the Vita -vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self - VERSION ${VITA_VERSION} - NAME ${VITA_APP_NAME} - FILE ${DUSK_ASSETS_ZIP} dusk.dsk -) diff --git a/cmake/targets/wii.cmake b/cmake/targets/wii.cmake deleted file mode 100644 index fa6be9c4..00000000 --- a/cmake/targets/wii.cmake +++ /dev/null @@ -1,38 +0,0 @@ -include(cmake/targets/dolphin.cmake) - -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_WII -) - -# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h. -set(DUSK_SAVE_WII_METHOD "SD" CACHE STRING - "Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\ -compatible memory card emulation), or SD (SD card via libfat)" -) -set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD") - -target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD} -) - -# Generate Homebrew Channel meta.xml from project identity variables -string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC) -configure_file( - "${CMAKE_SOURCE_DIR}/docker/dolphin/meta.xml.in" - "${DUSK_BUILD_DIR}/meta.xml" - @ONLY -) - -# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images -if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") - add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD - COMMAND ${Python3_EXECUTABLE} - "${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py" - "WII" - "${DUSK_BINARY_TARGET_NAME_DOL}" - "${DUSK_ASSETS_ZIP}" - "${DUSK_GAME_NAME}" - "${DUSK_BUILD_DIR}" - COMMENT "Building Wii ISO images (NTSC-J, NTSC-U, PAL)" - ) -endif() diff --git a/cmake/toolchains/aarch64-linux-gnu.cmake b/cmake/toolchains/aarch64-linux-gnu.cmake deleted file mode 100644 index ac7ef615..00000000 --- a/cmake/toolchains/aarch64-linux-gnu.cmake +++ /dev/null @@ -1,29 +0,0 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(CMAKE_SYSTEM_PROCESSOR aarch64) - -set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) -set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) -set(CMAKE_ASM_COMPILER aarch64-linux-gnu-gcc) - -set(CMAKE_SYSROOT /) -set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu) -set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu) - -set(CMAKE_FIND_ROOT_PATH - /usr/aarch64-linux-gnu - /usr/lib/aarch64-linux-gnu - /usr/include/aarch64-linux-gnu -) - -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - -set(ENV{PKG_CONFIG_LIBDIR} "/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig") -set(ENV{PKG_CONFIG_PATH} "/usr/lib/aarch64-linux-gnu/pkgconfig") - -set(CMAKE_PREFIX_PATH "/usr/aarch64-linux-gnu;/usr/lib/aarch64-linux-gnu") - -# Optional: helps some Find modules -set(SDL2_DIR "/usr/lib/aarch64-linux-gnu/cmake/SDL2" CACHE PATH "") \ No newline at end of file diff --git a/docker/dolphin/Dockerfile b/docker/dolphin/Dockerfile deleted file mode 100644 index 5dbd3f95..00000000 --- a/docker/dolphin/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM ghcr.io/extremscorner/libogc2 -WORKDIR /workdir -RUN apt update && \ - dkp-pacman -Syu --noconfirm && \ - apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso && \ - dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd -VOLUME ["/workdir"] \ No newline at end of file diff --git a/docker/dolphin/meta.xml.in b/docker/dolphin/meta.xml.in deleted file mode 100644 index ee6a7e3c..00000000 --- a/docker/dolphin/meta.xml.in +++ /dev/null @@ -1,10 +0,0 @@ - - - @DUSK_GAME_NAME@ - @PROJECT_VERSION@ - @DUSK_BUILD_DATE@ - @DUSK_GAME_AUTHOR@ - @DUSK_GAME_SHORT_DESCRIPTION@ - @DUSK_GAME_LONG_DESCRIPTION@ - - diff --git a/docker/knulli/Dockerfile b/docker/knulli/Dockerfile deleted file mode 100644 index 43a0624d..00000000 --- a/docker/knulli/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM debian:trixie -ENV DEBIAN_FRONTEND=noninteractive -WORKDIR /workdir -RUN dpkg --add-architecture arm64 && \ - apt-get update && \ - apt-get install -y --no-install-recommends \ - crossbuild-essential-arm64 \ - ca-certificates \ - pkg-config \ - cmake \ - make \ - ninja-build \ - git \ - file \ - python3 \ - python3-pip \ - python3-polib \ - python3-pil \ - python3-dotenv \ - python3-pyqt5 \ - python3-opengl \ - liblua5.4-dev:arm64 \ - xz-utils:arm64 \ - libbz2-dev:arm64 \ - zlib1g-dev:arm64 \ - libzip-dev:arm64 \ - libssl-dev:arm64 \ - libsdl2-dev:arm64 \ - liblzma-dev:arm64 \ - libopengl0:arm64 \ - libgl1:arm64 \ - libegl1:arm64 \ - libgles2:arm64 \ - libgl1-mesa-dev:arm64 && \ - rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/docker/linux/Dockerfile b/docker/linux/Dockerfile deleted file mode 100644 index f4ab671e..00000000 --- a/docker/linux/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM ubuntu:latest -WORKDIR /workdir -RUN apt-get update -RUN apt-get install -y \ - build-essential \ - cmake \ - python3 \ - python3-pip \ - python3-polib \ - python3-pil \ - libsdl2-dev \ - libgl1-mesa-dev \ - libzip-dev \ - python3-dotenv \ - python3-pyqt5 \ - python3-opengl \ - xz-utils \ - liblzma-dev \ - libbz2-dev \ - zlib1g-dev \ - libzip-dev \ - libbz2-dev \ - git \ - libssl-dev \ No newline at end of file diff --git a/docker/psp/Dockerfile b/docker/psp/Dockerfile deleted file mode 100644 index dff1cb8e..00000000 --- a/docker/psp/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM pspdev/pspdev:latest -WORKDIR /workdir -RUN apk add --no-cache \ - python3 \ - py3-pip \ - py3-dotenv -VOLUME ["/workdir"] \ No newline at end of file diff --git a/docker/vita/Dockerfile b/docker/vita/Dockerfile deleted file mode 100644 index cb0e6a93..00000000 --- a/docker/vita/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM vitasdk/vitasdk:latest -WORKDIR /workdir - -# Install vitaGL and its dependencies (vitashark, SceShaccCg) via vdpm -RUN which vdpm - -# Install Python (needed for Dusk code generation tools) -RUN apk add --no-cache \ - python3 \ - py3-pip \ - py3-dotenv - -VOLUME ["/workdir"] diff --git a/editor/client/public/common/chunkterrain.js b/editor/client/public/common/chunkterrain.js deleted file mode 100644 index 4a19360e..00000000 --- a/editor/client/public/common/chunkterrain.js +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -"use strict"; - -// ChunkTerrain - JS port of the terrain-quad generation performed by -// tools/asset/chunk/__main__.py (build_terrain_verts / _tile_quad / -// _RAMP_CORNERS) so the editor's 3D preview can show the same geometry the -// Python compiler will bake into the chunk's terrain mesh, without waiting -// for a compile pass. Vertex layout matches DMF: interleaved [u, v, x, y, z]. - -const ChunkTerrain = (() => { - const CHUNK_WIDTH = 16; - const CHUNK_HEIGHT = 16; - const CHUNK_DEPTH = 4; - const TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH; - - // World-space Z distance of one Z-layer/story, in world-float space. - // Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h. - const WORLD_LAYER_HEIGHT = Math.SQRT1_2; - - const TILE_SHAPE_NULL = 0; - const TILE_SHAPE_GROUND = 1; - const TILE_SHAPE_RAMP_NORTH = 2; - const TILE_SHAPE_RAMP_EAST = 3; - const TILE_SHAPE_RAMP_SOUTH = 4; - const TILE_SHAPE_RAMP_WEST = 5; - const TILE_SHAPE_RAMP_NORTHEAST = 6; - const TILE_SHAPE_RAMP_NORTHWEST = 7; - const TILE_SHAPE_RAMP_SOUTHEAST = 8; - const TILE_SHAPE_RAMP_SOUTHWEST = 9; - const TILE_SHAPE_RAMP_NORTHEAST_INNER = 10; - const TILE_SHAPE_RAMP_NORTHWEST_INNER = 11; - const TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12; - const TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13; - - // Per-shape corner heights as [sw, se, ne, nw] offsets from tile base Z. - // NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1. - // Diagonal outer-corner ramps: only the named corner raised, rest at 0. - // Diagonal inner-corner ramps: only the corner opposite the named one is - // lowered to 0, the rest (including the named corner) stay raised at 1. - const RAMP_CORNERS = { - [TILE_SHAPE_GROUND]: [0.0, 0.0, 0.0, 0.0], - [TILE_SHAPE_RAMP_NORTH]: [0.0, 0.0, 1.0, 1.0], - [TILE_SHAPE_RAMP_SOUTH]: [1.0, 1.0, 0.0, 0.0], - [TILE_SHAPE_RAMP_EAST]: [0.0, 1.0, 1.0, 0.0], - [TILE_SHAPE_RAMP_WEST]: [1.0, 0.0, 0.0, 1.0], - [TILE_SHAPE_RAMP_NORTHEAST]: [0.0, 0.0, 1.0, 0.0], - [TILE_SHAPE_RAMP_NORTHWEST]: [0.0, 0.0, 0.0, 1.0], - [TILE_SHAPE_RAMP_SOUTHEAST]: [0.0, 1.0, 0.0, 0.0], - [TILE_SHAPE_RAMP_SOUTHWEST]: [1.0, 0.0, 0.0, 0.0], - [TILE_SHAPE_RAMP_NORTHEAST_INNER]: [0.0, 1.0, 1.0, 1.0], - [TILE_SHAPE_RAMP_NORTHWEST_INNER]: [1.0, 0.0, 1.0, 1.0], - [TILE_SHAPE_RAMP_SOUTHEAST_INNER]: [1.0, 1.0, 1.0, 0.0], - [TILE_SHAPE_RAMP_SOUTHWEST_INNER]: [1.0, 1.0, 0.0, 1.0], - }; - - function tileIndex(x, y, z) { - return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT; - } - - // Six vertices (2 CCW triangles) for a quad with per-corner Z heights. - // Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1). - function pushTileQuad(out, u0, u1, v0, v1, fx, fy, swZ, seZ, neZ, nwZ) { - out.push( - u0, v0, fx, fy, swZ, - u1, v0, fx + 1, fy, seZ, - u1, v1, fx + 1, fy + 1, neZ, - u0, v0, fx, fy, swZ, - u1, v1, fx + 1, fy + 1, neZ, - u0, v1, fx, fy + 1, nwZ, - ); - } - - // Generate terrain quads for every non-null tile in a flat tiles array - // (length TILE_COUNT, indexed via tileIndex). Returns a Float32Array of - // interleaved [u, v, x, y, z] vertices, matching DMF.VERTEX_FLOATS. - function buildTerrainVerts(tiles) { - const out = []; - for(let z = 0; z < CHUNK_DEPTH; z++) { - for(let y = 0; y < CHUNK_HEIGHT; y++) { - for(let x = 0; x < CHUNK_WIDTH; x++) { - const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL; - const corners = RAMP_CORNERS[type]; - if(!corners) continue; - - const u0 = x / CHUNK_WIDTH; - const u1 = (x + 1) / CHUNK_WIDTH; - const v0 = y / CHUNK_HEIGHT; - const v1 = (y + 1) / CHUNK_HEIGHT; - const [sw, se, ne, nw] = corners; - - pushTileQuad( - out, u0, u1, v0, v1, x, y, - (z + sw) * WORLD_LAYER_HEIGHT, - (z + se) * WORLD_LAYER_HEIGHT, - (z + ne) * WORLD_LAYER_HEIGHT, - (z + nw) * WORLD_LAYER_HEIGHT - ); - } - } - } - return new Float32Array(out); - } - - // Four edges (SW-SE, SE-NE, NE-NW, NW-SW) of a tile's quad as line - // segments, lifted slightly above the terrain surface to avoid - // z-fighting - same corner-height convention as pushTileQuad(). - function pushTileGridLines(out, fx, fy, swZ, seZ, neZ, nwZ) { - const eps = 0.01; - const sw = [fx, fy, swZ + eps]; - const se = [fx + 1, fy, seZ + eps]; - const ne = [fx + 1, fy + 1, neZ + eps]; - const nw = [fx, fy + 1, nwZ + eps]; - const edges = [sw, se, se, ne, ne, nw, nw, sw]; - for(const [x, y, z] of edges) out.push(0, 0, x, y, z); - } - - // Generate grid-outline line segments for every non-null tile, following - // the same per-corner heights as buildTerrainVerts() so lines hug ramps - // instead of cutting through them. Returns a Float32Array of interleaved - // [u, v, x, y, z] vertices meant to be drawn with gl.LINES (pairs). - function buildGridLines(tiles) { - const out = []; - for(let z = 0; z < CHUNK_DEPTH; z++) { - for(let y = 0; y < CHUNK_HEIGHT; y++) { - for(let x = 0; x < CHUNK_WIDTH; x++) { - const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL; - const corners = RAMP_CORNERS[type]; - if(!corners) continue; - - const [sw, se, ne, nw] = corners; - pushTileGridLines( - out, x, y, - (z + sw) * WORLD_LAYER_HEIGHT, - (z + se) * WORLD_LAYER_HEIGHT, - (z + ne) * WORLD_LAYER_HEIGHT, - (z + nw) * WORLD_LAYER_HEIGHT - ); - } - } - } - return new Float32Array(out); - } - - return Object.freeze({ - CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_COUNT, WORLD_LAYER_HEIGHT, - TILE_SHAPE_NULL, TILE_SHAPE_GROUND, - TILE_SHAPE_RAMP_NORTH, TILE_SHAPE_RAMP_EAST, - TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST, - TILE_SHAPE_RAMP_NORTHEAST, TILE_SHAPE_RAMP_NORTHWEST, - TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST, - TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER, - TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER, - RAMP_CORNERS, tileIndex, buildTerrainVerts, buildGridLines, - }); -})(); diff --git a/editor/client/public/common/dmf.js b/editor/client/public/common/dmf.js deleted file mode 100644 index 2a293cb5..00000000 --- a/editor/client/public/common/dmf.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -"use strict"; - -// DMF - Dusk Mesh Format -// -// Header (12 bytes): -// [0-3] "DMF\0" magic -// [4-7] uint32 version (little-endian) -// [8-11] uint32 vertCount (little-endian) -// -// Followed by vertCount vertices, each 20 bytes: -// [0-7] float32[2] uv (little-endian) -// [8-19] float32[3] pos (little-endian) - -const DMF = (() => { - const MAGIC = [0x44, 0x4d, 0x46, 0x00]; // "DMF\0" - const VERSION = 1; - const HEADER_SIZE = 12; - const VERTEX_FLOATS = 5; // uv[2] + pos[3] - const VERTEX_SIZE = VERTEX_FLOATS * 4; - - // Decode a DMF ArrayBuffer into { version, vertexCount, floats }, where - // floats is a Float32Array of tightly interleaved [u, v, x, y, z] per - // vertex - ready to hand straight to gl.bufferData(). - function decode(buffer) { - const bytes = new Uint8Array(buffer); - const view = new DataView(buffer); - - if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DMF"); - if( - bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || - bytes[2] !== MAGIC[2] || bytes[3] !== MAGIC[3] - ) { - throw new Error("Invalid DMF magic bytes - not a DMF file"); - } - - const version = view.getUint32(4, true); - if(version !== VERSION) { - throw new Error(`Unsupported DMF version: ${version}`); - } - - const vertexCount = view.getUint32(8, true); - const expected = HEADER_SIZE + vertexCount * VERTEX_SIZE; - if(bytes.length < expected) { - throw new Error(`DMF vertex data truncated (expected ${expected} bytes, got ${bytes.length})`); - } - - const floats = new Float32Array(vertexCount * VERTEX_FLOATS); - for(let i = 0; i < floats.length; i++) { - floats[i] = view.getFloat32(HEADER_SIZE + i * 4, true); - } - - return { version, vertexCount, floats }; - } - - return Object.freeze({ - decode, - VERSION, HEADER_SIZE, VERTEX_FLOATS, VERTEX_SIZE, - }); -})(); diff --git a/editor/client/public/index.html b/editor/client/public/index.html deleted file mode 100644 index 2eb5fe25..00000000 --- a/editor/client/public/index.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
-

Dusk Map Editor

-

Browse and edit project assets.

-
-
diff --git a/editor/client/public/map/index.html b/editor/client/public/map/index.html deleted file mode 100644 index 5e491709..00000000 --- a/editor/client/public/map/index.html +++ /dev/null @@ -1,92 +0,0 @@ - - -
-
-
-
- - -
-
- - -
-
- - -
- - - - -
- - - - -
-
- - -
- - -
-
 
-
- -
-
- - - - - 0 -
-
-
-
- -
- -
Meshes
-
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    -
    - -
    -

    Preview (scroll to zoom)

    - -
    -
    - - - - - diff --git a/editor/client/public/map/map.css b/editor/client/public/map/map.css deleted file mode 100644 index 54b8843d..00000000 --- a/editor/client/public/map/map.css +++ /dev/null @@ -1,65 +0,0 @@ -/* Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -.tile-palette { - display: flex; - flex-direction: column; - flex-wrap: wrap; - gap: 6px; - max-height: 600px; -} - -.tile-swatch { - position: relative; - width: 32px; - height: 32px; - border: 2px solid transparent; - border-radius: 4px; - cursor: pointer; - padding: 0; -} - -.tile-swatch.active { - border-color: #fff; -} - -.tile-swatch-icon { - position: absolute; - top: 0; - left: 0; - pointer-events: none; -} - -.dpad { - display: grid; - grid-template-columns: repeat(3, 2rem); - grid-template-rows: repeat(3, 2rem); - grid-template-areas: - ". n ." - "w . e" - ". s ."; - gap: 2px; -} - -.dpad-n { grid-area: n; } -.dpad-w { grid-area: w; } -.dpad-e { grid-area: e; } -.dpad-s { grid-area: s; } -.dpad button { padding: 0; } - -.tile-grid { - image-rendering: pixelated; - cursor: crosshair; - touch-action: none; - max-width: 100%; - height: auto; -} - -#previewCanvas { - width: 100%; - aspect-ratio: 1 / 1; - height: auto; -} diff --git a/editor/client/public/map/map.js b/editor/client/public/map/map.js deleted file mode 100644 index 0d883c12..00000000 --- a/editor/client/public/map/map.js +++ /dev/null @@ -1,920 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -"use strict"; - -(() => { - const SHAPES = [ - { type: ChunkTerrain.TILE_SHAPE_NULL, name: "Erase", color: "#20232a" }, - { type: ChunkTerrain.TILE_SHAPE_GROUND, name: "Ground", color: "#4caf50" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTH, name: "Ramp North", color: "#2196f3" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_EAST, name: "Ramp East", color: "#03a9f4" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTH, name: "Ramp South", color: "#00bcd4" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_WEST, name: "Ramp West", color: "#009688" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST, name: "Ramp NE", color: "#ff9800" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST, name: "Ramp NW", color: "#ff5722" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST, name: "Ramp SE", color: "#9c27b0" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST, name: "Ramp SW", color: "#e91e63" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER, name: "Ramp NE Inner", color: "#795548" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER, name: "Ramp NW Inner", color: "#607d8b" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER, name: "Ramp SE Inner", color: "#8bc34a" }, - { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER, name: "Ramp SW Inner", color: "#ffc107" }, - ]; - const SHAPE_COLORS = Object.fromEntries(SHAPES.map((s) => [s.type, s.color])); - - // Arrow direction per ramp, in canvas/screen space: north is up (dy < 0), - // east is right (dx > 0) - matches renderGrid()'s north-is-up convention. - const SHAPE_DIRECTIONS = { - [ChunkTerrain.TILE_SHAPE_RAMP_NORTH]: { dx: 0, dy: -1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_SOUTH]: { dx: 0, dy: 1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_EAST]: { dx: 1, dy: 0 }, - [ChunkTerrain.TILE_SHAPE_RAMP_WEST]: { dx: -1, dy: 0 }, - [ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST]: { dx: 1, dy: -1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST]: { dx: -1, dy: -1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST]: { dx: 1, dy: 1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST]: { dx: -1, dy: 1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER]: { dx: 1, dy: -1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER]: { dx: -1, dy: -1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER]: { dx: 1, dy: 1 }, - [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 }, - }; - - const INNER_CORNER_SHAPES = new Set([ - ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER, - ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER, - ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER, - ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER, - ]); - - // Draws a single-headed arrow from (tailX, tailY) to (tipX, tipY). - function drawArrowSegment(ctx, tailX, tailY, tipX, tipY, size) { - ctx.strokeStyle = "#ffffff"; - ctx.fillStyle = "#ffffff"; - ctx.lineWidth = Math.max(1, size * 0.08); - ctx.lineCap = "round"; - - ctx.beginPath(); - ctx.moveTo(tailX, tailY); - ctx.lineTo(tipX, tipY); - ctx.stroke(); - - const headLen = size * 0.18; - const angle = Math.atan2(tipY - tailY, tipX - tailX); - const leftAngle = angle + Math.PI * 0.8; - const rightAngle = angle - Math.PI * 0.8; - ctx.beginPath(); - ctx.moveTo(tipX, tipY); - ctx.lineTo(tipX + Math.cos(leftAngle) * headLen, tipY + Math.sin(leftAngle) * headLen); - ctx.lineTo(tipX + Math.cos(rightAngle) * headLen, tipY + Math.sin(rightAngle) * headLen); - ctx.closePath(); - ctx.fill(); - } - - // Draws an arrow centered at (cx, cy) pointing toward the ramp's high - // side, so ramp direction is visible at a glance on both the palette - // swatches and the tile grid. Used for cardinal ramps and outer-corner - // ramps (a single raised corner). - function drawArrow(ctx, cx, cy, size, dx, dy) { - const mag = Math.hypot(dx, dy) || 1; - const ux = dx / mag, uy = dy / mag; - const len = size * 0.32; - drawArrowSegment(ctx, cx - ux * len, cy - uy * len, cx + ux * len, cy + uy * len, size); - } - - // Draws a plain right-angle bracket - two line segments meeting at 90 - // degrees at the tile's corner, one running along each of the two edges - // adjacent to it - so inner-corner ramps (three corners raised, one - // dropped) read as "the adjacent sides meeting in a 90-degree corner", - // distinct from the single diagonal arrow used for outer-corner ramps. - function drawCornerBracket(ctx, cx, cy, size, dx, dy) { - const cornerDist = size * 0.42; - const armLen = size * 0.34; - const cornerX = cx + dx * cornerDist; - const cornerY = cy + dy * cornerDist; - - ctx.strokeStyle = "#ffffff"; - ctx.lineWidth = Math.max(1, size * 0.08); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - - ctx.beginPath(); - ctx.moveTo(cornerX - dx * armLen, cornerY); - ctx.lineTo(cornerX, cornerY); - ctx.lineTo(cornerX, cornerY - dy * armLen); - ctx.stroke(); - } - - // Draws the direction icon for a ramp tile `type` in a `size`x`size` area - // centered at (cx, cy): a diagonal arrow for cardinal/outer-corner ramps, - // or a right-angle bracket for inner-corner ramps. No-op for shapes with - // no direction (ground, erase). - function drawShapeIcon(ctx, cx, cy, size, type) { - const dir = SHAPE_DIRECTIONS[type]; - if(!dir) return; - if(INNER_CORNER_SHAPES.has(type)) { - drawCornerBracket(ctx, cx, cy, size, dir.dx, dir.dy); - } else { - drawArrow(ctx, cx, cy, size, dir.dx, dir.dy); - } - } - - // Draws a small pencil icon centered in a `size`x`size` canvas. - function drawPencilIcon(ctx, size) { - ctx.save(); - ctx.translate(size / 2, size / 2); - ctx.rotate(-Math.PI / 4); - - const shaftW = size * 0.18; - const shaftH = size * 0.62; - - ctx.fillStyle = "#e0e0e0"; - ctx.fillRect(-shaftW / 2, shaftH * 0.28, shaftW, shaftH * 0.22); - - ctx.fillStyle = "#f0c419"; - ctx.fillRect(-shaftW / 2, -shaftH * 0.5, shaftW, shaftH * 0.78); - - ctx.fillStyle = "#8d6e63"; - ctx.beginPath(); - ctx.moveTo(-shaftW / 2, -shaftH * 0.5); - ctx.lineTo(shaftW / 2, -shaftH * 0.5); - ctx.lineTo(0, -shaftH * 0.5 - shaftW); - ctx.closePath(); - ctx.fill(); - - ctx.restore(); - } - - // Draws a small paint bucket icon centered in a `size`x`size` canvas. - function drawBucketIcon(ctx, size) { - ctx.save(); - ctx.translate(size / 2, size / 2); - - const w = size * 0.5, h = size * 0.34; - - ctx.strokeStyle = "#e0e0e0"; - ctx.lineWidth = Math.max(1, size * 0.06); - ctx.beginPath(); - ctx.arc(0, -h * 0.4, w * 0.4, Math.PI, 0); - ctx.stroke(); - - ctx.fillStyle = "#03a9f4"; - ctx.beginPath(); - ctx.moveTo(-w / 2, -h * 0.2); - ctx.lineTo(w / 2, -h * 0.2); - ctx.lineTo(w * 0.32, h * 0.6); - ctx.lineTo(-w * 0.32, h * 0.6); - ctx.closePath(); - ctx.fill(); - - ctx.beginPath(); - ctx.arc(-w * 0.55, h * 0.75, size * 0.06, 0, Math.PI * 2); - ctx.fill(); - - ctx.restore(); - } - - const TOOLS = [ - { id: "pencil", name: "Pencil", draw: drawPencilIcon }, - { id: "bucket", name: "Paint Bucket", draw: drawBucketIcon }, - ]; - - let coord = { x: 0, y: 0, z: 0 }; - let tiles = new Int32Array(ChunkTerrain.TILE_COUNT); - let meshes = []; - let chunkExists = false; - let currentZLevel = 0; - let selectedShape = ChunkTerrain.TILE_SHAPE_GROUND; - let zoomWorldH = 20; - let dirty = false; - let hoverTile = null; - let activeTool = "pencil"; - - let mapRenderer = null; - let terrainMesh = null; - let gridLinesMesh = null; - let terrainTexturePromise = null; - let modelIndex = null; - let neighborChunks = []; - const modelCache = new Map(); - - const UNDO_LIMIT = 100; - let undoStack = []; - - function markDirty() { - dirty = true; - } - - function snapshotState() { - return { - tiles: tiles.slice(), - meshes: meshes.map((m) => ({ file: m.file, pos: m.pos.slice() })), - }; - } - - function pushUndo() { - undoStack.push(snapshotState()); - if(undoStack.length > UNDO_LIMIT) undoStack.shift(); - } - - function undo() { - if(!undoStack.length) return; - const snap = undoStack.pop(); - tiles = snap.tiles; - meshes = snap.meshes; - dirty = true; - renderGrid(); - renderMeshList(); - renderPreview(); - updateStatus("Undid last change."); - } - - // Returns true if it's OK to proceed (no unsaved changes, or the user - // confirmed discarding them). - function confirmDiscardIfDirty() { - if(!dirty) return true; - return window.confirm("You have unsaved changes to this chunk. Discard them?"); - } - - function statusLine() { - return document.getElementById("statusLine"); - } - - function updateStatus(msg, isError) { - const el = statusLine(); - el.textContent = msg; - el.classList.toggle("text-danger", !!isError); - el.classList.toggle("text-muted", !isError); - } - - async function apiReadJson(path) { - const res = await fetch(`/api/read?path=${encodeURIComponent(path)}`); - const data = await res.json(); - return { ok: res.ok, data }; - } - - async function apiWrite(path, content) { - return fetch(`/api/write?path=${encodeURIComponent(path)}`, { - method: "POST", - body: JSON.stringify({ content }), - }); - } - - async function apiDelete(path) { - return fetch(`/api/delete?path=${encodeURIComponent(path)}`, { method: "POST" }); - } - - async function apiCompileChunk(x, y, z) { - const res = await fetch(`/api/compile-chunk?x=${x}&y=${y}&z=${z}`, { method: "POST" }); - return res.json(); - } - - async function apiLs(path) { - const res = await fetch(`/api/ls?path=${encodeURIComponent(path)}`); - return res.json(); - } - - async function apiFind(path, ext) { - const res = await fetch(`/api/find?path=${encodeURIComponent(path)}&ext=${encodeURIComponent(ext)}`); - return res.json(); - } - - function base64ToBuffer(base64) { - const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); - return bytes.buffer; - } - - async function loadImageTexture(assetPath) { - const { ok, data } = await apiReadJson(assetPath); - if(!ok || data.error) return null; - const blob = new Blob([base64ToBuffer(data.content)], { type: "image/png" }); - const bitmap = await createImageBitmap(blob); - return mapRenderer.createTextureFromImage(bitmap); - } - - async function readDmfVertices(assetPath) { - const { ok, data } = await apiReadJson(assetPath); - if(!ok || data.error) throw new Error(`Failed to read ${assetPath}`); - return DMF.decode(base64ToBuffer(data.content)).floats; - } - - async function ensureModelIndex() { - if(modelIndex) return modelIndex; - const res = await apiFind("assets/models", ".json"); - modelIndex = new Map(); - for(const file of res.files || []) { - const base = file.split("/").pop().replace(/\.json$/, ""); - modelIndex.set(base, file); - } - return modelIndex; - } - - // Scales a mesh's stored [x, y, z] offset for preview, matching the - // Z scaling mapChunkLoaded() applies at runtime (chunk.meshOffsets[m][2] - // * WORLD_LAYER_HEIGHT) - x/y stay 1:1 since only Z is height-scaled. - function scaledMeshOffset(pos) { - return [pos[0], pos[1], pos[2] * ChunkTerrain.WORLD_LAYER_HEIGHT]; - } - - // Resolves a chunk mesh entry's bare filename (e.g. "house_5_3.dmf") to - // its model JSON by basename, mirroring find_model() in - // tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare - // filename, not a path. - async function resolveModelForMeshFile(file) { - await ensureModelIndex(); - const base = file.replace(/\.[^.]+$/, ""); - const rel = modelIndex.get(base); - if(!rel) return null; - - const modelPath = `assets/models/${rel}`; - if(modelCache.has(modelPath)) return modelCache.get(modelPath); - - const promise = (async () => { - const { ok, data } = await apiReadJson(modelPath); - if(!ok || data.error) throw new Error(`Failed to read model ${modelPath}`); - const modelDef = JSON.parse(data.content); - const meshFloats = await readDmfVertices(`assets/${modelDef.mesh}`); - const mesh = mapRenderer.createMesh(meshFloats); - const texture = modelDef.texture ? await loadImageTexture(`assets/${modelDef.texture}`) : null; - const colorBytes = modelDef.color || [255, 255, 255, 255]; - const color = colorBytes.map((c) => c / 255); - return { mesh, texture, color }; - })(); - modelCache.set(modelPath, promise); - return promise; - } - - const MAP_NAME = "overworld"; - - function chunkPaths(x, y, z) { - return { - raw: `assetsraw/maps/${MAP_NAME}/chunks/chunk_${x}_${y}_${z}.json`, - dcf: `assets/maps/${MAP_NAME}/chunks/${x}_${y}_${z}.dcf`, - terrainDmf: `assets/maps/${MAP_NAME}/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`, - terrainModel: `assets/maps/${MAP_NAME}/models/chunks/chunk_${x}_${y}_${z}_0.json`, - }; - } - - // Loads the 8 same-Z neighbors around `coord` (read-only, for context in - // the 3D preview) and pre-builds their render data so renderPreview() - // doesn't have to rebuild GL buffers on every paint stroke. - async function loadNeighborChunks() { - const offsets = []; - for(let dy = -1; dy <= 1; dy++) { - for(let dx = -1; dx <= 1; dx++) { - if(dx || dy) offsets.push({ dx, dy }); - } - } - - const results = await Promise.all(offsets.map(async ({ dx, dy }) => { - const nx = coord.x + dx, ny = coord.y + dy, nz = coord.z; - const { ok, data } = await apiReadJson(chunkPaths(nx, ny, nz).raw); - if(!ok || data.error) return null; - - const json = JSON.parse(data.content); - const neighborTiles = new Int32Array(ChunkTerrain.TILE_COUNT); - for(const t of json.tiles || []) { - const [tx, ty, tz] = t.pos; - neighborTiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type; - } - const neighborMeshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] })); - - const floats = ChunkTerrain.buildTerrainVerts(neighborTiles); - const terrain = floats.length ? { mesh: mapRenderer.createMesh(floats) } : null; - - const models = []; - for(const m of neighborMeshes) { - try { - const resolved = await resolveModelForMeshFile(m.file); - if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) }); - } catch(e) { - console.warn("Failed to load neighbor mesh preview", m.file, e); - } - } - - return { - offset: [dx * ChunkTerrain.CHUNK_WIDTH, dy * ChunkTerrain.CHUNK_HEIGHT, 0], - terrain, - models, - }; - })); - - neighborChunks = results.filter(Boolean); - } - - async function loadChunk(x, y, z) { - coord = { x, y, z }; - tiles = new Int32Array(ChunkTerrain.TILE_COUNT); - meshes = []; - chunkExists = false; - - const { ok, data } = await apiReadJson(chunkPaths(x, y, z).raw); - if(ok && !data.error) { - chunkExists = true; - const json = JSON.parse(data.content); - for(const t of json.tiles || []) { - const [tx, ty, tz] = t.pos; - tiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type; - } - meshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] })); - } - - await loadNeighborChunks(); - dirty = false; - undoStack = []; - - renderGrid(); - renderMeshList(); - renderPreview(); - updateStatus(chunkExists - ? `Loaded chunk_${x}_${y}_${z}.json` - : `New (empty) chunk at ${x}, ${y}, ${z} - paint a tile and save to create it.`); - } - - async function saveChunk() { - const tilesArr = []; - for(let z = 0; z < ChunkTerrain.CHUNK_DEPTH; z++) { - for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) { - for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) { - const type = tiles[ChunkTerrain.tileIndex(x, y, z)]; - if(type && type !== ChunkTerrain.TILE_SHAPE_NULL) { - tilesArr.push({ pos: [x, y, z], type, tile: 0 }); - } - } - } - } - - const paths = chunkPaths(coord.x, coord.y, coord.z); - - if(tilesArr.length === 0 && meshes.length === 0) { - await Promise.all(Object.values(paths).map((p) => apiDelete(p))); - chunkExists = false; - dirty = false; - updateStatus(`Chunk ${coord.x}, ${coord.y}, ${coord.z} is empty - deleted.`); - return; - } - - const json = { tiles: tilesArr, meshes: meshes.map((m) => ({ file: m.file, pos: m.pos })) }; - await apiWrite(paths.raw, JSON.stringify(json, null, 2)); - chunkExists = true; - dirty = false; - updateStatus("Saved. Compiling..."); - - const result = await apiCompileChunk(coord.x, coord.y, coord.z); - if(result.ok) { - updateStatus(`Compiled OK.\n${result.stdout}`); - } else { - updateStatus(`Compile FAILED (code ${result.returncode}):\n${result.stderr}`, true); - } - } - - function buildPalette() { - const el = document.getElementById("tilePalette"); - el.innerHTML = ""; - for(const s of SHAPES) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "tile-swatch" + (s.type === selectedShape ? " active" : ""); - btn.style.background = s.color; - btn.title = s.name; - btn.addEventListener("click", () => { - selectedShape = s.type; - buildPalette(); - }); - - if(SHAPE_DIRECTIONS[s.type]) { - const icon = document.createElement("canvas"); - icon.width = 32; - icon.height = 32; - icon.className = "tile-swatch-icon"; - drawShapeIcon(icon.getContext("2d"), 16, 16, 32, s.type); - btn.appendChild(icon); - } - - el.appendChild(btn); - } - } - - function buildToolPalette() { - const el = document.getElementById("toolPalette"); - el.innerHTML = ""; - for(const tool of TOOLS) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "tile-swatch" + (tool.id === activeTool ? " active" : ""); - btn.style.background = "#2a2d33"; - btn.title = tool.name; - btn.addEventListener("click", () => { - activeTool = tool.id; - buildToolPalette(); - }); - - const icon = document.createElement("canvas"); - icon.width = 32; - icon.height = 32; - icon.className = "tile-swatch-icon"; - tool.draw(icon.getContext("2d"), 32); - btn.appendChild(icon); - - el.appendChild(btn); - } - } - - function canvasToTile(e, canvas) { - const rect = canvas.getBoundingClientRect(); - const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH; - const px = (e.clientX - rect.left) * (canvas.width / rect.width); - const py = (e.clientY - rect.top) * (canvas.height / rect.height); - const x = Math.floor(px / cell); - const yFromTop = Math.floor(py / cell); - const y = ChunkTerrain.CHUNK_HEIGHT - 1 - yFromTop; - if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) return null; - return { x, y }; - } - - function renderGrid() { - const canvas = document.getElementById("gridCanvas"); - const ctx = canvas.getContext("2d"); - const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) { - for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) { - const type = tiles[ChunkTerrain.tileIndex(x, y, currentZLevel)]; - const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell; - ctx.fillStyle = SHAPE_COLORS[type] || "#15171b"; - ctx.fillRect(x * cell, sy, cell, cell); - ctx.strokeStyle = "rgba(255, 255, 255, 0.25)"; - ctx.strokeRect(x * cell, sy, cell, cell); - - drawShapeIcon(ctx, x * cell + cell / 2, sy + cell / 2, cell, type); - } - } - - ctx.fillStyle = "#ffffff"; - for(const m of meshes) { - const [mx, my, mz] = m.pos; - if(Math.round(mz) !== currentZLevel) continue; - const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell; - ctx.beginPath(); - ctx.arc(mx * cell + cell / 2, sy + cell / 2, cell * 0.3, 0, Math.PI * 2); - ctx.fill(); - } - - if(hoverTile) { - const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - hoverTile.y) * cell; - ctx.strokeStyle = "#ffeb3b"; - ctx.lineWidth = 2; - ctx.strokeRect(hoverTile.x * cell + 1, sy + 1, cell - 2, cell - 2); - } - } - - function hitTestMesh(e, canvas) { - const rect = canvas.getBoundingClientRect(); - const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH; - const px = (e.clientX - rect.left) * (canvas.width / rect.width); - const py = (e.clientY - rect.top) * (canvas.height / rect.height); - - let closest = -1; - let closestDist = cell * 0.5; - meshes.forEach((m, i) => { - const [mx, my, mz] = m.pos; - if(Math.round(mz) !== currentZLevel) return; - const cx = mx * cell + cell / 2; - const cy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell + cell / 2; - const dist = Math.hypot(px - cx, py - cy); - if(dist < closestDist) { closestDist = dist; closest = i; } - }); - return closest; - } - - function moveMeshTo(e, index) { - const canvas = document.getElementById("gridCanvas"); - const t = canvasToTile(e, canvas); - if(!t) return; - meshes[index].pos[0] = t.x; - meshes[index].pos[1] = t.y; - markDirty(); - renderGrid(); - renderMeshList(); - renderPreview(); - } - - function paintAt(e) { - const canvas = document.getElementById("gridCanvas"); - const t = canvasToTile(e, canvas); - if(!t) return; - tiles[ChunkTerrain.tileIndex(t.x, t.y, currentZLevel)] = selectedShape; - markDirty(); - renderGrid(); - renderPreview(); - } - - // 4-connected flood fill of same-type tiles at the current Z-level, - // starting from (startX, startY), replacing them with selectedShape. - function floodFill(startX, startY) { - const targetType = tiles[ChunkTerrain.tileIndex(startX, startY, currentZLevel)]; - if(targetType === selectedShape) return; - - const stack = [[startX, startY]]; - const visited = new Set(); - while(stack.length) { - const [x, y] = stack.pop(); - const key = `${x},${y}`; - if(visited.has(key)) continue; - if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) continue; - visited.add(key); - - const idx = ChunkTerrain.tileIndex(x, y, currentZLevel); - if(tiles[idx] !== targetType) continue; - tiles[idx] = selectedShape; - stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]); - } - } - - function fillAt(e) { - const canvas = document.getElementById("gridCanvas"); - const t = canvasToTile(e, canvas); - if(!t) return; - pushUndo(); - floodFill(t.x, t.y); - markDirty(); - renderGrid(); - renderPreview(); - } - - function rebuildTerrainMesh() { - const floats = ChunkTerrain.buildTerrainVerts(tiles); - terrainMesh = floats.length ? mapRenderer.createMesh(floats) : null; - - const lineFloats = ChunkTerrain.buildGridLines(tiles); - gridLinesMesh = lineFloats.length ? mapRenderer.createMesh(lineFloats) : null; - } - - // Keeps the preview canvas's backing-store resolution matched to its - // actual displayed CSS size (times devicePixelRatio) - the canvas - // otherwise stays at its fixed width/height attributes (512x512) while - // CSS stretches it to fill the column, which upscales and blurs the - // WebGL output on wider or high-DPI screens. - function resizePreviewCanvas() { - const canvas = document.getElementById("previewCanvas"); - const dpr = window.devicePixelRatio || 1; - const displayWidth = Math.max(1, Math.round(canvas.clientWidth * dpr)); - const displayHeight = Math.max(1, Math.round(canvas.clientHeight * dpr)); - if(canvas.width !== displayWidth || canvas.height !== displayHeight) { - canvas.width = displayWidth; - canvas.height = displayHeight; - } - } - - async function renderPreview() { - if(!mapRenderer) return; - resizePreviewCanvas(); - rebuildTerrainMesh(); - - const models = []; - for(const m of meshes) { - try { - const resolved = await resolveModelForMeshFile(m.file); - if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) }); - } catch(e) { - console.warn("Failed to load mesh preview", m.file, e); - } - } - - let terrainTexture = null; - if(terrainMesh || neighborChunks.some((n) => n.terrain)) { - if(!terrainTexturePromise) terrainTexturePromise = loadImageTexture("assets/tiles.png"); - terrainTexture = await terrainTexturePromise; - } - - const neighbors = neighborChunks.map((n) => ({ - offset: n.offset, - terrain: n.terrain ? { mesh: n.terrain.mesh, texture: terrainTexture } : null, - models: n.models, - })); - - const zLevelHeight = currentZLevel * ChunkTerrain.WORLD_LAYER_HEIGHT; - mapRenderer.render({ - target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, zLevelHeight], - worldH: zoomWorldH, - terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null, - gridLines: gridLinesMesh ? { mesh: gridLinesMesh } : null, - models, - neighbors, - highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: zLevelHeight } : null, - }); - } - - function renderMeshList() { - const ul = document.getElementById("meshList"); - ul.innerHTML = ""; - meshes.forEach((m, i) => { - const li = document.createElement("li"); - li.className = "list-group-item d-flex align-items-center gap-2 flex-wrap"; - - const label = document.createElement("span"); - label.className = "me-auto"; - label.textContent = m.file; - li.appendChild(label); - - ["X", "Y", "Z"].forEach((axis, axisIndex) => { - const input = document.createElement("input"); - input.type = "number"; - input.className = "form-control form-control-sm"; - input.style.width = "5rem"; - input.title = axis; - input.value = m.pos[axisIndex]; - input.addEventListener("change", () => { - pushUndo(); - m.pos[axisIndex] = parseFloat(input.value) || 0; - markDirty(); - renderGrid(); - renderPreview(); - }); - li.appendChild(input); - }); - - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "btn btn-sm btn-outline-danger"; - btn.textContent = "Remove"; - btn.addEventListener("click", () => { - pushUndo(); - meshes.splice(i, 1); - markDirty(); - renderMeshList(); - renderGrid(); - renderPreview(); - }); - - li.appendChild(btn); - ul.appendChild(li); - }); - } - - async function buildMeshPicker() { - await ensureModelIndex(); - const select = document.getElementById("meshPicker"); - select.innerHTML = ""; - for(const [base, rel] of [...modelIndex.entries()].sort()) { - const opt = document.createElement("option"); - opt.value = base; - opt.textContent = rel; - select.appendChild(opt); - } - } - - async function buildChunkBrowseList() { - const list = document.getElementById("chunkBrowseList"); - list.innerHTML = ""; - const res = await apiLs(`assetsraw/maps/${MAP_NAME}/chunks`); - for(const entry of res.entries || []) { - const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/); - if(!m) continue; - const [, x, y, z] = m; - - const li = document.createElement("li"); - const a = document.createElement("a"); - a.className = "dropdown-item"; - a.href = "#"; - a.textContent = `${x}, ${y}, ${z}`; - a.addEventListener("click", (e) => { - e.preventDefault(); - goToChunk(parseInt(x, 10), parseInt(y, 10), parseInt(z, 10)); - }); - - li.appendChild(a); - list.appendChild(li); - } - } - - function setCoordInputs(x, y, z) { - document.getElementById("coordX").value = x; - document.getElementById("coordY").value = y; - document.getElementById("coordZ").value = z; - } - - function goToChunk(x, y, z) { - if(!confirmDiscardIfDirty()) return; - setCoordInputs(x, y, z); - loadChunk(x, y, z); - } - - function bindEvents() { - const gridCanvas = document.getElementById("gridCanvas"); - let painting = false; - let draggingMeshIndex = -1; - - gridCanvas.addEventListener("mousedown", (e) => { - if(activeTool === "bucket") { - fillAt(e); - return; - } - - const hit = hitTestMesh(e, gridCanvas); - pushUndo(); - if(hit !== -1) { - draggingMeshIndex = hit; - return; - } - painting = true; - paintAt(e); - }); - window.addEventListener("mouseup", () => { painting = false; draggingMeshIndex = -1; }); - gridCanvas.addEventListener("mousemove", (e) => { - hoverTile = canvasToTile(e, gridCanvas); - if(draggingMeshIndex !== -1) { moveMeshTo(e, draggingMeshIndex); return; } - if(painting) { paintAt(e); return; } - renderGrid(); - renderPreview(); - }); - gridCanvas.addEventListener("mouseleave", () => { - hoverTile = null; - renderGrid(); - renderPreview(); - }); - - function setZLevel(z) { - currentZLevel = Math.max(0, Math.min(ChunkTerrain.CHUNK_DEPTH - 1, z)); - document.getElementById("zLevel").value = currentZLevel; - document.getElementById("zLevelLabel").textContent = currentZLevel; - renderGrid(); - renderPreview(); - } - - document.getElementById("zLevel").addEventListener("input", (e) => { - setZLevel(parseInt(e.target.value, 10)); - }); - document.getElementById("zLevelDown").addEventListener("click", () => setZLevel(currentZLevel - 1)); - document.getElementById("zLevelUp").addEventListener("click", () => setZLevel(currentZLevel + 1)); - - document.getElementById("btnLoad").addEventListener("click", () => { - const x = parseInt(document.getElementById("coordX").value, 10) || 0; - const y = parseInt(document.getElementById("coordY").value, 10) || 0; - const z = parseInt(document.getElementById("coordZ").value, 10) || 0; - goToChunk(x, y, z); - }); - - // NORTH=+Y, EAST=+X (matches tile-shape convention used by the tile - // grid and tools/asset/chunk/__main__.py). - document.getElementById("navN").addEventListener("click", () => goToChunk(coord.x, coord.y + 1, coord.z)); - document.getElementById("navS").addEventListener("click", () => goToChunk(coord.x, coord.y - 1, coord.z)); - document.getElementById("navE").addEventListener("click", () => goToChunk(coord.x + 1, coord.y, coord.z)); - document.getElementById("navW").addEventListener("click", () => goToChunk(coord.x - 1, coord.y, coord.z)); - document.getElementById("navUp").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z + 1)); - document.getElementById("navDown").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z - 1)); - - function runSave() { - saveChunk().catch((e) => updateStatus(`Save failed: ${e.message}`, true)); - } - - document.getElementById("btnSave").addEventListener("click", runSave); - - document.getElementById("btnAddMesh").addEventListener("click", () => { - const base = document.getElementById("meshPicker").value; - if(!base) return; - const pos = [ - parseFloat(document.getElementById("meshX").value) || 0, - parseFloat(document.getElementById("meshY").value) || 0, - parseFloat(document.getElementById("meshZ").value) || 0, - ]; - pushUndo(); - meshes.push({ file: `${base}.dmf`, pos }); - markDirty(); - renderMeshList(); - renderGrid(); - renderPreview(); - }); - - document.getElementById("previewCanvas").addEventListener("wheel", (e) => { - e.preventDefault(); - zoomWorldH = Math.min(60, Math.max(4, zoomWorldH + Math.sign(e.deltaY))); - renderPreview(); - }, { passive: false }); - - window.addEventListener("resize", () => renderPreview()); - - window.addEventListener("keydown", (e) => { - if(!(e.ctrlKey || e.metaKey)) return; - const key = e.key.toLowerCase(); - if(key === "z") { - e.preventDefault(); - undo(); - } else if(key === "s") { - e.preventDefault(); - runSave(); - } - }); - } - - document.addEventListener("DOMContentLoaded", async () => { - buildPalette(); - buildToolPalette(); - mapRenderer = MapRenderer.create(document.getElementById("previewCanvas")); - bindEvents(); - await Promise.all([buildChunkBrowseList(), buildMeshPicker()]); - await loadChunk(coord.x, coord.y, coord.z); - }); -})(); diff --git a/editor/client/public/map/renderer.js b/editor/client/public/map/renderer.js deleted file mode 100644 index fc9c7470..00000000 --- a/editor/client/public/map/renderer.js +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -"use strict"; - -// MapRenderer - a minimal, dependency-free WebGL renderer for the chunk -// preview pane. The camera formula is ported 1:1 from -// src/dusk/scene/overworld/sceneoverworld.c (lines 62-81) so the preview -// approximates the game's own fixed-angle 3rd-person camera: the eye sits -// worldH units "behind" the target along Y and a proportional distance -// "above" along Z, always looking at the target, up = (0, 1, 0). -// -// The material model mirrors SHADER_UNLIT: a texture sampler multiplied by -// a flat RGBA tint, with a 1x1 white fallback texture standing in for -// untextured models. - -const MapRenderer = (() => { - const VERT_SRC = ` - attribute vec2 aUV; - attribute vec3 aPos; - uniform mat4 uModel; - uniform mat4 uView; - uniform mat4 uProjection; - varying vec2 vUV; - void main() { - vUV = aUV; - gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0); - } - `; - - const FRAG_SRC = ` - precision mediump float; - varying vec2 vUV; - uniform sampler2D uTexture; - uniform vec4 uColor; - void main() { - gl_FragColor = texture2D(uTexture, vUV) * uColor; - } - `; - - function mat4Identity(out) { - out.fill(0); - out[0] = out[5] = out[10] = out[15] = 1; - return out; - } - - function mat4Translation(out, v) { - mat4Identity(out); - out[12] = v[0]; - out[13] = v[1]; - out[14] = v[2]; - return out; - } - - function mat4Perspective(out, fovy, aspect, near, far) { - const f = 1.0 / Math.tan(fovy / 2); - out.fill(0); - out[0] = f / aspect; - out[5] = f; - out[10] = (far + near) / (near - far); - out[11] = -1; - out[14] = (2 * far * near) / (near - far); - return out; - } - - function vec3Sub(a, b) { - return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; - } - - function vec3Cross(a, b) { - return [ - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0], - ]; - } - - function vec3Normalize(a) { - const len = Math.hypot(a[0], a[1], a[2]) || 1; - return [a[0] / len, a[1] / len, a[2] / len]; - } - - function mat4LookAt(out, eye, center, up) { - const zAxis = vec3Normalize(vec3Sub(eye, center)); - const xAxis = vec3Normalize(vec3Cross(up, zAxis)); - const yAxis = vec3Cross(zAxis, xAxis); - - out[0] = xAxis[0]; out[1] = yAxis[0]; out[2] = zAxis[0]; out[3] = 0; - out[4] = xAxis[1]; out[5] = yAxis[1]; out[6] = zAxis[1]; out[7] = 0; - out[8] = xAxis[2]; out[9] = yAxis[2]; out[10] = zAxis[2]; out[11] = 0; - out[12] = -(xAxis[0] * eye[0] + xAxis[1] * eye[1] + xAxis[2] * eye[2]); - out[13] = -(yAxis[0] * eye[0] + yAxis[1] * eye[1] + yAxis[2] * eye[2]); - out[14] = -(zAxis[0] * eye[0] + zAxis[1] * eye[1] + zAxis[2] * eye[2]); - out[15] = 1; - return out; - } - - // Ported from sceneoverworld.c:62-81. Returns { eye, view }. - function computeCamera(target, worldH) { - const fov = Math.PI / 4; // glm_rad(45.0f) - const zDist = (worldH * 0.5) / Math.tan(fov * 0.5); - const offset = -worldH; - - const eye = [target[0], target[1] + offset, target[2] + zDist]; - const view = mat4LookAt(new Float32Array(16), eye, target, [0, 1, 0]); - return { eye, view, fov }; - } - - function compileShader(gl, type, src) { - const shader = gl.createShader(type); - gl.shaderSource(shader, src); - gl.compileShader(shader); - if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - const info = gl.getShaderInfoLog(shader); - gl.deleteShader(shader); - throw new Error(`Shader compile failed: ${info}`); - } - return shader; - } - - function create(canvas) { - const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); - if(!gl) throw new Error("WebGL is not available in this browser"); - - const program = gl.createProgram(); - gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, VERT_SRC)); - gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC)); - gl.linkProgram(program); - if(!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error(`Program link failed: ${gl.getProgramInfoLog(program)}`); - } - - const attribs = { - aUV: gl.getAttribLocation(program, "aUV"), - aPos: gl.getAttribLocation(program, "aPos"), - }; - const uniforms = { - uModel: gl.getUniformLocation(program, "uModel"), - uView: gl.getUniformLocation(program, "uView"), - uProjection: gl.getUniformLocation(program, "uProjection"), - uTexture: gl.getUniformLocation(program, "uTexture"), - uColor: gl.getUniformLocation(program, "uColor"), - }; - - const whiteTexture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, whiteTexture); - gl.texImage2D( - gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, - new Uint8Array([255, 255, 255, 255]), - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - - gl.enable(gl.DEPTH_TEST); - gl.enable(gl.BLEND); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - // Terrain/mesh winding isn't guaranteed consistent relative to this - // preview's fixed camera angle, so backface culling stays off - this - // is an authoring preview, not a performance-sensitive renderer, and - // an invisible mesh is a worse failure mode than a visible backface. - - // Uploads interleaved [u, v, x, y, z] float data (DMF/terrain layout) - // into a GL buffer. Returns { buffer, vertexCount }. - function createMesh(floats) { - const buffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.bufferData(gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW); - return { buffer, vertexCount: floats.length / 5 }; - } - - function createTextureFromImage(image) { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - return texture; - } - - function drawMesh(mesh, model, texture, color, depthWrite, mode) { - gl.depthMask(depthWrite !== false); - gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer); - gl.enableVertexAttribArray(attribs.aUV); - gl.vertexAttribPointer(attribs.aUV, 2, gl.FLOAT, false, 20, 0); - gl.enableVertexAttribArray(attribs.aPos); - gl.vertexAttribPointer(attribs.aPos, 3, gl.FLOAT, false, 20, 8); - - gl.uniformMatrix4fv(uniforms.uModel, false, model); - gl.uniform4f(uniforms.uColor, color[0], color[1], color[2], color[3]); - - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture || whiteTexture); - gl.uniform1i(uniforms.uTexture, 0); - - gl.drawArrays(mode || gl.TRIANGLES, 0, mesh.vertexCount); - } - - // Builds a 4-vertex outline of the tile at (x, y, z), lifted slightly - // to avoid z-fighting with the terrain surface beneath it. - function buildHighlightMesh(x, y, z) { - const eps = 0.02; - return createMesh(new Float32Array([ - 0, 0, x, y, z + eps, - 0, 0, x + 1, y, z + eps, - 0, 0, x + 1, y + 1, z + eps, - 0, 0, x, y + 1, z + eps, - ])); - } - - // Neighboring chunks are drawn dimmer and semi-transparent, so they - // read as context rather than part of the chunk being edited. - const NEIGHBOR_ALPHA = 0.35; - const NEIGHBOR_DARKEN = 0.5; - - // scene = { - // target: [x, y, z], worldH: number, - // terrain: { mesh, texture } | null, - // gridLines: { mesh } | null, - // models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }], - // neighbors: [{ - // offset: [x,y,z], - // terrain: { mesh, texture } | null, - // models: [{ mesh, texture, color, offset }], - // }], - // highlight: { x, y, z } | null, - // } - function render(scene) { - const width = canvas.width, height = canvas.height; - gl.viewport(0, 0, width, height); - gl.clearColor(0.16, 0.18, 0.22, 1); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - gl.useProgram(program); - gl.depthMask(true); - - const { view, fov } = computeCamera(scene.target, scene.worldH); - const projection = mat4Perspective(new Float32Array(16), fov, width / height, 0.1, 1000); - gl.uniformMatrix4fv(uniforms.uProjection, false, projection); - gl.uniformMatrix4fv(uniforms.uView, false, view); - - const identity = mat4Identity(new Float32Array(16)); - - if(scene.terrain) { - drawMesh(scene.terrain.mesh, identity, scene.terrain.texture, [1, 1, 1, 1], true); - } - - if(scene.gridLines) { - drawMesh(scene.gridLines.mesh, identity, null, [0, 0, 0, 0.35], false, gl.LINES); - } - - for(const model of scene.models) { - const modelMatrix = mat4Translation(new Float32Array(16), model.offset); - drawMesh(model.mesh, modelMatrix, model.texture, model.color, true); - } - - for(const neighbor of scene.neighbors || []) { - if(neighbor.terrain) { - const modelMatrix = mat4Translation(new Float32Array(16), neighbor.offset); - drawMesh( - neighbor.terrain.mesh, modelMatrix, neighbor.terrain.texture, - [NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_ALPHA], false, - ); - } - - for(const model of neighbor.models) { - const worldOffset = [ - neighbor.offset[0] + model.offset[0], - neighbor.offset[1] + model.offset[1], - neighbor.offset[2] + model.offset[2], - ]; - const modelMatrix = mat4Translation(new Float32Array(16), worldOffset); - const color = [ - model.color[0] * NEIGHBOR_DARKEN, - model.color[1] * NEIGHBOR_DARKEN, - model.color[2] * NEIGHBOR_DARKEN, - model.color[3] * NEIGHBOR_ALPHA, - ]; - drawMesh(model.mesh, modelMatrix, model.texture, color, false); - } - } - - if(scene.highlight) { - const { x, y, z } = scene.highlight; - const highlightMesh = buildHighlightMesh(x, y, z); - drawMesh(highlightMesh, identity, null, [1, 0.92, 0.23, 1], false, gl.LINE_LOOP); - } - } - - return { gl, createMesh, createTextureFromImage, render }; - } - - return Object.freeze({ create, computeCamera }); -})(); diff --git a/editor/client/public/template.html b/editor/client/public/template.html deleted file mode 100644 index 5ea9aba2..00000000 --- a/editor/client/public/template.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - Dusk Map Editor - - - - -
    - - - diff --git a/editor/server/server.py b/editor/server/server.py deleted file mode 100644 index 8985ce34..00000000 --- a/editor/server/server.py +++ /dev/null @@ -1,298 +0,0 @@ -""" -Map Editor HTTP Server - -Serves editor/client/public/ as static files with smart routing: - / -> index.html - /page -> page.html or page/index.html - /index -> index.html - -API endpoints (all paths relative to project root): - GET /api/ls?path= directory listing - GET /api/read?path= file contents - GET /api/find?path=&ext=<.json> recursive file listing by extension - POST /api/write?path= write file (JSON body: {"content":"..."}) - POST /api/mkdir?path= create directory - POST /api/delete?path= delete a file - POST /api/compile-chunk?x=&y=&z= recompile a chunk JSON to .dcf -""" - -import os -import re -import sys -import json -import mimetypes -import subprocess -from http.server import HTTPServer, BaseHTTPRequestHandler -from urllib.parse import urlparse, parse_qs, unquote -from pathlib import Path - -CHUNK_COORD_RE = re.compile(r"^-?\d+$") - -PORT = 3000 -SCRIPT_DIR = Path(__file__).parent.resolve() -PUBLIC_DIR = SCRIPT_DIR.parent / "client" / "public" -PROJECT_ROOT = SCRIPT_DIR.parent.parent.resolve() - - -class Handler(BaseHTTPRequestHandler): - def do_GET(self): - parsed = urlparse(self.path) - path = unquote(parsed.path) - if path.startswith("/api/"): - params = parse_qs(parsed.query) - if path == "/api/ls": - self.api_ls(params) - elif path == "/api/read": - self.api_read(params) - elif path == "/api/find": - self.api_find(params) - else: - self.send_error(404) - else: - self.serve_static(path) - - def do_POST(self): - parsed = urlparse(self.path) - params = parse_qs(parsed.query) - if parsed.path == "/api/write": - self.api_write(params) - elif parsed.path == "/api/mkdir": - self.api_mkdir(params) - elif parsed.path == "/api/delete": - self.api_delete(params) - elif parsed.path == "/api/compile-chunk": - self.api_compile_chunk(params) - else: - self.send_error(404) - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def resolve_project_path(self, rel): - rel = rel.lstrip("/") - target = (PROJECT_ROOT / rel).resolve() - if not str(target).startswith(str(PROJECT_ROOT)): - return None - return target - - def send_json(self, data, code=200): - body = json.dumps(data, indent=2).encode("utf-8") - self.send_response(code) - self.send_header("Content-Type", "application/json") - self.send_header("Content-Length", len(body)) - self.end_headers() - self.wfile.write(body) - - def read_body(self): - length = int(self.headers.get("Content-Length", 0)) - return self.rfile.read(length) - - # ------------------------------------------------------------------ - # API handlers - # ------------------------------------------------------------------ - - def api_ls(self, params): - rel = params.get("path", [""])[0] - target = self.resolve_project_path(rel) - if target is None: - self.send_json({"error": "invalid path"}, 400) - return - if not target.exists(): - self.send_json({"error": "path not found"}, 404) - return - if not target.is_dir(): - self.send_json({"error": "not a directory"}, 400) - return - - entries = [] - for entry in sorted(target.iterdir(), key=lambda e: (e.is_file(), e.name)): - entries.append({ - "name": entry.name, - "type": "dir" if entry.is_dir() else "file", - "size": entry.stat().st_size if entry.is_file() else None, - }) - self.send_json({ - "path": str(target.relative_to(PROJECT_ROOT)), - "entries": entries, - }) - - def api_read(self, params): - rel = params.get("path", [""])[0] - target = self.resolve_project_path(rel) - if target is None: - self.send_json({"error": "invalid path"}, 400) - return - if not target.exists(): - self.send_json({"error": "file not found"}, 404) - return - if not target.is_file(): - self.send_json({"error": "not a file"}, 400) - return - - try: - content = target.read_text(encoding="utf-8") - self.send_json({ - "path": str(target.relative_to(PROJECT_ROOT)), - "content": content, - }) - except UnicodeDecodeError: - import base64 - content = base64.b64encode(target.read_bytes()).decode("ascii") - self.send_json({ - "path": str(target.relative_to(PROJECT_ROOT)), - "content": content, - "encoding": "base64", - }) - - def api_write(self, params): - rel = params.get("path", [""])[0] - target = self.resolve_project_path(rel) - if target is None: - self.send_json({"error": "invalid path"}, 400) - return - - body = self.read_body() - try: - data = json.loads(body) - content = data.get("content", "") - except (json.JSONDecodeError, UnicodeDecodeError): - content = body.decode("utf-8") - - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(content, encoding="utf-8") - self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))}) - - def api_mkdir(self, params): - rel = params.get("path", [""])[0] - target = self.resolve_project_path(rel) - if target is None: - self.send_json({"error": "invalid path"}, 400) - return - target.mkdir(parents=True, exist_ok=True) - self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))}) - - def api_delete(self, params): - rel = params.get("path", [""])[0] - target = self.resolve_project_path(rel) - if target is None: - self.send_json({"error": "invalid path"}, 400) - return - if target.is_file(): - target.unlink() - self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))}) - - def api_find(self, params): - rel = params.get("path", [""])[0] - ext = params.get("ext", [""])[0] - target = self.resolve_project_path(rel) - if target is None: - self.send_json({"error": "invalid path"}, 400) - return - if not target.is_dir(): - self.send_json({"error": "not a directory"}, 400) - return - - pattern = f"*{ext}" if ext else "*" - paths = sorted( - str(p.relative_to(target)).replace(os.sep, "/") - for p in target.rglob(pattern) - if p.is_file() - ) - self.send_json({"path": str(target.relative_to(PROJECT_ROOT)), "files": paths}) - - def api_compile_chunk(self, params): - x = params.get("x", [""])[0] - y = params.get("y", [""])[0] - z = params.get("z", [""])[0] - if not (CHUNK_COORD_RE.match(x) and CHUNK_COORD_RE.match(y) and CHUNK_COORD_RE.match(z)): - self.send_json({"error": "invalid chunk coordinates"}, 400) - return - - json_path = ( - PROJECT_ROOT / "assetsraw" / "maps" / "overworld" / "chunks" - / f"chunk_{x}_{y}_{z}.json" - ) - if not json_path.is_file(): - self.send_json({"error": "chunk json not found"}, 404) - return - - result = subprocess.run( - [sys.executable, "-m", "tools.asset.chunk", str(json_path)], - cwd=str(PROJECT_ROOT), - capture_output=True, - text=True, - ) - self.send_json({ - "ok": result.returncode == 0, - "returncode": result.returncode, - "stdout": result.stdout, - "stderr": result.stderr, - }) - - # ------------------------------------------------------------------ - # Static file serving - # ------------------------------------------------------------------ - - def serve_static(self, path): - clean = path.lstrip("/") - candidates = [] - - if clean == "" or clean == "index" or clean == "index.html": - candidates = ["index.html"] - else: - candidates.append(clean) - if "." not in Path(clean).name: - candidates.append(clean + ".html") - candidates.append(clean + "/index.html") - - for candidate in candidates: - file_path = PUBLIC_DIR / candidate - if file_path.is_file(): - self.serve_file(file_path) - return - - self.send_error(404) - - def serve_file(self, file_path): - mime, _ = mimetypes.guess_type(str(file_path)) - mime = mime or "application/octet-stream" - - template = PUBLIC_DIR / "template.html" - if ( - mime == "text/html" - and file_path.name != "template.html" - and template.is_file() - ): - fragment = file_path.read_text(encoding="utf-8") - shell = template.read_text(encoding="utf-8") - marker = shell.find('id="editor"') - if marker == -1: - content = shell.encode("utf-8") - else: - tag_end = shell.index(">", marker) + 1 - close_idx = shell.index("", tag_end) - content = (shell[:tag_end] + fragment + shell[close_idx:]).encode("utf-8") - else: - content = file_path.read_bytes() - - self.send_response(200) - self.send_header("Content-Type", mime) - self.send_header("Content-Length", len(content)) - self.end_headers() - self.wfile.write(content) - - def log_message(self, fmt, *args): - print(f" {self.address_string()} {fmt % args}") - - -if __name__ == "__main__": - print(f"Map Editor") - print(f" http://localhost:{PORT}") - print(f" public : {PUBLIC_DIR}") - print(f" project: {PROJECT_ROOT}") - server = HTTPServer(("", PORT), Handler) - try: - server.serve_forever() - except KeyboardInterrupt: - print("\nStopped.") diff --git a/editor/start.sh b/editor/start.sh deleted file mode 100755 index e1871d04..00000000 --- a/editor/start.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -e -cd "$(dirname "$0")" -python3 server/server.py diff --git a/scripts/build-gamecube-docker.sh b/scripts/build-gamecube-docker.sh deleted file mode 100755 index 69d2777b..00000000 --- a/scripts/build-gamecube-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh" \ No newline at end of file diff --git a/scripts/build-gamecube-iso-docker.sh b/scripts/build-gamecube-iso-docker.sh deleted file mode 100755 index 24975feb..00000000 --- a/scripts/build-gamecube-iso-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh" diff --git a/scripts/build-gamecube-iso.sh b/scripts/build-gamecube-iso.sh deleted file mode 100755 index b04180d6..00000000 --- a/scripts/build-gamecube-iso.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -if [ -z "$DEVKITPRO" ]; then - echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation." - exit 1 -fi - -mkdir -p build-gamecube-iso -cmake -S. -Bbuild-gamecube-iso \ - -DDUSK_TARGET_SYSTEM=gamecube \ - -DDUSK_DOLPHIN_BUILD_TYPE=ISO \ - -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" -cd build-gamecube-iso -make -j$(nproc) VERBOSE=1 diff --git a/scripts/build-gamecube.sh b/scripts/build-gamecube.sh deleted file mode 100755 index cf1ebb52..00000000 --- a/scripts/build-gamecube.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -if [ -z "$DEVKITPRO" ]; then - echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation." - exit 1 -fi - -mkdir -p build-gamecube -cmake -S. -Bbuild-gamecube \ - -DDUSK_TARGET_SYSTEM=gamecube \ - -DDUSK_DOLPHIN_BUILD_TYPE=DOL \ - -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \ - -DDKP_OGC_PLATFORM_LIBRARY=libogc2 -cd build-gamecube -make -j$(nproc) VERBOSE=1 \ No newline at end of file diff --git a/scripts/build-knulli-docker.sh b/scripts/build-knulli-docker.sh deleted file mode 100755 index 26f8bfc6..00000000 --- a/scripts/build-knulli-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-knulli -f docker/knulli/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh" \ No newline at end of file diff --git a/scripts/build-knulli.sh b/scripts/build-knulli.sh deleted file mode 100755 index 5a1dbcf9..00000000 --- a/scripts/build-knulli.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -cmake -S . -B build-knulli -G Ninja \ - -DDUSK_BUILD_TESTS=ON \ - -DDUSK_TARGET_SYSTEM=knulli \ - -DCMAKE_TOOLCHAIN_FILE=./cmake/toolchains/aarch64-linux-gnu.cmake \ - -DCMAKE_BUILD_TYPE=Release -cmake --build build-knulli -- -j$(nproc) - -# Copy necessary libs out -mkdir -p ./build-knulli/dusk -cp ./build-knulli/Dusk ./build-knulli/dusk/Dusk -cp ./build-knulli/dusk.dsk ./build-knulli/dusk/dusk.dsk -echo '#!/bin/bash' > build-knulli/dusk/Dusk.sh -echo 'cd "$(dirname "$(readlink -f "$0")")"' >> build-knulli/dusk/Dusk.sh -echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(dirname "$(readlink -f "$0")")' >> build-knulli/dusk/Dusk.sh -echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/gl4es' >> build-knulli/dusk/Dusk.sh -echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib' >> build-knulli/dusk/Dusk.sh -echo '$(dirname "$(readlink -f "$0")")/Dusk' >> build-knulli/dusk/Dusk.sh -chmod +x build-knulli/dusk/Dusk.sh -cp /usr/lib/aarch64-linux-gnu/liblua5.4.so.0 build-knulli/dusk/ -# cp /usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0 build-knulli/dusk/ -# cp /usr/lib/aarch64-linux-gnu/libGL.so.1 build-knulli/dusk/ -# cp /usr/lib/aarch64-linux-gnu/libEGL.so.1 build-knulli/dusk/ -# cp /usr/lib/aarch64-linux-gnu/libGLESv2.so.2 build-knulli/dusk/ \ No newline at end of file diff --git a/scripts/build-linux-docker.sh b/scripts/build-linux-docker.sh deleted file mode 100755 index b46fddd9..00000000 --- a/scripts/build-linux-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-linux -f docker/linux/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh" \ No newline at end of file diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh deleted file mode 100755 index f4affb2e..00000000 --- a/scripts/build-linux.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -cmake -S . -B build-linux -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux -cmake --build build-linux -- -j$(nproc) \ No newline at end of file diff --git a/scripts/build-psp-docker.sh b/scripts/build-psp-docker.sh deleted file mode 100755 index 348fdce2..00000000 --- a/scripts/build-psp-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-psp -f docker/psp/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh" \ No newline at end of file diff --git a/scripts/build-psp.sh b/scripts/build-psp.sh deleted file mode 100755 index cef02d6a..00000000 --- a/scripts/build-psp.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -if [ -z "$PSPDEV" ]; then - echo "PSPDEV environment variable is not set. Please set it to the path of your PSP development environment." - exit 1 -fi - -mkdir -p build-psp -cd build-psp -psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 .. CMAKE_BUILD_TYPE=Release -make -j$(nproc) -# psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 -DCMAKE_BUILD_TYPE=Debug .. -# make \ No newline at end of file diff --git a/scripts/build-vita-docker.sh b/scripts/build-vita-docker.sh deleted file mode 100755 index a0308cb4..00000000 --- a/scripts/build-vita-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-vita -f docker/vita/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh" diff --git a/scripts/build-vita.sh b/scripts/build-vita.sh deleted file mode 100755 index d6a3813d..00000000 --- a/scripts/build-vita.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -if [ -z "$VITASDK" ]; then - echo "VITASDK environment variable is not set. Please set it to the path of your VitaSDK installation." - exit 1 -fi - -mkdir -p build-vita -cd build-vita -cmake \ - -DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake \ - -DDUSK_TARGET_SYSTEM=vita \ - .. -make -j$(nproc) diff --git a/scripts/build-wii-docker.sh b/scripts/build-wii-docker.sh deleted file mode 100755 index 087f551a..00000000 --- a/scripts/build-wii-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh" \ No newline at end of file diff --git a/scripts/build-wii-iso-docker.sh b/scripts/build-wii-iso-docker.sh deleted file mode 100755 index cd05520a..00000000 --- a/scripts/build-wii-iso-docker.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . -docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh" diff --git a/scripts/build-wii-iso.sh b/scripts/build-wii-iso.sh deleted file mode 100755 index 04b084e3..00000000 --- a/scripts/build-wii-iso.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -if [ -z "$DEVKITPRO" ]; then - echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation." - exit 1 -fi - -mkdir -p build-wii-iso -cmake -S. -Bbuild-wii-iso \ - -DDUSK_TARGET_SYSTEM=wii \ - -DDUSK_DOLPHIN_BUILD_TYPE=ISO \ - -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" -cd build-wii-iso -make -j$(nproc) VERBOSE=1 diff --git a/scripts/build-wii.sh b/scripts/build-wii.sh deleted file mode 100755 index 348cdaa6..00000000 --- a/scripts/build-wii.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -if [ -z "$DEVKITPRO" ]; then - echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation." - exit 1 -fi - -mkdir -p build-wii -cmake -S. -Bbuild-wii \ - -DDUSK_TARGET_SYSTEM=wii \ - -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \ - -DDUSK_DOLPHIN_BUILD_TYPE=DOL -cd build-wii -make -j$(nproc) VERBOSE=1 -mv Dusk.dol boot.dol \ No newline at end of file diff --git a/scripts/test-linux-docker.sh b/scripts/test-linux-docker.sh deleted file mode 100755 index d5c70904..00000000 --- a/scripts/test-linux-docker.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -docker build -t dusk-linux -f docker/linux/Dockerfile . -docker run \ - --rm \ - -v "${GITHUB_WORKSPACE}:/workdir" \ - dusk-linux \ - /bin/bash -c "./scripts/test-linux.sh" \ No newline at end of file diff --git a/scripts/test-linux.sh b/scripts/test-linux.sh deleted file mode 100755 index 2c63bff4..00000000 --- a/scripts/test-linux.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -e -rm -rf build-tests -cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux -cmake --build build-tests -- -j$(nproc) -ctest --output-on-failure --test-dir build-tests \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt deleted file mode 100644 index 2f5cd6de..00000000 --- a/src/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -add_subdirectory(dusk) - -if(DUSK_NETWORK) - add_subdirectory(dusknetwork) -endif() - -if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli") - add_subdirectory(dusklinux) - add_subdirectory(dusksdl2) - add_subdirectory(duskgl) - add_subdirectory(duskmad) - -elseif(DUSK_TARGET_SYSTEM STREQUAL "psp") - add_subdirectory(duskpsp) - add_subdirectory(dusksdl2) - add_subdirectory(duskgl) - -elseif(DUSK_TARGET_SYSTEM STREQUAL "vita") - add_subdirectory(duskvita) - add_subdirectory(dusksdl2) - add_subdirectory(duskgl) - -elseif( - DUSK_TARGET_SYSTEM STREQUAL "wii" OR - DUSK_TARGET_SYSTEM STREQUAL "gamecube" -) - add_subdirectory(duskdolphin) - add_subdirectory(duskmad) - -endif() \ No newline at end of file diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt deleted file mode 100644 index c0347f89..00000000 --- a/src/dusk/CMakeLists.txt +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Required Libraries -if(NOT cglm_FOUND) - find_package(cglm REQUIRED) - target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC cglm) -endif() - -if(NOT libzip_FOUND) - find_package(libzip REQUIRED) - target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip) -endif() - -# assetdsk.c calls crc32() directly (to verify dusk.dsk archive checksums). -# find_package(libzip) above already resolves ZLIB as a side effect, so -# ZLIB_FOUND may already be true without ZLIB::ZLIB having been linked to -# our target - link it unconditionally rather than guarding on ZLIB_FOUND. -find_package(ZLIB REQUIRED) -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ZLIB::ZLIB) - -if(NOT stb_image_FOUND) - find_package(stb REQUIRED) - if(STB_IMAGE_FOUND) - target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image) - else() - message(FATAL_ERROR - "stb_image not found. Please ensure stb is correctly fetched." - ) - endif() -endif() - -if(NOT yyjson_FOUND) - find_package(yyjson REQUIRED) - if(yyjson_FOUND) - target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson) - else() - message(FATAL_ERROR - "yyjson not found. Please ensure yyjson is correctly fetched." - ) - endif() -endif() - -if(DUSK_BACKTRACE) - target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic) - target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC - DUSK_BACKTRACE - ) -endif() - -if(DUSK_TRACK_MEMORY) - target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - DUSK_TRACK_MEMORY - ) -endif() - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Main Binary Source -target_sources(${DUSK_BINARY_TARGET_NAME} - PUBLIC - main.c -) - -# Subdirs -add_subdirectory(animation) -add_subdirectory(assert) -add_subdirectory(asset) -add_subdirectory(audio) -add_subdirectory(console) -add_subdirectory(display) -add_subdirectory(log) -add_subdirectory(engine) -add_subdirectory(error) -add_subdirectory(input) -add_subdirectory(locale) -add_subdirectory(perf) -add_subdirectory(rpg) -add_subdirectory(scene) -add_subdirectory(system) -add_subdirectory(time) -add_subdirectory(ui) -add_subdirectory(save) -add_subdirectory(util) -add_subdirectory(thread) \ No newline at end of file diff --git a/src/dusk/animation/CMakeLists.txt b/src/dusk/animation/CMakeLists.txt deleted file mode 100644 index a03d91ad..00000000 --- a/src/dusk/animation/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - easing.c - animation.c - keyframe.c -) diff --git a/src/dusk/animation/animation.c b/src/dusk/animation/animation.c deleted file mode 100644 index 65fa2b20..00000000 --- a/src/dusk/animation/animation.c +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#include "animation.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" - -void animationInit( - animation_t *anim, - keyframe_t *keyframes, - uint16_t *keyframeCounts, - const uint16_t layerCount -) { - assertNotNull(anim, "Animation pointer cannot be null."); - assertNotNull(keyframes, "Keyframes pointer cannot be null."); - assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null."); - assertTrue(layerCount > 0, "Layer count must be greater than zero."); - - memoryZero(anim, sizeof(animation_t)); - anim->keyframes = keyframes; - anim->keyframeCounts = keyframeCounts; - anim->layerCount = layerCount; - - // Determine duration - float_t duration = 0.0f; - for(uint16_t layer = 0; layer < layerCount; layer++) { - uint16_t keyframeCount = keyframeCounts[layer]; - assertTrue(keyframeCount > 0, "Keyframe count invalid."); - keyframe_t *layerKeyframes = keyframes + layer * keyframeCount; - - #ifdef DUSK_ASSERTIONS - // Check that the keyframes are sorted by time. - for(uint16_t i = 1; i < keyframeCount; i++) { - assertTrue( - layerKeyframes[i].time >= layerKeyframes[i - 1].time, - "Keyframes must be sorted by time." - ); - } - #endif - - keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1; - duration = mathMax(duration, lastKeyframe->time); - } - assertTrue(duration > 0, "Animation duration must be greater than 0."); - anim->duration = duration; -} - -float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) { - assertNotNull(anim, "Animation pointer cannot be null."); - assertTrue(layer < anim->layerCount, "Layer index out of bounds."); - - uint16_t keyframeCount = anim->keyframeCounts[layer]; - keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount; - return keyframeGetValue(layerKeyframes, keyframeCount, anim->time); -} - -void animationUpdate( - animation_t *anim, - const float_t deltaTime -) { - assertNotNull(anim, "Animation pointer cannot be null."); - assertTrue(deltaTime >= 0, "Delta time must be non-negative."); - - bool_t justCompleted = false; - - if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) { - bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0; - bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0; - assertFalse( - loop && pingpong, - "Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG." - ); - - bool_t backward; - if(pingpong) { - backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0; - } else { - backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0; - } - - // Resolve boundary crossings one at a time, so a single large deltaTime - // can correctly loop/pingpong across multiple boundaries in one call. - float_t remaining = deltaTime; - while(remaining > 0.0f) { - float_t toBoundary = ( - backward ? anim->time : (anim->duration - anim->time) - ); - if(remaining < toBoundary) { - anim->time += backward ? -remaining : remaining; - break; - } - - remaining -= toBoundary; - anim->time = backward ? 0.0f : anim->duration; - - bool_t stopHere; - if(backward) { - stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0; - } else { - stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0; - } - - if(stopHere) { - justCompleted = true; - break; - } else if(pingpong) { - backward = !backward; - if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD; - else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD; - } else if(loop) { - anim->time = backward ? anim->duration : 0.0f; - if(anim->onLoop) anim->onLoop(anim->user); - } else { - justCompleted = true; - break; - } - } - - if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED; - } - - // Call onUpdate for each layer. - for(uint16_t layer = 0; layer < anim->layerCount; layer++) { - float_t value = animationGetLayerValue(anim, layer); - if(anim->onUpdate) anim->onUpdate(layer, value, anim->user); - } - - if(justCompleted && anim->onComplete) anim->onComplete(anim->user); -} \ No newline at end of file diff --git a/src/dusk/animation/animation.h b/src/dusk/animation/animation.h deleted file mode 100644 index f8cea8b8..00000000 --- a/src/dusk/animation/animation.h +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#pragma once -#include "keyframe.h" - -#define ANIMATION_FLAG_LOOP (1 << 0) -#define ANIMATION_FLAG_REVERSE (1 << 1) -#define ANIMATION_FLAG_PINGPONG (1 << 2) -#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3) -#define ANIMATION_FLAG_STOP_END (1 << 4) - -// Internal - tracks which direction a pingponging animation is currently -// travelling. Do not set this manually, it is managed by animationUpdate(). -#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7) - -// Internal - set once the animation has stopped advancing (see -// animationUpdate()). Do not set this manually. There is currently no way to -// restart a completed animation short of clearing this bit and resetting -// anim->time by hand. -#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6) - -typedef struct { - keyframe_t *keyframes; - uint16_t *keyframeCounts; - uint16_t layerCount; - float_t time; - float_t duration; - uint8_t flags; - - void *user; - void (*onUpdate)(const uint16_t layer, const float_t value, void *user); - void (*onComplete)(void *user); - void (*onLoop)(void *user); -} animation_t; - -/** - * Initializes an animation with the given keyframes and layer count. - * - * @param anim Pointer to the animation to initialize. - * @param keyframes Pointer to the array of keyframes for each layer. - * @param keyframeCount Number of keyframes in each layer. - * @param layerCount Number of layers in the animation. - */ -void animationInit( - animation_t *anim, - keyframe_t *keyframes, - uint16_t *keyframeCounts, - const uint16_t layerCount -); - -/** - * Sets the current time of the animation, clamping it to the valid range. - * This will call the onUpdate callback but none of the other callbacks. - * - * @param anim Pointer to the animation to set the time for. - * @param time The new time to set for the animation. - */ -void animationSetTime(animation_t *anim, const float_t time); - -/** - * Gets the current value of a specific layer in the animation based on the - * current animation time. - */ -float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer); - -/** - * Updates the animation state based on the elapsed time. Advances anim->time - * by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then - * resolves whatever happens when it reaches the 0 or duration boundary: - * - * - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing - * in the opposite direction, forever, unless stopped (see below). - * - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps - * playing in the same direction, forever, unless stopped (see below). - * - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the - * animation reaches that specific boundary, it clamps there and stops - * (firing onComplete) instead of looping/pingponging past it. - * - If none of the above apply at a boundary, the animation clamps there and - * stops, firing onComplete. - * - * onUpdate is called for every layer on every call. onLoop is called each - * time a loop wraps around. onComplete is called at most once, the moment - * the animation stops advancing. - * - * @param anim Pointer to the animation to update. - * @param deltaTime Time elapsed since the last update (in seconds). - */ -void animationUpdate( - animation_t *anim, - const float_t deltaTime -); \ No newline at end of file diff --git a/src/dusk/animation/easing.c b/src/dusk/animation/easing.c deleted file mode 100644 index 19cbf12a..00000000 --- a/src/dusk/animation/easing.c +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#include "easing.h" -#include "assert/assert.h" -#include "util/math.h" - -const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = { - easingLinear, - easingInSine, - easingOutSine, - easingInOutSine, - easingInQuad, - easingOutQuad, - easingInOutQuad, - easingInCubic, - easingOutCubic, - easingInOutCubic, - easingInQuart, - easingOutQuart, - easingInOutQuart, - easingInBack, - easingOutBack, - easingInOutBack, -}; - -float_t easingApply(const easingtype_t type, const float_t t) { - assertTrue(type < EASING_COUNT, "Invalid easing type"); - return EASING_FUNCTIONS[type](t); -} - -float_t easingLinear(const float_t t) { - return t; -} - -float_t easingInSine(const float_t t) { - return 1.0f - cosf(t * MATH_PI * 0.5f); -} - -float_t easingOutSine(const float_t t) { - return sinf(t * MATH_PI * 0.5f); -} - -float_t easingInOutSine(const float_t t) { - return -(cosf(MATH_PI * t) - 1.0f) * 0.5f; -} - -float_t easingInQuad(const float_t t) { - return t * t; -} - -float_t easingOutQuad(const float_t t) { - float_t u = 1.0f - t; - return 1.0f - u * u; -} - -float_t easingInOutQuad(const float_t t) { - if(t < 0.5f) return 2.0f * t * t; - float_t u = -2.0f * t + 2.0f; - return 1.0f - u * u * 0.5f; -} - -float_t easingInCubic(const float_t t) { - return t * t * t; -} - -float_t easingOutCubic(const float_t t) { - float_t u = 1.0f - t; - return 1.0f - u * u * u; -} - -float_t easingInOutCubic(const float_t t) { - if(t < 0.5f) return 4.0f * t * t * t; - float_t u = -2.0f * t + 2.0f; - return 1.0f - u * u * u * 0.5f; -} - -float_t easingInQuart(const float_t t) { - return t * t * t * t; -} - -float_t easingOutQuart(const float_t t) { - float_t u = 1.0f - t; - return 1.0f - u * u * u * u; -} - -float_t easingInOutQuart(const float_t t) { - if(t < 0.5f) return 8.0f * t * t * t * t; - float_t u = -2.0f * t + 2.0f; - return 1.0f - u * u * u * u * 0.5f; -} - -float_t easingInBack(const float_t t) { - return EASING_C3 * t * t * t - EASING_C1 * t * t; -} - -float_t easingOutBack(const float_t t) { - float_t u = t - 1.0f; - return 1.0f + EASING_C3 * u * u * u + EASING_C1 * u * u; -} - -float_t easingInOutBack(const float_t t) { - if(t < 0.5f) { - float_t u = 2.0f * t; - return u * u * ((EASING_C2 + 1.0f) * u - EASING_C2) * 0.5f; - } - float_t u = 2.0f * t - 2.0f; - return (u * u * ((EASING_C2 + 1.0f) * u + EASING_C2) + 2.0f) * 0.5f; -} diff --git a/src/dusk/animation/easing.h b/src/dusk/animation/easing.h deleted file mode 100644 index 117988b5..00000000 --- a/src/dusk/animation/easing.h +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#pragma once -#include "dusk.h" - -#define EASING_PI 3.14159265358979323846f -#define EASING_C1 1.70158f -#define EASING_C2 (EASING_C1 * 1.525f) -#define EASING_C3 (EASING_C1 + 1.0f) - -typedef enum { - EASING_LINEAR, - EASING_IN_SINE, - EASING_OUT_SINE, - EASING_IN_OUT_SINE, - EASING_IN_QUAD, - EASING_OUT_QUAD, - EASING_IN_OUT_QUAD, - EASING_IN_CUBIC, - EASING_OUT_CUBIC, - EASING_IN_OUT_CUBIC, - EASING_IN_QUART, - EASING_OUT_QUART, - EASING_IN_OUT_QUART, - EASING_IN_BACK, - EASING_OUT_BACK, - EASING_IN_OUT_BACK, - - EASING_COUNT -} easingtype_t; - -typedef float_t (*easingfn_t)(const float_t t); - -extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT]; - -/** - * Applies the specified easing function to t. - * - * @param type The easing type to apply. - * @param t The input time, in the range [0, 1]. - * @return The eased value, in the range [0, 1]. - */ -float_t easingApply(const easingtype_t type, const float_t t); - -float_t easingLinear(const float_t t); -float_t easingInSine(const float_t t); -float_t easingOutSine(const float_t t); -float_t easingInOutSine(const float_t t); -float_t easingInQuad(const float_t t); -float_t easingOutQuad(const float_t t); -float_t easingInOutQuad(const float_t t); -float_t easingInCubic(const float_t t); -float_t easingOutCubic(const float_t t); -float_t easingInOutCubic(const float_t t); -float_t easingInQuart(const float_t t); -float_t easingOutQuart(const float_t t); -float_t easingInOutQuart(const float_t t); -float_t easingInBack(const float_t t); -float_t easingOutBack(const float_t t); -float_t easingInOutBack(const float_t t); diff --git a/src/dusk/animation/keyframe.c b/src/dusk/animation/keyframe.c deleted file mode 100644 index a50338ef..00000000 --- a/src/dusk/animation/keyframe.c +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "keyframe.h" -#include "assert/assert.h" -#include "util/math.h" - -float_t keyframeGetValue( - const keyframe_t *keyframes, - const uint32_t keyframeCount, - const float_t time -) { - assertNotNull(keyframes, "Keyframes pointer cannot be null."); - assertTrue(keyframeCount > 0, "Keyframe count must be more than 0."); - assertTrue(time >= 0, "Time must be non-negative."); - #ifdef DUSK_ASSERTIONS - // Checks that the keyframes are sorted by time. - for(uint32_t i = 1; i < keyframeCount; i++) { - assertTrue( - keyframes[i].time >= keyframes[i - 1].time, - "Keyframes must be sorted by time." - ); - } - #endif - - keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1); - if(time >= last->time) return last->value; - - // Since time < last->time (checked above), current is guaranteed to stop - // at or before reaching last, so no separate end-of-array check is needed. - keyframe_t *current = (keyframe_t *)keyframes; - keyframe_t *start = current; - while(current->time <= time) { - start = current; - current++; - } - keyframe_t *end = current; - - float_t t = (time - start->time) / (end->time - start->time); - return mathLerp(start->value, end->value, easingApply(start->easing, t)); -} \ No newline at end of file diff --git a/src/dusk/animation/keyframe.h b/src/dusk/animation/keyframe.h deleted file mode 100644 index 96a0a4f6..00000000 --- a/src/dusk/animation/keyframe.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#pragma once -#include "easing.h" - -typedef struct { - float_t time; - float_t value; - easingtype_t easing; -} keyframe_t; - -/** - * Gets the value of a keyframe at a given time. - * - * @param keyframes The keyframes to get the value from. - * @param keyframeCount The number of keyframes in the array. - * @param time The time at which to get the value, in seconds. - * @return The value of the keyframe at the given time. - */ -float_t keyframeGetValue( - const keyframe_t *keyframes, - const uint32_t keyframeCount, - const float_t time -); \ No newline at end of file diff --git a/src/dusk/assert/CMakeLists.txt b/src/dusk/assert/CMakeLists.txt deleted file mode 100644 index af43017d..00000000 --- a/src/dusk/assert/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - assert.c -) \ No newline at end of file diff --git a/src/dusk/assert/assert.c b/src/dusk/assert/assert.c deleted file mode 100644 index d6eebed6..00000000 --- a/src/dusk/assert/assert.c +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assert.h" -#include "log/log.h" -#include "util/string.h" -#include "util/memory.h" - -#ifdef DUSK_THREAD_PTHREAD - pthread_t ASSERT_MAIN_THREAD_ID = 0; -#endif - -#ifndef DUSK_ASSERTIONS_FAKED - void assertInit(void) { - #ifdef DUSK_THREAD_PTHREAD - ASSERT_MAIN_THREAD_ID = pthread_self(); - #endif - } - - #ifdef DUSK_TEST_ASSERT - void assertTrueImpl( - const char *file, - const int32_t line, - const bool x, - const char *message - ) { - mock_assert( - x == true, - message, - file, - line - ); - } - #else - #ifdef DUSK_BACKTRACE - #include - #include - - static void assertLogBacktrace(void) { - void *frames[64]; - int count = backtrace(frames, 64); - char **symbols = backtrace_symbols(frames, count); - memoryTrack(symbols); - logError("Stack trace:\n"); - if(symbols) { - for(int i = 0; i < count; i++) { - logError(" %s\n", symbols[i]); - } - memoryFree(symbols); - } - } - #endif - - void assertTrueImpl( - const char *file, - const int32_t line, - const bool x, - const char *message - ) { - if(x != true) { - logError( - "Assertion Failed in %s:%i\n\n%s\n\n", - file, - line, - message - ); - #ifdef DUSK_BACKTRACE - assertLogBacktrace(); - #endif - abort(); - } - } - #endif - - void assertFalseImpl( - const char *file, - const int32_t line, - bool x, - const char *message - ) { - assertTrueImpl(file, line, !x, message); - } - - void assertUnreachableImpl( - const char *file, - const int32_t line, - const char *message - ) { - assertTrueImpl(file, line, false, message); - } - - void assertNotNullImpl( - const char *file, - const int32_t line, - const void *pointer, - const char *message - ) { - assertTrueImpl( - file, - line, - pointer != NULL, - message - ); - - // Ensure we can touch it - volatile char temp; - temp = *((char*)pointer); - } - - void assertNullImpl( - const char *file, - const int32_t line, - const void *pointer, - const char *message - ) { - assertTrueImpl( - file, - line, - pointer == NULL, - message - ); - } - - void assertDeprecatedImpl( - const char *file, - const int32_t line, - const char *message - ) { - assertUnreachableImpl(file, line, message); - } - - void assertStringEqualImpl( - const char *file, - const int32_t line, - const char *a, - const char *b, - const char *message - ) { - assertTrueImpl(file, line, stringCompare(a, b) == 0, message); - } - - void assertIsMainThreadImpl( - const char *file, - const int32_t line, - const char *message - ) { - #ifdef DUSK_THREAD_PTHREAD - assertTrueImpl( - file, line, pthread_self() == ASSERT_MAIN_THREAD_ID, message - ); - #endif - } - - void assertNotMainThreadImpl( - const char *file, - const int32_t line, - const char *message - ) { - #ifdef DUSK_THREAD_PTHREAD - assertTrueImpl( - file, line, pthread_self() != ASSERT_MAIN_THREAD_ID, message - ); - #endif - } -#endif - diff --git a/src/dusk/assert/assert.h b/src/dusk/assert/assert.h deleted file mode 100644 index b3fb2a21..00000000 --- a/src/dusk/assert/assert.h +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#ifdef DUSK_TEST_ASSERT - #include - - #ifdef DUSK_ASSERTIONS_FAKED - #error "Cannot fake assertions when DUSK_TEST_ASSERT is set." - #endif -#endif - -#ifdef DUSK_THREAD_PTHREAD - #include "thread/thread.h" - extern pthread_t ASSERT_MAIN_THREAD_ID; -#endif - -#ifndef DUSK_ASSERTIONS_FAKED - #define DUSK_ASSERTIONS 1 - - /** - * Initializes the assert system. Must be the very first call in engine - * startup. - */ - void assertInit(void); - - /** - * Assert a given value to be true. - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param x Value to assert as true. - * @param message Message to throw against assertion failure. - */ - void assertTrueImpl( - const char *file, - const int32_t line, - const bool_t x, - const char *message - ); - - /** - * Asserts a given statement to be false. - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param x Value to assert as false. - * @param message Message to throw against assertion failure. - */ - void assertFalseImpl( - const char *file, - const int32_t line, - const bool_t x, - const char *message - ); - - /** - * Asserts that a given line of code is unreachable. Essentially a forced - * assertion failure, good for "edge cases" - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param message Message to throw against assertion failure. - */ - void assertUnreachableImpl( - const char *file, - const int32_t line, - const char *message - ); - - /** - * Assert a given pointer to not point to a null pointer. - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param pointer Pointer to assert is not a null pointer. - * @param message Message to throw against assertion failure. - */ - void assertNotNullImpl( - const char *file, - const int32_t line, - const void *pointer, - const char *message - ); - - /** - * Asserts a given pointer to be a nullptr. - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param pointer Pointer to assert is nullptr. - * @param message Message to throw against assertion failure. - */ - void assertNullImpl( - const char *file, - const int32_t line, - const void *pointer, - const char *message - ); - - /** - * Asserts a function as being deprecated. - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param message Message to throw against assertion failure. - */ - void assertDeprecatedImpl( - const char *file, - const int32_t line, - const char *message - ); - - /** - * Asserts two strings to be equal. - * - * @param file File that the assertion is being made from. - * @param line Line that the assertion is being made from. - * @param a First string to compare. - * @param b Second string to compare. - * @param message Message to throw against assertion failure. - */ - void assertStringEqualImpl( - const char *file, - const int32_t line, - const char *a, - const char *b, - const char *message - ); - - /** - * Asserts that the current thread is the main thread. - * - * @param message Message to throw against assertion failure. - */ - void assertIsMainThreadImpl( - const char *file, - const int32_t line, - const char *message - ); - - /** - * Asserts that the current thread is NOT the main thread. - * - * @param message Message to throw against assertion failure. - */ - void assertNotMainThreadImpl( - const char *file, - const int32_t line, - const char *message - ); - - /** - * Asserts a given value to be true. - * - * @param x Value to assert as true. - * @param message Message to throw against assertion failure. - */ - #define assertTrue(x, message) \ - assertTrueImpl(__FILE__, __LINE__, x, message) - - /** - * Asserts a given statement to be false. - * - * @param x Value to assert as false. - * @param message Message to throw against assertion failure. - */ - #define assertFalse(x, message) \ - assertFalseImpl(__FILE__, __LINE__, x, message) - - /** - * Asserts that a given line of code is unreachable. Essentially a forced - * assertion failure, good for "edge cases" - * - * @param message Message to throw against assertion failure. - */ - #define assertUnreachable(message) \ - assertUnreachableImpl(__FILE__, __LINE__, message) - - - /** - * Asserts a pointer is not null. - * - * @param pointer Pointer to check. - * @param message Message to throw against assertion failure. - */ - #define assertNotNull(pointer, message) \ - assertNotNullImpl(__FILE__, __LINE__, pointer, message) - - /** - * Asserts a pointer is null. - * - * @param pointer Pointer to check. - * @param message Message to throw against assertion failure. - */ - #define assertNull(pointer, message) \ - assertNullImpl(__FILE__, __LINE__, pointer, message) - - /** - * Asserts a function or code path is deprecated. - * - * @param message Message to throw against assertion failure. - */ - #define assertDeprecated(message) \ - assertDeprecatedImpl(__FILE__, __LINE__, message) - - /** - * Asserts a string's length is less than a maximum. - * - * @param str String to check. - * @param len Maximum length. - * @param message Message to throw against assertion failure. - */ - #define assertStrLenMax(str, len, message) \ - assertTrue(strlen(str) < len, message) - - /** - * Asserts a string's length is at least a minimum. - * - * @param str String to check. - * @param len Minimum length. - * @param message Message to throw against assertion failure. - */ - #define assertStrLenMin(str, len, message) \ - assertTrue(strlen(str) >= len, message) - - /** - * Asserts two strings to be equal. - * - * @param a First string to compare. - * @param b Second string to compare. - * @param message Message to throw against assertion failure. - */ - #define assertStringEqual(a, b, message) \ - assertStringEqualImpl(__FILE__, __LINE__, a, b, message) - - /** - * Asserts that the current thread is the main thread. - * - * @param message Message to throw against assertion failure. - */ - #define assertIsMainThread(message) \ - assertIsMainThreadImpl(__FILE__, __LINE__, message) - - /** - * Asserts that the current thread is NOT the main thread. - * - * @param message Message to throw against assertion failure. - */ - #define assertNotMainThread(message) \ - assertNotMainThreadImpl(__FILE__, __LINE__, message) - -#else - // If assertions are faked, we define the macros to do nothing. - #define assertInit() ((void)0) - #define assertMainThreadInit() ((void)0) - #define assertIsMainThread(message) ((void)0) - #define assertNotMainThread(message) ((void)0) - #define assertTrue(x, message) ((void)0) - #define assertFalse(x, message) ((void)0) - #define assertUnreachable(message) ((void)0) - #define assertNotNull(pointer, message) ((void)0) - #define assertNull(pointer, message) ((void)0) - #define assertDeprecated(message) ((void)0) - #define assertStrLenMax(str, len, message) ((void)0) - #define assertStrLenMin(str, len, message) ((void)0) - #define assertStringEqual(a, b, message) ((void)0) - #define assertIsMainThread(message) ((void)0) - #define assertNotMainThread(message) ((void)0) - -#endif - -// Static Assertions -#define assertStructSize(struct, size) \ - _Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size) - -// EOF diff --git a/src/dusk/asset/CMakeLists.txt b/src/dusk/asset/CMakeLists.txt deleted file mode 100644 index a3d838a4..00000000 --- a/src/dusk/asset/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - asset.c - assetdsk.c - assetfile.c -) - -# Subdirs -add_subdirectory(loader) \ No newline at end of file diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c deleted file mode 100644 index 19f42c37..00000000 --- a/src/dusk/asset/asset.c +++ /dev/null @@ -1,455 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "asset.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assert.h" -#include "engine/engine.h" -#include "util/string.h" -#include "console/console.h" -#include - -asset_t ASSET; - -errorret_t assetInit(void) { - memoryZero(&ASSET, sizeof(asset_t)); - - threadMutexInit(&ASSET.zipLock); - - for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - threadMutexInit(&ASSET.loading[i].mutex); - } - - // assetInitPlatform must either define ASSET.zip or throw an error. - errorChain(assetInitPlatform()); - assertNotNull(ASSET.zip, "Asset zip null without error."); - threadInit(&ASSET.loadThread, assetUpdateAsync); - threadStart(&ASSET.loadThread); - - errorOk(); -} - -bool_t assetFileExists(const char_t *filename) { - assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long."); - - threadMutexLock(&ASSET.zipLock); - bool_t found = zip_name_locate(ASSET.zip, filename, 0) >= 0; - threadMutexUnlock(&ASSET.zipLock); - return found; -} - -assetentry_t * assetGetEntry( - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -) { - // Is there an existing asset? - assetentry_t *entry = ASSET.entries; - do { - if(entry->type == ASSET_LOADER_TYPE_NULL) { - entry++; - continue; - } - if(stringEquals(entry->name, name)) { - assertTrue(entry->type == type, "Asset entry type mismatch."); - return entry; - } - entry++; - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); - - // We did not find one existing. Find first available slot, reaping - // zero-ref entries to make room if none are immediately available. - bool_t reaped = false; - for(;;) { - entry = ASSET.entries; - do { - if(entry->type != ASSET_LOADER_TYPE_NULL) { - entry++; - continue; - } - - if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) { - assetEntryInit(entry, name, type, input); - return entry; - } - entry++; - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); - - if(reaped) break; - reaped = true; - errorCatch(assetReapUnused()); - } - - assertUnreachable("No available asset entry slots."); - return NULL; -} - -uint32_t assetGetEntriesOfType( - assetentry_t **outEntries, - const assetloadertype_t type -) { - assertNotNull(outEntries, "Output entries cannot be NULL."); - - uint32_t count = 0; - assetentry_t *entry = ASSET.entries; - do { - if(entry->type == type) { - outEntries[count++] = entry; - } - entry++; - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); - - return count; -} - -errorret_t assetRequireLoaded(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL."); - assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - assertIsMainThread("Currently only works on main thread."); - - if(entry->state == ASSET_ENTRY_STATE_LOADED) { - errorOk(); - } - - // Lock to prevent the reaper from collecting the entry mid-spin. - assetEntryLock(entry); - - while(entry->state != ASSET_ENTRY_STATE_LOADED) { - // A failed load transitions to ERROR, not LOADED - without this check - // this loop spins forever on any load failure (assetUpdate() itself - // still returns OK, since a single asset failing isn't meant to halt - // the whole update loop - see its ASSET_ENTRY_STATE_ERROR handling). - if(entry->state == ASSET_ENTRY_STATE_ERROR) { - assetEntryUnlock(entry); - errorThrow("Failed to load asset: %s", entry->name); - } - - usleep(1000); - errorret_t ret = assetUpdate(); - if(errorIsNotOk(ret)) { - assetEntryUnlock(entry); - errorChain(ret); - } - } - - assetEntryUnlock(entry); - errorOk(); -} - -errorret_t assetRequireDisposed(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL."); - assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - assertIsMainThread("Currently only works on main thread."); - - if(entry->type == ASSET_LOADER_TYPE_NULL) { - errorOk(); - } - - if( - entry->state == ASSET_ENTRY_STATE_NOT_STARTED || - entry->state == ASSET_ENTRY_STATE_ERROR - ) { - errorOk(); - } - - assertTrue( - entry->refs.count == 0, - "Cannot require disposal of an entry with active references." - ); - - // Lock to prevent the reaper from collecting the entry mid-spin. - assetEntryLock(entry); - - while(entry->type != ASSET_LOADER_TYPE_NULL) { - usleep(1000); - errorret_t ret = assetUpdate(); - if(errorIsNotOk(ret)) { - assetEntryUnlock(entry); - errorChain(ret); - } - } - - assetEntryUnlock(entry); - errorOk(); -} - -assetentry_t * assetLock( - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -) { - assetentry_t *entry = assetGetEntry(name, type, input); - assetEntryLock(entry); - return entry; -} - -void assetUnlock(const char_t *name) { - assertNotNull(name, "Name cannot be NULL."); - - assetentry_t *entry = ASSET.entries; - do { - if( - entry->type != ASSET_LOADER_TYPE_NULL && - stringEquals(entry->name, name) - ) { - assetEntryUnlock(entry); - return; - } - entry++; - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); - - assertUnreachable("Asset entry not found for unlock."); -} - -void assetUnlockEntry(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL."); - assetEntryUnlock(entry); -} - -errorret_t assetReapUnused(void) { - assertIsMainThread("assetReapUnused must be called from the main thread."); - - // Repeatedly find and dispose zero-ref LOADED entries until none remain. - // This handles dependency chains where an entry (e.g. a model) holds refs - // on child entries (mesh, texture): dispose parents first so child ref - // counts drop to zero, then pick up the children on the next pass. Without - // this, a forward-only scan fails when a shared child entry appears before - // a parent that still holds a ref to it. - bool_t any; - do { - any = false; - assetentry_t *entry = ASSET.entries; - do { - if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; } - if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; } - if(entry->refs.count > 0) { entry++; continue; } - errorChain(assetEntryDispose(entry)); - any = true; - entry++; - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); - } while(any); - - errorOk(); -} - -errorret_t assetUpdate(void) { - assertIsMainThread("assetUpdate must be called from the main thread."); - - // Determine how many available loading slots we have. - assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX]; - uint8_t availableLoadingCount = 0; - assetloading_t *loading = ASSET.loading; - assetentry_t *entry; - - - do { - // We only care about NULL entry references. Nothing async touches this so - // it's fine to use raw here. - if(loading->entry != NULL) { - loading++; - continue; - } - availableLoading[availableLoadingCount++] = loading; - loading++; - } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); - - - // Now we can check for pending asset entries, we can't do anything if there - // is no available slots though. - if(availableLoadingCount > 0) { - entry = ASSET.entries; - do { - // Is this asset "ready to start loading" ? - if(entry->type == ASSET_LOADER_TYPE_NULL) { - entry++; - continue; - } - - // We only care about assets not started. - if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) { - entry++; - continue; - } - - // Pop a loading slot for this asset entry. - loading = availableLoading[--availableLoadingCount]; - - // Start loading this asset. - assetEntryStartLoading(entry, loading); - entry++; - - // Did we run out of loading slots? - if(availableLoadingCount == 0) { - break; - } - } while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX); - } - - // Now walk over all the loading slots and see what needs to be done. - loading = ASSET.loading; - do { - // Is the loading slot in use? Entry can only be modified synchronously. - if(loading->entry == NULL) { - loading++; - continue; - } - - // Lock the loading slot. This will prevent any async modifications. - threadMutexLock(&loading->mutex); - - // Check the state of the entry. - switch(loading->entry->state) { - // This thing is pending synchronous loading. - case ASSET_ENTRY_STATE_PENDING_SYNC: - loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC; - // Unlock before calling loadSync. The sync loader may re-enter - // assetUpdate (e.g. a script loading another asset), and the async - // thread never touches LOADING_SYNC entries, so this is safe. - threadMutexUnlock(&loading->mutex); - - errorret_t ret = ( - ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading) - ); - - // After a sync load, these are the only valid states. - assertTrue( - loading->entry->state == ASSET_ENTRY_STATE_LOADED || - loading->entry->state == ASSET_ENTRY_STATE_ERROR || - loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC || - loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC, - "Loader did not set entry state to loaded or error on finished load." - ); - - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - assertTrue( - loading->entry->state == ASSET_ENTRY_STATE_ERROR, - "Loader did not set entry state to error on failed load." - ); - } else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) { - assetentry_t *loadedEntry = loading->entry; - loading->entry = NULL; - if(loadedEntry->onLoaded) { - loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser); - } - } - - loading++; - break; - - case ASSET_ENTRY_STATE_LOADING_SYNC: - // A re-entrant assetUpdate call (e.g. from a script loading another - // asset) will see this entry mid-sync-load. Skip it. - threadMutexUnlock(&loading->mutex); - loading++; - continue; - - // Done loading, we can just free it up. - case ASSET_ENTRY_STATE_LOADED: - loading->entry = NULL; - threadMutexUnlock(&loading->mutex); - loading++; - break; - - case ASSET_ENTRY_STATE_ERROR: { - assetentry_t *errEntry = loading->entry; - loading->entry = NULL; - threadMutexUnlock(&loading->mutex); - if(errEntry->onError) { - errEntry->onError(errEntry, errEntry->onErrorUser); - } - loading++; - break; - } - - default: - threadMutexUnlock(&loading->mutex); - loading++; - continue; - } - } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); - - errorOk(); -} - -void assetUpdateAsync(thread_t *thread) { - assertNotMainThread("assetUpdateAsync must not run on the main thread."); - - while(!threadShouldStop(thread)) { - // Walk over each asset - assetloading_t *loading; - loading = ASSET.loading; - - do { - threadMutexLock(&loading->mutex); - - if(loading->entry == NULL) { - threadMutexUnlock(&loading->mutex); - loading++; - continue; - } - - switch(loading->entry->state) { - case ASSET_ENTRY_STATE_PENDING_ASYNC: - loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC; - assertNotNull( - ASSET_LOADER_CALLBACKS[loading->type].loadAsync, - "Loader does not support async loading." - ); - errorret_t ret = ( - ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading) - ); - - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - assertTrue( - loading->entry->state == ASSET_ENTRY_STATE_ERROR, - "Loader did not set entry state to error on failed load." - ); - } - - threadMutexUnlock(&loading->mutex); - loading++; - break; - - case ASSET_ENTRY_STATE_LOADING_ASYNC: - assertUnreachable( - "Entry is in a pending async state still?" - ); - break; - - default: - threadMutexUnlock(&loading->mutex); - loading++; - continue; - } - } while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX); - - if(threadShouldStop(thread)) break; - usleep(1000); - } -} - -errorret_t assetDispose(void) { - assertIsMainThread("Must be called from the main thread."); - threadStop(&ASSET.loadThread); - - errorChain(assetReapUnused()); - - // Cleanup zip file. - if(ASSET.zip != NULL) { - if(zip_close(ASSET.zip) != 0) { - errorThrow("Failed to close asset zip archive."); - } - ASSET.zip = NULL; - } - - errorChain(assetDisposePlatform()); - threadMutexDispose(&ASSET.zipLock); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h deleted file mode 100644 index 15e98110..00000000 --- a/src/dusk/asset/asset.h +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "asset/assetplatform.h" -#include "assetfile.h" -#include "thread/thread.h" -#include "asset/loader/assetentry.h" -#include "asset/loader/assetloading.h" - -#ifndef assetInitPlatform - #error "Platform must define assetInitPlatform function." -#endif -#ifndef assetDisposePlatform - #error "Platform must define assetDisposePlatform function." -#endif - -#define ASSET_FILE_NAME "dusk.dsk" -#define ASSET_HEADER_SIZE 3 - -#define ASSET_LOADING_COUNT_MAX 10 -#define ASSET_ENTRY_COUNT_MAX 64 - -#define ASSET_BASE_DIRECTORY_MAX 256 - -typedef struct asset_s { - // The dusk.dsk archive - a single uncompressed (ZIP_STORED) zip, so - // 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; - - // 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 - // loose file) - populated by each platform's own assetInitXxx() once it - // locates the file. Save devices that persist to a real filesystem path - // (Linux, PSP, Wii SD) use this as their base directory instead of a - // separate hardcoded location, so saves live next to wherever the game - // itself was actually launched from rather than some unrelated fixed - // path. Not meaningful for devices with no filesystem-path concept at - // all (GameCube/Wii memory card, Wii NAND's ISFS namespace). - char_t baseDirectory[ASSET_BASE_DIRECTORY_MAX]; - - assetplatform_t platform; - - // Guards every libzip call against ASSET.zip (zip_fopen, - // zip_fread, zip_fclose, zip_fseek, zip_stat, zip_name_locate - see - // assetfile.c/assetFileExists). libzip is documented as not thread-safe, - // and this asset system genuinely calls it from multiple real threads at - // once (main thread, the background load thread below, and - on - // platforms that stream PCM from an asset on their own thread, like PSP - // - an audio feeder thread too). Confirmed necessary on real PSP - // hardware: without this lock, concurrent zip_fopen/zip_fread calls from - // different threads corrupted reads (EINVAL, then zlib data errors) once - // dusk.dsk stopped being read entirely into memory up front. - threadmutex_t zipLock; - - // Background loading thread. - thread_t loadThread; - - // Assets that are mid loading. - assetloading_t loading[ASSET_LOADING_COUNT_MAX]; - assetentry_t entries[ASSET_ENTRY_COUNT_MAX]; -} asset_t; - -extern asset_t ASSET; - -/** - * Initializes the asset system. - * - * @return An error code if the asset system could not be initialized properly. - */ -errorret_t assetInit(void); - -/** - * Checks if an asset file exists. - * - * @param filename The filename of the asset to check. - * @return true if the asset file exists, false otherwise. - */ -bool_t assetFileExists(const char_t *filename); - -/** - * Gets, or creates, a new asset entry. Internal - prefer assetLock. - * - * @param name Filename of the asset. - * @param type Type of the asset. - * @param input Loader-specific parameters. - */ -assetentry_t * assetGetEntry( - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -); - -/** - * Gets all asset entries of a given type. - * - * @param outEntries Output array to write the entries to. - * @param type Type of the asset entries to get. - * @return The number of entries written to outEntries. - */ -uint32_t assetGetEntriesOfType( - assetentry_t **outEntries, - const assetloadertype_t type -); - -/** - * Gets, creates, and locks an asset entry. The asset will begin loading on - * the next assetUpdate. Call assetUnlock when done to allow the entry to be - * reclaimed. - * - * @param name Filename of the asset. - * @param type Type of the asset. - * @param input Loader-specific parameters. - * @return The locked asset entry. - */ -assetentry_t * assetLock( - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -); - -/** - * Releases a lock on an asset entry by name. When all locks are released the - * entry will be reclaimed at the start of the next assetUpdate. - * - * @param name Filename of the asset to unlock. - */ -void assetUnlock(const char_t *name); - -/** - * Releases a lock on an asset entry by pointer. When all locks are released - * the entry will be reclaimed at the start of the next assetUpdate. - * - * @param entry The asset entry to unlock. - */ -void assetUnlockEntry(assetentry_t *entry); - -/** - * Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats - * until a full pass frees nothing further, since disposing a parent entry - * (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero, - * making it eligible for reaping too. - * - * @return An error code if any entry could not be disposed properly. - */ -errorret_t assetReapUnused(void); - -/** - * Requires an asset entry to be loaded. This will block until the asset entry - * is fully loaded. - * - * @param entry The asset entry to require. - * @return An error code if the asset entry could not be loaded properly. - */ -errorret_t assetRequireLoaded(assetentry_t *entry); - -/** - * Requires an asset entry to be disposed. This will block until the asset entry - * is fully disposed. - * - * @param entry The asset entry to require disposal of. - * @return An error code if the asset entry could not be disposed properly. - */ -errorret_t assetRequireDisposed(assetentry_t *entry); - -/** - * Updates the asset system. - * - * @return An error code if the asset system could not be updated properly. - */ -errorret_t assetUpdate(void); - -/** - * Starts the background asset loading thread. The thread runs assetUpdate - * in a loop with a short sleep until stopped. - * - * @param thread The thread runner. - */ -void assetUpdateAsync(thread_t *thread); - -/** - * Disposes/cleans up the asset system. - * - * @return An error code if the asset system could not be disposed properly. - */ -errorret_t assetDispose(void); \ No newline at end of file diff --git a/src/dusk/asset/assetdsk.c b/src/dusk/asset/assetdsk.c deleted file mode 100644 index 95397434..00000000 --- a/src/dusk/asset/assetdsk.c +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetdsk.h" -#include "util/memory.h" -#include "util/endian.h" -#include "assert/assert.h" -#include - -errorret_t assetDskParseHeader( - const uint8_t *bytes, - const size_t bytesSize, - assetdskheader_t *outHeader -) { - assertNotNull(bytes, "Bytes cannot be NULL."); - assertNotNull(outHeader, "Out header cannot be NULL."); - - if(bytesSize < ASSET_DSK_HEADER_SIZE) { - errorThrow("dusk.dsk header is truncated."); - } - - if(memoryCompare(bytes, ASSET_DSK_MAGIC, ASSET_DSK_MAGIC_SIZE) != 0) { - errorThrow("dusk.dsk has an invalid magic header."); - } - - // 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 - // Dolphin's big-endian PowerPC). - uint32_t fields[3]; - memoryCopy(fields, bytes + ASSET_DSK_MAGIC_SIZE, sizeof(fields)); - for(uint8_t i = 0; i < 3; i++) { - fields[i] = endianLittleToHost32(fields[i]); - } - - const uint32_t version = fields[0]; - if(version != ASSET_DSK_VERSION) { - errorThrow("dusk.dsk has an unsupported version: %u", version); - } - - outHeader->size = fields[1]; - outHeader->checksum = fields[2]; - - errorOk(); -} - -errorret_t assetDskOpenFromPath( - const char_t *path, - zip_t **outZip -) { - errorChain(assetDskOpenFromPathRange(path, 0, SIZE_MAX, outZip)); - errorOk(); -} - -errorret_t assetDskOpenFromPathRange( - const char_t *path, - const size_t baseOffset, - const size_t baseSize, - zip_t **outZip -) { - assertNotNull(path, "Path cannot be NULL."); - assertNotNull(outZip, "Out zip cannot be NULL."); - - *outZip = NULL; - - FILE *headerFile = fopen(path, "rb"); - if(headerFile == NULL) { - errorThrow("Failed to open dusk.dsk: %s", path); - } - - if(fseek(headerFile, (long) baseOffset, SEEK_SET) != 0) { - fclose(headerFile); - errorThrow("Failed to seek to dusk.dsk range in file: %s", path); - } - - uint8_t headerBytes[ASSET_DSK_HEADER_SIZE]; - size_t headerRead = fread(headerBytes, 1, sizeof(headerBytes), headerFile); - fclose(headerFile); - if(headerRead != sizeof(headerBytes)) { - errorThrow("Failed to read dusk.dsk header: %s", path); - } - - assetdskheader_t header; - errorChain(assetDskParseHeader(headerBytes, sizeof(headerBytes), &header)); - - if((size_t) ASSET_DSK_HEADER_SIZE + header.size > baseSize) { - errorThrow("dusk.dsk header describes a range beyond its containing file."); - } - - zip_error_t zipError; - zip_error_init(&zipError); - - zip_source_t *source = zip_source_file_create( - path, - (zip_uint64_t) (baseOffset + ASSET_DSK_HEADER_SIZE), - (zip_int64_t) header.size, - &zipError - ); - if(source == NULL) { - errorThrow( - "Failed to create dusk.dsk source: %s", zip_error_strerror(&zipError) - ); - } - - *outZip = zip_open_from_source(source, ZIP_RDONLY, &zipError); - if(*outZip == NULL) { - zip_source_free(source); - errorThrow( - "Failed to open dusk.dsk archive: %s", zip_error_strerror(&zipError) - ); - } - - errorOk(); -} - -errorret_t assetDskOpenFromBuffer( - uint8_t *buffer, - const size_t bufferSize, - zip_t **outZip -) { - assertNotNull(buffer, "Buffer cannot be NULL."); - assertNotNull(outZip, "Out zip cannot be NULL."); - - *outZip = NULL; - - assetdskheader_t header; - errorChain(assetDskParseHeader(buffer, bufferSize, &header)); - - if((size_t) ASSET_DSK_HEADER_SIZE + header.size > bufferSize) { - errorThrow("dusk.dsk header describes a range beyond the buffer."); - } - - uint8_t *data = buffer + ASSET_DSK_HEADER_SIZE; - uint32_t checksum = (uint32_t) crc32(0L, data, (uInt) header.size); - if(checksum != header.checksum) { - errorThrow("dusk.dsk archive failed checksum verification."); - } - - zip_error_t zipError; - zip_error_init(&zipError); - - // freep=0 - this is a non-owning window into the same caller-owned - // buffer, not an independent allocation libzip should free. - zip_source_t *source = zip_source_buffer_create( - data, header.size, 0, &zipError - ); - if(source == NULL) { - errorThrow( - "Failed to create dusk.dsk source: %s", zip_error_strerror(&zipError) - ); - } - - *outZip = zip_open_from_source(source, ZIP_RDONLY, &zipError); - if(*outZip == NULL) { - zip_source_free(source); - errorThrow( - "Failed to open dusk.dsk archive: %s", zip_error_strerror(&zipError) - ); - } - - errorOk(); -} diff --git a/src/dusk/asset/assetdsk.h b/src/dusk/asset/assetdsk.h deleted file mode 100644 index efb28800..00000000 --- a/src/dusk/asset/assetdsk.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include - -// "DSK2" - distinct from a plain zip's "PK\x03\x04" so a stray plain zip -// never gets misread as a valid dusk.dsk. -#define ASSET_DSK_MAGIC_SIZE 4 -#define ASSET_DSK_MAGIC "DSK" -#define ASSET_DSK_VERSION 2 - -// Magic + version + size + checksum -#define ASSET_DSK_HEADER_SIZE ( \ - ASSET_DSK_MAGIC_SIZE + \ - sizeof(uint32_t) + \ - sizeof(uint32_t) + \ - sizeof(uint32_t) \ -) - -/** - * Parsed dusk.dsk (DSK2 format) header. dusk.dsk is a single uncompressed - * (ZIP_STORED) zip archive (see tools/asset/pack) rather than DEFLATEd - - * the bulk of what's packed (audio, images) is already compressed at the - * file level, so DEFLATE-ing the whole archive on top buys negligible - * space for real CPU cost, and every entry gets the reliable repeated - * seeking/re-opening that libzip only supports for uncompressed entries - * (locale line-by-line reads, MP3 loop restarts, etc.) instead of just a - * hand-picked subset of files. - */ -typedef struct { - uint32_t size; - uint32_t checksum; -} assetdskheader_t; - -/** - * Parses a DSK2 header from a raw byte buffer (at least - * ASSET_DSK_HEADER_SIZE bytes), validating the magic/version and - * byte-swapping the little-endian fields to host order. - * - * @param bytes Buffer containing the header (and beyond). - * @param bytesSize Number of bytes available at `bytes`. - * @param outHeader Filled with the parsed header on success. - * @return OK on success, error if too short, bad magic, or unsupported version. - */ -errorret_t assetDskParseHeader( - const uint8_t *bytes, - const size_t bytesSize, - assetdskheader_t *outHeader -); - -/** - * Opens the dusk.dsk archive given a filesystem path, using a lazy/ - * windowed file-backed zip source - no more memory used than the - * existing per-platform small read buffers, matching the memory - * characteristics of a plain zip_open() on the whole file. The checksum - * is intentionally not verified here since doing so would require - * reading the whole (potentially large) archive just to compute it. - * - * See assetDskOpenFromPathRange() for the case where the DSK2 blob isn't - * the whole file (e.g. embedded inside another container). - * - * @param path Filesystem path to the dusk.dsk file. - * @param outZip Set to the opened archive on success. - * @return OK on success, error if the file is missing, too short, has a - * bad header, or the archive fails to open. - */ -errorret_t assetDskOpenFromPath( - const char_t *path, - zip_t **outZip -); - -/** - * Same as assetDskOpenFromPath(), but the DSK2 blob doesn't start at the - * beginning of `path` - it's a byte range embedded inside a larger - * container file (e.g. the PSAR region of an EBOOT.PBP). Every offset the - * header describes is relative to `baseOffset`; `baseSize` bounds them - * (the size of the embedded blob, not the whole container file) - pass - * SIZE_MAX to skip that check when the caller doesn't know/care. - * - * @param path Filesystem path to the container file. - * @param baseOffset Byte offset within `path` where the DSK2 blob starts. - * @param baseSize Number of bytes available at `baseOffset`. - * @param outZip Set to the opened archive on success. - * @return OK on success, error if the file is missing, too short, has a - * bad header, or the archive fails to open. - */ -errorret_t assetDskOpenFromPathRange( - const char_t *path, - const size_t baseOffset, - const size_t baseSize, - zip_t **outZip -); - -/** - * Opens the dusk.dsk archive already fully resident in memory (e.g. a - * PSAR embedded in an EBOOT.PBP, or an ISO-embedded file already read via - * DVD_ReadAbs - platforms that already buffer the whole file for reasons - * unrelated to this format). The returned archive doesn't take ownership - * 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 - * as the archive stays open. Verifies the checksum, since the bytes are - * already resident. - * - * @param buffer The whole dusk.dsk file's bytes. - * @param bufferSize Number of bytes at `buffer`. - * @param outZip Set to the opened archive on success. - * @return OK on success, error if too short, has a bad header/checksum, or - * the archive fails to open. - */ -errorret_t assetDskOpenFromBuffer( - uint8_t *buffer, - const size_t bufferSize, - zip_t **outZip -); diff --git a/src/dusk/asset/assetfile.c b/src/dusk/asset/assetfile.c deleted file mode 100644 index e6de7462..00000000 --- a/src/dusk/asset/assetfile.c +++ /dev/null @@ -1,436 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "asset/asset.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" - -errorret_t assetFileInit( - assetfile_t *file, - const char_t *filename, - void *params, - void *output -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long."); - - memoryZero(file, sizeof(assetfile_t)); - memoryCopy(file->filename, filename, ASSET_FILE_NAME_MAX); - file->params = params; - file->output = output; - - zip_stat_init(&file->stat); - threadMutexLock(&ASSET.zipLock); - int statResult = zip_stat(ASSET.zip, filename, 0, &file->stat); - threadMutexUnlock(&ASSET.zipLock); - if(statResult != 0) { - errorThrow("Failed to stat asset file: %s", filename); - } - - // Minimum file size. - file->size = (zip_int64_t)file->stat.size; - if(file->size <= 0) { - errorThrow("Invalid asset file size: %s", filename); - } - - errorOk(); -} - -errorret_t assetFileRewind(assetfile_t *file) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(file->zipFile, "Asset file must be opened before rewinding."); - - if(file->position == 0) { - errorOk(); - } - - // Prefer seeking within the still-open handle over closing and - // re-opening it. Repeatedly closing/re-opening the same compressed zip - // entry (once per rewind - e.g. once per locale string lookup) was - // confirmed unreliable on at least one platform's zip backend, silently - // skipping ranges of the decompressed content on some re-opens after - // the first. A real seek avoids that failure mode entirely, with no - // extra memory cost over the close+reopen fallback. - threadMutexLock(&ASSET.zipLock); - bool_t seekable = zip_file_is_seekable(file->zipFile); - int seekResult = seekable ? zip_fseek(file->zipFile, 0, SEEK_SET) : -1; - threadMutexUnlock(&ASSET.zipLock); - if(seekable) { - if(seekResult != 0) { - errorThrow("Failed to seek asset file: %s", file->filename); - } - file->position = 0; - errorOk(); - } - - errorChain(assetFileClose(file)); - errorChain(assetFileOpen(file)); - errorOk(); -} - -errorret_t assetFileOpen(assetfile_t *file) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(file->filename, "Asset file filename cannot be NULL."); - assertNull(file->zipFile, "Asset file already open."); - - threadMutexLock(&ASSET.zipLock); - file->zipFile = zip_fopen(ASSET.zip, file->filename, 0); - threadMutexUnlock(&ASSET.zipLock); - if(file->zipFile == NULL) { - errorThrow("Failed to open asset file: %s", file->filename); - } - file->position = 0; - errorOk(); -} - -errorret_t assetFileRead( - assetfile_t *file, - void *buffer, - const size_t bufferSize -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(file->zipFile, "Asset file must be opened before reading."); - - if(buffer == NULL) { - size_t bytesRemaining = bufferSize; - uint8_t tempBuffer[256]; - while(bytesRemaining > 0) { - size_t chunkSize = mathMin(bytesRemaining, sizeof(tempBuffer)); - // The recursive call below already advances file->position by - // chunkSize (the non-NULL branch does this itself) - do not also - // advance it here, or every skip ends up double-counted. - errorChain(assetFileRead(file, tempBuffer, chunkSize)); - bytesRemaining -= chunkSize; - } - file->lastRead = bufferSize; - errorOk(); - } - - // Some zip_fread() implementations (seen on PSP) reject a single call - // asking for the entire (potentially large) file at once with EINVAL; - // the line reader above only ever asks for up to 1024 bytes per call and - // works fine, so read in bounded chunks here too. - size_t totalRead = 0; - uint8_t *dest = (uint8_t *)buffer; - while(totalRead < bufferSize) { - size_t chunkSize = mathMin( - bufferSize - totalRead, ASSET_FILE_READ_CHUNK_MAX - ); - threadMutexLock(&ASSET.zipLock); - zip_int64_t bytesRead = zip_fread( - file->zipFile, dest + totalRead, chunkSize - ); - errorret_t readError = errorOkImpl(); - if(bytesRead < 0) { - // Built (not thrown) while still holding the lock, so - // zip_file_strerror() reads the just-failed zipFile's error state - // before another thread can touch it - errorThrow() itself isn't - // used here since it returns immediately, which would leave - // ASSET.zipLock held forever. - readError = errorThrowImpl( - &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, - "Failed to read from asset file: %s (%s)", - file->filename, zip_file_strerror(file->zipFile) - ); - } - threadMutexUnlock(&ASSET.zipLock); - if(errorIsNotOk(readError)) { - errorChain(readError); - } - if(bytesRead == 0) break; - totalRead += (size_t)bytesRead; - } - file->position += totalRead; - file->lastRead = totalRead; - errorOk(); -} - -errorret_t assetFileClose(assetfile_t *file) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(file->zipFile, "Asset file must be opened before closing."); - - threadMutexLock(&ASSET.zipLock); - int closeResult = zip_fclose(file->zipFile); - threadMutexUnlock(&ASSET.zipLock); - if(closeResult != 0) { - errorThrow("Failed to close asset file: %s", file->filename); - } - file->zipFile = NULL; - file->position = 0; - errorOk(); -} - -errorret_t assetFileDispose(assetfile_t *file) { - if(file->zipFile != NULL) { - errorChain(assetFileClose(file)); - } - memoryZero(file, sizeof(assetfile_t)); - errorOk(); -} - -errorret_t assetFileReadEntire( - assetfile_t *file, - uint8_t **outBuffer, - size_t *outSize -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(outBuffer, "outBuffer cannot be NULL."); - assertNotNull(outSize, "outSize cannot be NULL."); - assertTrue( - file->size > 0, - "Asset file has no size; call assetFileInit first." - ); - - // File should be closed currently. - assertNull(file->zipFile, "Asset file must be closed before reading entire."); - - // Open file - errorret_t ret = assetFileOpen(file); - if(errorIsNotOk(ret)) { - errorChain(ret); - } - - // Set output. - size_t size = (size_t)file->size; - uint8_t *buffer = (uint8_t *)memoryAllocate(size); - - // Read entire file. - ret = assetFileRead(file, buffer, size); - if(errorIsNotOk(ret)) { - memoryFree(buffer); - errorChain(ret); - } - - // Close the file. - ret = assetFileClose(file); - if(errorIsNotOk(ret)) { - memoryFree(buffer); - errorChain(ret); - } - - *outBuffer = buffer; - *outSize = size; - errorOk(); -} - -// Line Reader; -void assetFileLineReaderInit( - assetfilelinereader_t *reader, - assetfile_t *file, - uint8_t *readBuffer, - const size_t readBufferSize, - uint8_t *outBuffer, - const size_t outBufferSize -) { - assertNotNull(reader, "Line reader cannot be NULL."); - assertNotNull(file, " File cannot be NULL."); - assertNotNull(readBuffer, "Read buffer cannot be NULL."); - assertNotNull(outBuffer, "Output buffer cannot be NULL."); - assertTrue(readBufferSize > 0, "Read buffer size must be greater than 0."); - assertTrue(outBufferSize > 0, "Output buffer size must be greater than 0."); - - memoryZero(reader, sizeof(assetfilelinereader_t)); - - reader->file = file; - reader->readBuffer = readBuffer; - reader->readBufferSize = readBufferSize; - reader->outBuffer = outBuffer; - reader->outBufferSize = outBufferSize; -} - -size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader) { - assertNotNull(reader, "Reader cannot be NULL."); - assertTrue(reader->bufferEnd >= reader->bufferStart, "Invalid buffer state."); - return reader->bufferEnd - reader->bufferStart; -} - -const uint8_t *assetFileLineReaderUnreadPtr( - const assetfilelinereader_t *reader -) { - assertNotNull(reader, "Reader cannot be NULL."); - assertNotNull(reader->readBuffer, "Read buffer cannot be NULL."); - return reader->readBuffer + reader->bufferStart; -} - -errorret_t assetFileLineReaderAppend( - assetfilelinereader_t *reader, - const uint8_t *src, - size_t srcLength -) { - assertNotNull(reader, "Reader cannot be NULL."); - assertNotNull(reader->outBuffer, "Out buffer cannot be NULL."); - - if(srcLength == 0) { - errorOk(); - } - - /* reserve room for optional NUL terminator */ - if(reader->lineLength + srcLength >= reader->outBufferSize) { - errorThrow("Line length exceeds output buffer size."); - } - - memoryCopy(reader->outBuffer + reader->lineLength, src, srcLength); - reader->lineLength += srcLength; - errorOk(); -} - -void assetFileLineReaderTerminate(assetfilelinereader_t *reader) { - assertNotNull(reader, "Reader cannot be NULL."); - assertNotNull(reader->outBuffer, "Out buffer cannot be NULL."); - assertTrue( - reader->lineLength < reader->outBufferSize, - "Line length exceeds out buffer." - ); - reader->outBuffer[reader->lineLength] = '\0'; -} - -ssize_t assetFileLineReaderFindNewline( - const assetfilelinereader_t *reader -) { - size_t i; - - assertNotNull(reader, "Reader cannot be NULL."); - assertNotNull(reader->readBuffer, "Read buffer cannot be NULL."); - - for(i = reader->bufferStart; i < reader->bufferEnd; ++i) { - if(reader->readBuffer[i] == '\n') { - return (ssize_t)i; - } - } - - return -1; -} - -errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) { - - assertNotNull(reader, "Reader cannot be NULL."); - assertNotNull(reader->file, "File cannot be NULL."); - assertNotNull(reader->readBuffer, "Read buffer cannot be NULL."); - - if(reader->eof) errorOk(); - - errorret_t ret; - - size_t unreadBytes = assetFileLineReaderUnreadBytes(reader); - - /* If buffer is fully consumed, refill from start. */ - if(unreadBytes == 0) { - reader->bufferStart = 0; - reader->bufferEnd = 0; - - errorChain(assetFileRead( - reader->file, - reader->readBuffer, - reader->readBufferSize - )); - - if(reader->file->lastRead == 0) { - reader->eof = true; - errorOk(); - } - - reader->bufferStart = 0; - reader->bufferEnd = reader->file->lastRead; - errorOk(); - } - - /* - * There are unread bytes left but no newline in them. - * 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->bufferStart == 0) { - /* - * Entire read buffer is unread and contains no newline. - * Caller must have a large enough outBuffer to accumulate across fills, - * so we consume these bytes into outBuffer before refilling. - */ - errorOk(); - } - - memoryMove( - reader->readBuffer, - reader->readBuffer + reader->bufferStart, - unreadBytes - ); - reader->bufferStart = 0; - reader->bufferEnd = unreadBytes; - } - - errorChain(assetFileRead( - reader->file, - reader->readBuffer + reader->bufferEnd, - reader->readBufferSize - reader->bufferEnd - )); - - if(reader->file->lastRead == 0) { - reader->eof = true; - errorOk(); - } - - reader->bufferEnd += reader->file->lastRead; - errorOk(); -} - -errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) { - assertNotNull(reader, "Reader cannot be NULL."); - assertNotNull(reader->file, "File cannot be NULL."); - assertNotNull(reader->readBuffer, "Read buffer cannot be NULL."); - assertNotNull(reader->outBuffer, "Out buffer cannot be NULL."); - - reader->lineLength = 0; - - for(;;) { - ssize_t newlineIndex = assetFileLineReaderFindNewline(reader); - - if(newlineIndex >= 0) { - size_t chunkLength = (size_t)newlineIndex - reader->bufferStart; - errorret_t ret; - - /* strip CR in CRLF */ - if( - chunkLength > 0 && - reader->readBuffer[(size_t)newlineIndex - 1] == '\r' - ) { - chunkLength--; - } - - errorChain(assetFileLineReaderAppend( - reader, - reader->readBuffer + reader->bufferStart, - chunkLength - )); - - reader->bufferStart = (size_t)newlineIndex + 1; - assetFileLineReaderTerminate(reader); - errorOk(); - } - - if(assetFileLineReaderUnreadBytes(reader) > 0) { - errorChain(assetFileLineReaderAppend( - reader, - assetFileLineReaderUnreadPtr(reader), - assetFileLineReaderUnreadBytes(reader) - )); - - reader->bufferStart = reader->bufferEnd; - } - - if(reader->eof) { - if(reader->lineLength > 0) { - assetFileLineReaderTerminate(reader); - errorOk(); - } - - errorThrow("End of file reached."); - } - - errorChain(assetFileLineReaderFill(reader)); - } -} \ No newline at end of file diff --git a/src/dusk/asset/assetfile.h b/src/dusk/asset/assetfile.h deleted file mode 100644 index 2d02e1c9..00000000 --- a/src/dusk/asset/assetfile.h +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include - -#define ASSET_FILE_NAME_MAX 128 - -// Max bytes requested per zip_fread() call in assetFileRead(). Some -// zip_fread() implementations (seen on PSP) reject a single call asking -// for very large amounts of data at once; the locale line reader has -// always used 1024-byte reads successfully, so that's the proven-safe cap. -#define ASSET_FILE_READ_CHUNK_MAX 1024 - -typedef struct assetfile_s assetfile_t; - -typedef errorret_t (*assetfileloader_t)(assetfile_t *file); - -// Describes a file not yet loaded. -typedef struct assetfile_s { - char_t filename[ASSET_FILE_NAME_MAX]; - void *params; - void *output; - - zip_stat_t stat; - zip_int64_t size; - zip_int64_t position; - zip_int64_t lastRead; - zip_file_t *zipFile; -} assetfile_t; - -/** - * Initializes the asset file structure in preparation for loading. This will - * stat the file but not open the handle. - * - * @param file The asset file structure to initialize. - * @param filename The name of the asset file to load. - * @param params Optional loader params. - * @param output Output pointer for the loader. - * @return Error indicating success or failure. - */ -errorret_t assetFileInit( - assetfile_t *file, - const char_t *filename, - void *params, - void *output -); - -/** - * Opens the asset file for reading. After opening the loader is responsible - * for closing the file. - * - * @param file The asset file to open. - * @return An error code if the file could not be opened. - */ -errorret_t assetFileOpen(assetfile_t *file); - -/** - * Rewind the file to the initial position. - * - * @param file The asset file to rewind. - */ -errorret_t assetFileRewind(assetfile_t *file); - -/** - * Read bytes from the asset file. Assumes the file has already been opened - * prior to trying to read anything. - * - * @param file File to read from. - * @param buffer Buffer to read the file data into., or NULL to skip bytes. - * @param size Size of the buffer to read into. - */ -errorret_t assetFileRead( - assetfile_t *file, - void *buffer, - const size_t bufferSize -); - -/** - * Closes the asset file and releases any resources associated with it. - * - * @param file The asset file to close. - * @return An error code if the file could not be closed properly. - */ -errorret_t assetFileClose(assetfile_t *file); - -/** - * Disposes the asset file structure, closing any open handles and zeroing - * out the structure. - * - * @param file The asset file to dispose. - * @return An error code if the file could not be disposed properly. - */ -errorret_t assetFileDispose(assetfile_t *file); - -/** - * Reads the entire contents of the asset file into a newly allocated buffer. - * The caller is responsible for freeing the buffer with memoryFree. - * - * @param file The asset file to read. Must be initialized but not open. - * @param outBuffer Receives a pointer to the allocated buffer. - * @param outSize Receives the number of bytes written to the buffer. - * @return An error code if the file could not be read. - */ -errorret_t assetFileReadEntire( - assetfile_t *file, - uint8_t **outBuffer, - size_t *outSize -); - -typedef struct { - assetfile_t *file; - uint8_t *readBuffer; - size_t readBufferSize; - uint8_t *outBuffer; - size_t outBufferSize; - - // A - size_t bufferStart; - size_t bufferEnd; - bool_t eof;//? - - // Updated each reach: - size_t lineLength; -} assetfilelinereader_t; - -/** - * Initializes a line reader for the given asset file. The line reader will read - * lines from the file into the provided line buffer, using the provided buffer - * for reading chunks of the file. - * - * @param file The asset file to read from. Must already be opened. - * @param readBuffer Buffer to use for reading chunks of the file. - * @param readBufferSize Size of the read buffer. - * @param outBuffer Buffer to read lines into. Lines will be null-terminated. - * @param outBufferSize Size of the output buffer. - * @return An initialized line reader structure. - */ -void assetFileLineReaderInit( - assetfilelinereader_t *reader, - assetfile_t *file, - uint8_t *readBuffer, - const size_t readBufferSize, - uint8_t *outBuffer, - const size_t outBufferSize -); - -/** - * Returns the number of bytes still unread in the line reader's read buffer. - * - * @param reader The line reader to check. - * @return Number of unread bytes remaining in the read buffer. - */ -size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader); - -/** - * Returns a pointer to the first unread byte in the line reader's read - * buffer. - * - * @param reader The line reader to check. - * @return Pointer to the first unread byte. - */ -const uint8_t *assetFileLineReaderUnreadPtr( - const assetfilelinereader_t *reader -); - -/** - * Appends src to the line reader's current line buffer, growing lineLength. - * - * @param reader The line reader whose out buffer to append to. - * @param src Bytes to append. - * @param srcLength Number of bytes in src. - * @return Error state if any (the line would exceed the output buffer). - */ -errorret_t assetFileLineReaderAppend( - assetfilelinereader_t *reader, - const uint8_t *src, - size_t srcLength -); - -/** - * Null-terminates the line reader's current line buffer at lineLength. - * - * @param reader The line reader whose out buffer to terminate. - */ -void assetFileLineReaderTerminate(assetfilelinereader_t *reader); - -/** - * Searches the line reader's unread buffered bytes for a newline character. - * - * @param reader The line reader to search. - * @return The index of the newline within readBuffer, or -1 if not found. - */ -ssize_t assetFileLineReaderFindNewline(const assetfilelinereader_t *reader); - -/** - * Refills the line reader's read buffer from the underlying file once its - * buffered bytes are fully consumed. A no-op once the file is at EOF. - * - * @param reader The line reader to refill. - * @return Error state if any. - */ -errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader); - -/** - * Reads the next line from the asset file into the line buffer. The line - * buffer is null-terminated and does not include the newline character. - * - * @param reader The line reader to read from. - * @return An error code if a failure occurs, or errorOk() if a line was read - * successfully. If the end of the file is reached, errorEndOfFile() is - * returned. - */ -errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader); \ No newline at end of file diff --git a/src/dusk/asset/loader/CMakeLists.txt b/src/dusk/asset/loader/CMakeLists.txt deleted file mode 100644 index 21932838..00000000 --- a/src/dusk/asset/loader/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# 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 - assetentry.c - assetloader.c -) - - -# Subdirs -add_subdirectory(display) -add_subdirectory(locale) -add_subdirectory(json) -add_subdirectory(chunk) -add_subdirectory(dmf) -add_subdirectory(wav) -add_subdirectory(mp3) \ No newline at end of file diff --git a/src/dusk/asset/loader/assetentry.c b/src/dusk/asset/loader/assetentry.c deleted file mode 100644 index cfa5697f..00000000 --- a/src/dusk/asset/loader/assetentry.c +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetentry.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" - -void assetEntryInit( - assetentry_t *entry, - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -) { - assertNotNull(entry, "Entry cannot be NULL"); - assertStrLenMin(name, 1, "Name cannot be empty"); - assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long"); - assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type."); - assertIsMainThread("Must be called from the main thread."); - - memoryZero(entry, sizeof(assetentry_t)); - stringCopy(entry->name, name, ASSET_FILE_NAME_MAX); - entry->type = type; - entry->state = ASSET_ENTRY_STATE_NOT_STARTED; - if(input) { - entry->inputData = *input; - entry->input = &entry->inputData; - } else { - memoryZero(&entry->inputData, sizeof(assetloaderinput_t)); - entry->input = NULL; - } - refInit(&entry->refs, entry, NULL, NULL, NULL); -} - -void assetEntryLock(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - - refLock(&entry->refs); -} - -void assetEntryUnlock(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - - refUnlock(&entry->refs); -} - -void assetEntryStartLoading( - assetentry_t *entry, - assetloading_t *loading -) { - assertNotNull(entry, "Entry cannot be NULL"); - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - assertTrue( - entry->state == ASSET_ENTRY_STATE_NOT_STARTED, - "Can only start loading from NOT_STARTED state." - ); - assertIsMainThread("Must be called from the main thread."); - - entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - memoryZero(&loading->loading, sizeof(assetloaderloading_t)); - loading->type = entry->type; - loading->entry = entry; - // At this point the asset manager will manage this thing's loading -} - -errorret_t assetEntryDispose(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type."); - assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type."); - assertIsMainThread("Must be called from the main thread."); - assertTrue( - entry->refs.count == 0, - "Asset entry still refed at dispose time." - ); - - if(entry->onUnloaded) entry->onUnloaded(entry, entry->onUnloadedUser); - errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry)); - memoryZero(entry, sizeof(assetentry_t)); - errorOk(); -} diff --git a/src/dusk/asset/loader/assetentry.h b/src/dusk/asset/loader/assetentry.h deleted file mode 100644 index 493af0b1..00000000 --- a/src/dusk/asset/loader/assetentry.h +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/loader/assetloading.h" -#include "util/ref.h" - -typedef enum { - ASSET_ENTRY_STATE_NOT_STARTED, - ASSET_ENTRY_STATE_PENDING_ASYNC, - ASSET_ENTRY_STATE_LOADING_ASYNC, - ASSET_ENTRY_STATE_PENDING_SYNC, - ASSET_ENTRY_STATE_LOADING_SYNC, - ASSET_ENTRY_STATE_LOADED, - ASSET_ENTRY_STATE_ERROR -} assetentrystate_t; - -typedef struct assetentry_s assetentry_t; - -/** - * A single asset entry callback. Each entry supports at most one subscriber - * per event - a second assignment without clearing the first is a bug. - * - * @param entry The assetentry_t the event fired on. - * @param user The user pointer passed alongside the callback. - */ -typedef void (*assetentrycallback_t)(assetentry_t *entry, void *user); - -struct assetentry_s { - char_t name[ASSET_FILE_NAME_MAX]; - assetloadertype_t type; - assetloaderoutput_t data; - assetentrystate_t state; - ref_t refs; - assetloaderinput_t *input; - assetloaderinput_t inputData; - - /** - * Fired once when loading completes successfully. - * Always invoked on the main thread. - */ - assetentrycallback_t onLoaded; - void *onLoadedUser; - - /** - * Fired once when the entry is disposed/reaped. The asset data is still - * accessible when the callback runs. Always invoked on the main thread. - */ - assetentrycallback_t onUnloaded; - void *onUnloadedUser; - - /** - * Fired once when loading fails. - * Always invoked on the main thread. - */ - assetentrycallback_t onError; - void *onErrorUser; -}; - -/** - * Initializes an asset entry with the given name and type. This does not load - * the asset. - * - * @param entry The asset entry to initialize. - * @param name The name of the asset, used as a key for loading and caching. - * @param type The type of asset this entry represents. - * @param input Data that will be passed to the loader about how it should load. - */ -void assetEntryInit( - assetentry_t *entry, - const char_t *name, - const assetloadertype_t type, - assetloaderinput_t *input -); - -/** - * Locks an asset entry, preventing it from being freed until it is unlocked. - * - * @param entry The asset entry to lock. - */ -void assetEntryLock(assetentry_t *entry); - -/** - * Unlocks an asset entry, allowing it to be freed if there are no more locks. - * - * @param entry The asset entry to unlock. - */ -void assetEntryUnlock(assetentry_t *entry); - -/** - * Starts loading the given asset entry using an assetloading slot. This will - * be called by the asset manager when it deems it's a good time to begin the - * loading of this asset entry. - * - * Currently we return the error but in future this will not be returned. - * - * @param entry The asset entry to start loading. - * @param loading The assetloading slot to use for loading this asset entry. - * @return Any error that occurs during loading. - */ -void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading); - -/** - * Disposes an asset entry, freeing any resources it holds. - * Fires the onUnloaded event before releasing asset data. - * - * @param entry The asset entry to dispose. - * @return Any error that occurs during disposal. - */ -errorret_t assetEntryDispose(assetentry_t *entry); diff --git a/src/dusk/asset/loader/assetloader.c b/src/dusk/asset/loader/assetloader.c deleted file mode 100644 index ad808032..00000000 --- a/src/dusk/asset/loader/assetloader.c +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetloader.h" - -assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = { - [ASSET_LOADER_TYPE_NULL] = { 0 }, - - [ASSET_LOADER_TYPE_MESH] = { - .loadSync = assetMeshLoaderSync, - .loadAsync = assetMeshLoaderAsync, - .dispose = assetMeshDispose - }, - - [ASSET_LOADER_TYPE_MODEL] = { - .loadSync = assetModelLoaderSync, - .loadAsync = assetModelLoaderAsync, - .dispose = assetModelDispose - }, - - [ASSET_LOADER_TYPE_TEXTURE] = { - .loadSync = assetTextureLoaderSync, - .loadAsync = assetTextureLoaderAsync, - .dispose = assetTextureDispose - }, - - [ASSET_LOADER_TYPE_TILESET] = { - .loadSync = assetTilesetLoaderSync, - .loadAsync = assetTilesetLoaderAsync, - .dispose = assetTilesetDispose - }, - - [ASSET_LOADER_TYPE_LOCALE] = { - .loadSync = assetLocaleLoaderSync, - .loadAsync = assetLocaleLoaderAsync, - .dispose = assetLocaleDispose - }, - - [ASSET_LOADER_TYPE_JSON] = { - .loadSync = assetJsonLoaderSync, - .loadAsync = assetJsonLoaderAsync, - .dispose = assetJsonDispose - }, - - [ASSET_LOADER_TYPE_CHUNK] = { - .loadSync = assetChunkLoaderSync, - .loadAsync = assetChunkLoaderAsync, - .dispose = assetChunkDispose - }, - - [ASSET_LOADER_TYPE_WAV] = { - .loadSync = assetWavLoaderSync, - .loadAsync = assetWavLoaderAsync, - .dispose = assetWavDispose - }, - - [ASSET_LOADER_TYPE_MP3] = { - .loadSync = assetMp3LoaderSync, - .loadAsync = assetMp3LoaderAsync, - .dispose = assetMp3Dispose - }, -}; diff --git a/src/dusk/asset/loader/assetloader.h b/src/dusk/asset/loader/assetloader.h deleted file mode 100644 index c18c108f..00000000 --- a/src/dusk/asset/loader/assetloader.h +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/loader/dmf/assetmeshloader.h" -#include "asset/loader/dmf/assetmodelloader.h" -#include "asset/loader/display/assettextureloader.h" -#include "asset/loader/display/assettilesetloader.h" -#include "asset/loader/locale/assetlocaleloader.h" -#include "asset/loader/json/assetjsonloader.h" -#include "asset/loader/chunk/assetchunkloader.h" -#include "asset/loader/wav/assetwavloader.h" -#include "asset/loader/mp3/assetmp3loader.h" - -typedef enum { - ASSET_LOADER_TYPE_NULL, - - ASSET_LOADER_TYPE_MESH, - ASSET_LOADER_TYPE_MODEL, - ASSET_LOADER_TYPE_TEXTURE, - ASSET_LOADER_TYPE_TILESET, - ASSET_LOADER_TYPE_LOCALE, - ASSET_LOADER_TYPE_JSON, - ASSET_LOADER_TYPE_CHUNK, - ASSET_LOADER_TYPE_WAV, - ASSET_LOADER_TYPE_MP3, - - ASSET_LOADER_TYPE_COUNT -} assetloadertype_t; - -typedef union { - assetmeshloaderloading_t mesh; - assetmodelloaderloading_t model; - assettextureloaderloading_t texture; - assettilesetloaderloading_t tileset; - assetlocaleloaderloading_t locale; - assetjsonloaderloading_t json; - assetchunkloaderloading_t chunk; - assetwavloaderloading_t wav; - assetmp3loaderloading_t mp3; -} assetloaderloading_t; - -typedef union { - assetmeshoutput_t mesh; - assetmodeloutput_t model; - assettextureoutput_t texture; - assettilesetoutput_t tileset; - assetlocaleoutput_t locale; - assetjsonoutput_t json; - assetchunkoutput_t chunk; - assetwavoutput_t wav; - assetmp3output_t mp3; -} assetloaderoutput_t; - -typedef union { - assettextureloaderinput_t texture; - assettilesetloaderinput_t tileset; - assetlocaleloaderinput_t locale; - assetjsonloaderinput_t json; - assetchunkloaderinput_t chunk; - assetwavloaderinput_t wav; - assetmp3loaderinput_t mp3; -} assetloaderinput_t; - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -typedef errorret_t (assetloadersynccallback_t)(assetloading_t *loading); -typedef errorret_t (assetloaderasynccallback_t)(assetloading_t *loading); -typedef errorret_t (assetloaderdisposecallback_t)(assetentry_t *entry); - -typedef struct { - assetloadersynccallback_t *loadSync; - assetloaderasynccallback_t *loadAsync; - assetloaderdisposecallback_t *dispose; -} assetloadercallbacks_t; - -extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT]; - -/** - * Shorthand method to both chain an error (against the loader state) and to - * set the asset entry state to error. - * - * @param loading The asset loading slot. - * @param ret The error return value to check and chain if it's an error. - */ -#define assetLoaderErrorChain(loading, _expr) {\ - errorret_t _alec = (_expr); \ - if(errorIsNotOk(_alec)) { \ - (loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \ - errorChain(_alec); \ - } \ -} - -/** - * Shorthand method to both throw an error (against the loader state) and to - * set the asset entry state to error. - * - * @param loading The asset loading slot. - * @param ... Format string and arguments for the error message. - */ -#define assetLoaderErrorThrow(loading, ...) {\ - loading->entry->state = ASSET_ENTRY_STATE_ERROR; \ - errorThrow(__VA_ARGS__); \ -} diff --git a/src/dusk/asset/loader/assetloading.h b/src/dusk/asset/loader/assetloading.h deleted file mode 100644 index bf15937f..00000000 --- a/src/dusk/asset/loader/assetloading.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "assetloader.h" -#include "asset/assetfile.h" -#include "thread/threadmutex.h" - -typedef struct assetentry_s assetentry_t; - -typedef struct assetloading_s { - threadmutex_t mutex; - assetloadertype_t type; - assetentry_t *entry; - assetloaderloading_t loading; -} assetloading_t; - -typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading); - -typedef struct { - assetloadingcallback_t *loadSync; -} assetloadingcallbacks_t; diff --git a/src/dusk/asset/loader/chunk/CMakeLists.txt b/src/dusk/asset/loader/chunk/CMakeLists.txt deleted file mode 100644 index 64e16290..00000000 --- a/src/dusk/asset/loader/chunk/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - assetchunkloader.c -) diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c deleted file mode 100644 index a2325027..00000000 --- a/src/dusk/asset/loader/chunk/assetchunkloader.c +++ /dev/null @@ -1,484 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetchunkloader.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "asset/asset.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" -#include "asset/loader/assetloader.h" -#include "asset/loader/json/assetjsonloader.h" -#include "display/color.h" -#include "display/texture/texture.h" -#include "rpg/overworld/worldpos.h" -#include "yyjson.h" - -#define ASSET_CHUNK_TERRAIN_TEXTURE "tiles.png" - -// Per-shape corner Z offsets (sw, se, ne, nw) as offsets from a tile's own -// base Z, ported 1:1 from tools/asset/chunk/__main__.py's _RAMP_CORNERS. -// TILE_SHAPE_NULL's row is never read - assetChunkTilesHaveTerrain/ -// assetChunkBuildTerrain skip those tiles outright. -static const float_t CHUNK_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = { - [TILE_SHAPE_GROUND] = {0.0f, 0.0f, 0.0f, 0.0f}, - [TILE_SHAPE_RAMP_NORTH] = {0.0f, 0.0f, 1.0f, 1.0f}, - [TILE_SHAPE_RAMP_SOUTH] = {1.0f, 1.0f, 0.0f, 0.0f}, - [TILE_SHAPE_RAMP_EAST] = {0.0f, 1.0f, 1.0f, 0.0f}, - [TILE_SHAPE_RAMP_WEST] = {1.0f, 0.0f, 0.0f, 1.0f}, - [TILE_SHAPE_RAMP_NORTHEAST] = {0.0f, 0.0f, 1.0f, 0.0f}, - [TILE_SHAPE_RAMP_NORTHWEST] = {0.0f, 0.0f, 0.0f, 1.0f}, - [TILE_SHAPE_RAMP_SOUTHEAST] = {0.0f, 1.0f, 0.0f, 0.0f}, - [TILE_SHAPE_RAMP_SOUTHWEST] = {1.0f, 0.0f, 0.0f, 0.0f}, - [TILE_SHAPE_RAMP_NORTHEAST_INNER] = {0.0f, 1.0f, 1.0f, 1.0f}, - [TILE_SHAPE_RAMP_NORTHWEST_INNER] = {1.0f, 0.0f, 1.0f, 1.0f}, - [TILE_SHAPE_RAMP_SOUTHEAST_INNER] = {1.0f, 1.0f, 1.0f, 0.0f}, - [TILE_SHAPE_RAMP_SOUTHWEST_INNER] = {1.0f, 1.0f, 0.0f, 1.0f}, -}; - -errorret_t assetChunkLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Should be called from an async thread."); - errorOk(); -} - -static bool_t assetChunkTilesHaveTerrain(const tile_t *tiles) { - for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) { - if(tiles[i].shape != TILE_SHAPE_NULL) return true; - } - return false; -} - -// Reads the "tiles" array - a dense, fixed-size CHUNK_TILE_COUNT grid, -// one slot per (x, y) column at index x + y * CHUNK_WIDTH (position is -// 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/.json" - a hardcoded stand-in for -// tools/asset/chunk/__main__.py's find_model() recursive search, valid -// as long as every building model lives flat under that one directory -// (true today - see the raw-json-maps plan notes for the tradeoff). -static void assetChunkParseMeshes( - yyjson_val *root, assetchunkoutput_t *out, uint8_t *ioIndex -) { - yyjson_val *meshesArr = yyjson_obj_get(root, "meshes"); - if(!yyjson_is_arr(meshesArr)) return; - - size_t idx, len; - yyjson_val *meshVal; - yyjson_arr_foreach(meshesArr, idx, len, meshVal) { - if(*ioIndex >= CHUNK_MESH_COUNT_MAX) break; - - yyjson_val *fileVal = yyjson_obj_get(meshVal, "file"); - if(!fileVal || !yyjson_is_str(fileVal)) continue; - - char_t baseName[CHUNK_MESH_NAME_MAX]; - stringCopy(baseName, yyjson_get_str(fileVal), sizeof(baseName)); - char_t *dot = stringFindLastChar(baseName, '.'); - if(dot != NULL) *dot = '\0'; - - uint8_t m = *ioIndex; - stringFormat( - out->modelNames[m], CHUNK_MESH_NAME_MAX, - "models/buildings/%s.json", baseName - ); - - vec3 offset = {0.0f, 0.0f, 0.0f}; - yyjson_val *posArr = yyjson_obj_get(meshVal, "pos"); - if(yyjson_is_arr(posArr) && yyjson_arr_size(posArr) >= 3) { - offset[0] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 0)); - offset[1] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 1)); - offset[2] = (float_t)yyjson_get_num(yyjson_arr_get(posArr, 2)); - } - memoryCopy(out->meshOffsets[m], offset, sizeof(vec3)); - - (*ioIndex)++; - } -} - -static void assetChunkParseEntities(yyjson_val *root, assetchunkoutput_t *out) { - out->entitySpawnCount = 0; - - yyjson_val *arr = yyjson_obj_get(root, "entities"); - if(!yyjson_is_arr(arr)) return; - - size_t idx, len; - yyjson_val *val; - yyjson_arr_foreach(arr, idx, len, val) { - if(out->entitySpawnCount >= CHUNK_ENTITY_SPAWN_COUNT_MAX) break; - - const char_t *type = yyjson_get_str(yyjson_obj_get(val, "type")); - yyjson_val *posArr = yyjson_obj_get(val, "pos"); - if(type == NULL || !yyjson_is_arr(posArr) || yyjson_arr_size(posArr) < 3) { - continue; - } - worldpos_t pos = { - .x = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 0)), - .y = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 1)), - .z = (worldunit_t)yyjson_get_int(yyjson_arr_get(posArr, 2)), - }; - - chunkentityspawn_t *spawn = &out->entitySpawns[out->entitySpawnCount]; - if(stringEquals(type, "global")) { - spawn->kind = CHUNK_ENTITY_SPAWN_KIND_GLOBAL; - spawn->globalId = - (uint16_t)yyjson_get_int(yyjson_obj_get(val, "globalId")); - spawn->itemId = 0; - spawn->itemQuantity = 0; - } else if(stringEquals(type, "item")) { - spawn->kind = CHUNK_ENTITY_SPAWN_KIND_ITEM; - spawn->globalId = 0; - spawn->itemId = (uint16_t)yyjson_get_int(yyjson_obj_get(val, "itemId")); - spawn->itemQuantity = - (uint8_t)yyjson_get_int(yyjson_obj_get(val, "quantity")); - } else { - continue; - } - spawn->position = pos; - out->entitySpawnCount++; - } -} - -static void assetChunkParseAreas(yyjson_val *root, assetchunkoutput_t *out) { - out->areaSpawnCount = 0; - - yyjson_val *arr = yyjson_obj_get(root, "areas"); - if(!yyjson_is_arr(arr)) return; - - size_t idx, len; - yyjson_val *val; - yyjson_arr_foreach(arr, idx, len, val) { - if(out->areaSpawnCount >= CHUNK_AREA_COUNT_MAX) break; - - yyjson_val *minArr = yyjson_obj_get(val, "min"); - yyjson_val *maxArr = yyjson_obj_get(val, "max"); - if( - !yyjson_is_arr(minArr) || yyjson_arr_size(minArr) < 3 || - !yyjson_is_arr(maxArr) || yyjson_arr_size(maxArr) < 3 - ) continue; - - chunkareaspawn_t *area = &out->areaSpawns[out->areaSpawnCount]; - area->min = (worldpos_t){ - .x = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 0)), - .y = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 1)), - .z = (worldunit_t)yyjson_get_int(yyjson_arr_get(minArr, 2)), - }; - area->max = (worldpos_t){ - .x = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 0)), - .y = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 1)), - .z = (worldunit_t)yyjson_get_int(yyjson_arr_get(maxArr, 2)), - }; - area->callbackId = - (uint16_t)yyjson_get_int(yyjson_obj_get(val, "callbackId")); - area->notify = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "notify")); - area->trigger = (uint8_t)yyjson_get_int(yyjson_obj_get(val, "trigger")); - out->areaSpawnCount++; - } -} - -// Six vertices (2 CCW triangles) for one tile's quad, matching -// tools/asset/chunk/__main__.py's _tile_quad. Corner order: SW=(x,y), -// SE=(x+1,y), NE=(x+1,y+1), NW=(x,y+1). NORTH=+Y, EAST=+X. -static void assetChunkAppendTileQuad( - meshvertex_t *verts, uint32_t *vertCount, - const uint32_t x, const uint32_t y, - const float_t sw, const float_t se, const float_t ne, const float_t nw -) { - const float_t u0 = (float_t)x / (float_t)CHUNK_WIDTH; - const float_t u1 = (float_t)(x + 1) / (float_t)CHUNK_WIDTH; - const float_t v0 = (float_t)y / (float_t)CHUNK_HEIGHT; - const float_t v1 = (float_t)(y + 1) / (float_t)CHUNK_HEIGHT; - const float_t fx = (float_t)x; - const float_t fy = (float_t)y; - - meshvertex_t *v = verts + *vertCount; - v[0] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} }; - v[1] = (meshvertex_t){ .uv = {u1, v0}, .pos = {fx + 1, fy, se} }; - v[2] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} }; - v[3] = (meshvertex_t){ .uv = {u0, v0}, .pos = {fx, fy, sw} }; - v[4] = (meshvertex_t){ .uv = {u1, v1}, .pos = {fx + 1, fy + 1, ne} }; - v[5] = (meshvertex_t){ .uv = {u0, v1}, .pos = {fx, fy + 1, nw} }; - *vertCount += 6; -} - -// Builds a mesh_t straight from the tile grid - one quad per non-null -// tile column, shaped by CHUNK_RAMP_CORNERS - the runtime equivalent of -// tools/asset/chunk/__main__.py's build_terrain_verts. Only called once -// assetChunkTilesHaveTerrain has confirmed at least one real tile exists. -// Mirrors assetMeshLoaderSync's meshInit/meshFlush/free-after-upload -// sequence (assetmeshloader.c) since this produces the same -// assetmeshoutput_t shape, just from generated verts instead of a file. -static errorret_t assetChunkBuildTerrain( - const tile_t *tiles, assetmeshoutput_t *out -) { - meshvertex_t *verts = memoryAllocate( - CHUNK_TILE_COUNT * 6 * sizeof(meshvertex_t) - ); - uint32_t vertCount = 0; - - for(uint32_t y = 0; y < CHUNK_HEIGHT; y++) { - for(uint32_t x = 0; x < CHUNK_WIDTH; x++) { - const tile_t *tile = &tiles[x + y * CHUNK_WIDTH]; - if(tile->shape == TILE_SHAPE_NULL) continue; - - const float_t *corners = CHUNK_RAMP_CORNERS[tile->shape]; - const float_t fz = (float_t)tile->z; - assetChunkAppendTileQuad( - verts, &vertCount, x, y, - (fz + corners[0]) * WORLD_LAYER_HEIGHT, - (fz + corners[1]) * WORLD_LAYER_HEIGHT, - (fz + corners[2]) * WORLD_LAYER_HEIGHT, - (fz + corners[3]) * WORLD_LAYER_HEIGHT - ); - } - } - - 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 - - errorOk(); -} - -errorret_t assetChunkLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - assetchunkoutput_t *out = &loading->entry->data.chunk; - - switch(loading->loading.chunk.state) { - case ASSET_CHUNK_LOADING_STATE_INITIAL: - loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); - - case ASSET_CHUNK_LOADING_STATE_PARSE: - break; - - case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS: - while(loading->loading.chunk.modelIndex < out->meshCount) { - uint8_t m = loading->loading.chunk.modelIndex; - if(out->modelEntries[m] == NULL) { - out->modelEntries[m] = assetLock( - out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL - ); - assertNotNull(out->modelEntries[m], "Failed to lock model."); - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); - } - if(out->modelEntries[m]->state == ASSET_ENTRY_STATE_ERROR) { - assetLoaderErrorThrow( - loading, "Model failed to load: %s", out->modelNames[m] - ); - } - if(out->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) { - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); - } - loading->loading.chunk.modelIndex++; - } - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); - - default: - errorOk(); - } - - // --- PARSE: lock+require the chunk's own JSON as a sub-asset, exactly - // like assetmodelloader.c's ASSET_MODEL_LOADING_STATE_LOCK_ASSETS does - // for its model descriptor, then read everything out of it before - // unlocking (the yyjson_doc/vals only stay valid while jsonEntry does). - char_t jsonKey[ASSET_FILE_NAME_MAX]; - stringFormat(jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name); - assetloaderinput_t jsonInput; - memoryZero(&jsonInput, sizeof(jsonInput)); - stringCopy( - jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX - ); - assetentry_t *jsonEntry = - assetLock(jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput); - errorret_t jsonRet = assetRequireLoaded(jsonEntry); - if(errorIsNotOk(jsonRet)) { - assetUnlockEntry(jsonEntry); - assetLoaderErrorThrow( - loading, "Failed to load chunk JSON: %s", loading->entry->name - ); - } - - yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json); - - tile_t tiles[CHUNK_TILE_COUNT]; - assetChunkParseTiles(root, tiles); - out->hasTerrain = assetChunkTilesHaveTerrain(tiles); - - uint8_t meshIndex = out->hasTerrain ? 1 : 0; - assetChunkParseMeshes(root, out, &meshIndex); - out->meshCount = meshIndex; - - assetChunkParseEntities(root, out); - assetChunkParseAreas(root, out); - - assetUnlockEntry(jsonEntry); - - 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 - ); - } - - assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA }; - assetentry_t *texEntry = assetLock( - ASSET_CHUNK_TERRAIN_TEXTURE, ASSET_LOADER_TYPE_TEXTURE, &texInput - ); - errorret_t texRet = assetRequireLoaded(texEntry); - if(errorIsNotOk(texRet)) { - assetUnlockEntry(texEntry); - meshDispose(&terrainMesh.mesh); - memoryFree(out->tiles); - out->tiles = NULL; - assetLoaderErrorThrow( - loading, "Failed to load chunk terrain texture: %s", - loading->entry->name - ); - } - out->terrainTexEntry = texEntry; - - out->terrainMeshEntry = memoryAllocate(sizeof(assetentry_t)); - memoryZero(out->terrainMeshEntry, sizeof(assetentry_t)); - out->terrainMeshEntry->type = ASSET_LOADER_TYPE_MESH; - out->terrainMeshEntry->state = ASSET_ENTRY_STATE_LOADED; - out->terrainMeshEntry->data.mesh = terrainMesh; - - out->terrainModelEntry = memoryAllocate(sizeof(assetentry_t)); - memoryZero(out->terrainModelEntry, sizeof(assetentry_t)); - out->terrainModelEntry->type = ASSET_LOADER_TYPE_MODEL; - out->terrainModelEntry->state = ASSET_ENTRY_STATE_LOADED; - out->terrainModelEntry->data.model.meshEntry = out->terrainMeshEntry; - out->terrainModelEntry->data.model.texEntry = out->terrainTexEntry; - out->terrainModelEntry->data.model.color = COLOR_WHITE; - - out->modelEntries[0] = out->terrainModelEntry; - } - - if(out->meshCount == 0) { - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); - } - - loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS; - loading->loading.chunk.modelIndex = out->hasTerrain ? 1 : 0; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetChunkDispose(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - assetchunkoutput_t *out = &entry->data.chunk; - - if(out->tiles != NULL) { - memoryFree(out->tiles); - out->tiles = NULL; - } - - for(uint8_t m = 0; m < out->meshCount; m++) { - if(out->modelEntries[m] == NULL) continue; - if(out->hasTerrain && m == 0) continue; // synthetic - freed below. - assetUnlockEntry(out->modelEntries[m]); - out->modelEntries[m] = NULL; - } - - if(out->hasTerrain) { - if(out->terrainMeshEntry != NULL) { - assetmeshoutput_t *meshOut = &out->terrainMeshEntry->data.mesh; - if(meshOut->meshInitialized) { - meshDispose(&meshOut->mesh); - meshOut->meshInitialized = false; - } - if(meshOut->vertices != NULL) { - memoryFree(meshOut->vertices); - meshOut->vertices = NULL; - } - memoryFree(out->terrainMeshEntry); - out->terrainMeshEntry = NULL; - } - if(out->terrainModelEntry != NULL) { - memoryFree(out->terrainModelEntry); - out->terrainModelEntry = NULL; - } - if(out->terrainTexEntry != NULL) { - assetUnlockEntry(out->terrainTexEntry); - out->terrainTexEntry = NULL; - } - out->modelEntries[0] = NULL; - out->hasTerrain = false; - } - - errorOk(); -} diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h deleted file mode 100644 index 38e76032..00000000 --- a/src/dusk/asset/loader/chunk/assetchunkloader.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" -#include "rpg/overworld/chunk.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -typedef struct { - void *nothing; -} assetchunkloaderinput_t; - -typedef enum { - ASSET_CHUNK_LOADING_STATE_INITIAL, - ASSET_CHUNK_LOADING_STATE_PARSE, - ASSET_CHUNK_LOADING_STATE_LOAD_MODELS, - ASSET_CHUNK_LOADING_STATE_DONE -} assetchunkloadingstate_t; - -typedef struct { - assetchunkloadingstate_t state; - - // Index into assetchunkoutput_t.modelNames/modelEntries the external - // (non-terrain) model polling loop has reached - see - // ASSET_CHUNK_LOADING_STATE_LOAD_MODELS. - uint8_t modelIndex; -} assetchunkloaderloading_t; - -typedef enum { - CHUNK_ENTITY_SPAWN_KIND_GLOBAL, - CHUNK_ENTITY_SPAWN_KIND_ITEM -} chunkentityspawnkind_t; - -typedef struct { - chunkentityspawnkind_t kind; - uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL. - uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM. - uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM. - worldpos_t position; -} chunkentityspawn_t; - -typedef struct { - worldpos_t min; - worldpos_t max; - uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST. - uint8_t notify; - uint8_t trigger; -} chunkareaspawn_t; - -typedef struct { - tile_t *tiles; - uint8_t meshCount; - char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; - vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; - assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX]; - - // Terrain mesh generated at load time from the tile grid (no baked - // .dmf/.json - see assetChunkBuildTerrain in assetchunkloader.c). NULL - // (with hasTerrain false) for a chunk with no non-null tiles, in which - // case modelEntries[0] (if meshCount > 0) is the first external mesh - // instead - matches the old baked pipeline's "only write a terrain - // model if it has vertices" behavior. - // - // terrainMeshEntry/terrainModelEntry are heap-allocated, plain - // assetentry_t values that are never passed to assetLock/assetGetEntry/ - // assetUnlockEntry (only a real, name-keyed, ref-counted entry may be) - // - they exist purely so modelEntries[0] can point at something with - // the same ->state/->data.model/->data.mesh.mesh shape every other - // model/mesh entry has, so chunk.c/map.c/sceneoverworld.c don't need to - // know terrain isn't file-backed. Freed directly (not assetUnlockEntry) - // in assetChunkDispose. terrainTexEntry is a real, ref-counted texture - // entry (the shared "tiles.png") and must be assetUnlockEntry'd there. - bool_t hasTerrain; - assetentry_t *terrainMeshEntry; - assetentry_t *terrainModelEntry; - assetentry_t *terrainTexEntry; - - uint8_t entitySpawnCount; - chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX]; - - uint8_t areaSpawnCount; - chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX]; -} assetchunkoutput_t; - -/** - * Async loader stub for chunk assets. Chunk loading is performed entirely - * on the main thread (it delegates to the JSON asset loader and to - * meshInit/meshFlush, both main-thread-only); this callback is never - * reached. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure of the load operation. - */ -errorret_t assetChunkLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader for chunk assets. Locks the chunk's JSON descriptor - * as a sub-asset, parses tiles/external mesh references/entity spawns/ - * area spawns, generates the terrain mesh directly from the tile grid, - * and locks/waits for every referenced model (and the terrain's shared - * tileset texture) to finish loading. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure of the load operation. - */ -errorret_t assetChunkLoaderSync(assetloading_t *loading); - -/** - * Disposer for chunk assets. - * - * @param entry Asset entry containing the chunk data to dispose. - * @return Error code indicating success or failure of the dispose operation. - */ -errorret_t assetChunkDispose(assetentry_t *entry); diff --git a/src/dusk/asset/loader/display/CMakeLists.txt b/src/dusk/asset/loader/display/CMakeLists.txt deleted file mode 100644 index 562a1298..00000000 --- a/src/dusk/asset/loader/display/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - assettextureloader.c - assettilesetloader.c -) \ No newline at end of file diff --git a/src/dusk/asset/loader/display/assettextureloader.c b/src/dusk/asset/loader/display/assettextureloader.c deleted file mode 100644 index 733b12fb..00000000 --- a/src/dusk/asset/loader/display/assettextureloader.c +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assettextureloader.h" -#include "assert/assert.h" -#define STB_IMAGE_IMPLEMENTATION -#include "stb_image.h" -#include "log/log.h" -#include "util/endian.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = { - .read = assetTextureReader, - .skip = assetTextureSkipper, - .eof = assetTextureEOF -}; - -int assetTextureReader(void *user, char *data, int size) { - assertNotNull(data, "Data buffer for stb_image callbacks cannot be NULL."); - - assetfile_t *file = (assetfile_t*)user; - assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL."); - - errorret_t ret = assetFileRead(file, data, (size_t)size); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - return -1; - } - - return file->lastRead; -} - -void assetTextureSkipper(void *user, int n) { - assetfile_t *file = (assetfile_t*)user; - assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL."); - - errorret_t ret = assetFileRead(file, NULL, (size_t)n); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - } -} - -int assetTextureEOF(void *user) { - assetfile_t *file = (assetfile_t*)user; - assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL."); - - return file->position >= file->size; -} - -errorret_t assetTextureLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Should be called from an async thread."); - - // Only care about loading pixels. - if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){ - errorOk(); - } - - // Init the file - assertNull( - loading->loading.texture.data, "Pixels already defined?" - ); - - assetfile_t *file = &loading->loading.texture.file; - assetLoaderErrorChain(loading, assetFileInit( - file, - loading->entry->name, - NULL, - &loading->entry->data.texture - )); - assetLoaderErrorChain(loading, assetFileOpen(file)); - - // Determine channels - int channelsDesired; - switch(loading->entry->inputData.texture) { - case TEXTURE_FORMAT_RGBA: - channelsDesired = 4; - break; - - default: - assetLoaderErrorThrow(loading, "Bad texture format."); - } - - // Load image pixels. - loading->loading.texture.data = stbi_load_from_callbacks( - &ASSET_TEXTURE_STB_CALLBACKS, - file, - &loading->loading.texture.width, - &loading->loading.texture.height, - &loading->loading.texture.channels, - channelsDesired - ); - - // Close out the file. - assetLoaderErrorChain(loading, assetFileClose(file)); - assetLoaderErrorChain(loading, assetFileDispose(file)); - - // Ensure we loaded correctly. - if(loading->loading.texture.data == NULL) { - const char_t *errorStr = stbi_failure_reason(); - assetLoaderErrorThrow( - loading, "Failed to load texture from file %s.", errorStr - ); - } - - // Fixes a specific bug probably with Dolphin but for now just assuming endian - if(!isHostLittleEndian()) { - stbi__vertical_flip( - loading->loading.texture.data, - loading->loading.texture.width, - loading->loading.texture.height, - loading->loading.texture.channels - ); - } - - loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetTextureLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.texture.state) { - case ASSET_TEXTURE_LOADING_STATE_INITIAL: - loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE: - break; - - default: - errorOk(); - } - - // Create the texture. - assertNotNull( - loading->loading.texture.data, "Pixels should have been loaded by now." - ); - - assetLoaderErrorChain(loading, textureInit( - (texture_t*)&loading->entry->data.texture, - loading->loading.texture.width, - loading->loading.texture.height, - loading->entry->inputData.texture, - (texturedata_t){ - .rgbaColors = (color_t*)loading->loading.texture.data - } - )); - - // Free the pixels. - stbi_image_free(loading->loading.texture.data); - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetTextureDispose(assetentry_t *entry) { - assertNotNull(entry, "Asset entry cannot be NULL"); - assertIsMainThread("Must be called from the main thread."); - - return textureDispose(&entry->data.texture); -} \ No newline at end of file diff --git a/src/dusk/asset/loader/display/assettextureloader.h b/src/dusk/asset/loader/display/assettextureloader.h deleted file mode 100644 index b29ac5dc..00000000 --- a/src/dusk/asset/loader/display/assettextureloader.h +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" -#include "display/texture/texture.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; -typedef textureformat_t assettextureloaderinput_t; - -typedef enum { - ASSET_TEXTURE_LOADING_STATE_INITIAL, - ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS, - ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE, - ASSET_TEXTURE_LOADING_STATE_DONE -} assettextureloadingstate_t; - -typedef struct { - assetfile_t file; - assettextureloadingstate_t state; - - int channels, width, height; - uint8_t *data; -} assettextureloaderloading_t; - -typedef texture_t assettextureoutput_t; - -/** - * STB image read callback for asset files. - * - * @param user User data passed to the callback, should be an assetfile_t*. - * @param data Buffer to read the file data into. - * @param size Size of the buffer to read into. - * @return Number of bytes read, or -1 on error. - */ -int assetTextureReader(void *user, char *data, int size); - -/** - * STB image skip callback for asset files. - * - * @param user User data passed to the callback, should be an assetfile_t*. - * @param n Number of bytes to skip in the file. - */ -void assetTextureSkipper(void *user, int n); - -/** - * STB image EOF callback for asset files. - * - * @param user User data passed to the callback, should be an assetfile_t*. - * @return Non-zero if end of file has been reached, zero otherwise. - */ -int assetTextureEOF(void *user); - -/** - * Synchronous loader for texture assets. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure of the load operation. - */ -errorret_t assetTextureLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader for texture assets. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure of the load operation. - */ -errorret_t assetTextureLoaderSync(assetloading_t *loading); - -/** - * Disposer for texture assets. - * - * @param entry Asset entry containing the texture to dispose. - * @return Error code indicating success or failure of the dispose operation. - */ -errorret_t assetTextureDispose(assetentry_t *entry); \ No newline at end of file diff --git a/src/dusk/asset/loader/display/assettilesetloader.c b/src/dusk/asset/loader/display/assettilesetloader.c deleted file mode 100644 index 3e9cba92..00000000 --- a/src/dusk/asset/loader/display/assettilesetloader.c +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assettilesetloader.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/endian.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -errorret_t assetTilesetLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Should be called from an async thread."); - - if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) { - errorOk(); - } - - assertNull(loading->loading.tileset.data, "Data already defined?"); - - assetfile_t *file = &loading->loading.tileset.file; - assetLoaderErrorChain(loading, - assetFileInit(file, loading->entry->name, NULL, NULL) - ); - - uint8_t *data = memoryAllocate(file->size); - assetLoaderErrorChain(loading, assetFileOpen(file)); - assetLoaderErrorChain(loading, assetFileRead(file, data, file->size)); - assetLoaderErrorChain(loading, assetFileClose(file)); - assetLoaderErrorChain(loading, assetFileDispose(file)); - assertTrue( - file->lastRead == file->size, - "Failed to read entire tileset file." - ); - - loading->loading.tileset.data = data; - loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetTilesetLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.tileset.state) { - case ASSET_TILESET_LOADING_STATE_INITIAL: - loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_READ_FILE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_TILESET_LOADING_STATE_PARSE: - break; - - default: - errorOk(); - } - - uint8_t *data = loading->loading.tileset.data; - assertNotNull(data, "Tileset data should have been loaded by now."); - - tileset_t *out = &loading->entry->data.tileset; - - if(data[0] != 'D' || data[1] != 'T' || data[2] != 'F') { - memoryFree(data); - assetLoaderErrorThrow(loading, "Invalid tileset header"); - } - - if(data[3] != 0x00) { - memoryFree(data); - assetLoaderErrorThrow(loading, "Unsupported tileset version"); - } - - out->tileWidth = endianLittleToHost16(*(uint16_t *)(data + 4)); - out->tileHeight = endianLittleToHost16(*(uint16_t *)(data + 6)); - out->columns = endianLittleToHost16(*(uint16_t *)(data + 8)); - out->rows = endianLittleToHost16(*(uint16_t *)(data + 10)); - - if(out->tileWidth == 0) { - memoryFree(data); - assetLoaderErrorThrow(loading, "Tile width cannot be 0"); - } - if(out->tileHeight == 0) { - memoryFree(data); - assetLoaderErrorThrow(loading, "Tile height cannot be 0"); - } - if(out->columns == 0) { - memoryFree(data); - assetLoaderErrorThrow(loading, "Column count cannot be 0"); - } - if(out->rows == 0) { - memoryFree(data); - assetLoaderErrorThrow(loading, "Row count cannot be 0"); - } - - out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16)); - out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20)); - - if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) { - memoryFree(data); - assetLoaderErrorThrow(loading, "Invalid v0 value in tileset"); - } - - out->tileCount = out->columns * out->rows; - memoryFree(data); - loading->loading.tileset.data = NULL; - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetTilesetDispose(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - errorOk(); -} diff --git a/src/dusk/asset/loader/display/assettilesetloader.h b/src/dusk/asset/loader/display/assettilesetloader.h deleted file mode 100644 index 646b4064..00000000 --- a/src/dusk/asset/loader/display/assettilesetloader.h +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" -#include "display/texture/tileset.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -typedef struct { - void *nothing; -} assettilesetloaderinput_t; - -typedef enum { - ASSET_TILESET_LOADING_STATE_INITIAL, - ASSET_TILESET_LOADING_STATE_READ_FILE, - ASSET_TILESET_LOADING_STATE_PARSE, - ASSET_TILESET_LOADING_STATE_DONE -} assettilesetloadingstate_t; - -typedef struct { - assetfile_t file; - assettilesetloadingstate_t state; - - uint8_t *data; -} assettilesetloaderloading_t; - -typedef tileset_t assettilesetoutput_t; - -/** - * Asynchronous loader for tileset assets. Reads the raw DTF file bytes into - * the loading buffer so the sync phase can parse without blocking the main - * thread on I/O. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure of the load operation. - */ -errorret_t assetTilesetLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader for tileset assets. Parses the DTF binary previously - * read by the async phase and populates the output tileset_t. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure of the load operation. - */ -errorret_t assetTilesetLoaderSync(assetloading_t *loading); - -/** - * Disposer for tileset assets. - * - * @param entry Asset entry containing the tileset to dispose. - * @return Error code indicating success or failure of the dispose operation. - */ -errorret_t assetTilesetDispose(assetentry_t *entry); diff --git a/src/dusk/asset/loader/dmf/CMakeLists.txt b/src/dusk/asset/loader/dmf/CMakeLists.txt deleted file mode 100644 index 7210daa0..00000000 --- a/src/dusk/asset/loader/dmf/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - assetmeshloader.c - assetmodelloader.c -) diff --git a/src/dusk/asset/loader/dmf/assetmeshloader.c b/src/dusk/asset/loader/dmf/assetmeshloader.c deleted file mode 100644 index de40a622..00000000 --- a/src/dusk/asset/loader/dmf/assetmeshloader.c +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetmeshloader.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/endian.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -errorret_t assetMeshLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Should be called from an async thread."); - - if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) { - errorOk(); - } - - assertNull(loading->loading.mesh.data, "Data already defined?"); - - assetfile_t *file = &loading->loading.mesh.file; - assetLoaderErrorChain(loading, - assetFileInit(file, loading->entry->name, NULL, NULL) - ); - - uint8_t *raw = memoryAllocate(file->size); - assetLoaderErrorChain(loading, assetFileOpen(file)); - assetLoaderErrorChain(loading, assetFileRead(file, raw, file->size)); - assetLoaderErrorChain(loading, assetFileClose(file)); - assetLoaderErrorChain(loading, assetFileDispose(file)); - assertTrue(file->lastRead == file->size, "Failed to read entire DMF file."); - - if(raw[0] != 'D' || raw[1] != 'M' || raw[2] != 'F') { - memoryFree(raw); - assetLoaderErrorThrow(loading, "Invalid DMF file header"); - } - - uint32_t version = endianLittleToHost32(*(uint32_t *)(raw + 4)); - if(version != ASSET_MESH_FILE_VERSION) { - memoryFree(raw); - assetLoaderErrorThrow(loading, "Unsupported DMF version %u", version); - } - - uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8)); - meshvertex_t *vertices = NULL; - if(vertCount > 0) { - // 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin - // require this for the DMA'd vertex data to actually reach the GPU - // coherently. Static compiled-in vertex arrays happen to get this from - // the linker; a plain memoryAllocate here would not. - vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t)); - memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t)); - - for(uint32_t v = 0; v < vertCount; v++) { - vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]); - vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]); - vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]); - vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]); - vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]); - } - } - memoryFree(raw); - - loading->loading.mesh.vertCount = vertCount; - loading->loading.mesh.data = (uint8_t *)vertices; - loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_CREATE_MESH; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetMeshLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_MESH, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.mesh.state) { - case ASSET_MESH_LOADING_STATE_INITIAL: - loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_READ_FILE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_MESH_LOADING_STATE_CREATE_MESH: - break; - - default: - errorOk(); - } - - uint32_t vertCount = loading->loading.mesh.vertCount; - meshvertex_t *vertices = (meshvertex_t *)loading->loading.mesh.data; - loading->loading.mesh.data = NULL; - - assetmeshoutput_t *out = &loading->entry->data.mesh; - out->vertices = vertices; - - if(vertCount == 0) { - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); - } - - errorret_t ret = meshInit( - &out->mesh, - MESH_PRIMITIVE_TYPE_TRIANGLES, - (int32_t)vertCount, - out->vertices - ); - if(errorIsNotOk(ret)) { - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - memoryFree(out->vertices); - out->vertices = NULL; - errorChain(ret); - } - out->meshInitialized = true; - - ret = meshFlush(&out->mesh, 0, (int32_t)vertCount); - if(errorIsNotOk(ret)) { - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - meshDispose(&out->mesh); - out->meshInitialized = false; - memoryFree(out->vertices); - out->vertices = NULL; - errorChain(ret); - } - - #if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY) - // VBO owns the data now; CPU copy is no longer needed. The platform - // mesh object itself still needs meshDispose later - tracked via - // out->meshInitialized, independent of the CPU buffer's lifetime. - memoryFree(out->vertices); - out->vertices = NULL; - #endif - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetMeshDispose(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - assetmeshoutput_t *out = &entry->data.mesh; - if(out->meshInitialized) { - errorChain(meshDispose(&out->mesh)); - out->meshInitialized = false; - } - if(out->vertices != NULL) { - memoryFree(out->vertices); - out->vertices = NULL; - } - errorOk(); -} diff --git a/src/dusk/asset/loader/dmf/assetmeshloader.h b/src/dusk/asset/loader/dmf/assetmeshloader.h deleted file mode 100644 index ebe66cdd..00000000 --- a/src/dusk/asset/loader/dmf/assetmeshloader.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" -#include "display/mesh/mesh.h" - -#define ASSET_MESH_FILE_VERSION 1 - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -typedef enum { - ASSET_MESH_LOADING_STATE_INITIAL, - ASSET_MESH_LOADING_STATE_READ_FILE, - ASSET_MESH_LOADING_STATE_CREATE_MESH, - ASSET_MESH_LOADING_STATE_DONE -} assetmeshloadingstate_t; - -typedef struct { - assetfile_t file; - assetmeshloadingstate_t state; - uint32_t vertCount; - uint8_t *data; -} assetmeshloaderloading_t; - -typedef struct { - mesh_t mesh; - bool_t meshInitialized; - meshvertex_t *vertices; -} assetmeshoutput_t; - -/** - * Asynchronous loader for DMF mesh assets. Reads the file, validates the - * header, and prepares a ready-to-use vertex buffer so the sync phase only - * needs to upload to the GPU. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure. - */ -errorret_t assetMeshLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader for DMF mesh assets. Initializes the mesh from the - * vertex buffer prepared by the async phase and flushes it to the GPU. - * - * @param loading Loading information for the asset being loaded. - * @return Error code indicating success or failure. - */ -errorret_t assetMeshLoaderSync(assetloading_t *loading); - -/** - * Disposer for DMF mesh assets. Disposes the mesh and frees the vertex - * buffer. - * - * @param entry Asset entry containing the mesh data to dispose. - * @return Error code indicating success or failure. - */ -errorret_t assetMeshDispose(assetentry_t *entry); diff --git a/src/dusk/asset/loader/dmf/assetmodelloader.c b/src/dusk/asset/loader/dmf/assetmodelloader.c deleted file mode 100644 index 64bdd826..00000000 --- a/src/dusk/asset/loader/dmf/assetmodelloader.c +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetmodelloader.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "asset/asset.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" -#include "asset/loader/json/assetjsonloader.h" -#include "display/texture/texture.h" -#include "yyjson.h" - -errorret_t assetModelLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Should be called from an async thread."); - errorOk(); -} - -errorret_t assetModelLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_MODEL, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.model.state) { - case ASSET_MODEL_LOADING_STATE_INITIAL: - loading->loading.model.state = ASSET_MODEL_LOADING_STATE_LOCK_ASSETS; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); - break; - - case ASSET_MODEL_LOADING_STATE_LOCK_ASSETS: { - // Lock the model's JSON descriptor as a JSON sub-asset. The entry - // key is prefixed with "json:" to avoid a type-collision with the - // model entry itself (both share the same filename). The JSON loader - // reads the real file path supplied in the input. - char_t jsonKey[ASSET_FILE_NAME_MAX]; - stringFormat( - jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name - ); - assetloaderinput_t jsonInput; - memoryZero(&jsonInput, sizeof(jsonInput)); - stringCopy( - jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX - ); - assetentry_t *jsonEntry = assetLock( - jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput - ); - errorret_t ret = assetRequireLoaded(jsonEntry); - if(errorIsNotOk(ret)) { - assetUnlockEntry(jsonEntry); - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorChain(ret); - } - - yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json); - - // Parse required mesh path. - yyjson_val *meshVal = yyjson_obj_get(root, "mesh"); - if(!meshVal || !yyjson_is_str(meshVal)) { - assetUnlockEntry(jsonEntry); - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorThrow("Model JSON missing 'mesh' string"); - } - const char *meshStr = yyjson_get_str(meshVal); - size_t meshLen = yyjson_get_len(meshVal); - if(meshLen >= ASSET_FILE_NAME_MAX) { - assetUnlockEntry(jsonEntry); - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorThrow("Model mesh path exceeds max length"); - } - char_t meshName[ASSET_FILE_NAME_MAX]; - memoryCopy(meshName, meshStr, meshLen + 1); - - // Parse optional texture path (absent = color only, no texture). - char_t textureName[ASSET_FILE_NAME_MAX]; - textureName[0] = '\0'; - yyjson_val *texVal = yyjson_obj_get(root, "texture"); - if(texVal && yyjson_is_str(texVal)) { - const char *texStr = yyjson_get_str(texVal); - size_t texLen = yyjson_get_len(texVal); - if(texLen >= ASSET_FILE_NAME_MAX) { - assetUnlockEntry(jsonEntry); - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorThrow("Model texture path exceeds max length"); - } - memoryCopy(textureName, texStr, texLen + 1); - } - - // Parse optional color (RGBA array 0-255, default white). - color_t color = COLOR_WHITE; - yyjson_val *colorVal = yyjson_obj_get(root, "color"); - if(colorVal && yyjson_is_arr(colorVal)) { - uint8_t ch[4] = {255, 255, 255, 255}; - size_t colorIdx, colorLen; - yyjson_val *colorElem; - yyjson_arr_foreach(colorVal, colorIdx, colorLen, colorElem) { - if(colorIdx >= 4) break; - if(yyjson_is_int(colorElem)) { - ch[colorIdx] = (uint8_t)yyjson_get_int(colorElem); - } - } - color.r = ch[0]; - color.g = ch[1]; - color.b = ch[2]; - color.a = ch[3]; - } - - // Release JSON entry; all needed fields are now in local variables. - assetUnlockEntry(jsonEntry); - - // Lock and load the mesh sub-asset. - assetentry_t *meshEntry = - assetLock(meshName, ASSET_LOADER_TYPE_MESH, NULL); - ret = assetRequireLoaded(meshEntry); - if(errorIsNotOk(ret)) { - assetUnlockEntry(meshEntry); - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorChain(ret); - } - - // Lock and load the optional texture sub-asset. - assetentry_t *texEntry = NULL; - if(textureName[0] != '\0') { - assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA }; - texEntry = assetLock(textureName, ASSET_LOADER_TYPE_TEXTURE, &texInput); - ret = assetRequireLoaded(texEntry); - if(errorIsNotOk(ret)) { - assetUnlockEntry(texEntry); - assetUnlockEntry(meshEntry); - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorChain(ret); - } - } - - assetmodeloutput_t *out = &loading->entry->data.model; - out->meshEntry = meshEntry; - out->texEntry = texEntry; - out->color = color; - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); - break; - } - - default: - errorOk(); - } - - errorOk(); -} - -errorret_t assetModelDispose(assetentry_t *entry) { - assertNotNull(entry, "Entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_MODEL, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - assetmodeloutput_t *out = &entry->data.model; - if(out->meshEntry != NULL) { - assetUnlockEntry(out->meshEntry); - out->meshEntry = NULL; - } - if(out->texEntry != NULL) { - assetUnlockEntry(out->texEntry); - out->texEntry = NULL; - } - errorOk(); -} diff --git a/src/dusk/asset/loader/dmf/assetmodelloader.h b/src/dusk/asset/loader/dmf/assetmodelloader.h deleted file mode 100644 index 555407c6..00000000 --- a/src/dusk/asset/loader/dmf/assetmodelloader.h +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/color.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -typedef enum { - ASSET_MODEL_LOADING_STATE_INITIAL, - ASSET_MODEL_LOADING_STATE_LOCK_ASSETS -} assetmodelloadingstate_t; - -typedef struct { - assetmodelloadingstate_t state; -} assetmodelloaderloading_t; - -typedef struct { - assetentry_t *meshEntry; - assetentry_t *texEntry; - color_t color; -} assetmodeloutput_t; - -/** - * Async loader stub for model assets. Model loading is performed entirely - * on the main thread; this callback is never reached. - * - * @param loading Loading information for the asset being loaded. - * @returns Always success. - */ -errorret_t assetModelLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader for model assets. Locks the JSON descriptor file as - * a sub-asset, waits for it to load, parses the mesh path, optional - * texture path, and color tint, then releases the JSON entry. Locks and - * waits for mesh and optional texture sub-assets sequentially to stay - * within ASSET_LOADING_COUNT_MAX slot limits. - * - * @param loading Loading information for the asset being loaded. - * @returns Error code indicating success or failure. - */ -errorret_t assetModelLoaderSync(assetloading_t *loading); - -/** - * Disposer for model assets. Releases the locks on the mesh and texture - * sub-asset entries. - * - * @param entry Asset entry containing the model to dispose. - * @returns Error code indicating success or failure. - */ -errorret_t assetModelDispose(assetentry_t *entry); diff --git a/src/dusk/asset/loader/json/CMakeLists.txt b/src/dusk/asset/loader/json/CMakeLists.txt deleted file mode 100644 index 7215ecbf..00000000 --- a/src/dusk/asset/loader/json/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - assetjsonloader.c -) \ No newline at end of file diff --git a/src/dusk/asset/loader/json/assetjsonloader.c b/src/dusk/asset/loader/json/assetjsonloader.c deleted file mode 100644 index c8505cca..00000000 --- a/src/dusk/asset/loader/json/assetjsonloader.c +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetjsonloader.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -errorret_t assetJsonLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Async loader should not be on main thread."); - - if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) { - errorOk(); - } - - assertNull(loading->loading.json.buffer, "Buffer already defined?"); - - assetfile_t *file = &loading->loading.json.file; - const char_t *filePath = ( - loading->entry->input != NULL && - loading->entry->input->json.path[0] != '\0' - ) ? loading->entry->input->json.path : loading->entry->name; - assetLoaderErrorChain(loading, assetFileInit(file, filePath, NULL, NULL)); - - if(file->size > ASSET_JSON_FILE_SIZE_MAX) { - assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size"); - } - - size_t fileSize = (size_t)file->size; - uint8_t *buffer = memoryAllocate(fileSize); - assetLoaderErrorChain(loading, assetFileOpen(file)); - assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize)); - assertTrue(file->lastRead == file->size, "Failed to read entire JSON file."); - assetLoaderErrorChain(loading, assetFileClose(file)); - assetLoaderErrorChain(loading, assetFileDispose(file)); - - loading->loading.json.buffer = buffer; - loading->loading.json.size = fileSize; - loading->loading.json.state = ASSET_JSON_LOADING_STATE_PARSE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetJsonLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.json.state) { - case ASSET_JSON_LOADING_STATE_INITIAL: - loading->loading.json.state = ASSET_JSON_LOADING_STATE_READ_FILE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_JSON_LOADING_STATE_PARSE: - break; - - default: - errorOk(); - } - - uint8_t *buffer = loading->loading.json.buffer; - assertNotNull(buffer, "JSON buffer should have been loaded by now."); - - loading->entry->data.json = yyjson_read( - (char *)buffer, - loading->loading.json.size, - YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS - ); - memoryFree(buffer); - loading->loading.json.buffer = NULL; - - if(!loading->entry->data.json) { - assetLoaderErrorThrow(loading, "Failed to parse JSON"); - } - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetJsonDispose(assetentry_t *entry) { - assertNotNull(entry, "Asset entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - yyjson_doc_free(entry->data.json); - entry->data.json = NULL; - - errorOk(); -} diff --git a/src/dusk/asset/loader/json/assetjsonloader.h b/src/dusk/asset/loader/json/assetjsonloader.h deleted file mode 100644 index e3c2d251..00000000 --- a/src/dusk/asset/loader/json/assetjsonloader.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" -#include "yyjson.h" - -#define ASSET_JSON_FILE_SIZE_MAX 1024*256 - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -/** - * Optional file path override. When path[0] is non-zero the JSON loader - * reads this path from the archive instead of loading->entry->name. Use - * this to decouple the asset pool key from the actual file location (e.g. - * when a parent loader locks a JSON entry under a synthetic key to avoid - * a type-collision with an entry of the same name but different type). - */ -typedef struct { - char_t path[ASSET_FILE_NAME_MAX]; -} assetjsonloaderinput_t; - -typedef enum { - ASSET_JSON_LOADING_STATE_INITIAL, - ASSET_JSON_LOADING_STATE_READ_FILE, - ASSET_JSON_LOADING_STATE_PARSE, - ASSET_JSON_LOADING_STATE_DONE -} assetjsonloadingstate_t; - -typedef struct { - assetfile_t file; - assetjsonloadingstate_t state; - uint8_t *buffer; - size_t size; -} assetjsonloaderloading_t; - -typedef yyjson_doc * assetjsonoutput_t; - -errorret_t assetJsonLoaderAsync(assetloading_t *loading); -errorret_t assetJsonLoaderSync(assetloading_t *loading); -errorret_t assetJsonDispose(assetentry_t *entry); diff --git a/src/dusk/asset/loader/locale/CMakeLists.txt b/src/dusk/asset/loader/locale/CMakeLists.txt deleted file mode 100644 index 256a5142..00000000 --- a/src/dusk/asset/loader/locale/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - assetlocaleloader.c -) \ No newline at end of file diff --git a/src/dusk/asset/loader/locale/assetlocaleloader.c b/src/dusk/asset/loader/locale/assetlocaleloader.c deleted file mode 100644 index 839e55b8..00000000 --- a/src/dusk/asset/loader/locale/assetlocaleloader.c +++ /dev/null @@ -1,996 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetlocaleloader.h" -#include "util/memory.h" -#include "util/math.h" -#include "util/string.h" -#include "assert/assert.h" - -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -errorret_t assetLocaleLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Async loader should not be on main thread."); - - if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) { - errorOk(); - } - - assetlocalefile_t *localeFile = &loading->entry->data.locale; - memoryZero(localeFile, sizeof(assetlocalefile_t)); - assetLoaderErrorChain(loading, assetFileInit( - &localeFile->file, loading->entry->name, NULL, NULL - )); - assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file)); - - char_t buffer[1024]; - assetLoaderErrorChain(loading, assetLocaleGetHeader( - localeFile, buffer, sizeof(buffer) - )); - assetLoaderErrorChain(loading, assetLocaleParseHeader( - localeFile, buffer, sizeof(buffer) - )); - - loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_DONE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetLocaleLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.locale.state) { - case ASSET_LOCALE_LOADER_STATE_INITIAL: - loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_LOAD_HEADER; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_LOCALE_LOADER_STATE_DONE: - break; - - default: - errorOk(); - } - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetLocaleDispose(assetentry_t *entry) { - assertNotNull(entry, "Asset entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - assetlocalefile_t *localeFile = &entry->data.locale; - - if(localeFile->cache != NULL) { - memoryFree(localeFile->cache); - localeFile->cache = NULL; - } - - errorChain(assetFileClose(&localeFile->file)); - return assetFileDispose(&localeFile->file); -} - -// These functions probably need some cleaning; -errorret_t assetLocaleParseHeader( - assetlocalefile_t *localeFile, - char_t *headerBuffer, - const size_t headerBufferSize -) { - assertNotNull(localeFile, "Locale file cannot be NULL."); - assertNotNull(headerBuffer, "Header buffer cannot be NULL."); - assertTrue(headerBufferSize > 0, "Header buffer size must be > 0."); - - // Find "Plural-Forms: " line and parse out plural form info - char_t *pluralFormsLine = strstr(headerBuffer, "Plural-Forms:"); - if(!pluralFormsLine) { - errorOk(); - } - - pluralFormsLine += strlen("Plural-Forms:"); - - // Expect nplurals - char_t *npluralsStr = strstr(pluralFormsLine, "nplurals="); - if(!npluralsStr) { - errorThrow("Failed to find nplurals in Plural-Forms header."); - } - npluralsStr += strlen("nplurals="); - localeFile->pluralStateCount = (uint8_t)atoi(npluralsStr); - - if(localeFile->pluralStateCount == 0) { - errorThrow("nplurals must be greater than 0."); - } - if(localeFile->pluralStateCount > ASSET_LOCALE_FILE_PLURAL_FORM_COUNT) { - errorThrow( - "nplurals exceeds maximum supported plural forms: %d > %d", - localeFile->pluralStateCount, - ASSET_LOCALE_FILE_PLURAL_FORM_COUNT - ); - } - - // Expect plural= - char_t *pluralStr = strstr(pluralFormsLine, "plural="); - if(!pluralStr) { - errorThrow("Failed to find plural in Plural-Forms header."); - } - pluralStr += strlen("plural="); - - // Expect ( [expressions] ) - char_t *openParen = strchr(pluralStr, '('); - char_t *closeParen = strrchr(pluralStr, ')'); - if(!openParen || !closeParen || closeParen < openParen) { - errorThrow("Failed to find plural expression in Plural-Forms header."); - } - - // Parse: - // n [op] value ? index : n [op] value ? index : ... : final_index - char_t *ptr = openParen + 1; - uint8_t pluralIndex = 0; - uint8_t definedCount = 0; - - while(1) { - while(*ptr == ' ') ptr++; - - // Allow grouped subexpressions like: - // (n<7 ? 2 : 3) - // or - // (((3))) - uint8_t parenDepth = 0; - while(*ptr == '(') { - parenDepth++; - ptr++; - while(*ptr == ' ') ptr++; - } - - // Final fallback: just an integer - if(*ptr != 'n') { - char_t *endPtr = NULL; - int32_t finalIndex = (int32_t)strtol(ptr, &endPtr, 10); - if(endPtr == ptr) { - errorThrow("Expected final plural index."); - } - ptr = endPtr; - - while(*ptr == ' ') ptr++; - while(parenDepth > 0) { - if(*ptr != ')') { - errorThrow("Expected ')' after final plural index."); - } - ptr++; - parenDepth--; - - while(*ptr == ' ') ptr++; - } - - if(*ptr != ')') { - errorThrow("Expected ')' at end of plural expression."); - } - - if(finalIndex < 0 || finalIndex >= localeFile->pluralStateCount) { - errorThrow( - "Final plural expression index out of bounds: %d (nplurals: %d)", - finalIndex, - localeFile->pluralStateCount - ); - } - - localeFile->pluralDefaultIndex = (uint8_t)finalIndex; - definedCount++; - break; - } - - if(pluralIndex >= localeFile->pluralStateCount - 1) { - errorThrow( - "Too many plural conditions. Expected %d clauses for nplurals=%d.", - localeFile->pluralStateCount - 1, - localeFile->pluralStateCount - ); - } - - ptr++; // skip 'n' - - while(*ptr == ' ') ptr++; - - // Determine operator - assetlocalepluraloperation_t op; - if(strncmp(ptr, "==", 2) == 0) { - op = ASSET_LOCALE_PLURAL_OP_EQUAL; - ptr += 2; - } else if(strncmp(ptr, "!=", 2) == 0) { - op = ASSET_LOCALE_PLURAL_OP_NOT_EQUAL; - ptr += 2; - } else if(strncmp(ptr, "<=", 2) == 0) { - op = ASSET_LOCALE_PLURAL_OP_LESS_EQUAL; - ptr += 2; - } else if(strncmp(ptr, ">=", 2) == 0) { - op = ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL; - ptr += 2; - } else if(*ptr == '<') { - op = ASSET_LOCALE_PLURAL_OP_LESS; - ptr++; - } else if(*ptr == '>') { - op = ASSET_LOCALE_PLURAL_OP_GREATER; - ptr++; - } else { - errorThrow("Unsupported plural operator."); - } - - while(*ptr == ' ') ptr++; - - // Parse the comparitor value - char_t *endPtr = NULL; - int32_t value = (int32_t)strtol(ptr, &endPtr, 10); - if(endPtr == ptr) { - errorThrow("Expected value for plural expression."); - } - ptr = endPtr; - - while(*ptr == ' ') ptr++; - - // Parse ternary operator - if(*ptr != '?') { - errorThrow("Expected '?' after plural expression."); - } - ptr++; - - while(*ptr == ' ') ptr++; - - // Parse the indice - endPtr = NULL; - int32_t index = (int32_t)strtol(ptr, &endPtr, 10); - if(endPtr == ptr) { - errorThrow("Expected index for plural expression."); - } - ptr = endPtr; - - if(index < 0 || index >= localeFile->pluralStateCount) { - errorThrow( - "Plural expression index out of bounds: %d (nplurals: %d)", - index, - localeFile->pluralStateCount - ); - } - - // Store plural expression. - localeFile->pluralIndices[pluralIndex] = (uint8_t)index; - localeFile->pluralOps[pluralIndex] = op; - localeFile->pluralValues[pluralIndex] = value; - pluralIndex++; - definedCount++; - - while(*ptr == ' ') ptr++; - - // Close any grouping parens that wrapped this conditional branch - while(parenDepth > 0) { - if(*ptr != ')') { - break; - } - ptr++; - parenDepth--; - - while(*ptr == ' ') ptr++; - } - - if(*ptr != ':') { - errorThrow("Expected ':' after plural expression."); - } - ptr++; - } - - // Must define exactly nplurals outcomes: - // (nplurals - 1) conditional results + 1 final fallback result - if( - pluralIndex != localeFile->pluralStateCount - 1 || - definedCount != localeFile->pluralStateCount - ) { - errorThrow("Plural expression count does not match nplurals."); - } - - errorOk(); -} - - -uint8_t assetLocaleEvaluatePlural( - assetlocalefile_t *file, - const int32_t pluralCount -) { - assertNotNull(file, "Locale file cannot be NULL."); - assertTrue(pluralCount >= 0, "Plural count cannot be negative."); - - for(uint8_t i = 0; i < file->pluralStateCount - 1; i++) { - int32_t value = file->pluralValues[i]; - switch(file->pluralOps[i]) { - case ASSET_LOCALE_PLURAL_OP_EQUAL: - if(pluralCount == value) return file->pluralIndices[i]; - break; - - case ASSET_LOCALE_PLURAL_OP_NOT_EQUAL: - if(pluralCount != value) return file->pluralIndices[i]; - break; - - case ASSET_LOCALE_PLURAL_OP_LESS: - if(pluralCount < value) return file->pluralIndices[i]; - break; - - case ASSET_LOCALE_PLURAL_OP_LESS_EQUAL: - if(pluralCount <= value) return file->pluralIndices[i]; - break; - - case ASSET_LOCALE_PLURAL_OP_GREATER: - if(pluralCount > value) return file->pluralIndices[i]; - break; - - case ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL: - if(pluralCount >= value) return file->pluralIndices[i]; - break; - } - } - - return file->pluralDefaultIndex; -} - -errorret_t assetLocaleLineSkipBlanks( - assetfilelinereader_t *reader, - uint8_t *lineBuffer -) { - while(!reader->eof) { - // Skip blank lines - if(lineBuffer[0] == '\0') { - errorret_t r = assetFileLineReaderNext(reader); - if(errorIsNotOk(r)) { - errorCatch(r); - break; - } - continue; - } - - // Skip comment lines - if(lineBuffer[0] == '#') { - errorret_t r = assetFileLineReaderNext(reader); - if(errorIsNotOk(r)) { - errorCatch(r); - break; - } - continue; - } - - // Is line only spaces? - size_t lineLength = strlen((char_t *)lineBuffer); - size_t i; - bool_t onlySpaces = true; - for(i = 0; i < lineLength; i++) { - if(lineBuffer[i] != ' ') { - onlySpaces = false; - break; - } - } - - if(onlySpaces) { - errorret_t r = assetFileLineReaderNext(reader); - if(errorIsNotOk(r)) { - errorCatch(r); - break; - } - continue; - } - break; - } - - errorOk(); -} - -errorret_t assetLocaleLineUnbuffer( - assetfilelinereader_t *reader, - uint8_t *lineBuffer, - uint8_t *stringBuffer, - const size_t stringBufferSize -) { - stringBuffer[0] = '\0'; - - // At the point this funciton is called, we are looking for an opening quote. - char_t *start = strchr((char_t *)lineBuffer, '"'); - if(!start) { - errorThrow("Expected open (0) \""); - } - - char *end = strchr(start + 1, '"'); - if(!end) { - errorThrow("Expected close (0) \""); - } - *end = '\0'; - - if(strlen(start) >= stringBufferSize) { - errorThrow("String buffer overflow"); - } - memoryCopy(stringBuffer, start + 1, strlen(start)); - - // Now start buffering lines out - while(!reader->eof) { - errorret_t r = assetFileLineReaderNext(reader); - if(errorIsNotOk(r)) { - errorCatch(r); - break; - } - - // Skip blank lines - r = assetLocaleLineSkipBlanks(reader, lineBuffer); - if(errorIsNotOk(r)) { - errorCatch(r); - break; - } - - // Skip starting spaces - char_t *ptr = (char_t *)lineBuffer; - while(*ptr == ' ') { - ptr++; - } - - // Only consider lines starting with quote - if(*ptr != '"') { - break; - } - - ptr++; // move past first quote - - bool_t escaping = false; - char_t *dest = (char_t *)stringBuffer + strlen((char_t *)stringBuffer); - while(*ptr) { - if(escaping) { - // Handle escape sequences - switch(*ptr) { - case 'n': *dest++ = '\n'; break; - case 't': *dest++ = '\t'; break; - case '\\': *dest++ = '\\'; break; - case '"': *dest++ = '"'; break; - default: - errorThrow("Unknown escape sequence: \\%c", *ptr); - } - escaping = false; - } else if(*ptr == '\\') { - escaping = true; - } else if(*ptr == '"') { - // End of string - break; - } else { - // Regular character - *dest++ = *ptr; - } - if((size_t)(dest - (char_t *)stringBuffer) >= stringBufferSize) { - errorThrow("String buffer overflow"); - } - ptr++; - } - *dest = '\0'; - } - - errorOk(); -} - -errorret_t assetLocaleCacheFind( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *stringBuffer, - const size_t stringBufferSize, - bool_t *outHit -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(messageId, "Message ID cannot be NULL."); - assertNotNull(outHit, "outHit cannot be NULL."); - assertStrLenMin(messageId, 1, "Message ID cannot be empty."); - - *outHit = false; - if(file->cache == NULL) errorOk(); - - assetlocalecache_t *cache = file->cache; - for(uint8_t i = 0; i < ASSET_LOCALE_CACHE_COUNT; i++) { - assetlocalecacheentry_t *entry = &cache->entries[i]; - if(entry->messageId[0] == '\0') break;// unused tail - nothing further - if(entry->pluralCount != pluralCount) continue; - if(!stringEquals(entry->messageId, messageId)) continue; - - // Move to the front of the LRU order (a no-op when already there). - if(i > 0) { - assetlocalecacheentry_t hit = *entry; - memoryMove( - &cache->entries[1], &cache->entries[0], - i * sizeof(assetlocalecacheentry_t) - ); - cache->entries[0] = hit; - } - - size_t len = strlen(cache->entries[0].value); - if(len >= stringBufferSize) errorThrow("String buffer overflow"); - memoryCopy(stringBuffer, cache->entries[0].value, len + 1); - *outHit = true; - errorOk(); - } - - errorOk(); -} - -void assetLocaleCacheInsert( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - const char_t *value -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(messageId, "Message ID cannot be NULL."); - assertNotNull(value, "Value cannot be NULL."); - assertStrLenMin(messageId, 1, "Message ID cannot be empty."); - - // Caching is a pure optimization - a message ID or value too long for a - // fixed cache slot is just never cached, not an error. - if(strlen(messageId) >= ASSET_LOCALE_CACHE_KEY_MAX) return; - if(strlen(value) >= ASSET_LOCALE_CACHE_VALUE_MAX) return; - - if(file->cache == NULL) { - file->cache = - (assetlocalecache_t *)memoryAllocate(sizeof(assetlocalecache_t)); - memoryZero(file->cache, sizeof(assetlocalecache_t)); - } - - // Shift every entry down one slot, dropping the least-recently-used one - // off the end, to make room for the new entry at the front. This assumes - // messageId is never already present elsewhere in the cache, which holds - // as long as callers only insert after a confirmed assetLocaleCacheFind - // miss (a hit would have returned before reaching this point). - memoryMove( - &file->cache->entries[1], &file->cache->entries[0], - (ASSET_LOCALE_CACHE_COUNT - 1) * sizeof(assetlocalecacheentry_t) - ); - - assetlocalecacheentry_t *entry = &file->cache->entries[0]; - stringCopy(entry->messageId, messageId, ASSET_LOCALE_CACHE_KEY_MAX); - entry->pluralCount = pluralCount; - stringCopy(entry->value, value, ASSET_LOCALE_CACHE_VALUE_MAX); -} - -// Shared by assetLocaleGetString (external callers, messageId required -// non-empty) and assetLocaleGetHeader (messageId "", the PO format's own -// sentinel for its header entry - see assetLocaleGetHeader). Caching is -// skipped entirely for an empty messageId: assetLocaleCacheFind/Insert -// both reject one outright (same guard as assetLocaleGetString's own), -// and it's a one-shot boot-time read anyway, not worth caching. -errorret_t assetLocaleGetStringLookup( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *stringBuffer, - const size_t stringBufferSize -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(messageId, "Message ID cannot be NULL."); - assertTrue(pluralCount >= 0, "Plural index cannot be negative."); - assertNotNull(stringBuffer, "String buffer cannot be NULL."); - assertTrue(stringBufferSize > 0, "String buffer size must be > 0"); - - bool_t cacheHit = false; - if(messageId[0] != '\0') { - errorChain(assetLocaleCacheFind( - file, messageId, pluralCount, stringBuffer, stringBufferSize, &cacheHit - )); - if(cacheHit) errorOk(); - } - - assetfilelinereader_t reader; - - bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false; - uint8_t msgidBuffer[256]; - uint8_t msgidPluralBuffer[256]; - uint8_t readBuffer[1024]; - uint8_t lineBuffer[1024]; - uint8_t pluralIndex = 0xFF; - - msgidBuffer[0] = '\0'; - msgidPluralBuffer[0] = '\0'; - stringBuffer[0] = '\0'; - - // Rewind and start reading lines. - errorChain(assetFileRewind(&file->file)); - assetFileLineReaderInit( - &reader, - &file->file, - readBuffer, - sizeof(readBuffer), - lineBuffer, - sizeof(lineBuffer) - ); - - // Prime the reader with the first line before scanning; outBuffer holds - // uninitialized memory until the first Next() call fills it. - errorChain(assetFileLineReaderNext(&reader)); - - // Skip blanks, comments, etc and start looking for msgid's - errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); - - while(true) { - // Is this msgid? - if(memoryCompare(lineBuffer, "msgid", 5) != 0) { - if(reader.eof) break; - errorChain(assetFileLineReaderNext(&reader)); - msgidBuffer[0] = '\0'; - continue; - } - - // Unbuffer the msgid - assetLocaleLineUnbuffer( - &reader, lineBuffer, (uint8_t *)msgidBuffer, sizeof(msgidBuffer) - ); - - // Is this the needle? - if(memoryCompare(msgidBuffer, messageId, strlen(messageId)) != 0) { - continue; - } - - msgidFound = true; - break; - } - if(!msgidFound) { - errorThrow("Failed to find message ID: %s", messageId); - } - - // We are either going to see a msgstr or a msgid_plural - while(true) { - errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); - - // Is msgid_plural? - if( - !msgidPluralFound && - memoryCompare(lineBuffer, "msgid_plural", 12) == 0 - ) { - // Yes, start reading plural ID - assetLocaleLineUnbuffer( - &reader, - lineBuffer, - (uint8_t *)msgidPluralBuffer, - sizeof(msgidPluralBuffer) - ); - msgidPluralFound = true; - - // At this point we determine the plural index to use by running the - // plural formula - pluralIndex = assetLocaleEvaluatePlural( - file, - pluralCount - ); - continue; - } - - // Should be msgstr if not plural. - if(memoryCompare(lineBuffer, "msgstr", 6) != 0) { - errorThrow("Expected msgstr after msgid, found: %s", lineBuffer); - continue; - } - - // If plural we need an index - if(msgidPluralFound) { - // Skip blank chars - char_t *ptr = (char_t *)lineBuffer + 6; - while(*ptr == ' ') { - ptr++; - } - - if(*ptr != '[') { - errorThrow("Expected [ for plural form, found: %s", lineBuffer); - } - - ptr++; // move past [ - - // Parse until ] - char *end = strchr(ptr, ']'); - if(!end) { - errorThrow("Expected ] for plural form, found: %s", lineBuffer); - } - *end = '\0'; - - int32_t index = atoi(ptr); - if(index != pluralIndex) { - // Not the plural form we want, skip to the next useable line - while(!reader.eof) { - errorChain(assetFileLineReaderNext(&reader)); - errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer)); - if( - lineBuffer[0] == '\"' || - lineBuffer[0] == '\0' || - lineBuffer[0] == '#' - ) continue; - break; - } - continue; - } - - // Undo damage to line buffer from unbuffering. - *end = ']'; - } - - // Parse out msgstr - errorChain(assetLocaleLineUnbuffer( - &reader, lineBuffer, (uint8_t *)stringBuffer, stringBufferSize - )); - msgstrFound = true; - break; - }; - - if(!msgstrFound) { - errorThrow("Failed to find msgstr for message ID: %s", messageId); - } - - if(messageId[0] != '\0') { - assetLocaleCacheInsert(file, messageId, pluralCount, stringBuffer); - } - - errorOk(); -} - -errorret_t assetLocaleGetString( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *stringBuffer, - const size_t stringBufferSize -) { - assertNotNull(messageId, "Message ID cannot be NULL."); - assertStrLenMin(messageId, 1, "Message ID cannot be empty."); - - return assetLocaleGetStringLookup( - file, messageId, pluralCount, stringBuffer, stringBufferSize - ); -} - -errorret_t assetLocaleGetHeader( - assetlocalefile_t *file, - char_t *stringBuffer, - const size_t stringBufferSize -) { - return assetLocaleGetStringLookup( - file, "", 0, stringBuffer, stringBufferSize - ); -} - -errorret_t assetLocaleGetStringWithVA( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralIndex, - char_t *buffer, - const size_t bufferSize, - ... -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(messageId, "Message ID cannot be NULL."); - assertNotNull(buffer, "Buffer cannot be NULL."); - assertTrue(bufferSize > 0, "Buffer size must be > 0."); - assertTrue(pluralIndex >= 0, "Plural cannot be negative."); - - char_t *tempBuffer; - tempBuffer = memoryAllocate(bufferSize); - errorret_t ret = assetLocaleGetString( - file, - messageId, - pluralIndex, - tempBuffer, - bufferSize - ); - if(errorIsNotOk(ret)) { - memoryFree(tempBuffer); - return ret; - } - - va_list args; - va_start(args, bufferSize); - int result = vsnprintf(buffer, bufferSize, tempBuffer, args); - va_end(args); - memoryFree(tempBuffer); - - if(result < 0) { - errorThrow("Failed to format locale string for ID: %s", messageId); - } - - return ret; -} - -errorret_t assetLocaleGetStringWithArgs( - assetlocalefile_t *file, - const char_t *id, - const int32_t plural, - char_t *buffer, - const size_t bufferSize, - const assetlocalearg_t *args, - const size_t argCount -) { - assertNotNull(id, "Message ID cannot be NULL."); - assertNotNull(buffer, "Buffer cannot be NULL."); - assertTrue(bufferSize > 0, "Buffer size must be > 0."); - assertTrue(plural >= 0, "Plural cannot be negative."); - assertTrue( - argCount == 0 || args != NULL, - "Args cannot be NULL when argCount > 0." - ); - - char_t *format = memoryAllocate(bufferSize); - if(format == NULL) { - errorThrow("Failed to allocate format buffer."); - } - - errorret_t ret = assetLocaleGetString( - file, - id, - plural, - format, - bufferSize - ); - if(errorIsNotOk(ret)) { - memoryFree(format); - return ret; - } - - size_t inIndex = 0; - size_t outIndex = 0; - size_t nextArg = 0; - - buffer[0] = '\0'; - - while(format[inIndex] != '\0') { - if(format[inIndex] != '%') { - if(outIndex + 1 >= bufferSize) { - memoryFree(format); - errorThrow("Formatted locale string buffer overflow for ID: %s", id); - } - - buffer[outIndex++] = format[inIndex++]; - continue; - } - - inIndex++; - - /* Escaped percent */ - if(format[inIndex] == '%') { - if(outIndex + 1 >= bufferSize) { - memoryFree(format); - errorThrow("Formatted locale string buffer overflow for ID: %s", id); - } - - buffer[outIndex++] = '%'; - inIndex++; - continue; - } - - if(nextArg >= argCount) { - memoryFree(format); - errorThrow("Not enough locale arguments for ID: %s", id); - } - - { - char_t specBuffer[32]; - char_t valueBuffer[256]; - size_t specLength = 0; - int written = 0; - char_t specifier; - - specBuffer[specLength++] = '%'; - - /* Allow flags / width / precision */ - while(format[inIndex] != '\0') { - char_t ch = format[inIndex]; - - if( - ch == '-' || ch == '+' || ch == ' ' || ch == '#' || ch == '0' || - ch == '.' || (ch >= '0' && ch <= '9') - ) { - if(specLength + 1 >= sizeof(specBuffer)) { - memoryFree(format); - errorThrow("Format specifier too long for ID: %s", id); - } - - specBuffer[specLength++] = ch; - inIndex++; - continue; - } - - break; - } - - if(format[inIndex] == '\0') { - memoryFree(format); - errorThrow("Incomplete format specifier for ID: %s", id); - } - - specifier = format[inIndex++]; - - if(specifier != 's' && specifier != 'd' && specifier != 'f') { - memoryFree(format); - errorThrow( - "Unsupported format specifier '%%%c' for ID: %s", - specifier, - id - ); - } - - specBuffer[specLength++] = specifier; - specBuffer[specLength] = '\0'; - - switch(specifier) { - case 's': - if(args[nextArg].type != ASSET_LOCALE_ARG_STRING) { - memoryFree(format); - errorThrow("Expected string locale argument for ID: %s", id); - } - - written = snprintf( - valueBuffer, - sizeof(valueBuffer), - specBuffer, - args[nextArg].stringValue ? args[nextArg].stringValue : "" - ); - break; - - case 'd': - if(args[nextArg].type != ASSET_LOCALE_ARG_INT) { - memoryFree(format); - errorThrow("Expected int locale argument for ID: %s", id); - } - - written = snprintf( - valueBuffer, - sizeof(valueBuffer), - specBuffer, - args[nextArg].intValue - ); - break; - - case 'f': - if( - args[nextArg].type != ASSET_LOCALE_ARG_FLOAT && - args[nextArg].type != ASSET_LOCALE_ARG_INT - ) { - memoryFree(format); - errorThrow("Expected float or int locale argument for ID: %s", id); - } - - float_t floatValue = ( - args[nextArg].type == ASSET_LOCALE_ARG_FLOAT ? - args[nextArg].floatValue : - (float_t)args[nextArg].intValue - ); - - written = snprintf( - valueBuffer, - sizeof(valueBuffer), - specBuffer, - floatValue - ); - break; - } - - nextArg++; - - if(written < 0) { - memoryFree(format); - errorThrow("Failed to format locale string for ID: %s", id); - } - - if(outIndex + (size_t)written >= bufferSize) { - memoryFree(format); - errorThrow("Formatted locale string buffer overflow for ID: %s", id); - } - - memoryCopy(buffer + outIndex, valueBuffer, (size_t)written); - outIndex += (size_t)written; - } - } - - buffer[outIndex] = '\0'; - memoryFree(format); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/asset/loader/locale/assetlocaleloader.h b/src/dusk/asset/loader/locale/assetlocaleloader.h deleted file mode 100644 index fefcc557..00000000 --- a/src/dusk/asset/loader/locale/assetlocaleloader.h +++ /dev/null @@ -1,425 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -/** Input passed to the locale loader - currently unused. */ -typedef struct { void *nothing; } assetlocaleloaderinput_t; - -typedef enum { - ASSET_LOCALE_LOADER_STATE_INITIAL, - ASSET_LOCALE_LOADER_STATE_LOAD_HEADER, - ASSET_LOCALE_LOADER_STATE_DONE -} assetlocaleloaderstate_t; - -/** Per-slot scratch data used while the locale file is loading. */ -typedef struct { - assetlocaleloaderstate_t state; -} assetlocaleloaderloading_t; - -/** Maximum number of distinct plural forms a locale file may declare. */ -#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6 - -/** - * Comparison operator used in a plural-form expression. - * - * Each condition in the plural-form header is evaluated as - * `n value` - * where `n` is the runtime plural count. - */ -typedef enum { - ASSET_LOCALE_PLURAL_OP_EQUAL, - ASSET_LOCALE_PLURAL_OP_NOT_EQUAL, - ASSET_LOCALE_PLURAL_OP_LESS, - ASSET_LOCALE_PLURAL_OP_LESS_EQUAL, - ASSET_LOCALE_PLURAL_OP_GREATER, - ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL -} assetlocalepluraloperation_t; - -/** - * Discriminator tag for a locale string argument. - * @see assetlocalearg_t - */ -typedef enum { - ASSET_LOCALE_ARG_STRING, - ASSET_LOCALE_ARG_INT, - ASSET_LOCALE_ARG_FLOAT -} assetlocaleargtype_t; - -/** - * A single typed argument for locale string substitution. - * - * Used with @ref assetLocaleGetStringWithArgs to fill `%s`, `%d`, or `%f` - * placeholders inside a translated string. - */ -typedef struct { - /** Which union member is active. */ - assetlocaleargtype_t type; - union { - const char_t *stringValue; - int32_t intValue; - float_t floatValue; - }; -} assetlocalearg_t; - -/** Number of recently resolved strings @ref assetlocalecache_t remembers. */ -#define ASSET_LOCALE_CACHE_COUNT 128 - -/** Max length (excluding null terminator) of a cacheable message ID. */ -#define ASSET_LOCALE_CACHE_KEY_MAX 64 - -/** Max length (excluding null terminator) of a cacheable resolved string. */ -#define ASSET_LOCALE_CACHE_VALUE_MAX 256 - -/** One (messageId, pluralCount) -> resolved string cache slot. */ -typedef struct { - char_t messageId[ASSET_LOCALE_CACHE_KEY_MAX]; - int32_t pluralCount; - char_t value[ASSET_LOCALE_CACHE_VALUE_MAX]; -} assetlocalecacheentry_t; - -/** - * Fixed-size move-to-front LRU cache of recently resolved locale strings. - * assetLocaleGetString() re-scans and re-decompresses the whole PO file on - * every miss, which is expensive to repeat for strings a screen fetches - * every time it opens (menu labels, etc) - this cache lets identical - * (messageId, pluralCount) lookups skip that entirely. - * - * Lazily allocated on the first cache insert (see assetlocalefile_t.cache) - * so locale entries that are never queried, or a file that's disposed - * before anything is cached, never pay for it. - */ -typedef struct { - /** - * Entries ordered most-recently-used first. An entry with - * messageId[0] == '\0' (and every entry after it) is unused. - */ - assetlocalecacheentry_t entries[ASSET_LOCALE_CACHE_COUNT]; -} assetlocalecache_t; - -/** - * Runtime state for an open locale file. - * - * Loaded once by @ref assetLocaleLoaderSync and kept alive for the lifetime - * of the asset entry. The plural-form fields are populated from the PO header - * by @ref assetLocaleParseHeader. - * - * Plural evaluation works as a linear scan over `pluralStateCount - 1` - * conditions; if none match, `pluralDefaultIndex` is returned. - */ -typedef struct { - /** Underlying file handle used to rewind and re-read the PO data. */ - assetfile_t file; - - /** Comparison operator for each conditional plural clause. */ - assetlocalepluraloperation_t pluralOps[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT]; - - /** Right-hand value for each conditional plural clause. */ - int32_t pluralValues[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT]; - - /** Form index returned when the corresponding condition is true. */ - int32_t pluralIndices[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT]; - - /** Total number of plural forms declared by `nplurals=`. */ - uint8_t pluralStateCount; - - /** Form index used when no conditional clause matches. */ - uint8_t pluralDefaultIndex; - - /** - * Recently resolved string cache, or NULL if nothing has been cached yet. - * Heap-allocated rather than embedded because assetlocalefile_t lives - * inside the shared assetloaderoutput_t union alongside every other - * asset type - embedding a 128-entry cache there would size that union - * (and therefore every one of the ASSET_ENTRY_COUNT_MAX asset slots, - * regardless of what type of asset occupies it) up by the same amount. - */ - assetlocalecache_t *cache; -} assetlocalefile_t; - -/** Convenience alias - the loaded output type of a locale asset entry. */ -typedef assetlocalefile_t assetlocaleoutput_t; - -/** - * Asynchronous loader callback. Opens the locale file, reads the PO header, - * and parses plural-form rules. All I/O happens here so the main thread is - * not blocked. Sets entry state to `ASSET_ENTRY_STATE_PENDING_SYNC` on - * success or `ASSET_ENTRY_STATE_ERROR` on failure. - * - * @param loading The loading slot for this asset entry. - * @return OK on success, error otherwise. - */ -errorret_t assetLocaleLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader callback. Confirms the async phase completed and marks - * the entry as `ASSET_ENTRY_STATE_LOADED`. - * - * @param loading The loading slot for this asset entry. - * @return OK on success, error otherwise. - */ -errorret_t assetLocaleLoaderSync(assetloading_t *loading); - -/** - * Dispose callback. Closes the open file handle and zeros the locale data. - * - * @param entry The asset entry to dispose. - * @return OK on success, error otherwise. - */ -errorret_t assetLocaleDispose(assetentry_t *entry); - -/** - * Parses the `Plural-Forms:` line from a PO header string and populates the - * plural-form fields of `localeFile`. If no `Plural-Forms:` key is present - * the function returns OK without modifying the struct. - * - * Supports the `nplurals=N; plural=();` syntax where `` is a - * chain of `n value ? index : ...` ternary conditions ending with a - * fallback index. - * - * @param localeFile Struct whose plural fields will be filled in. - * @param headerBuffer The raw PO header string (msgstr of msgid ""). - * @param headerBufferSize Size of `headerBuffer` in bytes. - * @return OK on success, error if the plural expression is malformed. - */ -errorret_t assetLocaleParseHeader( - assetlocalefile_t *localeFile, - char_t *headerBuffer, - const size_t headerBufferSize -); - -/** - * Evaluates which plural form index to use for a given count. - * - * Walks the conditions stored in `file` in order; returns the index for the - * first condition that matches `pluralCount`. Falls back to - * `file->pluralDefaultIndex` if none match. - * - * @param file Locale file with parsed plural rules. - * @param pluralCount The runtime count (e.g. number of items). - * @return Zero-based plural form index. - */ -uint8_t assetLocaleEvaluatePlural( - assetlocalefile_t *file, - const int32_t pluralCount -); - -/** - * Advances the line reader past blank lines, comment lines (starting with - * `#`), and lines containing only spaces. Stops at the first content line or - * end of file. - * - * @param reader Line reader positioned at the current line. - * @param lineBuffer Output buffer that the reader writes each line into. - * @return OK on success, error if reading fails. - */ -errorret_t assetLocaleLineSkipBlanks( - assetfilelinereader_t *reader, - uint8_t *lineBuffer -); - -/** - * Reads a PO quoted string value from the current line and any immediately - * following continuation lines that begin with `"`. - * - * Handles standard C escape sequences (`\n`, `\t`, `\\`, `\"`). - * - * @param reader Line reader positioned at the line containing the opening - * quote (e.g. `msgstr "..."`). - * @param lineBuffer Buffer filled on each @ref assetFileLineReaderNext - * call; also used to detect continuation lines. - * @param stringBuffer Destination for the unescaped string content. - * @param stringBufferSize Capacity of `stringBuffer` in bytes. - * @return OK on success, error if a quote or escape sequence is malformed or - * the buffer would overflow. - */ -errorret_t assetLocaleLineUnbuffer( - assetfilelinereader_t *reader, - uint8_t *lineBuffer, - uint8_t *stringBuffer, - const size_t stringBufferSize -); - -/** - * Searches a locale file's cache for a previously resolved - * (messageId, pluralCount) pair. - * - * On a hit, copies the cached value into `stringBuffer` (erroring if it - * doesn't fit, same contract as @ref assetLocaleGetString) and moves the - * entry to the front of the cache's move-to-front LRU order. On a miss, - * `stringBuffer` is left untouched. - * - * @param file Locale file whose cache to search. Its cache may be NULL - * (nothing cached yet), which is treated as a miss. - * @param messageId Message ID to look up. - * @param pluralCount Plural count the original lookup used. - * @param stringBuffer Destination buffer, filled only on a hit. - * @param stringBufferSize Capacity of `stringBuffer` in bytes. - * @param outHit Set to true on a cache hit, false on a miss. - * @return OK on success (hit or miss), error if a hit's cached value does - * not fit `stringBuffer`. - */ -errorret_t assetLocaleCacheFind( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *stringBuffer, - const size_t stringBufferSize, - bool_t *outHit -); - -/** - * Records a resolved (messageId, pluralCount) -> value string at the front - * of the cache's move-to-front LRU order, evicting the least-recently-used - * entry if the cache is already full. Lazily allocates the cache (see - * assetlocalefile_t.cache) on the first call. - * - * Caching is a pure optimization: if `messageId` or `value` is too long to - * fit the cache's fixed-size slots, this silently does nothing rather than - * erroring or growing the cache. - * - * @param file Locale file whose cache to insert into. - * @param messageId Message ID that was looked up. - * @param pluralCount Plural count the lookup was made with. - * @param value Resolved string to cache. - */ -void assetLocaleCacheInsert( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - const char_t *value -); - -/** - * Shared lookup core behind assetLocaleGetString and assetLocaleGetHeader: - * checks the file's cache first (see @ref assetLocaleCacheFind; skipped - * entirely for an empty messageId, since the cache rejects one), on a - * miss rewinds the file and scans from the beginning, then caches the - * result (see @ref assetLocaleCacheInsert, same empty-messageId skip) - * before returning. For plural entries (`msgid_plural`) the - * `pluralCount` is evaluated against the loaded plural rules to select - * the correct `msgstr[N]` form. - * - * Unlike assetLocaleGetString, messageId may be `""` here (see - * assetLocaleGetHeader) - this is the shared implementation both of - * them delegate to, not a public entry point of its own in practice. - * - * @param file Locale file to search. Must be open. - * @param messageId PO message ID to find, or `""` for the header entry. - * @param pluralCount Count used to select the plural form (ignored for - * singular entries). - * @param stringBuffer Buffer to receive the translated string. - * @param stringBufferSize Capacity of `stringBuffer` in bytes. - * @return OK on success, error if the message ID is not found or I/O fails. - */ -errorret_t assetLocaleGetStringLookup( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *stringBuffer, - const size_t stringBufferSize -); - -/** - * Looks up a translated string by message ID from the open locale file. - * See @ref assetLocaleGetStringLookup for the lookup/caching mechanics. - * - * @param file Locale file to search. Must be open. - * @param messageId PO message ID to find. Must be non-empty - use - * @ref assetLocaleGetHeader to read the header entry. - * @param pluralCount Count used to select the plural form (ignored for - * singular entries). - * @param stringBuffer Buffer to receive the translated string. - * @param stringBufferSize Capacity of `stringBuffer` in bytes. - * @return OK on success, error if the message ID is not found or I/O fails. - */ -errorret_t assetLocaleGetString( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *stringBuffer, - const size_t stringBufferSize -); - -/** - * Reads the PO file's header entry (the `msgid ""` block every PO file - * starts with, holding metadata like the plural-forms rule) - see @ref - * assetLocaleParseHeader, its caller. A dedicated function rather than - * calling assetLocaleGetString(file, "", ...) since messageId "" is a - * PO-format-internal sentinel, not a real (untrusted) caller-supplied - * message ID, so it doesn't go through assetLocaleGetString's normal - * non-empty validation. - * - * @param file Locale file to search. Must be open. - * @param stringBuffer Buffer to receive the raw header block. - * @param stringBufferSize Capacity of `stringBuffer` in bytes. - * @return OK on success, error if the header entry is missing or I/O - * fails. - */ -errorret_t assetLocaleGetHeader( - assetlocalefile_t *file, - char_t *stringBuffer, - const size_t stringBufferSize -); - -/** - * Looks up a translated string and formats it with a `printf`-style variadic - * argument list. - * - * Retrieves the raw format string via @ref assetLocaleGetString then applies - * `vsnprintf` with the provided arguments. - * - * @param file Locale file to search. Must be open. - * @param messageId PO message ID to find. - * @param pluralCount Count used to select the plural form. - * @param buffer Buffer to receive the formatted string. - * @param bufferSize Capacity of `buffer` in bytes. - * @param ... Format arguments matching the specifiers in the translated - * string. - * @return OK on success, error if the message is not found or formatting - * fails. - */ -errorret_t assetLocaleGetStringWithVA( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *buffer, - const size_t bufferSize, - ... -); - -/** - * Looks up a translated string and substitutes typed arguments into its - * `%s`, `%d`, and `%f` placeholders. - * - * Unlike @ref assetLocaleGetStringWithVA this variant accepts an explicit - * array of @ref assetlocalearg_t values, which is safer to use from - * generated or scripted call sites. - * - * @param file Locale file to search. Must be open. - * @param messageId PO message ID to find. - * @param pluralCount Count used to select the plural form. - * @param buffer Buffer to receive the formatted string. - * @param bufferSize Capacity of `buffer` in bytes. - * @param args Array of typed arguments; may be NULL when `argCount` is 0. - * @param argCount Number of elements in `args`. - * @return OK on success, error if the message is not found, an argument type - * mismatches the specifier, or the buffer would overflow. - */ -errorret_t assetLocaleGetStringWithArgs( - assetlocalefile_t *file, - const char_t *messageId, - const int32_t pluralCount, - char_t *buffer, - const size_t bufferSize, - const assetlocalearg_t *args, - const size_t argCount -); diff --git a/src/dusk/asset/loader/mp3/CMakeLists.txt b/src/dusk/asset/loader/mp3/CMakeLists.txt deleted file mode 100644 index 6355a69f..00000000 --- a/src/dusk/asset/loader/mp3/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - assetmp3loader.c -) diff --git a/src/dusk/asset/loader/mp3/assetmp3loader.c b/src/dusk/asset/loader/mp3/assetmp3loader.c deleted file mode 100644 index 5c4a5045..00000000 --- a/src/dusk/asset/loader/mp3/assetmp3loader.c +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetmp3loader.h" -#include "util/memory.h" -#include "util/math.h" -#include "assert/assert.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -// How far into the file (after any ID3v2 tag) assetMp3ParseHeader() will -// search for the first valid MPEG frame sync. Real encoders write it -// within the first handful of bytes; this is a generous sane upper bound -// (matching assetwavloader.c's own approach to bounding a stack buffer) -// rather than a real expectation of needing that much. -#define ASSET_MP3_HEADER_SEARCH_MAX 8192 - -// MPEG-1 Layer III bitrates (kbps), indexed by the header's 4-bit bitrate -// index. Index 0 ("free format") and 15 (reserved) aren't supported. -static const uint16_t ASSET_MP3_BITRATE_MPEG1_L3[16] = { - 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 -}; - -// MPEG-2/2.5 Layer III bitrates (kbps) - a different table from MPEG-1's. -static const uint16_t ASSET_MP3_BITRATE_MPEG2_L3[16] = { - 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 -}; - -static const uint32_t ASSET_MP3_SAMPLE_RATE_MPEG1[4] = { - 44100, 48000, 32000, 0 -}; -static const uint32_t ASSET_MP3_SAMPLE_RATE_MPEG2[4] = { - 22050, 24000, 16000, 0 -}; -static const uint32_t ASSET_MP3_SAMPLE_RATE_MPEG25[4] = { - 11025, 12000, 8000, 0 -}; - -errorret_t assetMp3LoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Async loader should not be on main thread."); - - if(loading->loading.mp3.state != ASSET_MP3_LOADER_STATE_READ_HEADER) { - errorOk(); - } - - assetmp3file_t *mp3File = &loading->entry->data.mp3; - memoryZero(mp3File, sizeof(assetmp3file_t)); - - assetfile_t file; - assetLoaderErrorChain(loading, assetFileInit( - &file, loading->entry->name, NULL, NULL - )); - assetLoaderErrorChain(loading, assetFileOpen(&file)); - assetLoaderErrorChain(loading, assetMp3ParseHeader(&file, mp3File)); - assetLoaderErrorChain(loading, assetFileClose(&file)); - assetLoaderErrorChain(loading, assetFileDispose(&file)); - - loading->loading.mp3.state = ASSET_MP3_LOADER_STATE_DONE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetMp3LoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_MP3, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.mp3.state) { - case ASSET_MP3_LOADER_STATE_INITIAL: - loading->loading.mp3.state = ASSET_MP3_LOADER_STATE_READ_HEADER; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_MP3_LOADER_STATE_DONE: - break; - - default: - errorOk(); - } - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetMp3Dispose(assetentry_t *entry) { - assertNotNull(entry, "Asset entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_MP3, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - errorOk(); -} - -errorret_t assetMp3ParseHeader(assetfile_t *file, assetmp3file_t *mp3File) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(mp3File, "Mp3 file cannot be NULL."); - - memoryZero(mp3File, sizeof(assetmp3file_t)); - - // Skip a leading ID3v2 tag if present: "ID3" + version(2) + flags(1) + a - // 4-byte syncsafe (7 usable bits/byte) size covering everything after - // this 10-byte header. - uint8_t id3Header[10]; - errorChain(assetFileRead(file, id3Header, sizeof(id3Header))); - - size_t searchBase; - if(memoryCompare(id3Header, "ID3", 3) == 0) { - const uint32_t tagSize = ( - ((uint32_t) (id3Header[6] & 0x7F) << 21) | - ((uint32_t) (id3Header[7] & 0x7F) << 14) | - ((uint32_t) (id3Header[8] & 0x7F) << 7) | - ((uint32_t) (id3Header[9] & 0x7F)) - ); - errorChain(assetFileRead(file, NULL, tagSize)); - searchBase = sizeof(id3Header) + tagSize; - } else { - // No ID3v2 tag - rewind the probe bytes just consumed so the frame - // sync search below starts from the true beginning of the file. - errorChain(assetFileRewind(file)); - searchBase = 0; - } - - const size_t searchWindowSize = mathMin( - (size_t) ASSET_MP3_HEADER_SEARCH_MAX, - (size_t) file->size > searchBase ? (size_t) file->size - searchBase : 0 - ); - if(searchWindowSize < 4) { - errorThrow( - "MP3 file is too short to contain a frame header: %s", file->filename - ); - } - - uint8_t *window = (uint8_t *) memoryAllocate(searchWindowSize); - errorret_t readRet = assetFileRead(file, window, searchWindowSize); - if(errorIsNotOk(readRet)) { - memoryFree(window); - errorChain(readRet); - } - - bool_t found = false; - size_t frameIndex = 0; - - for(size_t i = 0; i + 4 <= searchWindowSize; i++) { - if(window[i] != 0xFF || (window[i + 1] & 0xE0) != 0xE0) continue; - - const uint8_t versionBits = (window[i + 1] >> 3) & 0x3; - const uint8_t layerBits = (window[i + 1] >> 1) & 0x3; - // reserved version, or not Layer III - if(versionBits == 1 || layerBits != 1) continue; - - const uint8_t bitrateIndex = (window[i + 2] >> 4) & 0xF; - const uint8_t sampleRateIndex = (window[i + 2] >> 2) & 0x3; - if( - bitrateIndex == 0 || bitrateIndex == 15 || sampleRateIndex == 3 - ) continue; - - const uint8_t channelMode = (window[i + 3] >> 6) & 0x3; - const bool_t isMpeg1 = versionBits == 3; - - mp3File->bitrateKbps = isMpeg1 - ? ASSET_MP3_BITRATE_MPEG1_L3[bitrateIndex] - : ASSET_MP3_BITRATE_MPEG2_L3[bitrateIndex]; - mp3File->sampleRate = isMpeg1 - ? ASSET_MP3_SAMPLE_RATE_MPEG1[sampleRateIndex] - : (versionBits == 2 - ? ASSET_MP3_SAMPLE_RATE_MPEG2[sampleRateIndex] - : ASSET_MP3_SAMPLE_RATE_MPEG25[sampleRateIndex]); - mp3File->samplesPerFrame = isMpeg1 ? 1152 : 576; - mp3File->channels = channelMode == 3 ? 1 : 2; - - // A Xing/Info header, if present, sits right after the side info - - // whose size depends on MPEG version and channel mode - immediately - // following the 4-byte header (plus a 2-byte CRC, if the protection - // bit says one follows). - const bool_t hasCrc = (window[i + 1] & 0x1) == 0; - const size_t sideInfoSize = isMpeg1 - ? (mp3File->channels == 1 ? 17 : 32) - : (mp3File->channels == 1 ? 9 : 17); - const size_t xingOffset = i + 4 + (hasCrc ? 2 : 0) + sideInfoSize; - - if( - xingOffset + 8 <= searchWindowSize && - ( - memoryCompare(window + xingOffset, "Xing", 4) == 0 || - memoryCompare(window + xingOffset, "Info", 4) == 0 - ) - ) { - const uint8_t *flagsBytes = window + xingOffset + 4; - const uint32_t flags = ( - ((uint32_t) flagsBytes[0] << 24) | ((uint32_t) flagsBytes[1] << 16) | - ((uint32_t) flagsBytes[2] << 8) | (uint32_t) flagsBytes[3] - ); - if((flags & 0x1) != 0 && xingOffset + 12 <= searchWindowSize) { - const uint8_t *framesBytes = window + xingOffset + 8; - const uint32_t xingFrameCount = ( - ((uint32_t) framesBytes[0] << 24) | - ((uint32_t) framesBytes[1] << 16) | - ((uint32_t) framesBytes[2] << 8) | - (uint32_t) framesBytes[3] - ); - // The Xing/Info header's frame count includes the header's own - // frame (this one, at `i`) - which libmad (and sceMp3) will - // attempt to decode like any other, but which doesn't contribute a - // real decodable frame of audio the way every other one does. - // Confirmed against a real LAME-encoded file: its Xing header - // declared 40 frames, but decoding actually produced exactly 39 - // frames' worth of real PCM - without this -1, playback would - // permanently stall one frame short of the declared total (audible - // as "never loops"), since real decode output can never catch up - // to an overstated total. - if(xingFrameCount > 0) { - mp3File->totalFrames = - (size_t) (xingFrameCount - 1) * mp3File->samplesPerFrame; - } - } - } - - frameIndex = i; - found = true; - break; - } - - memoryFree(window); - - if(!found) { - errorThrow( - "MP3 file has no valid MPEG-1/2/2.5 Layer III frame within the first " - "%u bytes: %s", - (uint32_t) searchWindowSize, file->filename - ); - } - - mp3File->dataOffset = searchBase + frameIndex; - mp3File->dataSize = (size_t) file->size - mp3File->dataOffset; - - // No Xing/Info header found - estimate assuming constant bitrate (the - // common case when one's genuinely absent; a real VBR file without one - // is rare and will just get an approximate duration/loop point instead - // of an exact one). - if(mp3File->totalFrames == 0) { - const float_t durationSeconds = ( - ((float_t) mp3File->dataSize * 8.0f) / - ((float_t) mp3File->bitrateKbps * 1000.0f) - ); - mp3File->totalFrames = - (size_t) (durationSeconds * (float_t) mp3File->sampleRate); - } - - errorOk(); -} diff --git a/src/dusk/asset/loader/mp3/assetmp3loader.h b/src/dusk/asset/loader/mp3/assetmp3loader.h deleted file mode 100644 index 6cb0bf8a..00000000 --- a/src/dusk/asset/loader/mp3/assetmp3loader.h +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "asset/assetfile.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -typedef struct { void *nothing; } assetmp3loaderinput_t; - -typedef enum { - ASSET_MP3_LOADER_STATE_INITIAL, - ASSET_MP3_LOADER_STATE_READ_HEADER, - ASSET_MP3_LOADER_STATE_DONE -} assetmp3loaderstate_t; - -typedef struct { - assetmp3loaderstate_t state; -} assetmp3loaderloading_t; - -/** - * Parsed metadata for an MP3 asset - deliberately just enough to configure - * a decoder and know the clip's total length, not anything about its - * actual compressed content (which is read/decoded on demand at playback - * time - see audiostreammp3.c). Only MPEG-1/2/2.5 Layer III ("MP3" in the - * everyday sense) is supported; Layers I/II are rejected. - */ -typedef struct { - uint32_t sampleRate; - uint8_t channels; - - // 1152 for MPEG-1, 576 for MPEG-2/2.5 - how many PCM samples (per - // channel) one compressed MPEG frame decodes to. - uint16_t samplesPerFrame; - - // The first frame's bitrate, in kbps. Used only as a fallback duration - // estimate (assuming CBR) when no Xing/Info header is found - see - // assetMp3ParseHeader()'s own comment. - uint32_t bitrateKbps; - - // Byte offset (from the start of the file) of the first real MPEG - // frame, i.e. right after any leading ID3v2 tag. - size_t dataOffset; - - // Bytes of compressed MPEG data, from dataOffset to the end of the file. - size_t dataSize; - - // Decoded PCM frame count for the whole clip - exact if a Xing/Info - // header was found (the common case for anything encoded with a modern - // tool), otherwise estimated from bitrateKbps/dataSize assuming CBR (see - // assetMp3ParseHeader()'s own comment) - unlike assetwavfile_t.dataSize, - // MP3 has no header field that gives this directly. - size_t totalFrames; -} assetmp3file_t; - -typedef assetmp3file_t assetmp3output_t; - -/** - * Async (background-thread) half of the MP3 loader - opens the asset file, - * parses its header (see assetMp3ParseHeader()), and closes it again. Never - * buffers the compressed MPEG data itself; that's read on demand at - * playback time by audiostreammp3.c via its own independent file handle. - * - * @param loading The asset loading slot. - * @return Error indicating success or failure. - */ -errorret_t assetMp3LoaderAsync(assetloading_t *loading); - -/** - * Sync (main-thread) half of the MP3 loader - a simple state machine that - * schedules the async header parse and marks the entry loaded once it's - * done, mirroring assetWavLoaderSync()'s shape exactly. - * - * @param loading The asset loading slot. - * @return Error indicating success or failure. - */ -errorret_t assetMp3LoaderSync(assetloading_t *loading); - -/** - * Disposes an MP3 asset entry. A no-op - an assetmp3file_t owns no heap - * allocations, and playback streams open their own independent file - * handles (see audiostreammp3.c). - * - * @param entry The asset entry to dispose. - * @return Error indicating success or failure. - */ -errorret_t assetMp3Dispose(assetentry_t *entry); - -/** - * Parses an MP3 file's metadata from an already-opened asset file - * positioned at its very start: skips a leading ID3v2 tag if present, - * scans forward for the first valid MPEG-1/2/2.5 Layer III frame header - * (rejecting anything else - other layers, or no valid frame found within - * a sane search window), and checks that frame for a Xing/Info header - * (found in the vast majority of real-world MP3s, VBR or CBR) to get an - * exact total-frame count; falls back to a CBR estimate from the first - * frame's bitrate and the file's remaining size if absent. - * - * Never reads more than a bounded lookahead window into memory - the bulk - * of the file (the actual compressed audio, following the parsed frame's - * position) is left untouched, exactly like assetWavParseHeader(). - * - * @param file Asset file to parse, positioned at offset 0. - * @param mp3File Filled with the parsed metadata on success. - * @return Error indicating success or failure. - */ -errorret_t assetMp3ParseHeader(assetfile_t *file, assetmp3file_t *mp3File); diff --git a/src/dusk/asset/loader/wav/CMakeLists.txt b/src/dusk/asset/loader/wav/CMakeLists.txt deleted file mode 100644 index 585adba9..00000000 --- a/src/dusk/asset/loader/wav/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - assetwavloader.c -) diff --git a/src/dusk/asset/loader/wav/assetwavloader.c b/src/dusk/asset/loader/wav/assetwavloader.c deleted file mode 100644 index fa32d7c7..00000000 --- a/src/dusk/asset/loader/wav/assetwavloader.c +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetwavloader.h" -#include "util/memory.h" -#include "util/endian.h" -#include "assert/assert.h" -#include "asset/loader/assetloading.h" -#include "asset/loader/assetentry.h" - -// "RIFF" + chunkSize + "WAVE", before any sub-chunks begin. -#define ASSET_WAV_RIFF_HEADER_SIZE 12 - -// A sub-chunk header is a 4-byte id followed by a 4-byte little-endian size. -#define ASSET_WAV_CHUNK_HEADER_SIZE 8 - -#define ASSET_WAV_FMT_CHUNK_SIZE_MIN 16 -// Sane upper bound for a stack buffer - real fmt chunks (PCM or otherwise) -// never come close to this; anything bigger is treated as malformed. -#define ASSET_WAV_FMT_CHUNK_SIZE_MAX 64 - -errorret_t assetWavLoaderAsync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertNotMainThread("Async loader should not be on main thread."); - - if(loading->loading.wav.state != ASSET_WAV_LOADER_STATE_READ_HEADER) { - errorOk(); - } - - assetwavfile_t *wavFile = &loading->entry->data.wav; - memoryZero(wavFile, sizeof(assetwavfile_t)); - - assetfile_t file; - assetLoaderErrorChain(loading, assetFileInit( - &file, loading->entry->name, NULL, NULL - )); - assetLoaderErrorChain(loading, assetFileOpen(&file)); - assetLoaderErrorChain(loading, assetWavParseHeader(&file, wavFile)); - assetLoaderErrorChain(loading, assetFileClose(&file)); - assetLoaderErrorChain(loading, assetFileDispose(&file)); - - loading->loading.wav.state = ASSET_WAV_LOADER_STATE_DONE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); -} - -errorret_t assetWavLoaderSync(assetloading_t *loading) { - assertNotNull(loading, "Loading cannot be NULL"); - assertTrue(loading->type == ASSET_LOADER_TYPE_WAV, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - switch(loading->loading.wav.state) { - case ASSET_WAV_LOADER_STATE_INITIAL: - loading->loading.wav.state = ASSET_WAV_LOADER_STATE_READ_HEADER; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; - errorOk(); - break; - - case ASSET_WAV_LOADER_STATE_DONE: - break; - - default: - errorOk(); - } - - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t assetWavDispose(assetentry_t *entry) { - assertNotNull(entry, "Asset entry cannot be NULL"); - assertTrue(entry->type == ASSET_LOADER_TYPE_WAV, "Invalid type."); - assertIsMainThread("Must be called from the main thread."); - - errorOk(); -} - -errorret_t assetWavParseHeader( - assetfile_t *file, - assetwavfile_t *wavFile -) { - assertNotNull(file, "Asset file cannot be NULL."); - assertNotNull(wavFile, "Wav file cannot be NULL."); - - uint8_t riffHeader[ASSET_WAV_RIFF_HEADER_SIZE]; - errorChain(assetFileRead(file, riffHeader, sizeof(riffHeader))); - - if(memoryCompare(riffHeader, "RIFF", 4) != 0) { - errorThrow("WAV file has an invalid RIFF header: %s", file->filename); - } - if(memoryCompare(riffHeader + 8, "WAVE", 4) != 0) { - errorThrow("WAV file has an invalid WAVE header: %s", file->filename); - } - - bool_t foundFormat = false; - bool_t foundData = false; - uint16_t audioFormat = 0; - - // Walk the chunk list, reading only chunk headers (and the small "fmt " - // body) - every other chunk, "data"'s sample bytes included, is skipped - // via a NULL-buffer read rather than buffered into memory. Stops as soon - // as "data" is found; nothing after it matters here. - while(!foundData && (size_t) file->position < (size_t) file->size) { - uint8_t chunkHeader[ASSET_WAV_CHUNK_HEADER_SIZE]; - errorChain(assetFileRead(file, chunkHeader, sizeof(chunkHeader))); - - uint32_t chunkSizeLE; - memoryCopy(&chunkSizeLE, chunkHeader + 4, sizeof(chunkSizeLE)); - const uint32_t chunkSize = endianLittleToHost32(chunkSizeLE); - - // Chunks are padded to an even total size - the pad byte (if any) - // isn't included in chunkSize but still needs to be skipped over. - const uint32_t chunkSizePadded = chunkSize + (chunkSize % 2); - - if(memoryCompare(chunkHeader, "fmt ", 4) == 0) { - if( - chunkSize < ASSET_WAV_FMT_CHUNK_SIZE_MIN || - chunkSize > ASSET_WAV_FMT_CHUNK_SIZE_MAX - ) { - errorThrow("WAV 'fmt ' chunk has an unsupported size: %u", chunkSize); - } - - uint8_t fmtBuffer[ASSET_WAV_FMT_CHUNK_SIZE_MAX]; - errorChain(assetFileRead(file, fmtBuffer, chunkSize)); - if(chunkSize % 2 != 0) { - errorChain(assetFileRead(file, NULL, 1)); - } - - uint16_t u16; - uint32_t u32; - - memoryCopy(&u16, fmtBuffer + 0, sizeof(u16)); - audioFormat = endianLittleToHost16(u16); - - memoryCopy(&u16, fmtBuffer + 2, sizeof(u16)); - wavFile->channels = (uint8_t) endianLittleToHost16(u16); - - memoryCopy(&u32, fmtBuffer + 4, sizeof(u32)); - wavFile->sampleRate = endianLittleToHost32(u32); - - memoryCopy(&u16, fmtBuffer + 14, sizeof(u16)); - wavFile->bitsPerSample = (uint8_t) endianLittleToHost16(u16); - - foundFormat = true; - } else if(memoryCompare(chunkHeader, "data", 4) == 0) { - wavFile->dataOffset = (size_t) file->position; - wavFile->dataSize = chunkSize; - foundData = true; - } else { - errorChain(assetFileRead(file, NULL, chunkSizePadded)); - } - } - - if(!foundFormat) { - errorThrow("WAV file is missing its 'fmt ' chunk: %s", file->filename); - } - if(!foundData) { - errorThrow("WAV file is missing its 'data' chunk: %s", file->filename); - } - if(audioFormat != 1) { - errorThrow( - "Unsupported WAV audio format: %u (only PCM is supported): %s", - audioFormat, file->filename - ); - } - if(wavFile->bitsPerSample != 16 && wavFile->bitsPerSample != 24) { - errorThrow( - "Unsupported WAV bits per sample: %u (only 16/24-bit is supported): %s", - wavFile->bitsPerSample, file->filename - ); - } - if(wavFile->channels == 0) { - errorThrow("WAV file declares 0 channels: %s", file->filename); - } - - errorOk(); -} diff --git a/src/dusk/asset/loader/wav/assetwavloader.h b/src/dusk/asset/loader/wav/assetwavloader.h deleted file mode 100644 index 35002d4d..00000000 --- a/src/dusk/asset/loader/wav/assetwavloader.h +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetfile.h" - -typedef struct assetloading_s assetloading_t; -typedef struct assetentry_s assetentry_t; - -/** Input passed to the wav loader - currently unused. */ -typedef struct { void *nothing; } assetwavloaderinput_t; - -typedef enum { - ASSET_WAV_LOADER_STATE_INITIAL, - ASSET_WAV_LOADER_STATE_READ_HEADER, - ASSET_WAV_LOADER_STATE_DONE -} assetwavloaderstate_t; - -/** Per-slot scratch data used while the wav file is loading. */ -typedef struct { - assetwavloaderstate_t state; -} assetwavloaderloading_t; - -/** - * Parsed metadata for a WAV asset - only the RIFF/WAVE/fmt/data chunk - * headers are ever read; the PCM sample data itself is never loaded here - * (see @ref assetWavParseHeader). Playback reads sample data directly - * from the archive on demand instead - see audiostreampcm.h. - */ -typedef struct { - /** Sample rate of the PCM data, in Hz. */ - uint32_t sampleRate; - - /** Number of interleaved channels in the PCM data. */ - uint8_t channels; - - /** - * Bits per sample of the *source* file data - 16 or 24 (see - * assetWavParseHeader). Playback always reads 16-bit samples out via - * audioStreamPcmRead() regardless of this - a 24-bit source is - * truncated to 16-bit there, since none of this project's audio - * backends (PSP/Dolphin hardware, SDL2 on Linux) accept anything wider. - */ - uint8_t bitsPerSample; - - /** Byte offset from the start of the file to the first PCM sample. */ - size_t dataOffset; - - /** Size of the PCM data, in bytes. */ - size_t dataSize; -} assetwavfile_t; - -/** Convenience alias - the loaded output type of a wav asset entry. */ -typedef assetwavfile_t assetwavoutput_t; - -/** - * Asynchronous loader callback. Opens the WAV file and reads just enough of - * it to locate and parse the `fmt ` chunk and locate (not read) the `data` - * chunk - see @ref assetWavParseHeader. All I/O happens here so the main - * thread is not blocked. Sets entry state to `ASSET_ENTRY_STATE_PENDING_SYNC` - * on success or `ASSET_ENTRY_STATE_ERROR` on failure. - * - * @param loading The loading slot for this asset entry. - * @return OK on success, error otherwise. - */ -errorret_t assetWavLoaderAsync(assetloading_t *loading); - -/** - * Synchronous loader callback. Confirms the async phase completed and marks - * the entry as `ASSET_ENTRY_STATE_LOADED`. - * - * @param loading The loading slot for this asset entry. - * @return OK on success, error otherwise. - */ -errorret_t assetWavLoaderSync(assetloading_t *loading); - -/** - * Dispose callback. The wav asset owns no allocations of its own (its data - * chunk is read directly from the archive by whichever streams are playing - * it, each through their own handle - see audiostreampcm.h), so this is - * currently a no-op beyond the standard asserts. - * - * @param entry The asset entry to dispose. - * @return OK on success, error otherwise. - */ -errorret_t assetWavDispose(assetentry_t *entry); - -/** - * Parses a RIFF/WAVE file's chunk structure from an already-open asset - * file, reading only chunk headers (and the small `fmt ` chunk body) - - * every other chunk, including `data`'s actual sample bytes, is skipped - * over via `assetFileRead(file, NULL, size)` rather than read into memory, - * so this never buffers the (potentially large) PCM payload. - * - * Stops as soon as the `data` chunk header is found, recording its file - * offset and declared size in `wavFile` without reading any of its bytes - - * the file is left positioned at the start of the PCM data. - * - * Only PCM (audio format 1), 16- or 24-bit-per-sample WAV data is - * supported - see audiostreampcm.h's audioStreamPcmRead() for how a - * 24-bit source gets truncated to the 16-bit output every platform - * backend expects. Requires the `fmt ` chunk to appear before `data`, per - * the WAV spec's recommended ordering. - * - * @param file An open asset file, positioned at the start of the WAV data. - * @param wavFile Struct whose fields will be filled in. - * @return OK on success, error if the file is malformed or an unsupported - * format. - */ -errorret_t assetWavParseHeader( - assetfile_t *file, - assetwavfile_t *wavFile -); diff --git a/src/dusk/audio/CMakeLists.txt b/src/dusk/audio/CMakeLists.txt deleted file mode 100644 index 2b9098d8..00000000 --- a/src/dusk/audio/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# 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 - audio.c -) - -# Subdirs -add_subdirectory(mixer) -add_subdirectory(stream) \ No newline at end of file diff --git a/src/dusk/audio/audio.c b/src/dusk/audio/audio.c deleted file mode 100644 index f941c442..00000000 --- a/src/dusk/audio/audio.c +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audio.h" -#include "audio/mixer/audiomixer.h" -#include "util/memory.h" -#include "assert/assert.h" - -audio_t AUDIO; - -errorret_t audioInit() { - memoryZero(&AUDIO, sizeof(audio_t)); - - errorChain(audioPlatformInit()); - errorChain(audioMixerInit()); - - errorOk(); -} - -audiostream_t * audioAquireStream(assetentry_t *asset) { - assertNotNull(asset, "Asset cannot be NULL."); - // Same check audioStreamInit() makes - see its own comment on why this - // is an assert (a programmer error, not untrusted data). Checking here - // too catches the mistake as early as possible, before any stream slot - // is handed out for it. - assertTrue( - audioStreamTypeForAssetType(asset->type) != AUDIO_STREAM_TYPE_NULL, - "Unsupported asset type for an audio stream." - ); - - for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) { - audiostream_t *stream = &AUDIO.streams[i]; - if(stream->type == AUDIO_STREAM_TYPE_NULL) { - return stream; - } - } - - return NULL; -} - -errorret_t audioUpdateEarly() { - errorChain(audioPlatformUpdate()); - errorChain(audioMixerUpdateEarly()); - - for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) { - audiostream_t *stream = &AUDIO.streams[i]; - errorChain(audioStreamUpdate(stream)); - } - - errorOk(); -} - -errorret_t audioUpdateLate() { - errorChain(audioMixerUpdateLate()); - - errorOk(); -} - -errorret_t audioDispose() { - for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) { - audiostream_t *stream = &AUDIO.streams[i]; - errorChain(audioStreamDispose(stream)); - } - - errorChain(audioMixerDispose()); - errorChain(audioPlatformDispose()); - - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/audio/audio.h b/src/dusk/audio/audio.h deleted file mode 100644 index 3ded86b5..00000000 --- a/src/dusk/audio/audio.h +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "audio/stream/audiostream.h" -#include "audio/audioplatform.h" - -#ifndef audioPlatformInit - #error "audioPlatformInit is not defined" -#endif - -#ifndef audioPlatformUpdate - #error "audioPlatformUpdate is not defined" -#endif - -#ifndef audioPlatformDispose - #error "audioPlatformDispose is not defined" -#endif - -typedef struct { - audiostream_t streams[AUDIO_STREAMS_MAX]; -} audio_t; - -extern audio_t AUDIO; - -/** - * Initializes the audio subsystem. - * - * @return Error indicating success or failure of the operation. - */ -errorret_t audioInit(); - -/** - * Aquires an available audio stream for playing the given asset. Can return - * NULL if there is no available stream slot. - * - * The returned stream is not yet configured to play `asset` - call - * audioStreamInit(stream, asset) next. - * - * @param asset The asset the caller intends to play - validated eagerly so - * an unsupported asset type is caught here rather than only - * once audioStreamInit() is called. - * @return Pointer to an available audio stream, or NULL if none are free. - */ -audiostream_t * audioAquireStream(assetentry_t *asset); - -/** - * Updates the audio subsystem, updating every active stream. Should be - * called once per frame, before rendering - keeps stream buffering/decode - * work off the render thread's critical path (see duskmad's MP3 decode - * ring for why this matters). - * - * @return Error indicating success or failure of the operation. - */ -errorret_t audioUpdateEarly(); - -/** - * Updates the audio subsystem's end-of-frame work - currently just starting - * whatever the audio mixer queued this frame (see - * audioMixerUpdateLate()'s own comment). Should be called once per frame, - * after rendering. - * - * @return Error indicating success or failure of the operation. - */ -errorret_t audioUpdateLate(); - -/** - * Disposes the audio subsystem, stopping and disposing every active stream. - * - * @return Error indicating success or failure of the operation. - */ -errorret_t audioDispose(); diff --git a/src/dusk/audio/mixer/CMakeLists.txt b/src/dusk/audio/mixer/CMakeLists.txt deleted file mode 100644 index fb7309c3..00000000 --- a/src/dusk/audio/mixer/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - audiomixer.c - audiomixerchannel.c -) \ No newline at end of file diff --git a/src/dusk/audio/mixer/audiomixer.c b/src/dusk/audio/mixer/audiomixer.c deleted file mode 100644 index 41ec71b1..00000000 --- a/src/dusk/audio/mixer/audiomixer.c +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiomixer.h" -#include "audio/stream/audiostream.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assert.h" - -audiomixer_t AUDIO_MIXER; - -errorret_t audioMixerInit() { - memoryZero(&AUDIO_MIXER, sizeof(audiomixer_t)); - - audiomixerchannel_t channel = 0; - while(channel < AUDIO_MIXER_CHANNEL_COUNT) { - AUDIO_MIXER.channels[channel].fade = 1.0f; - // -1 means "no fade in progress" - see its own comment; zero-init - // would otherwise look like an active fade at time 0. - AUDIO_MIXER.channels[channel].fadeTime = -1.0f; - channel++; - } - - errorOk(); -} - -errorret_t audioMixerUpdateEarly() { - audiomixerchannel_t channel = 0; - while(channel < AUDIO_MIXER_CHANNEL_COUNT) { - // One channel's error (e.g. a failed asset load) must not stop every - // other channel from being updated this frame - caught and logged - // rather than propagated, same as audioStream*IsFinished()'s own - // per-stream error handling. - errorret_t ret = audioMixerChannelUpdateEarly( - channel, &AUDIO_MIXER.channels[channel] - ); - if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); - channel++; - } - - errorOk(); -} - -errorret_t audioMixerUpdateLate() { - audiomixerchannel_t channel = 0; - while(channel < AUDIO_MIXER_CHANNEL_COUNT) { - errorret_t ret = - audioMixerChannelUpdateLate(&AUDIO_MIXER.channels[channel]); - if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); - channel++; - } - - errorOk(); -} - -void audioMixerPlay( - const char_t *file, - const audiomixerchannel_t channel, - const float_t volume, - const float_t pan -) { - audioMixerQueuePlay(file, channel, volume, pan, false, 0, -1.0f, 0.0f); -} - -void audioMixerPlayLooped( - const char_t *file, - const audiomixerchannel_t channel, - const float_t volume, - const float_t pan, - const uint8_t loopCount, - const float_t loopStart, - const float_t loopTo -) { - audioMixerQueuePlay( - file, channel, volume, pan, true, loopCount, loopStart, loopTo - ); -} - -void audioMixerQueuePlay( - const char_t *file, - const audiomixerchannel_t channel, - const float_t volume, - const float_t pan, - const bool_t looping, - const uint8_t loopCount, - const float_t loopStart, - const float_t loopTo -) { - assertStrLenMin(file, 1, "File is required, to stop use audioMixerStop"); - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - assertTrue(volume > 0.0f && volume <= 1.0f, "Volume is out of range"); - assertTrue( - pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT, - "Pan is out of range" - ); - assertTrue( - !looping || loopStart < 0.0f || loopTo < loopStart, - "Loop points incorrect" - ); - - audiomixercommand_t *command = &AUDIO_MIXER.channels[channel].pending; - - command->command = AUDIO_MIXER_COMMAND_PLAY; - stringCopy(command->file, file, AUDIO_PATH_MAX); - command->volume = volume; - command->pan = pan; - command->looping = looping; - command->loopCount = loopCount; - command->loopStart = loopStart; - command->loopTo = loopTo; -} - -void audioMixerStop(const audiomixerchannel_t channel) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - - AUDIO_MIXER.channels[channel].pending.command = AUDIO_MIXER_COMMAND_STOP; -} - -void audioMixerPause(const audiomixerchannel_t channel) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - - audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; - if(stream != NULL) audioStreamPause(stream); -} - -void audioMixerResume(const audiomixerchannel_t channel) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - - audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; - if(stream != NULL) audioStreamPlay(stream); -} - -void audioMixerSetPan(const audiomixerchannel_t channel, const float_t pan) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - assertTrue( - pan >= AUDIO_STREAM_LEFT && pan <= AUDIO_STREAM_RIGHT, - "Pan is out of range" - ); - - audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; - if(stream != NULL) audioStreamSetDirectionality(stream, pan); -} - -void audioMixerSetLoop( - const audiomixerchannel_t channel, - const bool_t looping, - const uint8_t loopCount, - const float_t loopStart, - const float_t loopTo -) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - assertTrue( - !looping || loopStart < 0.0f || loopTo < loopStart, - "Loop points incorrect" - ); - - audiostream_t *stream = AUDIO_MIXER.channels[channel].stream; - if(stream == NULL) return; - - audioStreamSetLooping(stream, looping); - if(looping) { - audioStreamSetLoopPoints(stream, loopStart, loopTo); - audioStreamSetLoopLimit(stream, loopCount); - } -} - -void audioMixerFadeTo( - const audiomixerchannel_t channel, - const float_t from, - const float_t to, - const float_t duration, - const easingtype_t easing -) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - - audioMixerChannelFadeTo( - &AUDIO_MIXER.channels[channel], from, to, duration, easing - ); -} - -bool_t audioMixerIsFading(const audiomixerchannel_t channel) { - assertTrue(channel < AUDIO_MIXER_CHANNEL_COUNT, "Channel is out of range"); - - return audioMixerChannelIsFading(&AUDIO_MIXER.channels[channel]); -} - -errorret_t audioMixerDispose() { - audiomixerchannel_t channel = 0; - while(channel < AUDIO_MIXER_CHANNEL_COUNT) { - errorChain(audioMixerChannelStopStream(&AUDIO_MIXER.channels[channel])); - channel++; - } - - errorOk(); -} diff --git a/src/dusk/audio/mixer/audiomixer.h b/src/dusk/audio/mixer/audiomixer.h deleted file mode 100644 index a61f3ec9..00000000 --- a/src/dusk/audio/mixer/audiomixer.h +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" - -typedef struct { - audiomixerchannelstate_t channels[AUDIO_MIXER_CHANNEL_COUNT]; -} audiomixer_t; - -extern audiomixer_t AUDIO_MIXER; - -/** - * Initializes the audio mixer. - */ -errorret_t audioMixerInit(); - -/** - * Updates the audio mixer, called before rendering (the start of the - * frame) - see audioMixerChannelUpdateEarly()'s own comment, run for every - * channel. - */ -errorret_t audioMixerUpdateEarly(); - -/** - * Updates the audio mixer, called at the end of each frame - see - * audioMixerChannelUpdateLate()'s own comment, run for every channel. - */ -errorret_t audioMixerUpdateLate(); - -/** - * Same as audioMixerPlayLooped() but with no looping. - * - * @param file File to be played. - * @param channel Channel you want to play this sound on. - * @param volume Volume of the sound, from 0.0 (silent) to 1.0 (loudest). - * @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to - * AUDIO_STREAM_RIGHT. - */ -void audioMixerPlay( - const char_t *file, - const audiomixerchannel_t channel, - const float_t volume, - const float_t pan -); - -/** - * Queues a sound to be played on the next audio mixer update, which occurs at - * the end of each frame. - * - * Mixer channels can only hold one voice (audio stream) at a time. If you try - * to queue multiple sounds on the same channel only the last one will be played - * and will stop any actively playing on that channel currently. - * - * Volume is also mixed based on the channel, so supplying volume 1.0 is then - * multiplied by the channel's volume, e.g. 0.5*1.0 = 0.5. - * - * @param file File to be played. - * @param channel Channel you want to play this sound on. - * @param volume Volume of the sound, from 0.0 (silent) to 1.0 (loudest). - * @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to - * AUDIO_STREAM_RIGHT. - * @param loopCount How many times to loop the sound, 0 for infinite looping. - * @param loopStart Where the loop segment ends, in seconds, or -1 to loop - * the whole stream (the default). - * @param loopTo Where the loop segment starts, in seconds. - */ -void audioMixerPlayLooped( - const char_t *file, - const audiomixerchannel_t channel, - const float_t volume, - const float_t pan, - const uint8_t loopCount, - const float_t loopStart, - const float_t loopTo -); - -/** - * Shared implementation behind audioMixerPlay()/audioMixerPlayLooped() - - * queues an AUDIO_MIXER_COMMAND_PLAY for the given channel, to be loaded at - * the end of this frame and started at the start of the next (see - * audioMixerUpdateLate()/audioMixerUpdateEarly()). - * - * @param file File to be played. - * @param channel Channel you want to play this sound on. - * @param volume Volume of the sound, from 0.0 (silent) to 1.0 (loudest). - * @param pan Stereo panning of the sound, from AUDIO_STREAM_LEFT to - * AUDIO_STREAM_RIGHT. - * @param looping Whether the stream should loop once it finishes. - * @param loopCount How many times to loop the sound, 0 for infinite looping. - * Ignored unless looping is true. - * @param loopStart Where the loop segment ends, in seconds, or -1 to loop - * the whole stream. Ignored unless looping is true. - * @param loopTo Where the loop segment starts, in seconds. Ignored unless - * looping is true. - */ -void audioMixerQueuePlay( - const char_t *file, - const audiomixerchannel_t channel, - const float_t volume, - const float_t pan, - const bool_t looping, - const uint8_t loopCount, - const float_t loopStart, - const float_t loopTo -); - -/** - * Stops playing sound on the given channel. - * - * @param channel Channel to stop. - */ -void audioMixerStop(const audiomixerchannel_t channel); - -/** - * Pauses the given channel's currently playing stream, if any - a safe - * no-op if the channel isn't playing anything. Playback position is - * retained, so audioMixerResume() continues from the same point. - * - * @param channel Channel to pause. - */ -void audioMixerPause(const audiomixerchannel_t channel); - -/** - * Resumes the given channel's currently paused stream, if any - a safe - * no-op if the channel isn't playing anything. - * - * @param channel Channel to resume. - */ -void audioMixerResume(const audiomixerchannel_t channel); - -/** - * Sets the stereo panning of the given channel's currently playing stream, - * if any - a safe no-op if the channel isn't playing anything. Only - * affects what's playing right now; a later audioMixerPlay()/ - * audioMixerPlayLooped() call on this channel uses its own `pan` argument - * instead. - * - * @param channel Channel to update. - * @param pan Stereo panning, from AUDIO_STREAM_LEFT to AUDIO_STREAM_RIGHT. - */ -void audioMixerSetPan(const audiomixerchannel_t channel, const float_t pan); - -/** - * Sets the looping behaviour of the given channel's currently playing - * stream, if any - a safe no-op if the channel isn't playing anything. - * Only affects what's playing right now; a later audioMixerPlay()/ - * audioMixerPlayLooped() call on this channel uses its own loop arguments - * instead. - * - * @param channel Channel to update. - * @param looping Whether the stream should loop. - * @param loopCount How many times to loop, 0 for infinite. Ignored unless - * looping is true. - * @param loopStart Where the loop segment ends, in seconds, or -1 to loop - * the whole stream. Ignored unless looping is true. - * @param loopTo Where the loop segment starts, in seconds. Ignored unless - * looping is true. - */ -void audioMixerSetLoop( - const audiomixerchannel_t channel, - const bool_t looping, - const uint8_t loopCount, - const float_t loopStart, - const float_t loopTo -); - -/** - * Starts fading the given channel from `from` to `to` over `duration` - * seconds, eased by `easing` - see audioMixerChannelFadeTo()'s own - * comment. Continues running (advanced every audioMixerUpdateEarly()) even - * if the channel isn't currently playing anything. - * - * @param channel Channel to fade. - * @param from Starting fade value, from 0.0 (silent) to 1.0 (loudest). - * @param to Ending fade value, from 0.0 (silent) to 1.0 (loudest). - * @param duration How long the fade takes, in seconds. - * @param easing Easing curve to apply to the fade's progress. - */ -void audioMixerFadeTo( - const audiomixerchannel_t channel, - const float_t from, - const float_t to, - const float_t duration, - const easingtype_t easing -); - -/** - * Whether the given channel currently has a fade in progress (started via - * audioMixerFadeTo() and not yet finished). - * - * @param channel Channel to check. - * @return true if a fade is still in progress. - */ -bool_t audioMixerIsFading(const audiomixerchannel_t channel); - -/** - * Disposes the audio mixer. - */ -errorret_t audioMixerDispose(); diff --git a/src/dusk/audio/mixer/audiomixerchannel.c b/src/dusk/audio/mixer/audiomixerchannel.c deleted file mode 100644 index a7946aab..00000000 --- a/src/dusk/audio/mixer/audiomixerchannel.c +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiomixerchannel.h" -#include "asset/asset.h" -#include "audio/audio.h" -#include "audio/stream/audiostream.h" -#include "audio/stream/audiostreamtype.h" -#include "save/save.h" -#include "time/time.h" -#include "util/math.h" -#include "assert/assert.h" - -errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state) { - assertNotNull(state, "State cannot be NULL."); - - if(state->stream == NULL) errorOk(); - - errorChain(audioStreamDispose(state->stream)); - state->stream = NULL; - - errorOk(); -} - -void audioMixerChannelFadeTo( - audiomixerchannelstate_t *state, - const float_t from, - const float_t to, - const float_t duration, - const easingtype_t easing -) { - assertNotNull(state, "State cannot be NULL."); - - state->fade = from; - state->fadeFrom = from; - state->fadeTo = to; - state->fadeDuration = duration; - state->fadeEasing = easing; - // A non-positive duration finishes instantly - marked inactive right - // away (fadeTime < 0) rather than left at 0 to progress next frame, - // since there's nothing left to progress. - if(duration <= 0.0f) { - state->fade = to; - state->fadeTime = -1.0f; - } else { - state->fadeTime = 0.0f; - } -} - -bool_t audioMixerChannelIsFading(const audiomixerchannelstate_t *state) { - assertNotNull(state, "State cannot be NULL."); - - return state->fadeTime >= 0.0f; -} - -void audioMixerChannelApplyVolume( - const audiomixerchannel_t channel, - audiomixerchannelstate_t *state -) { - assertNotNull(state, "State cannot be NULL."); - - if(state->stream == NULL) return; - - // Advances any in-progress fade before it's read below - see - // audioMixerChannelFadeTo()'s own comment on fadeTime's meaning. - if(state->fadeTime >= 0.0f) { - state->fadeTime += TIME.delta; - const float_t t = mathClamp( - state->fadeTime / state->fadeDuration, 0.0f, 1.0f - ); - state->fade = state->fadeFrom + - (state->fadeTo - state->fadeFrom) * easingApply(state->fadeEasing, t); - if(state->fadeTime >= state->fadeDuration) { - state->fade = state->fadeTo; - state->fadeTime = -1.0f; - } - } - - // state->fade and both SAVE.settings volumes have no setter - written - // directly by callers - so this is the only place any of them ever gets - // validated, rather than at the point they're set. - assertTrue( - state->fade >= 0.0f && state->fade <= 1.0f, - "Channel fade is out of range." - ); - assertTrue( - SAVE.settings.audioChannelVolume[channel] >= 0.0f && - SAVE.settings.audioChannelVolume[channel] <= 1.0f, - "Channel volume is out of range." - ); - assertTrue( - SAVE.settings.audioMasterVolume >= 0.0f && - SAVE.settings.audioMasterVolume <= 1.0f, - "Master volume is out of range." - ); - - // Final computed volume. - float_t volume = ( - state->baseVolume * - state->fade * - SAVE.settings.audioChannelVolume[channel] * - SAVE.settings.audioMasterVolume - ); - audioStreamSetVolume(state->stream, volume); -} - -errorret_t audioMixerChannelUpdateEarly( - const audiomixerchannel_t channel, - audiomixerchannelstate_t *state -) { - assertNotNull(state, "State cannot be NULL."); - - audiomixercommand_t *command = &state->pending; - - if(command->command == AUDIO_MIXER_COMMAND_STOP) { - errorChain(audioMixerChannelStopStream(state)); - command->command = AUDIO_MIXER_COMMAND_NONE; - audioMixerChannelApplyVolume(channel, state); - errorOk(); - } - - if( - command->command != AUDIO_MIXER_COMMAND_PLAY || - command->loadingAsset == NULL - ) { - // A PLAY command with no loadingAsset yet (command->loadingAsset == - // NULL) was queued too late for the audioMixerChannelUpdateLate() that - // just ran immediately before this call to have locked it - the only - // way that happens is a command queued outside the normal per-frame - // cycle, e.g. during engine startup before any - // audioMixerChannelUpdateLate() has run yet. Left alone (still - // AUDIO_MIXER_COMMAND_PLAY) rather than treated as an error: the very - // next audioMixerChannelUpdateLate() will lock it, and the frame after - // that will reach here and apply it normally. - audioMixerChannelApplyVolume(channel, state); - errorOk(); - } - - // Blocks here if audioMixerChannelUpdateLate()'s load (kicked off at the - // end of the previous frame) hasn't finished yet - see this function's - // own doc comment on why that's accepted for now. - errorret_t loadRet = assetRequireLoaded(command->loadingAsset); - if(errorIsNotOk(loadRet)) { - assetUnlockEntry(command->loadingAsset); - command->loadingAsset = NULL; - command->command = AUDIO_MIXER_COMMAND_NONE; - errorChain(loadRet); - } - - errorChain(audioMixerChannelStopStream(state)); - - audiostream_t *stream = audioAquireStream(command->loadingAsset); - assertNotNull(stream, "No free audio stream slots available."); - - errorret_t initRet = audioStreamInit(stream, command->loadingAsset); - // The stream holds its own lock on the asset once initialized (or, on - // failure, audioStreamInit() has already released it itself) - either - // way this call's own lock is no longer needed. - assetUnlockEntry(command->loadingAsset); - command->loadingAsset = NULL; - command->command = AUDIO_MIXER_COMMAND_NONE; - errorChain(initRet); - - // Forwards to this channel's own onLoop/onEnd (persistent across every - // play on this channel) rather than this one stream instance's own - - // see audiomixerchannelstate_t.onLoop's own comment. - stream->user = state; - stream->onLoop = audioMixerChannelOnStreamLoop; - stream->onEnd = audioMixerChannelOnStreamEnd; - - audioStreamSetDirectionality(stream, command->pan); - if(command->looping) { - audioStreamSetLooping(stream, true); - audioStreamSetLoopPoints(stream, command->loopStart, command->loopTo); - audioStreamSetLoopLimit(stream, command->loopCount); - } - audioStreamPlay(stream); - - state->stream = stream; - state->baseVolume = command->volume; - - audioMixerChannelApplyVolume(channel, state); - - errorOk(); -} - -errorret_t audioMixerChannelUpdateLate(audiomixerchannelstate_t *state) { - assertNotNull(state, "State cannot be NULL."); - - audiomixercommand_t *command = &state->pending; - - if(command->command != AUDIO_MIXER_COMMAND_PLAY) errorOk(); - - assetloadertype_t assetType = audioStreamAssetTypeForPath(command->file); - assertTrue( - assetType != ASSET_LOADER_TYPE_NULL, "Unsupported audio file extension." - ); - - // Begins loading now - kicked off at the end of this frame so it has - // until the start of the next (audioMixerChannelUpdateEarly()) to finish - // before that has to block on it. - command->loadingAsset = assetLock(command->file, assetType, NULL); - - errorOk(); -} - -void audioMixerChannelOnStreamLoop(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - audiomixerchannelstate_t *state = (audiomixerchannelstate_t *) stream->user; - if(state->onLoop != NULL) state->onLoop(state); -} - -void audioMixerChannelOnStreamEnd(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - audiomixerchannelstate_t *state = (audiomixerchannelstate_t *) stream->user; - if(state->onEnd != NULL) state->onEnd(state); -} diff --git a/src/dusk/audio/mixer/audiomixerchannel.h b/src/dusk/audio/mixer/audiomixerchannel.h deleted file mode 100644 index dae2a35c..00000000 --- a/src/dusk/audio/mixer/audiomixerchannel.h +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "error/error.h" -#include "animation/easing.h" - -// Forward-declared rather than including asset/asset.h - this header only -// ever needs a pointer to it, and pulling in the full asset module here -// risks an include cycle for anything that reaches this header via a -// cutscene item (cutscene.h -> cutsceneitem.h -> the audio cutscene items -// -> this header -> asset/asset.h, mid-parse). audiomixerchannel.c itself -// still includes the full header. -typedef struct assetentry_s assetentry_t; - -typedef struct audiostream_s audiostream_t; - -#define AUDIO_PATH_MAX 256 - -typedef enum { - AUDIO_MIXER_CHANNEL_BGM_0, - AUDIO_MIXER_CHANNEL_VOICE_0, - AUDIO_MIXER_CHANNEL_VOICE_1, - AUDIO_MIXER_CHANNEL_SFX_0, - AUDIO_MIXER_CHANNEL_SFX_1, - AUDIO_MIXER_CHANNEL_SFX_2, - AUDIO_MIXER_CHANNEL_SFX_3, - AUDIO_MIXER_CHANNEL_COUNT -} audiomixerchannel_t; - -typedef enum { - AUDIO_MIXER_COMMAND_NONE, - AUDIO_MIXER_COMMAND_PLAY, - AUDIO_MIXER_COMMAND_STOP -} audiomixercommandtype_t; - -typedef struct { - // What audioMixerChannelUpdateEarly() should do to this channel at the - // start of the next frame - AUDIO_MIXER_COMMAND_NONE if nothing's - // queued. Set by audioMixerPlay()/audioMixerPlayLooped()/ - // audioMixerStop(); queuing a new command on a channel replaces, rather - // than stacks on top of, whatever was already queued for it this frame - - // only the most recent survives. - audiomixercommandtype_t command; - - // AUDIO_MIXER_COMMAND_PLAY only, below. - char_t file[AUDIO_PATH_MAX]; - float_t volume; - float_t pan; - bool_t looping; - uint8_t loopCount; - float_t loopStart; - float_t loopTo; - - // Set by audioMixerChannelUpdateLate() (the end of the frame this PLAY - // command was queued on), once it locks the requested file and kicks off - // its load. audioMixerChannelUpdateEarly() (the start of the next frame) - // blocks on this being fully loaded before actually starting playback - - // see its own comment. NULL except in that in-between window. - assetentry_t *loadingAsset; -} audiomixercommand_t; - -typedef struct audiomixerchannelstate_s audiomixerchannelstate_t; - -typedef struct audiomixerchannelstate_s { - // Queued action for the next audioMixerChannelUpdateEarly() to apply. - audiomixercommand_t pending; - - // This channel's currently playing stream, or NULL if nothing is - - // acquired from AUDIO.streams by audioMixerChannelUpdateEarly() when it - // applies an AUDIO_MIXER_COMMAND_PLAY, disposed the same way on - // AUDIO_MIXER_COMMAND_STOP or before a new AUDIO_MIXER_COMMAND_PLAY - // replaces it. - audiostream_t *stream; - - // The per-sound volume `stream` was last started with (the `volume` - // argument to whichever audioMixerPlay()/audioMixerPlayLooped() call - // started it) - combined with this channel's fade, its own volume - // (SAVE.settings.audioChannelVolume) and the mixer's master volume - // (SAVE.settings.audioMasterVolume), every frame, to get the volume - // actually passed to audioStreamSetVolume(). Meaningless while `stream` - // is NULL. - float_t baseVolume; - - // This channel's own fade multiplier, from 0.0 (fully faded out) to 1.0 - // (no fade applied, the default) - transient runtime state, unlike the - // channel/master volumes below (which are persisted user preferences). - // No setter - write it directly; audioMixerChannelApplyVolume() asserts - // it's in range the next time it reads it, rather than validating at the - // point it's set. - float_t fade; - - // Fade transition state, set by audioMixerChannelFadeTo() and advanced - // every frame by audioMixerChannelApplyVolume(), before it reads `fade` - // to compute the mixed volume. fadeTime < 0 means no fade is in progress - // (audioMixerChannelIsFading() is just `fadeTime >= 0`) - audioMixerInit() - // explicitly sets this to -1.0f, since zero-initializing it would - // otherwise look like an active fade at time 0. - float_t fadeFrom; - float_t fadeTo; - float_t fadeDuration; - float_t fadeTime; - easingtype_t fadeEasing; - - // Arbitrary caller data, handed back as-is to onLoop/onEnd below - never - // read or written by the mixer itself. NULL by default. - void *user; - - // Fired (from the main thread - safe to do arbitrary work, same - // guarantee as audiostream_t's own onLoop/onEnd) whenever this channel's - // currently playing stream loops, or reaches its natural end (including - // hitting its loop limit - see audioStreamSetLoopLimit()) and stops. - // Wired onto every stream this channel starts via - // audioMixerChannelOnStreamLoop()/OnStreamEnd() - not called directly by - // anything else. No setter - write these fields directly; NULL (the - // default) means not notified. Unlike `stream`/`pending`, these persist - // across every play on this channel, not just one. - void (*onLoop)(audiomixerchannelstate_t *state); - void (*onEnd)(audiomixerchannelstate_t *state); -} audiomixerchannelstate_t; - -/** - * Stops and disposes the given channel's currently playing stream, if any - - * a safe no-op if the channel is already idle. Shared by - * audioMixerChannelUpdateEarly() (AUDIO_MIXER_COMMAND_STOP, and replacing a - * channel's stream before a new AUDIO_MIXER_COMMAND_PLAY starts) and - * audioMixerDispose(). - * - * @param state The channel state to stop. - */ -errorret_t audioMixerChannelStopStream(audiomixerchannelstate_t *state); - -/** - * Computes the given channel's actual playback volume - its currently - * playing sound's own volume (baseVolume), this channel's fade, this - * channel's own volume (SAVE.settings.audioChannelVolume[channel]) and the - * mixer's master volume (SAVE.settings.audioMasterVolume) - and applies it - * via audioStreamSetVolume(). Asserts fade and both save-settings volumes - * are each within [0.0, 1.0] - the only validation any of them ever gets, - * since none of them have a setter. A safe no-op if the channel isn't - * playing anything. Called every audioMixerChannelUpdateEarly(), not just - * when a PLAY command starts a new stream - see its own comment. - * - * @param channel Which channel `state` is, to index - * SAVE.settings.audioChannelVolume with. - * @param state The channel state to update. - */ -void audioMixerChannelApplyVolume( - const audiomixerchannel_t channel, - audiomixerchannelstate_t *state -); - -/** - * Starts fading the given channel's `fade` multiplier from `from` to `to` - * over `duration` seconds, eased by `easing` - snaps `fade` to `from` - * immediately, then audioMixerChannelApplyVolume() advances it every frame - * from here on. A `duration` of 0 or less snaps straight to `to` and marks - * the fade as already finished (audioMixerChannelIsFading() returns - * false on the very next call). - * - * @param state The channel state to fade. - * @param from Starting fade value, from 0.0 (silent) to 1.0 (loudest). - * @param to Ending fade value, from 0.0 (silent) to 1.0 (loudest). - * @param duration How long the fade takes, in seconds. - * @param easing Easing curve to apply to the fade's progress. - */ -void audioMixerChannelFadeTo( - audiomixerchannelstate_t *state, - const float_t from, - const float_t to, - const float_t duration, - const easingtype_t easing -); - -/** - * Whether the given channel currently has a fade in progress (started via - * audioMixerChannelFadeTo() and not yet finished). - * - * @param state The channel state to check. - * @return true if a fade is still in progress. - */ -bool_t audioMixerChannelIsFading(const audiomixerchannelstate_t *state); - -/** - * Updates a single mixer channel, called before rendering (the start of - * the frame): applies whatever command (play, stop - including everything - * a play carries, pan/looping) audioMixerChannelUpdateLate() queued for - * loading at the end of the previous frame. If that load hasn't finished - * yet, this blocks (via assetRequireLoaded()) until it has - a deliberate - * stall for now rather than skipping a frame, since there's currently no - * way to only partially apply a channel's command. Also recomputes and - * reapplies this channel's mixed volume (see - * audioMixerChannelApplyVolume()) every time, regardless of whether a - * command was queued this frame, so a live change to this channel's fade, - * its own volume, or the master volume takes effect on whatever's already - * playing too. - * - * @param channel Which channel `state` is - see - * audioMixerChannelApplyVolume()'s own comment. - * @param state The channel state to update. - */ -errorret_t audioMixerChannelUpdateEarly( - const audiomixerchannel_t channel, - audiomixerchannelstate_t *state -); - -/** - * Updates a single mixer channel, called at the end of each frame, once - * every other system has had a chance to queue a command via - * audioMixerPlay()/audioMixerPlayLooped()/audioMixerStop(). For a queued - * AUDIO_MIXER_COMMAND_PLAY, locks its file and begins loading it (see - * assetLock()) - giving it from now until the next - * audioMixerChannelUpdateEarly() to finish before that has to block on it. - * Actually starting/stopping playback happens there, not here. - * - * @param state The channel state to update. - */ -errorret_t audioMixerChannelUpdateLate(audiomixerchannelstate_t *state); - -/** - * Trampoline wired onto every stream a channel starts (as that stream's - * onLoop) - forwards to the channel's own onLoop (see - * audiomixerchannelstate_t.onLoop's own comment) via stream->user, which - * audioMixerChannelUpdateEarly() points at the channel's own state. - * - * @param stream The stream that looped. - */ -void audioMixerChannelOnStreamLoop(audiostream_t *stream); - -/** - * Same as audioMixerChannelOnStreamLoop() but wired onto onEnd, forwarding - * to the channel's own onEnd. - * - * @param stream The stream that ended. - */ -void audioMixerChannelOnStreamEnd(audiostream_t *stream); diff --git a/src/dusk/audio/stream/CMakeLists.txt b/src/dusk/audio/stream/CMakeLists.txt deleted file mode 100644 index 81d37fdd..00000000 --- a/src/dusk/audio/stream/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# 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 - audiostream.c - audiostreampcm.c - audiostreammp3.c - audiostreamtype.c -) \ No newline at end of file diff --git a/src/dusk/audio/stream/audiostream.c b/src/dusk/audio/stream/audiostream.c deleted file mode 100644 index 64de5003..00000000 --- a/src/dusk/audio/stream/audiostream.c +++ /dev/null @@ -1,312 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostream.h" -#include "assert/assert.h" -#include "util/math.h" - -errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(asset, "Asset cannot be NULL."); - assertTrue( - asset->state == ASSET_ENTRY_STATE_LOADED, - "Asset must be loaded before it can back an audio stream." - ); - // A caller passing an asset type that can't back an audio stream is a - // programmer error, not something that can happen from untrusted data, so - // this is an assert rather than an errorThrow (see - // feedback_assert_vs_error convention). - assertTrue( - audioStreamTypeForAssetType(asset->type) != AUDIO_STREAM_TYPE_NULL, - "Unsupported asset type for an audio stream." - ); - - stream->state = 0; - stream->volume = 1.0f; - stream->directionality = AUDIO_STREAM_CENTER; - stream->loopStart = -1; - stream->loopTo = 0; - stream->duration = 0; - stream->startFrame = 0; - stream->seeking = false; - stream->user = NULL; - stream->onLoop = NULL; - stream->onEnd = NULL; - stream->loopCount = 0; - stream->lastLoopCount = 0; - stream->loopLimit = 0; - stream->loopRestartCount = 0; - - // Locked for as long as the stream is in use (see audiostream_t.asset's - // own comment) - released in audioStreamDispose(). - stream->asset = asset; - assetEntryLock(asset); - - // Type-specific setup (audioStreamPcmInit() / audioStreamMp3Init()) - // determines stream->type, sets stream->sampleRate/channels, and asks - // the platform implementation to set up its state. - errorret_t ret = asset->type == ASSET_LOADER_TYPE_MP3 - ? audioStreamMp3Init(stream) - : audioStreamPcmInit(stream); - if(errorIsNotOk(ret)) { - assetEntryUnlock(asset); - stream->asset = NULL; - errorChain(ret); - } - - errorOk(); -} - -size_t audioStreamGetTotalFrames(const audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - return stream->type == AUDIO_STREAM_TYPE_MP3 - ? audioStreamMp3GetTotalFrames(stream) - : audioStreamPcmGetTotalFrames(stream); -} - -errorret_t audioStreamSeek(audiostream_t *stream, const size_t frame) { - assertNotNull(stream, "Stream cannot be NULL."); - - errorChain( - stream->type == AUDIO_STREAM_TYPE_MP3 - ? audioStreamMp3Seek(stream, frame) - : audioStreamPcmSeek(stream, frame) - ); - errorOk(); -} - -errorret_t audioStreamRead( - audiostream_t *stream, - int16_t *buffer, - const size_t frameCount, - size_t *outFramesRead -) { - assertNotNull(stream, "Stream cannot be NULL."); - - errorChain( - stream->type == AUDIO_STREAM_TYPE_MP3 - ? audioStreamMp3Read(stream, buffer, frameCount, outFramesRead) - : audioStreamPcmRead(stream, buffer, frameCount, outFramesRead) - ); - errorOk(); -} - -void audioStreamPlay(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - stream->state |= AUDIO_STREAM_STATE_PLAYING; -} - -void audioStreamPause(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - stream->state &= ~AUDIO_STREAM_STATE_PLAYING; -} - -void audioStreamSetPosition(audiostream_t *stream, const float_t position) { - assertNotNull(stream, "Stream cannot be NULL."); - - // Wrap into [0, duration). - float_t t = position; - while(t < 0) t += stream->duration; - while(t >= stream->duration) t -= stream->duration; - - stream->startFrame = (size_t) (t * stream->sampleRate); - stream->seeking = true; - - // Force the next audioStreamUpdate() to re-buffer from startFrame instead - // of continuing whatever was already buffered - see the platform Buffer() - // implementations for how each one applies startFrame; PSP is the one - // exception (see audioStreamSetPosition's own doc comment). - stream->state &= ~AUDIO_STREAM_STATE_BUFFERED; -} - -void audioStreamSetLoopPoints( - audiostream_t *stream, - const float_t loopStart, - const float_t loopTo -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue( - loopStart < 0 || loopTo < loopStart, - "loopTo must be before loopStart." - ); - - stream->loopStart = loopStart; - stream->loopTo = loopTo; -} - -void audioStreamGetPanFactors( - const audiostream_t *stream, - float_t *outLeft, - float_t *outRight -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(outLeft, "outLeft cannot be NULL."); - assertNotNull(outRight, "outRight cannot be NULL."); - - // directionality is already a pan factor in AUDIO_STREAM_LEFT..RIGHT - // (-1.0..1.0) - assertTrue'd here rather than clamped, since a caller - // passing something outside that range is a programmer error (see - // audioStreamSetDirectionality()'s own assert). - const float_t pan = stream->directionality; - - *outLeft = pan > 0 ? (1.0f - pan) : 1.0f; - *outRight = pan < 0 ? (1.0f + pan) : 1.0f; -} - -size_t audioStreamComputeEndFrame( - const audiostream_t *stream, - const size_t startFrame, - const size_t totalFrames -) { - assertNotNull(stream, "Stream cannot be NULL."); - - size_t endFrame = totalFrames; - if((stream->state & AUDIO_STREAM_STATE_LOOPING) && stream->loopStart >= 0) { - endFrame = mathMin( - (size_t) (stream->loopStart * stream->sampleRate), totalFrames - ); - } - if(startFrame >= endFrame) endFrame = totalFrames; - - return endFrame; -} - -void audioStreamSetVolume(audiostream_t *stream, const float_t volume) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(volume >= 0.0f && volume <= 1.0f, "Volume is out of range."); - stream->volume = volume; - // TODO: Do I need to update the device output? PSP may require this -} - -void audioStreamSetDirectionality( - audiostream_t *stream, - const float_t directionality -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue( - directionality >= AUDIO_STREAM_LEFT && directionality <= AUDIO_STREAM_RIGHT, - "Directionality is out of range." - ); - stream->directionality = directionality; - // TODO: Need to update internal decoder? -} - -void audioStreamSetLooping(audiostream_t *stream, const bool_t looping) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(looping) { - stream->state |= AUDIO_STREAM_STATE_LOOPING; - } else { - stream->state &= ~AUDIO_STREAM_STATE_LOOPING; - } -} - -void audioStreamSetLoopLimit(audiostream_t *stream, const uint32_t loopLimit) { - assertNotNull(stream, "Stream cannot be NULL."); - stream->loopLimit = loopLimit; - stream->loopRestartCount = 0; -} - -errorret_t audioStreamUpdate(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->type == AUDIO_STREAM_TYPE_NULL) { - errorOk(); - } - - // Some platforms (PSP) loop entirely on their own background thread for - // gaplessness and can only safely notify onLoop by incrementing this - // counter from there, rather than calling onLoop directly off the main - // thread - see loopCount's own comment. Firing it here, unconditionally, - // catches up to the latest count in one call even if multiple loops - // happened between two Update() calls. - if(stream->loopCount != stream->lastLoopCount) { - stream->lastLoopCount = stream->loopCount; - - if(stream->onLoop != NULL) { - stream->onLoop(stream); - } - } - - // TODO: Once streamed (rather than fully in-memory) sources exist, this is - // where new data would be decoded/read in and re-buffered as playback - // consumes it. - // Checked in this order (finished-check before needs-buffering) so that a - // loop restart falls straight through into re-buffering within this same - // call, instead of leaving BUFFERED cleared for the caller to notice and - // act on next frame - that extra frame of latency was audible as a gap - // at every loop boundary. - if( - (stream->state & AUDIO_STREAM_STATE_PLAYING) && - (stream->state & AUDIO_STREAM_STATE_BUFFERED) && - audioStreamPlatformIsFinished(stream) - ) { - // A limit of 0 means unlimited; otherwise this pass's restart only goes - // ahead while fewer than loopLimit restarts have already happened - - // once it's reached, fall through to the same stop/onEnd path as a - // non-looping stream instead of restarting again. - const bool_t loopLimitReached = stream->loopLimit > 0 && - stream->loopRestartCount >= stream->loopLimit; - - if((stream->state & AUDIO_STREAM_STATE_LOOPING) && !loopLimitReached) { - stream->state &= ~AUDIO_STREAM_STATE_BUFFERED; - stream->loopRestartCount++; - - // Resume from loopTo rather than the very start of the buffer - only - // matters for platforms that reach this generic restart path at all - // (Linux and Dolphin, both of which re-buffer per pass and report - // "finished" at every loop boundary); PSP loops entirely on its own - // read-ahead ring instead (see loopRestartCount's own comment). - stream->startFrame = (size_t) (stream->loopTo * stream->sampleRate); - - if(stream->onLoop != NULL) { - stream->onLoop(stream); - } - } else { - stream->state &= - ~(AUDIO_STREAM_STATE_PLAYING | AUDIO_STREAM_STATE_BUFFERED); - - if(stream->onEnd != NULL) { - stream->onEnd(stream); - } - } - } - - if( - (stream->state & AUDIO_STREAM_STATE_PLAYING) && - !(stream->state & AUDIO_STREAM_STATE_BUFFERED) - ) { - errorChain(audioStreamPlatformBuffer(stream)); - stream->state |= AUDIO_STREAM_STATE_BUFFERED; - } - - errorOk(); -} - -errorret_t audioStreamDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->type != AUDIO_STREAM_TYPE_NULL) { - errorChain(audioStreamPlatformDispose(stream)); - } - - if(stream->type == AUDIO_STREAM_TYPE_PCM) { - errorChain(audioStreamPcmDispose(stream)); - } else if(stream->type == AUDIO_STREAM_TYPE_MP3) { - errorChain(audioStreamMp3Dispose(stream)); - } - - if(stream->asset != NULL) { - assetEntryUnlock(stream->asset); - stream->asset = NULL; - } - - stream->type = AUDIO_STREAM_TYPE_NULL; - - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/audio/stream/audiostream.h b/src/dusk/audio/stream/audiostream.h deleted file mode 100644 index d3c3889a..00000000 --- a/src/dusk/audio/stream/audiostream.h +++ /dev/null @@ -1,360 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/stream/audiostreamtype.h" -#include "audio/stream/audiostreampcm.h" -#include "audio/stream/audiostreammp3.h" -#include "audio/audiostreamplatform.h" -#include "asset/loader/assetentry.h" - -#ifndef audioStreamPlatformInit - #error "audioStreamPlatformInit is not defined" -#endif - -#ifndef audioStreamPlatformDispose - #error "audioStreamPlatformDispose is not defined" -#endif - -#ifndef audioStreamPlatformBuffer - #error "audioStreamPlatformBuffer is not defined" -#endif - -#ifndef audioStreamPlatformIsFinished - #error "audioStreamPlatformIsFinished is not defined" -#endif - -#define AUDIO_STREAMS_MAX 8 -#define AUDIO_STREAM_STATE_PLAYING (1 << 0) -#define AUDIO_STREAM_STATE_LOOPING (1 << 1) -#define AUDIO_STREAM_STATE_BUFFERED (1 << 2) -#define AUDIO_STREAM_CENTER 0.0f -#define AUDIO_STREAM_LEFT -1.0f -#define AUDIO_STREAM_RIGHT 1.0f - -typedef struct audiostream_s audiostream_t; - -typedef struct audiostream_s { - // Used for aquiring new data. - audistreamtype_t type; - - // What state the stream is in. volatile: PSP's output thread reads - // AUDIO_STREAM_STATE_PLAYING directly to honor a pause promptly instead - // of only reacting once the main thread's own per-frame gate notices - - // see audioStreamPSPThreadFeed()'s own comment. - volatile uint8_t state; - - // The asset backing this stream's data (e.g. a WAV file) - determines - // `type` (see audioStreamInit()) and is read from on demand by the - // type-specific module (e.g. audiostreampcm.h) rather than ever being - // fully decoded into memory up front. The stream holds its own lock on - // this entry (assetEntryLock()/assetEntryUnlock()) for as long as it's - // in use, independent of whatever lock(s) the caller that requested - // playback may also be holding. - assetentry_t *asset; - - // Loudness, from 0.0 (silent) to 1.0 (loudest). - float_t volume; - - // In stereo space, where do we send the audio - from AUDIO_STREAM_LEFT - // (-1.0) to AUDIO_STREAM_RIGHT (1.0), AUDIO_STREAM_CENTER (0.0) by - // default. - // TODO: Can we use a 3D vector + Pro Logic II? - float_t directionality; - - // In seconds, where the loop segment ends and playback jumps back to - // loopTo - -1 (the default) means "at the end of the stream", i.e. loop - // the whole buffer. Set via audioStreamSetLoopPoints(). - float_t loopStart; - - // In seconds, where a loop jumps back to once it reaches loopStart. - // Defaults to 0 (start of stream). Set via audioStreamSetLoopPoints(). - float_t loopTo; - - // Cached duration of the stream in seconds. - float_t duration; - - // Frame offset the next platform Buffer() call should start playback - // from - set by audioStreamSetPosition() (an explicit seek) and by - // audioStreamUpdate() itself (to loopTo's frame offset, for platforms - // that re-enter Buffer() on every loop restart rather than looping - // natively). Each platform's Buffer()-invoking entry point must read and - // reset this to 0 synchronously, in the same call that decided to - // (re)buffer - not later/asynchronously (e.g. from a feeder thread), - // since audioStreamUpdate() may already have moved on to something else - // that touches this field by the time an async reader gets to it. - size_t startFrame; - - // True when startFrame came from an explicit audioStreamSetPosition() - // seek rather than a natural loop restart. Platforms whose Buffer() call - // can leave previously-queued audio still playing underneath the new - // pass (currently just Linux's SDL queue, kept deliberately overlapping - // across a loop restart to avoid a gap) need this to tell "jump now, - // discarding whatever's still playing" (seek) apart from "let the old - // tail keep playing while the new pass queues underneath it" (loop - // restart) - both look identical as just "a pending startFrame" - // otherwise. Consumed (reset to false) the same way as startFrame. - bool_t seeking; - - // Callbacks - void *user; - void (*onLoop)(audiostream_t *stream); - void (*onEnd)(audiostream_t *stream); - - // Incremented by platform code (from whatever thread/context it runs in) - // each time a loop happens, instead of calling onLoop directly - onLoop - // may do arbitrary, possibly-slow work (console printing, game logic), - // which is only safe to run from the main thread inside audioStreamUpdate(). - // Calling it straight from a real-time audio thread risks starving the - // hardware buffer if it takes too long - confirmed as the real cause of - // a loud crackle on PSP once its feeder thread called onLoop inline. - volatile uint32_t loopCount; - - // audioStreamUpdate()'s own record of the last loopCount it fired - // onLoop for - only ever touched from the main thread. - uint32_t lastLoopCount; - - // Maximum number of times this stream is allowed to loop before stopping - // (firing onEnd) instead of restarting again - 0 (the default) means loop - // forever. Has no effect unless looping is also enabled via - // audioStreamSetLooping(). Set via audioStreamSetLoopLimit(). - uint32_t loopLimit; - - // How many loop restarts this stream has already committed to - compared - // against loopLimit each time a restart is considered, then incremented - // if it goes ahead; reset to 0 by audioStreamSetLoopLimit(). Distinct - // from loopCount (which tracks when a loop becomes audible, purely for - // onLoop notification, and is PSP-only) - this counts decided restarts, - // main-thread-only on every platform (PSP decides whether to loop from - // its own read-ahead ring, itself driven from the main thread via - // audioStreamPSPIsFinished() - see its own comment). - uint32_t loopRestartCount; - - // PCM format of the stream's decoded output - read identically by every - // platform backend regardless of stream type (PCM decodes straight - // through; MP3 decodes to this same format), so it lives here rather - // than duplicated in both audiostreampcm_t and audiostreammp3_t. Set by - // whichever type-specific Init function runs (audioStreamPcmInit() / - // audioStreamMp3Init()). - uint32_t sampleRate; - uint8_t channels; - - // Stream type specific data. - union { - audiostreampcm_t pcm; - audiostreammp3_t mp3; - }; - - // Platform-specific playback state (e.g. SDL2 device, PSP channel, ASND - // voice). Defined by each platform's audiostreamplatform.h. - audiostreamplatform_t platform; -} audiostream_t; - -/** - * Initializes an audio stream to play the given asset, determining the - * stream's type from the asset's loader type (e.g. a WAV asset becomes an - * AUDIO_STREAM_TYPE_PCM stream) and dispatching to that type's own setup - * (e.g. audioStreamPcmInit()). Does not begin playback; call - * audioStreamPlay() once this returns. - * - * The stream takes its own lock on `asset` (see audiostream_t.asset's own - * comment), released by audioStreamDispose() - the asset must already be - * loaded (ASSET_ENTRY_STATE_LOADED) when this is called. - * - * @param stream The audio stream to initialize. - * @param asset The loaded asset to play - its type must be one this - * function supports (ASSET_LOADER_TYPE_WAV or - * ASSET_LOADER_TYPE_MP3). - * @return Error indicating success or failure. - */ -errorret_t audioStreamInit(audiostream_t *stream, assetentry_t *asset); - -/** - * Returns the total number of decoded PCM frames (one sample per channel) - * available in the stream's underlying data, regardless of its type - - * dispatches to audioStreamPcmGetTotalFrames() or - * audioStreamMp3GetTotalFrames(). Platform backends should use this rather - * than a type-specific function directly, so they don't need to know or - * care which type they're driving. - * - * @param stream The audio stream to query. - * @return The total number of frames available. - */ -size_t audioStreamGetTotalFrames(const audiostream_t *stream); - -/** - * Seeks the stream's decode position to the given frame offset, regardless - * of its type - dispatches to audioStreamPcmSeek() or audioStreamMp3Seek(). - * - * @param stream The audio stream to seek. - * @param frame Frame offset to seek to, relative to the start of the - * stream's decoded data. - * @return Error indicating success or failure. - */ -errorret_t audioStreamSeek(audiostream_t *stream, const size_t frame); - -/** - * Reads up to frameCount frames of decoded PCM sample data from the - * stream's current position, regardless of its type - dispatches to - * audioStreamPcmRead() or audioStreamMp3Read(). See either for the exact - * short-read-at-end-of-stream contract, which is identical for both. - * - * @param stream The audio stream to read from. - * @param buffer Destination buffer, sized for at least frameCount frames. - * @param frameCount Maximum number of frames to read. - * @param outFramesRead Set to the number of frames actually read. - * @return Error indicating success or failure. - */ -errorret_t audioStreamRead( - audiostream_t *stream, - int16_t *buffer, - const size_t frameCount, - size_t *outFramesRead -); - -/** - * Begins or resumes playback of the given audio stream. - * - * @param stream The audio stream to play. - */ -void audioStreamPlay(audiostream_t *stream); - -/** - * Pauses playback of the given audio stream. Playback position is retained, - * so audioStreamPlay() resumes from the same point. - * - * @param stream The audio stream to pause. - */ -void audioStreamPause(audiostream_t *stream); - -/** - * Seeks the given audio stream to the given position, in seconds, wrapping - * into range if the position is outside [0, duration). Takes effect on the - * next audioStreamUpdate() call for platforms that re-buffer per frame - * (e.g. Linux); on PSP, which manages an entire playback pass on its own - * feeder thread once started, a seek only takes effect the next time the - * stream begins playing from a stopped state, not instantaneously mid-pass. - * - * @param stream The audio stream to seek. - * @param position The new playback position, in seconds. - */ -void audioStreamSetPosition(audiostream_t *stream, const float_t position); - -/** - * Sets the loop region for the given audio stream: once playback reaches - * loopStart, it jumps back to loopTo instead of continuing (or stopping, - * if not looping). Has no effect unless looping is also enabled via - * audioStreamSetLooping(). - * - * @param stream The audio stream to update. - * @param loopStart Where the loop segment ends, in seconds, or -1 to loop - * the whole stream (the default). - * @param loopTo Where the loop segment starts, in seconds. - */ -void audioStreamSetLoopPoints( - audiostream_t *stream, - const float_t loopStart, - const float_t loopTo -); - -/** - * Computes normalized left/right pan factors (0..1) for the given audio - * stream's directionality. Callers multiply these by their own - * platform-specific base volume representation. - * - * @param stream The audio stream to read directionality from. - * @param outLeft Set to the left channel's pan factor. - * @param outRight Set to the right channel's pan factor. - */ -void audioStreamGetPanFactors( - const audiostream_t *stream, - float_t *outLeft, - float_t *outRight -); - -/** - * Computes the stop frame for a playback pass starting at startFrame: the - * current loop segment's end (stream->loopStart, converted to frames and - * clamped to totalFrames) if looping is enabled and a loop start is set, - * or totalFrames itself otherwise - including when startFrame has already - * reached or passed the computed loop end (e.g. a seek landing in an - * outro past the loop point), in which case this pass plays out to the - * true end of the stream once instead of only the loop segment. - * - * @param stream The audio stream to compute this for. - * @param startFrame This pass's starting frame offset. - * @param totalFrames The stream's total decoded frame count - * (audioStreamGetTotalFrames()). - * @return This pass's stop frame offset. - */ -size_t audioStreamComputeEndFrame( - const audiostream_t *stream, - const size_t startFrame, - const size_t totalFrames -); - -/** - * Sets the playback volume of the given audio stream. - * - * @param stream The audio stream to update. - * @param volume The new volume, from 0.0 (silent) to 1.0 (loudest). - */ -void audioStreamSetVolume(audiostream_t *stream, const float_t volume); - -/** - * Sets the stereo directionality (panning) of the given audio stream. - * - * @param stream The audio stream to update. - * @param directionality The new panning value, from AUDIO_STREAM_LEFT to - * AUDIO_STREAM_RIGHT. - */ -void audioStreamSetDirectionality( - audiostream_t *stream, - const float_t directionality -); - -/** - * Sets whether the given audio stream loops back to loopTo (or the start - * of the buffer, by default - see audioStreamSetLoopPoints()) when it - * reaches loopStart (or the end of the buffer), rather than stopping and - * firing onEnd. - * - * @param stream The audio stream to update. - * @param looping Whether the stream should loop. - */ -void audioStreamSetLooping(audiostream_t *stream, const bool_t looping); - -/** - * Sets the maximum number of times the given audio stream is allowed to - * loop before stopping (firing onEnd) instead of restarting again. Has no - * effect unless looping is also enabled via audioStreamSetLooping(). Resets - * the stream's current loop-restart progress back to 0, so the stream gets - * the full count from this call onward. - * - * @param stream The audio stream to update. - * @param loopLimit Maximum number of loop restarts, or 0 (the default) to - * loop forever. - */ -void audioStreamSetLoopLimit(audiostream_t *stream, const uint32_t loopLimit); - -/** - * Updates the given audio stream, decoding new data and advancing playback - * as needed. Should be called every frame for every active stream. - * - * @param stream The audio stream to update. - * @return Error indicating success or failure. - */ -errorret_t audioStreamUpdate(audiostream_t *stream); - -/** - * Disposes the audio stream, stopping playback and releasing any resources - * associated with it. - * - * @param stream The audio stream to dispose. - * @return Error indicating success or failure. - */ -errorret_t audioStreamDispose(audiostream_t *stream); \ No newline at end of file diff --git a/src/dusk/audio/stream/audiostreammp3.c b/src/dusk/audio/stream/audiostreammp3.c deleted file mode 100644 index 84c682a8..00000000 --- a/src/dusk/audio/stream/audiostreammp3.c +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreammp3.h" -#include "audiostream.h" -#include "assert/assert.h" -#include "util/math.h" -#include "util/memory.h" - -errorret_t audioStreamMp3Init(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(stream->asset, "Stream must have an asset assigned."); - assertTrue( - stream->asset->type == ASSET_LOADER_TYPE_MP3, - "Asset is not an MP3 file." - ); - - const assetmp3file_t *mp3 = &stream->asset->data.mp3; - - stream->type = AUDIO_STREAM_TYPE_MP3; - stream->sampleRate = mp3->sampleRate; - stream->channels = mp3->channels; - stream->duration = (float_t) mp3->totalFrames / (float_t) mp3->sampleRate; - - stream->mp3.pendingFrames = 0; - stream->mp3.pendingPosition = 0; - stream->mp3.position = 0; - stream->mp3.totalFrames = mp3->totalFrames; - - // Each stream opens its own independent handle to the same underlying - // asset file - see audiostreampcm.c's audioStreamPcmInit() for why. - errorChain(assetFileInit( - &stream->mp3.file, stream->asset->name, NULL, NULL - )); - errorChain(assetFileOpen(&stream->mp3.file)); - errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset)); - - errorChain(audioStreamMp3DecoderInit(stream)); - - errorChain(audioStreamPlatformInit(stream)); - - errorOk(); -} - -errorret_t audioStreamMp3Dispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - errorChain(audioStreamMp3DecoderDispose(stream)); - errorChain(assetFileClose(&stream->mp3.file)); - errorChain(assetFileDispose(&stream->mp3.file)); - - errorOk(); -} - -size_t audioStreamMp3GetTotalFrames(const audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - return stream->mp3.totalFrames; -} - -errorret_t audioStreamMp3Seek(audiostream_t *stream, const size_t frame) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - if(frame == stream->mp3.position) { - errorOk(); - } - - // Every seek - forward or backward - restarts decoding from the very - // start of the compressed stream and discards frames until reaching the - // target; see this file's own header comment for why MP3 has no cheaper - // option, unlike audioStreamPcmSeek()'s direct byte-offset seek. - errorChain(audioStreamMp3DecoderRewind(stream)); - stream->mp3.position = 0; - stream->mp3.pendingFrames = 0; - stream->mp3.pendingPosition = 0; - - const size_t channels = stream->channels; - int16_t discard[AUDIO_MP3_MAX_SAMPLES_PER_FRAME]; - - while(stream->mp3.position < frame) { - size_t decodedFrames = 0; - errorChain( - audioStreamMp3DecoderDecodeFrame(stream, discard, &decodedFrames) - ); - if(decodedFrames == 0) { - // Ran out of stream before reaching the target - clamp rather than - // erroring, same tolerance audioStreamPcmRead() has for a - // shorter-than-declared asset. - break; - } - - const size_t framesNeeded = frame - stream->mp3.position; - if(decodedFrames <= framesNeeded) { - stream->mp3.position += decodedFrames; - continue; - } - - // This frame overshoots the target - keep the excess as pending so - // the next Read() starts exactly at `frame` instead of skipping past - // it. - const size_t keepFrames = decodedFrames - framesNeeded; - memoryMove( - discard, discard + (framesNeeded * channels), - keepFrames * channels * sizeof(int16_t) - ); - memoryCopy( - stream->mp3.pending, discard, keepFrames * channels * sizeof(int16_t) - ); - stream->mp3.pendingFrames = keepFrames; - stream->mp3.pendingPosition = 0; - stream->mp3.position = frame; - } - - errorOk(); -} - -errorret_t audioStreamMp3Read( - audiostream_t *stream, - int16_t *buffer, - const size_t frameCount, - size_t *outFramesRead -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(buffer, "Buffer cannot be NULL."); - assertNotNull(outFramesRead, "outFramesRead cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - const size_t channels = stream->channels; - size_t framesWritten = 0; - - while(framesWritten < frameCount) { - if(stream->mp3.pendingPosition >= stream->mp3.pendingFrames) { - size_t decodedFrames = 0; - errorChain(audioStreamMp3DecoderDecodeFrame( - stream, stream->mp3.pending, &decodedFrames - )); - stream->mp3.pendingFrames = decodedFrames; - stream->mp3.pendingPosition = 0; - if(decodedFrames == 0) break; // Genuine end of stream. - } - - const size_t framesAvailable = - stream->mp3.pendingFrames - stream->mp3.pendingPosition; - const size_t framesToCopy = - mathMin(frameCount - framesWritten, framesAvailable); - - memoryCopy( - buffer + (framesWritten * channels), - stream->mp3.pending + (stream->mp3.pendingPosition * channels), - framesToCopy * channels * sizeof(int16_t) - ); - - stream->mp3.pendingPosition += framesToCopy; - framesWritten += framesToCopy; - } - - stream->mp3.position += framesWritten; - *outFramesRead = framesWritten; - errorOk(); -} diff --git a/src/dusk/audio/stream/audiostreammp3.h b/src/dusk/audio/stream/audiostreammp3.h deleted file mode 100644 index a7b5563f..00000000 --- a/src/dusk/audio/stream/audiostreammp3.h +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "asset/assetfile.h" -#include "audio/audiostreammp3decoder.h" - -typedef struct audiostream_s audiostream_t; - -// How many interleaved PCM samples (not frames - samples, i.e. frames * -// channels) a single audioStreamMp3DecoderDecodeFrame() call is ever -// allowed to produce. Sizes the pending-sample buffer below, which every -// decoder backend writes directly into - this must be sized to the -// largest of them, not just "one MPEG frame": -// - libmad (software, Linux/Dolphin): always exactly one frame, 1152 -// samples/channel for MPEG-1 Layer III, doubled here for stereo. -// - sceMp3 (hardware, PSP): its own pcmBuf is provisioned at double that -// (see AUDIO_MP3_PSP_PCM_BUF_SIZE) - i.e. sceMp3Decode() can -// legitimately hand back more than one frame's worth in a single call. -// Sizing this for only one frame silently overflowed stream->mp3.pending -// on real hardware whenever that happened - confirmed as the cause of -// very intermittent audio corruption/clicking, since it only bit when -// sceMp3 actually returned the larger amount. -// So this covers the larger (PSP) case; libmad's decode is safely well -// within it. -#define AUDIO_MP3_MAX_SAMPLES_PER_FRAME (1152 * 2 * 2) - -typedef struct { - // This stream's own private handle into its asset's underlying file - - // see audiostreampcm_t.file's own comment for why this isn't shared - // across streams playing the same asset. Holds compressed MP3 bytes; - // the decoder backend (audiostreammp3decoder.h) reads from it directly. - assetfile_t file; - - // Opaque per-platform decoder state - a reserved sceMp3 handle plus its - // buffers on PSP, or libmad's mad_stream/mad_frame/mad_synth state plus a - // sliding compressed-byte window on the libmad-based software backend - // (Linux/Dolphin). Defined by whichever audiostreammp3decoder.h is - // actually visible when this file is compiled - see that header's own - // comment. - audiostreammp3decoder_t decoder; - - // One decoded MPEG frame's worth of PCM, held here across Read() calls - // since a frame (576 or 1152 samples/channel) rarely lines up exactly - // with whatever frameCount a caller asks for. pendingPosition marks how - // many of the first pendingFrames frames have already been consumed. - int16_t pending[AUDIO_MP3_MAX_SAMPLES_PER_FRAME]; - size_t pendingFrames; - size_t pendingPosition; - - // Current logical PCM frame position, relative to the start of the - // decoded stream - decoding is push-forward-only (see - // audioStreamMp3Seek()'s own comment on why a backward seek re-decodes - // from the start rather than truly random-accessing). - size_t position; - - // Total decoded PCM frame count for the whole clip - copied from the - // asset's parsed metadata at Init (see assetmp3file_t.totalFrames), - // exact if a Xing/Info header was present, otherwise an estimate. - size_t totalFrames; -} audiostreammp3_t; - -/** - * Configures the given MP3 audio stream from its already-assigned asset - * (see audiostream_t.asset - set by audioStreamInit(), which is what - * should be calling this, not application code directly): opens this - * stream's own private file handle onto the asset's compressed MPEG data, - * and initializes the platform decoder backend - * (audioStreamMp3DecoderInit()). - * - * Must be called after stream->asset is set and before audioStreamPlay(). - * - * @param stream The audio stream to configure. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3Init(audiostream_t *stream); - -/** - * Disposes the given MP3 audio stream's decoder backend and private file - * handle. - * - * @param stream The audio stream to dispose. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3Dispose(audiostream_t *stream); - -/** - * Returns the total number of frames (one sample per channel) available in - * the stream's underlying MP3 data - see assetmp3file_t.totalFrames's own - * comment on why this is sometimes an estimate rather than an exact count, - * unlike audioStreamPcmGetTotalFrames(). - * - * @param stream The audio stream to query. Must be AUDIO_STREAM_TYPE_MP3. - * @return The total number of frames available. - */ -size_t audioStreamMp3GetTotalFrames(const audiostream_t *stream); - -/** - * Seeks the stream's decode position to the given frame offset. Unlike - * audioStreamPcmSeek(), this is never cheap: MPEG frames aren't - * independently decodable (each one's bit reservoir can depend on data - * carried over from prior frames), so there's no equivalent of PCM's - * direct byte-offset seek - every call, forward or backward, rewinds the - * decoder to the very start of the compressed stream and decodes (and - * discards) frames until reaching the target. Acceptable for this engine's - * actual use (looping background music, where loopTo is typically near - * the start anyway), but a real, non-constant cost for a seek deep into a - * long track. - * - * @param stream The audio stream to seek. Must be AUDIO_STREAM_TYPE_MP3. - * @param frame Frame offset to seek to, relative to the start of the - * decoded stream. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3Seek(audiostream_t *stream, const size_t frame); - -/** - * Reads up to frameCount frames of PCM sample data from the stream's - * current position, advancing it by however many frames were actually - * read. Reads fewer than frameCount (down to zero) once the underlying - * MP3 data is exhausted, rather than erroring - same contract as - * audioStreamPcmRead(). - * - * @param stream The audio stream to read from. Must be AUDIO_STREAM_TYPE_MP3. - * @param buffer Destination buffer, sized for at least frameCount frames. - * @param frameCount Maximum number of frames to read. - * @param outFramesRead Set to the number of frames actually read. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3Read( - audiostream_t *stream, - int16_t *buffer, - const size_t frameCount, - size_t *outFramesRead -); diff --git a/src/dusk/audio/stream/audiostreampcm.c b/src/dusk/audio/stream/audiostreampcm.c deleted file mode 100644 index c29492a0..00000000 --- a/src/dusk/audio/stream/audiostreampcm.c +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreampcm.h" -#include "audiostream.h" -#include "assert/assert.h" -#include "util/math.h" -#include "util/memory.h" -#include "util/endian.h" - -errorret_t audioStreamPcmInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(stream->asset, "Stream must have an asset assigned."); - assertTrue( - stream->asset->type == ASSET_LOADER_TYPE_WAV, - "Asset is not a WAV file." - ); - - assetwavfile_t *wav = &stream->asset->data.wav; - - stream->type = AUDIO_STREAM_TYPE_PCM; - stream->sampleRate = wav->sampleRate; - stream->channels = wav->channels; - stream->duration = ( - (float_t) audioStreamPcmGetTotalFrames(stream) / - (float_t) wav->sampleRate - ); - - // Each stream opens its own independent handle to the same underlying - // asset file, rather than sharing a single handle on the asset entry - - // multiple streams playing the same asset concurrently (e.g. two - // simultaneous plays of the same sound effect) would otherwise fight - // over one shared read position. This isn't the final answer for that - // (a shared decode/cache layer would scale better than N independent - // decompression streams of the same data), but it's a correct one for - // now - a problem to revisit once it actually matters. - errorChain(assetFileInit( - &stream->pcm.file, stream->asset->name, NULL, NULL - )); - errorChain(assetFileOpen(&stream->pcm.file)); - errorChain(assetFileRead(&stream->pcm.file, NULL, wav->dataOffset)); - - errorChain(audioStreamPlatformInit(stream)); - - errorOk(); -} - -errorret_t audioStreamPcmDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM."); - - errorChain(assetFileClose(&stream->pcm.file)); - errorChain(assetFileDispose(&stream->pcm.file)); - - errorOk(); -} - -size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM."); - - const assetwavfile_t *wav = &stream->asset->data.wav; - const size_t sourceFrameSize = ( - stream->channels * (wav->bitsPerSample / 8) - ); - return wav->dataSize / sourceFrameSize; -} - -errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM."); - - assetwavfile_t *wav = &stream->asset->data.wav; - const size_t sourceFrameSize = ( - stream->channels * (wav->bitsPerSample / 8) - ); - const size_t targetByte = wav->dataOffset + (frame * sourceFrameSize); - - assertTrue( - frame * sourceFrameSize <= wav->dataSize, - "Seek frame is beyond the end of the PCM data." - ); - - const size_t currentByte = (size_t) stream->pcm.file.position; - if(targetByte < currentByte) { - // A rewind is needed to move the read cursor earlier once it's - // already advanced past a point - assetFileRewind() takes the cheap - // in-place seek path since every dusk.dsk entry is stored - // uncompressed. - errorChain(assetFileRewind(&stream->pcm.file)); - errorChain(assetFileRead(&stream->pcm.file, NULL, targetByte)); - } else if(targetByte > currentByte) { - errorChain(assetFileRead( - &stream->pcm.file, NULL, targetByte - currentByte - )); - } - - errorOk(); -} - -errorret_t audioStreamPcmRead( - audiostream_t *stream, - int16_t *buffer, - const size_t frameCount, - size_t *outFramesRead -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(buffer, "Buffer cannot be NULL."); - assertNotNull(outFramesRead, "outFramesRead cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_PCM, "Stream is not PCM."); - - assetwavfile_t *wav = &stream->asset->data.wav; - const size_t channels = stream->channels; - const size_t sourceSampleBytes = wav->bitsPerSample / 8; - const size_t sourceFrameSize = channels * sourceSampleBytes; - const size_t dataEndByte = wav->dataOffset + wav->dataSize; - const size_t currentByte = (size_t) stream->pcm.file.position; - - const size_t bytesAvailable = ( - currentByte < dataEndByte ? dataEndByte - currentByte : 0 - ); - const size_t framesAvailable = bytesAvailable / sourceFrameSize; - const size_t framesToRead = mathMin(frameCount, framesAvailable); - - if(framesToRead == 0) { - *outFramesRead = 0; - errorOk(); - } - - if(sourceSampleBytes == sizeof(int16_t)) { - // Output format already matches the source in width, but WAV samples - // are always little-endian per the format spec regardless of host - - // read straight through, then fix up byte order in place. A no-op on - // every little-endian platform (endianLittleToHost16() compiles down - // to returning its argument unchanged there) - only actually swaps - // anything on a big-endian host (Dolphin/GameCube/Wii). - errorChain(assetFileRead( - &stream->pcm.file, buffer, framesToRead * sourceFrameSize - )); - const size_t sampleCount = framesToRead * channels; - for(size_t i = 0; i < sampleCount; i++) { - buffer[i] = (int16_t) endianLittleToHost16((uint16_t) buffer[i]); - } - } else { - // 24-bit source (the only other width assetWavParseHeader() accepts) - - // read the raw 3-byte samples into a scratch buffer, then truncate - // each down to 16-bit, since none of PSP/Dolphin's audio hardware (or - // SDL2 on Linux) accepts anything wider - see assetwavfile_t's own - // comment on bitsPerSample. - assertTrue(sourceSampleBytes == 3, "Unsupported PCM sample width."); - - const size_t rawBytes = framesToRead * sourceFrameSize; - uint8_t *raw = memoryAllocate(rawBytes); - errorret_t ret = assetFileRead(&stream->pcm.file, raw, rawBytes); - if(errorIsNotOk(ret)) { - memoryFree(raw); - errorChain(ret); - } - - // 24-bit PCM samples are little-endian two's complement - the top two - // bytes of each (indices 1 and 2) already form that same value - // truncated to 16-bit (equivalent to an arithmetic right-shift by 8 - // bits, which preserves the sign correctly since byte 2 carries it). - const size_t sampleCount = framesToRead * channels; - for(size_t i = 0; i < sampleCount; i++) { - const uint8_t *sample = raw + (i * 3); - buffer[i] = (int16_t) (sample[1] | (sample[2] << 8)); - } - - memoryFree(raw); - } - - *outFramesRead = framesToRead; - errorOk(); -} diff --git a/src/dusk/audio/stream/audiostreampcm.h b/src/dusk/audio/stream/audiostreampcm.h deleted file mode 100644 index 0fc5e02c..00000000 --- a/src/dusk/audio/stream/audiostreampcm.h +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "asset/assetfile.h" - -typedef struct audiostream_s audiostream_t; - -typedef struct { - // This stream's own private handle into its asset's underlying file - - // deliberately not shared with any other stream reading the same asset - // (see audioStreamPcmInit's own comment for why). Its read cursor is - // what audioStreamPcmRead()/audioStreamPcmSeek() operate on. - assetfile_t file; -} audiostreampcm_t; - -/** - * Configures the given PCM audio stream from its already-assigned asset - * (see audiostream_t.asset - set by audioStreamInit(), which is what - * should be calling this, not application code directly) rather than - * taking raw decoded data: sampleRate/channels/duration are read from the - * asset's parsed WAV header, and a private file handle is opened for - * reading sample data on demand as playback consumes it (see - * audioStreamPcmRead()) - the PCM data itself is never read into memory - * all at once, so this works the same regardless of how long the - * underlying clip is. - * - * Must be called after stream->asset is set and before audioStreamPlay(). - * - * @param stream The audio stream to configure. - * @return Error indicating success or failure. - */ -errorret_t audioStreamPcmInit(audiostream_t *stream); - -/** - * Disposes the given PCM audio stream's own private file handle. - * - * @param stream The audio stream to dispose. - * @return Error indicating success or failure. - */ -errorret_t audioStreamPcmDispose(audiostream_t *stream); - -/** - * Returns the total number of frames (one sample per channel) available - * in the stream's underlying PCM data - NOT simply the asset's declared - * byte size divided by a 16-bit frame size, since the source data isn't - * always 16-bit even though audioStreamPcmRead() always produces 16-bit - * output (see assetwavfile_t.bitsPerSample). Platform backends should use - * this rather than computing frame counts from dataSize themselves. - * - * @param stream The audio stream to query. Must be AUDIO_STREAM_TYPE_PCM. - * @return The total number of frames available. - */ -size_t audioStreamPcmGetTotalFrames(const audiostream_t *stream); - -/** - * Seeks the stream's private read cursor to the given frame offset - * (relative to the start of the PCM data - the same units as - * audiostream_t's startFrame/loopStart/loopTo, once converted from - * seconds). Seeking backward re-reads from the start of the underlying - * asset file (see assetFileRewind()), which is a cheap in-place seek - * since every dusk.dsk entry is stored uncompressed. - * - * @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 - * data. - * @return Error indicating success or failure. - */ -errorret_t audioStreamPcmSeek(audiostream_t *stream, const size_t frame); - -/** - * Reads up to frameCount frames of PCM sample data from the stream's - * current read position, advancing it by however many frames were - * actually read. Reads fewer than frameCount (down to zero) once the - * underlying asset's PCM data is exhausted, rather than erroring - it's - * up to the caller (platform code, which already knows about loop points) - * to request no more than what it wants read from within the current - * loop segment or the true end of the clip. - * - * @param stream The audio stream to read from. Must be AUDIO_STREAM_TYPE_PCM. - * @param buffer Destination buffer, sized for at least frameCount frames. - * @param frameCount Maximum number of frames to read. - * @param outFramesRead Set to the number of frames actually read. - * @return Error indicating success or failure. - */ -errorret_t audioStreamPcmRead( - audiostream_t *stream, - int16_t *buffer, - const size_t frameCount, - size_t *outFramesRead -); diff --git a/src/dusk/audio/stream/audiostreamtype.c b/src/dusk/audio/stream/audiostreamtype.c deleted file mode 100644 index ccccd7f2..00000000 --- a/src/dusk/audio/stream/audiostreamtype.c +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreamtype.h" -#include "util/string.h" - -const audiostreamassettypeinfo_t -AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[ASSET_LOADER_TYPE_COUNT] = { - [ASSET_LOADER_TYPE_WAV] = { - .type = AUDIO_STREAM_TYPE_PCM, .extension = ".wav" - }, - [ASSET_LOADER_TYPE_MP3] = { - .type = AUDIO_STREAM_TYPE_MP3, .extension = ".mp3" - } -}; - -assetloadertype_t audioStreamAssetTypeForPath(const char_t *path) { - for(assetloadertype_t i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) { - const char_t *extension = AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[i].extension; - if(extension != NULL && stringEndsWithCaseInsensitive(path, extension)) { - return i; - } - } - - return ASSET_LOADER_TYPE_NULL; -} - -audistreamtype_t audioStreamTypeForAssetType( - const assetloadertype_t assetType -) { - return AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[assetType].type; -} - -assetloadertype_t audioStreamAssetTypeForStreamType( - const audistreamtype_t streamType -) { - if(streamType == AUDIO_STREAM_TYPE_NULL) return ASSET_LOADER_TYPE_NULL; - - for(assetloadertype_t i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) { - if(AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[i].type == streamType) { - return i; - } - } - - return ASSET_LOADER_TYPE_NULL; -} diff --git a/src/dusk/audio/stream/audiostreamtype.h b/src/dusk/audio/stream/audiostreamtype.h deleted file mode 100644 index d2f31d0a..00000000 --- a/src/dusk/audio/stream/audiostreamtype.h +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "asset/loader/assetloader.h" - -typedef enum { - AUDIO_STREAM_TYPE_NULL, - AUDIO_STREAM_TYPE_PCM, - AUDIO_STREAM_TYPE_MP3, - AUDIO_STREAM_TYPE_COUNT -} audistreamtype_t; - -typedef struct { - // The audistreamtype_t an audio stream backed by this asset type should - // become - AUDIO_STREAM_TYPE_NULL (the zero-initialized default) for any - // asset type that can't back an audio stream. - audistreamtype_t type; - - // This asset type's file extension, including the leading '.' (e.g. - // ".wav"). NULL for any asset type that can't back an audio stream. - const char_t *extension; -} audiostreamassettypeinfo_t; - -// Maps an assetloadertype_t to the info describing the audio stream it -// backs. Indexed directly by assetentry_t.type; see audioStreamInit()'s own -// use of this for the canonical "is this asset type supported" check. Use -// the functions below rather than indexing this directly, except where -// you already have an assetloadertype_t in hand. -extern const audiostreamassettypeinfo_t -AUDIO_STREAM_TYPE_FOR_ASSET_TYPE[ASSET_LOADER_TYPE_COUNT]; - -/** - * Determines which asset loader type a file path's extension corresponds - * to, by matching it against AUDIO_STREAM_TYPE_FOR_ASSET_TYPE's own - * extensions (case-insensitively). - * - * @param path The file path to inspect, e.g. "audio/boa.mp3". - * @return The matching assetloadertype_t, or ASSET_LOADER_TYPE_NULL if the - * path's extension doesn't match any audio asset type. - */ -assetloadertype_t audioStreamAssetTypeForPath(const char_t *path); - -/** - * Looks up the audistreamtype_t an audio stream backed by the given asset - * type should become. - * - * @param assetType The asset loader type to look up. - * @return The matching audistreamtype_t, or AUDIO_STREAM_TYPE_NULL if - * assetType can't back an audio stream. - */ -audistreamtype_t audioStreamTypeForAssetType(const assetloadertype_t assetType); - -/** - * Looks up which asset loader type backs audio streams of the given type - - * the reverse of audioStreamTypeForAssetType(). Scans - * AUDIO_STREAM_TYPE_FOR_ASSET_TYPE for the first entry whose type matches, - * since it isn't itself indexed by audistreamtype_t. - * - * @param streamType The audio stream type to look up. - * @return The matching assetloadertype_t, or ASSET_LOADER_TYPE_NULL if - * streamType is AUDIO_STREAM_TYPE_NULL or otherwise unmapped. - */ -assetloadertype_t audioStreamAssetTypeForStreamType( - const audistreamtype_t streamType -); diff --git a/src/dusk/console/CMakeLists.txt b/src/dusk/console/CMakeLists.txt deleted file mode 100644 index 2668e0b3..00000000 --- a/src/dusk/console/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - console.c -) diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c deleted file mode 100644 index edbe3b4d..00000000 --- a/src/dusk/console/console.c +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "console.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "input/input.h" -#include "log/log.h" -#include "engine/engine.h" - -console_t CONSOLE; - -void consoleInit(void) { - memoryZero(&CONSOLE, sizeof(console_t)); - CONSOLE.visible = false; - // CONSOLE.visible = true; - threadMutexInit(&CONSOLE.printMutex); -} - -void consolePrint(const char_t *message, ...) { - char_t buffer[CONSOLE_LINE_MAX]; - - va_list args; - va_start(args, message); - int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args); - va_end(args); - - threadMutexLock(&CONSOLE.printMutex); - memoryMove( - CONSOLE.line[0], - CONSOLE.line[1], - (CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX - ); - memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1); - threadMutexUnlock(&CONSOLE.printMutex); - - logDebug("%s\n", buffer); -} - -void consoleUpdate(void) { - #ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) return; - #endif - - if(inputPressed(INPUT_ACTION_CONSOLE)) { - CONSOLE.visible = !CONSOLE.visible; - } -} - -void consoleDispose(void) { - threadMutexDispose(&CONSOLE.printMutex); -} \ No newline at end of file diff --git a/src/dusk/console/console.h b/src/dusk/console/console.h deleted file mode 100644 index 8f0a8fc6..00000000 --- a/src/dusk/console/console.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "dusk.h" -#include "thread/thread.h" - -#define CONSOLE_LINE_MAX 128 -#define CONSOLE_HISTORY_MAX 16 -#define CONSOLE_EXEC_BUFFER_MAX 32 - -typedef struct { - char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; - bool_t visible; - threadmutex_t printMutex; -} console_t; - -extern console_t CONSOLE; - -/** - * Initializes the console. - */ -void consoleInit(void); - -/** - * Prints a message to the console history. - * - * @param message The message to print (printf-style). - */ -void consolePrint(const char_t *message, ...); - -/** - * Processes pending queued script lines. Call once per frame from main thread. - */ -void consoleUpdate(void); - -/** - * Disposes of the console. - */ -void consoleDispose(void); \ No newline at end of file diff --git a/src/dusk/display/CMakeLists.txt b/src/dusk/display/CMakeLists.txt deleted file mode 100644 index 0f6d6c6b..00000000 --- a/src/dusk/display/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - display.c -) - -# Subdirectories -add_subdirectory(framebuffer) -add_subdirectory(mesh) -add_subdirectory(screen) -add_subdirectory(shader) -add_subdirectory(spritebatch) -add_subdirectory(text) -add_subdirectory(texture) - -# Color definitions -dusk_run_python( - dusk_color_defs - tools.color - --csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv - --output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h -) -add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_color_defs) \ No newline at end of file diff --git a/src/dusk/display/color.csv b/src/dusk/display/color.csv deleted file mode 100644 index 62c2e1bc..00000000 --- a/src/dusk/display/color.csv +++ /dev/null @@ -1,23 +0,0 @@ -name,r,g,b,a -black,0,0,0,1 -white,1,1,1,1 -red,1,0,0,1 -green,0,1,0,1 -blue,0,0,1,1 -yellow,1,1,0,1 -cyan,0,1,1,1 -magenta,1,0,1,1 -transparent,0,0,0,0 -transparent_white,1,1,1,0 -transparent_black,0,0,0,0 -gray,0.5,0.5,0.5,1 -light_gray,0.75,0.75,0.75,1 -dark_gray,0.25,0.25,0.25,1 -orange,1,0.65,0,1 -purple,0.5,0,0.5,1 -brown,0.6,0.4,0.2,1 -pink,1,0.75,0.8,1 -lime,0.75,1,0,1 -navy,0,0,0.5,1 -teal,0,0.5,0.5,1 -cornflower_blue,0.39,0.58,0.93,1 \ No newline at end of file diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c deleted file mode 100644 index 98596d8e..00000000 --- a/src/dusk/display/display.c +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/display.h" -#include "display/framebuffer/framebuffer.h" -#include "scene/scene.h" -#include "display/spritebatch/spritebatch.h" -#include "display/mesh/quad.h" -#include "display/mesh/cube.h" -#include "display/mesh/sphere.h" -#include "display/mesh/plane.h" -#include "display/mesh/capsule.h" -#include "display/mesh/triprism.h" -#include "display/screen/screen.h" -#include "ui/ui.h" -#include "display/text/text.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "asset/asset.h" -#include "display/shader/shaderlist.h" -#include "time/time.h" - -display_t DISPLAY = { 0 }; - -errorret_t displayInit(void) { - memoryZero(&DISPLAY, sizeof(DISPLAY)); - - #ifdef displayPlatformInit - errorChain(displayPlatformInit()); - #endif - - // Set initial state - errorChain(displaySetState((displaystate_t){ .flags = 0 })); - - // Init the fixed textures - errorChain(textureInit( - &TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT, - TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS } - )); - errorChain(textureInit( - &TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT, - TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS } - )); - - // Standard meshes - errorChain(quadInit()); - errorChain(cubeInit()); - errorChain(sphereInit()); - errorChain(planeInit()); - errorChain(capsuleInit()); - errorChain(triPrismInit()); - - // Init the subsystems - errorChain(frameBufferInitBackBuffer()); - errorChain(spriteBatchInit()); - errorChain(textInit()); - errorChain(screenInit()); - - // Setup initial shader with default values - errorChain(shaderListInit()); - - errorOk(); -} - -errorret_t displayUpdate(void) { - #ifdef displayPlatformUpdate - errorChain(displayPlatformUpdate()); - #endif - - // Reset state - spriteBatchClear(); - errorChain(frameBufferBind(NULL)); - - // Bind screen and render scene - errorChain(screenBind()); - frameBufferClear( - FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH, - SCREEN.background - ); - - errorChain(sceneRender()); - - // Finish up - screenUnbind(); - screenRender(); - - // Swap and return. - #ifdef displayPlatformSwap - errorChain(displayPlatformSwap()); - #endif - errorOk(); -} - -errorret_t displaySetState(displaystate_t state) { - #ifdef displayPlatformSetState - errorChain(displayPlatformSetState(state)); - #endif - errorOk(); -} - -errorret_t displayDispose(void) { - errorChain(shaderListDispose()); - errorChain(spriteBatchDispose()); - screenDispose(); - errorChain(textDispose()); - errorChain(textureDispose(&TEXTURE_WHITE)); - errorChain(textureDispose(&TEXTURE_TEST)); - - #ifdef displayPlatformDispose - displayPlatformDispose(); - #endif - - // For now, we just return an OK error. - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/display/display.h b/src/dusk/display/display.h deleted file mode 100644 index 4665f550..00000000 --- a/src/dusk/display/display.h +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/displayplatform.h" - -// Expecting some definitions to be provided -#ifndef DUSK_DISPLAY_SIZE_DYNAMIC - #ifndef DUSK_DISPLAY_WIDTH - #error "DUSK_DISPLAY_WIDTH must be defined." - #endif - #ifndef DUSK_DISPLAY_HEIGHT - #error "DUSK_DISPLAY_HEIGHT must be defined" - #endif - #define DUSK_DISPLAY_WIDTH_DEFAULT DUSK_DISPLAY_WIDTH - #define DUSK_DISPLAY_HEIGHT_DEFAULT DUSK_DISPLAY_HEIGHT -#else - #ifndef DUSK_DISPLAY_WIDTH_DEFAULT - #error "DUSK_DISPLAY_WIDTH_DEFAULT must be defined." - #endif - #ifndef DUSK_DISPLAY_HEIGHT_DEFAULT - #error "DUSK_DISPLAY_HEIGHT_DEFAULT must be defined." - #endif - #ifdef DUSK_DISPLAY_WIDTH - #error "DUSK_DISPLAY_WIDTH should not be defined." - #endif - #ifdef DUSK_DISPLAY_HEIGHT - #error "DUSK_DISPLAY_HEIGHT should not be defined." - #endif -#endif - -#ifndef DUSK_DISPLAY_SCALE_UI - #define DUSK_DISPLAY_SCALE_UI 1 -#endif - -#ifndef DUSK_DISPLAY_SCALE_3D - #define DUSK_DISPLAY_SCALE_3D 1 -#endif - -// Main Display Struct, platform-speicifc -typedef displayplatform_t display_t; - -extern display_t DISPLAY; - -/** - * Initializes the display system. - * @return An errorret_t indicating success or failure. - */ -errorret_t displayInit(void); - -/** - * Tells the display system to actually draw the frame. - * @return An errorret_t indicating success or failure. - */ -errorret_t displayUpdate(void); - -/** - * Sets the display state. - * - * @param state The state to set. - * @return An errorret_t indicating success or failure. - */ -errorret_t displaySetState(displaystate_t state); - -/** - * Disposes of the display system. - * @return An errorret_t indicating success or failure. - */ -errorret_t displayDispose(void); \ No newline at end of file diff --git a/src/dusk/display/displaystate.h b/src/dusk/display/displaystate.h deleted file mode 100644 index ee27028a..00000000 --- a/src/dusk/display/displaystate.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#define DISPLAY_STATE_FLAG_CULL (1 << 0) -#define DISPLAY_STATE_FLAG_DEPTH_TEST (1 << 1) -#define DISPLAY_STATE_FLAG_BLEND (1 << 2) - -typedef struct { - uint8_t flags; -} displaystate_t; \ No newline at end of file diff --git a/src/dusk/display/framebuffer/CMakeLists.txt b/src/dusk/display/framebuffer/CMakeLists.txt deleted file mode 100644 index e4e2efc4..00000000 --- a/src/dusk/display/framebuffer/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - framebuffer.c -) \ No newline at end of file diff --git a/src/dusk/display/framebuffer/framebuffer.c b/src/dusk/display/framebuffer/framebuffer.c deleted file mode 100644 index 90cedaaa..00000000 --- a/src/dusk/display/framebuffer/framebuffer.c +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "framebuffer.h" -#include "display/display.h" -#include "assert/assert.h" -#include "util/memory.h" - -framebuffer_t FRAMEBUFFER_BACKBUFFER = {0}; -const framebuffer_t *FRAMEBUFFER_BOUND = &FRAMEBUFFER_BACKBUFFER; - -errorret_t frameBufferInitBackBuffer() { - memoryZero(&FRAMEBUFFER_BACKBUFFER, sizeof(framebuffer_t)); - errorChain(frameBufferPlatformInitBackBuffer()); - FRAMEBUFFER_BOUND = &FRAMEBUFFER_BACKBUFFER; - errorOk(); -} - -errorret_t frameBufferBind(framebuffer_t *framebuffer) { - if(framebuffer == NULL) { - frameBufferBind(&FRAMEBUFFER_BACKBUFFER); - FRAMEBUFFER_BOUND = &FRAMEBUFFER_BACKBUFFER; - errorOk(); - } - - errorChain(frameBufferPlatformBind(framebuffer)); - FRAMEBUFFER_BOUND = framebuffer; - errorOk(); -} - -uint32_t frameBufferGetWidth(const framebuffer_t *framebuffer) { - return frameBufferPlatformGetWidth(framebuffer); -} - -uint32_t frameBufferGetHeight(const framebuffer_t *framebuffer) { - return frameBufferPlatformGetHeight(framebuffer); -} - -float_t frameBufferGetAspect(const framebuffer_t *framebuffer) { - #ifdef frameBufferPlatformGetAspect - return frameBufferPlatformGetAspect(framebuffer); - #endif - - uint32_t width = frameBufferGetWidth(framebuffer); - uint32_t height = frameBufferGetHeight(framebuffer); - if(height == 0) return 1.0f; // Avoid divide by zero, just return 1:1 aspect. - return (float_t)width / (float_t)height; -} - -void frameBufferClear(const uint8_t flags, const color_t color) { - frameBufferPlatformClear(flags, color); -} - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - errorret_t frameBufferInit( - framebuffer_t *fb, - const uint32_t width, - const uint32_t height - ) { - assertNotNull(fb, "Framebuffer cannot be NULL"); - assertTrue(width > 0 && height > 0, "W/H must be greater than 0"); - - memoryZero(fb, sizeof(framebuffer_t)); - errorChain(frameBufferPlatformInit(fb, width, height)); - errorOk(); - } - - errorret_t frameBufferDispose(framebuffer_t *framebuffer) { - assertNotNull(framebuffer, "Framebuffer cannot be NULL"); - errorChain(frameBufferPlatformDispose(framebuffer)); - errorOk(); - } -#endif \ No newline at end of file diff --git a/src/dusk/display/framebuffer/framebuffer.h b/src/dusk/display/framebuffer/framebuffer.h deleted file mode 100644 index 70c47246..00000000 --- a/src/dusk/display/framebuffer/framebuffer.h +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/framebuffer/framebufferplatform.h" -#include "display/texture/texture.h" - -// Expected defs. -#ifndef frameBufferPlatformInitBackBuffer - #error "frameBufferPlatformInitBackBuffer not defined for this platform" -#endif -#ifndef frameBufferPlatformBind - #error "frameBufferPlatformBind not defined for this platform" -#endif -#ifndef frameBufferPlatformGetWidth - #error "frameBufferPlatformGetWidth not defined for this platform" -#endif -#ifndef frameBufferPlatformGetHeight - #error "frameBufferPlatformGetHeight not defined for this platform" -#endif -#ifndef frameBufferPlatformClear - #error "frameBufferPlatformClear not defined for this platform" -#endif - -#define FRAMEBUFFER_CLEAR_COLOR (1 << 0) -#define FRAMEBUFFER_CLEAR_DEPTH (1 << 1) - -typedef framebufferplatform_t framebuffer_t; - -extern framebuffer_t FRAMEBUFFER_BACKBUFFER; -extern const framebuffer_t *FRAMEBUFFER_BOUND; - -/** - * Initializes the backbuffer framebuffer. - * - * @return Error for initialization of the backbuffer. - */ -errorret_t frameBufferInitBackBuffer(void); - -/** - * Gets the width of the framebuffer. - * - * @param framebuffer The framebuffer to get the width of. - * @return The width of the framebuffer, or 0 if the framebuffer is NULL. - */ -uint32_t frameBufferGetWidth(const framebuffer_t *framebuffer); - -/** - * Gets the height of the framebuffer. - * - * @param framebuffer The framebuffer to get the height of. - * @return The height of the framebuffer, or 0 if the framebuffer is NULL. - */ -uint32_t frameBufferGetHeight(const framebuffer_t *framebuffer); - -/** - * Returns the aspect ratio of the framebuffer. This is ALMOST always just - * the width / height, however some platforms may choose to override this if - * they have stretched styled back buffers, e.g. 640x480 stretched. - * - * @param framebuffer The framebuffer to get the aspect ratio of. - * @return The aspect ratio of the framebuffer. - */ -float_t frameBufferGetAspect(const framebuffer_t *framebuffer); - -/** - * Binds the framebuffer for rendering, or the backbuffer if the framebuffer - * provided is NULL. - * - * @param framebuffer The framebuffer to bind, or NULL to bind the backbuffer. - * @return Error for binding the framebuffer. - */ -errorret_t frameBufferBind(framebuffer_t *framebuffer); - -/** - * Clears the currently bound framebuffer. - * - * @param flags The clear flags. - * @param color The color to clear the color buffer to (if clearing color). - */ -void frameBufferClear(uint8_t flags, color_t color); - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - #ifndef frameBufferPlatformInit - #error "frameBufferPlatformInit not defined for this platform" - #endif - - #ifndef frameBufferPlatformDispose - #error "frameBufferPlatformDispose not defined for this platform" - #endif - - /** - * Initializes a framebuffer. - * - * @param framebuffer The framebuffer to initialize. - * @param width The width of the framebuffer. - * @param height The height of the framebuffer. - * @return Error for initialization of the framebuffer. - */ - errorret_t frameBufferInit( - framebuffer_t *framebuffer, - const uint32_t width, - const uint32_t height - ); - - /** - * Disposes of the framebuffer. Will also be used for request disposing of the - * backbuffer. - * - * @param framebuffer The framebuffer to dispose of. - */ - errorret_t frameBufferDispose(framebuffer_t *framebuffer); -#endif \ No newline at end of file diff --git a/src/dusk/display/mesh/CMakeLists.txt b/src/dusk/display/mesh/CMakeLists.txt deleted file mode 100644 index b641eea9..00000000 --- a/src/dusk/display/mesh/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - mesh.c - quad.c - cube.c - sphere.c - plane.c - capsule.c - triprism.c -) \ No newline at end of file diff --git a/src/dusk/display/mesh/capsule.c b/src/dusk/display/mesh/capsule.c deleted file mode 100644 index 23e7d877..00000000 --- a/src/dusk/display/mesh/capsule.c +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "capsule.h" -#include "assert/assert.h" - -mesh_t CAPSULE_MESH_SIMPLE; -meshvertex_t CAPSULE_MESH_SIMPLE_VERTICES[CAPSULE_VERTEX_COUNT]; - -errorret_t capsuleInit() { - vec3 center = { 0.0f, 0.0f, 0.0f }; - capsuleBuffer( - CAPSULE_MESH_SIMPLE_VERTICES, - center, - 0.5f, - 0.5f, - CAPSULE_CAP_RINGS, - CAPSULE_SECTORS - ); - errorChain(meshInit( - &CAPSULE_MESH_SIMPLE, - CAPSULE_PRIMITIVE_TYPE, - CAPSULE_VERTEX_COUNT, - CAPSULE_MESH_SIMPLE_VERTICES - )); - errorOk(); -} - -void capsuleBuffer( - meshvertex_t *vertices, - const vec3 center, - const float_t radius, - const float_t halfHeight, - const int32_t capRings, - const int32_t sectors -) { - assertNotNull(vertices, "Vertices cannot be NULL"); - assertNotNull(center, "Center vector cannot be NULL"); - - const float_t cx = center[0]; - const float_t cy = center[1]; - const float_t cz = center[2]; - const float_t sectorStep = 2.0f * (float_t)GLM_PI / (float_t)sectors; - int32_t vi = 0; - - #define CAP_VERT(px, py, pz, u, v) \ - vertices[vi].pos[0] = (px); \ - vertices[vi].pos[1] = (py); \ - vertices[vi].pos[2] = (pz); \ - vertices[vi].uv[0] = (u); \ - vertices[vi].uv[1] = (v); \ - vi++; - - /* ---- Top hemisphere ---- */ - /* phi ranges from PI/2 (top pole) down to 0 (equator). */ - const float_t capStep = (float_t)GLM_PI_2 / (float_t)capRings; - for(int32_t i = 0; i < capRings; i++) { - const float_t phi1 = (float_t)GLM_PI_2 - (float_t)i * capStep; - const float_t phi2 = (float_t)GLM_PI_2 - (float_t)(i + 1) * capStep; - - const float_t ly1 = radius * sinf(phi1); - const float_t ly2 = radius * sinf(phi2); - const float_t lxz1 = radius * cosf(phi1); - const float_t lxz2 = radius * cosf(phi2); - - /* UV: top cap occupies v in [0.5 + halfHeightFrac .. 1.0]: we use a - * 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 v2 = 1.0f - (float_t)(i + 1) / (float_t)(2 * capRings + 1); - - for(int32_t j = 0; j < sectors; j++) { - const float_t t1 = (float_t)j * sectorStep; - const float_t t2 = (float_t)(j + 1) * sectorStep; - - const float_t u1 = (float_t)j / (float_t)sectors; - const float_t u2 = (float_t)(j + 1) / (float_t)sectors; - - const float_t x11 = lxz1 * cosf(t1), z11 = lxz1 * sinf(t1); - const float_t x12 = lxz1 * cosf(t2), z12 = lxz1 * sinf(t2); - const float_t x21 = lxz2 * cosf(t1), z21 = lxz2 * sinf(t1); - const float_t x22 = lxz2 * cosf(t2), z22 = lxz2 * sinf(t2); - - const float_t y1off = cy + halfHeight + ly1; - const float_t y2off = cy + halfHeight + ly2; - - CAP_VERT(cx+x11, y1off, cz+z11, u1, v1) - CAP_VERT(cx+x21, y2off, cz+z21, u1, v2) - CAP_VERT(cx+x12, y1off, cz+z12, u2, v1) - - CAP_VERT(cx+x12, y1off, cz+z12, u2, v1) - CAP_VERT(cx+x21, y2off, cz+z21, u1, v2) - CAP_VERT(cx+x22, y2off, cz+z22, u2, v2) - } - } - - /* ---- Cylindrical body ---- */ - { - const float_t yTop = cy + halfHeight; - const float_t yBot = cy - halfHeight; - const float_t vTop = ( - 1.0f - (float_t)capRings / (float_t)(2 * capRings + 1) - ); - const float_t vBot = ( - 1.0f - (float_t)(capRings + 1) / (float_t)(2 * capRings + 1) - ); - - for(int32_t j = 0; j < sectors; j++) { - const float_t t1 = (float_t)j * sectorStep; - const float_t t2 = (float_t)(j + 1) * sectorStep; - - const float_t u1 = (float_t)j / (float_t)sectors; - const float_t u2 = (float_t)(j + 1) / (float_t)sectors; - - const float_t x1 = radius * cosf(t1), z1 = radius * sinf(t1); - const float_t x2 = radius * cosf(t2), z2 = radius * sinf(t2); - - CAP_VERT(cx+x1, yTop, cz+z1, u1, vTop) - CAP_VERT(cx+x1, yBot, cz+z1, u1, vBot) - CAP_VERT(cx+x2, yTop, cz+z2, u2, vTop) - - CAP_VERT(cx+x2, yTop, cz+z2, u2, vTop) - CAP_VERT(cx+x1, yBot, cz+z1, u1, vBot) - CAP_VERT(cx+x2, yBot, cz+z2, u2, vBot) - } - } - - // Bottom hemisphere - for(int32_t i = 0; i < capRings; i++) { - const float_t phi1 = -(float_t)i * capStep; - const float_t phi2 = -(float_t)(i + 1) * capStep; - - const float_t ly1 = radius * sinf(phi1); - const float_t ly2 = radius * sinf(phi2); - const float_t lxz1 = radius * cosf(phi1); - const float_t lxz2 = radius * cosf(phi2); - - const float_t v1 = ( - 1.0f - (float_t)(capRings + 1 + i) / (float_t)(2 * capRings + 1) - ); - const float_t v2 = ( - 1.0f - (float_t)(capRings + 1 + i + 1) / (float_t)(2 * capRings + 1) - ); - - for(int32_t j = 0; j < sectors; j++) { - const float_t t1 = (float_t)j * sectorStep; - const float_t t2 = (float_t)(j + 1) * sectorStep; - - const float_t u1 = (float_t)j / (float_t)sectors; - const float_t u2 = (float_t)(j + 1) / (float_t)sectors; - - const float_t x11 = lxz1 * cosf(t1), z11 = lxz1 * sinf(t1); - const float_t x12 = lxz1 * cosf(t2), z12 = lxz1 * sinf(t2); - const float_t x21 = lxz2 * cosf(t1), z21 = lxz2 * sinf(t1); - const float_t x22 = lxz2 * cosf(t2), z22 = lxz2 * sinf(t2); - - const float_t y1off = cy - halfHeight + ly1; - const float_t y2off = cy - halfHeight + ly2; - - CAP_VERT(cx+x11, y1off, cz+z11, u1, v1) - CAP_VERT(cx+x21, y2off, cz+z21, u1, v2) - CAP_VERT(cx+x12, y1off, cz+z12, u2, v1) - - CAP_VERT(cx+x12, y1off, cz+z12, u2, v1) - CAP_VERT(cx+x21, y2off, cz+z21, u1, v2) - CAP_VERT(cx+x22, y2off, cz+z22, u2, v2) - } - } - - #undef CAP_VERT -} diff --git a/src/dusk/display/mesh/capsule.h b/src/dusk/display/mesh/capsule.h deleted file mode 100644 index f6960c1e..00000000 --- a/src/dusk/display/mesh/capsule.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/mesh/mesh.h" - -#define CAPSULE_CAP_RINGS 4 -#define CAPSULE_SECTORS 16 -#define CAPSULE_VERTEX_COUNT ((2 * CAPSULE_CAP_RINGS + 1) * CAPSULE_SECTORS * 6) -#define CAPSULE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES - -extern mesh_t CAPSULE_MESH_SIMPLE; -extern meshvertex_t CAPSULE_MESH_SIMPLE_VERTICES[CAPSULE_VERTEX_COUNT]; - -/** - * Initializes the simple unit capsule mesh centered at (0,0,0) with radius 0.5 - * and a cylindrical half-height of 0.5 (total height 2.0). - * - * @return Error for initialization of the capsule mesh. - */ -errorret_t capsuleInit(); - -/** - * Buffers a capsule (cylinder + two hemisphere caps) into the provided vertex - * array. The capsule's long axis is Y. Total vertex count is - * (2*capRings + 1) * sectors * 6. - * - * @param vertices Vertex array to write into. - * @param center Center position of the capsule. - * @param radius Radius of the cylinder and hemisphere caps. - * @param halfHeight Half the height of the cylindrical section only (caps - * extend an additional radius above/below). - * @param capRings Number of latitude rings per hemisphere cap. - * @param sectors Number of longitude segments around the circumference. - * @param color Color applied to all vertices. - */ -void capsuleBuffer( - meshvertex_t *vertices, - const vec3 center, - const float_t radius, - const float_t halfHeight, - const int32_t capRings, - const int32_t sectors -); diff --git a/src/dusk/display/mesh/cube.c b/src/dusk/display/mesh/cube.c deleted file mode 100644 index 4e70b81d..00000000 --- a/src/dusk/display/mesh/cube.c +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cube.h" -#include "assert/assert.h" - -mesh_t CUBE_MESH_SIMPLE; -meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT]; - -errorret_t cubeInit() { - vec3 min = { 0.0f, 0.0f, 0.0f }; - vec3 max = { 1.0f, 1.0f, 1.0f }; - cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max); - errorChain(meshInit( - &CUBE_MESH_SIMPLE, - CUBE_PRIMITIVE_TYPE, - CUBE_VERTEX_COUNT, - CUBE_MESH_SIMPLE_VERTICES - )); - errorOk(); -} - -#define CUBE_VERT(i, px, py, pz, u, v) \ - vertices[i].pos[0] = (px); \ - vertices[i].pos[1] = (py); \ - vertices[i].pos[2] = (pz); \ - vertices[i].uv[0] = (u); \ - vertices[i].uv[1] = (v); - -void cubeBuffer( - meshvertex_t *vertices, - const vec3 min, - const vec3 max -) { - assertNotNull(vertices, "Vertices cannot be NULL"); - assertNotNull(min, "Min vector cannot be NULL"); - assertNotNull(max, "Max vector cannot be NULL"); - - const float_t x0 = min[0], y0 = min[1], z0 = min[2]; - const float_t x1 = max[0], y1 = max[1], z1 = max[2]; - - // Front face (+Z normal): CCW when viewed from +Z - CUBE_VERT( 0, x0, y0, z1, 0.0f, 0.0f); - CUBE_VERT( 1, x1, y0, z1, 1.0f, 0.0f); - CUBE_VERT( 2, x1, y1, z1, 1.0f, 1.0f); - CUBE_VERT( 3, x0, y0, z1, 0.0f, 0.0f); - CUBE_VERT( 4, x1, y1, z1, 1.0f, 1.0f); - CUBE_VERT( 5, x0, y1, z1, 0.0f, 1.0f); - - // Back face (-Z normal): CCW when viewed from -Z - CUBE_VERT( 6, x1, y0, z0, 0.0f, 0.0f); - CUBE_VERT( 7, x0, y0, z0, 1.0f, 0.0f); - CUBE_VERT( 8, x0, y1, z0, 1.0f, 1.0f); - CUBE_VERT( 9, x1, y0, z0, 0.0f, 0.0f); - CUBE_VERT(10, x0, y1, z0, 1.0f, 1.0f); - CUBE_VERT(11, x1, y1, z0, 0.0f, 1.0f); - - // Right face (+X normal): CCW when viewed from +X - CUBE_VERT(12, x1, y0, z1, 0.0f, 0.0f); - CUBE_VERT(13, x1, y0, z0, 1.0f, 0.0f); - CUBE_VERT(14, x1, y1, z0, 1.0f, 1.0f); - CUBE_VERT(15, x1, y0, z1, 0.0f, 0.0f); - CUBE_VERT(16, x1, y1, z0, 1.0f, 1.0f); - CUBE_VERT(17, x1, y1, z1, 0.0f, 1.0f); - - // Left face (-X normal): CCW when viewed from -X - CUBE_VERT(18, x0, y0, z0, 0.0f, 0.0f); - CUBE_VERT(19, x0, y0, z1, 1.0f, 0.0f); - CUBE_VERT(20, x0, y1, z1, 1.0f, 1.0f); - CUBE_VERT(21, x0, y0, z0, 0.0f, 0.0f); - CUBE_VERT(22, x0, y1, z1, 1.0f, 1.0f); - CUBE_VERT(23, x0, y1, z0, 0.0f, 1.0f); - - // Top face (+Y normal): CCW when viewed from +Y - CUBE_VERT(24, x0, y1, z1, 0.0f, 0.0f); - CUBE_VERT(25, x1, y1, z1, 1.0f, 0.0f); - CUBE_VERT(26, x1, y1, z0, 1.0f, 1.0f); - CUBE_VERT(27, x0, y1, z1, 0.0f, 0.0f); - CUBE_VERT(28, x1, y1, z0, 1.0f, 1.0f); - CUBE_VERT(29, x0, y1, z0, 0.0f, 1.0f); - - // Bottom face (-Y normal): CCW when viewed from -Y - CUBE_VERT(30, x0, y0, z0, 0.0f, 0.0f); - CUBE_VERT(31, x1, y0, z0, 1.0f, 0.0f); - CUBE_VERT(32, x1, y0, z1, 1.0f, 1.0f); - CUBE_VERT(33, x0, y0, z0, 0.0f, 0.0f); - CUBE_VERT(34, x1, y0, z1, 1.0f, 1.0f); - CUBE_VERT(35, x0, y0, z1, 0.0f, 1.0f); - - #undef CUBE_VERT -} diff --git a/src/dusk/display/mesh/cube.h b/src/dusk/display/mesh/cube.h deleted file mode 100644 index 8a21b72c..00000000 --- a/src/dusk/display/mesh/cube.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/mesh/mesh.h" - -#define CUBE_FACE_COUNT 6 -#define CUBE_VERTICES_PER_FACE 6 -#define CUBE_VERTEX_COUNT (CUBE_FACE_COUNT * CUBE_VERTICES_PER_FACE) -#define CUBE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES - -extern mesh_t CUBE_MESH_SIMPLE; -extern meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT]; - -/** - * Initializes the simple unit cube mesh (0,0,0) to (1,1,1). - * - * @return Error for initialization of the cube mesh. - */ -errorret_t cubeInit(); - -/** - * Buffers a 3D axis-aligned cube into the provided vertex array. - * Writes CUBE_VERTEX_COUNT vertices (6 faces x 6 vertices, CCW winding). - * - * @param vertices The vertex array to buffer into. - * @param min The minimum XYZ corner of the cube. - * @param max The maximum XYZ corner of the cube. - * @param color The color applied to all vertices. - */ -void cubeBuffer( - meshvertex_t *vertices, - const vec3 min, - const vec3 max -); diff --git a/src/dusk/display/mesh/mesh.c b/src/dusk/display/mesh/mesh.c deleted file mode 100644 index 3c573505..00000000 --- a/src/dusk/display/mesh/mesh.c +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "mesh.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "util/math.h" - -errorret_t meshInit( - mesh_t *mesh, - const meshprimitivetype_t primitiveType, - const int32_t vertexCount, - const meshvertex_t *vertices -) { - assertNotNull(mesh, "Mesh cannot be NULL"); - assertNotNull(vertices, "Vertices cannot be NULL"); - assertTrue(vertexCount > 0, "Vertex count must be greater than 0"); - - memoryZero(mesh, sizeof(mesh_t)); - - errorChain(meshInitPlatform(mesh, primitiveType, vertexCount, vertices)); - errorOk(); -} - -errorret_t meshFlush( - mesh_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -) { - #ifdef meshFlushPlatform - assertNotNull(mesh, "Mesh cannot be NULL"); - assertTrue(vertexOffset >= 0, "Vertex offset must be non-negative."); - assertTrue( - vertexCount == -1 || vertexCount > 0, "Vertex count incorrect." - ); - - int32_t vertCount = meshGetVertexCount(mesh); - assertTrue( - vertexOffset < (vertCount - 1), "Need at least one vert to draw" - ); - - int32_t drawCount = vertexCount; - if(vertexCount == -1) { - drawCount = vertCount - vertexOffset; - } - - errorChain(meshFlushPlatform(mesh, vertexOffset, vertexCount)); - #endif - errorOk(); -} - -errorret_t meshDraw( - const mesh_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -) { - assertNotNull(mesh, "Mesh cannot be NULL"); - assertTrue(vertexOffset >= 0, "Vertex offset must be non-negative"); - assertTrue(vertexCount == -1 || vertexCount > 0, "Incorrect vert count"); - - int32_t vertDrawCount = vertexCount; - if(vertexCount == -1) { - const int32_t totalVertices = meshGetVertexCount(mesh); - vertDrawCount = totalVertices - vertexOffset; - } - - if(vertDrawCount == 0) { - errorOk(); - } - assertTrue( - vertexOffset + vertDrawCount <= meshGetVertexCount(mesh), - "Vertex offset and count must be within vertex count bounds" - ); - - errorChain(meshDrawPlatform(mesh, vertexOffset, vertDrawCount)); - errorOk(); -} - -void meshGetBounds( - const mesh_t *mesh, - vec3 outMin, - vec3 outMax -) { - assertNotNull(mesh, "Mesh cannot be NULL"); - assertNotNull(outMin, "Output min cannot be NULL"); - assertNotNull(outMax, "Output max cannot be NULL"); - - for(int i = 0; i < 3; i++) { - outMin[i] = FLT_MAX; - outMax[i] = -FLT_MAX; - } - - for(uint32_t i = 0; i < mesh->vertexCount; i++) { - meshvertex_t vert = mesh->vertices[i]; - for(int j = 0; j < 3; j++) { - outMin[j] = mathMin(outMin[j], vert.pos[j]); - outMax[j] = mathMax(outMax[j], vert.pos[j]); - } - } -} - -int32_t meshGetVertexCount(const mesh_t *mesh) { - assertNotNull(mesh, "Mesh cannot be NULL"); - return meshGetVertexCountPlatform(mesh); -} - -errorret_t meshDispose(mesh_t *mesh) { - assertNotNull(mesh, "Mesh cannot be NULL"); - errorChain(meshDisposePlatform(mesh)); - memoryZero(mesh, sizeof(mesh_t)); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/display/mesh/mesh.h b/src/dusk/display/mesh/mesh.h deleted file mode 100644 index 2056eacf..00000000 --- a/src/dusk/display/mesh/mesh.h +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#pragma once -#include "display/display.h" -#include "display/color.h" -#include "display/mesh/meshplatform.h" - -#ifndef meshInitPlatform - #error "meshInitPlatform must be defined" -#endif -#ifndef meshDrawPlatform - #error "meshDrawPlatform must be defined" -#endif -#ifndef meshGetVertexCountPlatform - #error "meshGetVertexCountPlatform must be defined" -#endif -#ifndef meshDisposePlatform - #error "meshDisposePlatform must be defined" -#endif - -typedef meshprimitivetypeplatform_t meshprimitivetype_t; -typedef meshplatform_t mesh_t; - -/** - * Initializes a mesh. - * - * @param mesh The mesh to initialize. - * @param primitiveType The OpenGL primitive type (e.g., GL_TRIANGLES). - * @param vertexCount The number of vertices in the mesh. - * @param vertices The vertex data for the mesh. - * @return An error indicating success or failure. - */ -errorret_t meshInit( - mesh_t *mesh, - const meshprimitivetype_t primitiveType, - const int32_t vertexCount, - const meshvertex_t *vertices -); - -/** - * Instructs the mesh to flush the vertices to the GPU. This is surprisingly - * only really necessary on modern devices, as we tend to let older devices - * read the vertices from the main memory directly. - * - * @param mesh Mesh to flush the vertices for. - * @param vertexOffset Start vertex to flush. - * @param vertexCount Count of vertices to flush, set to -1 for all. - * @return Error state. - */ -errorret_t meshFlush( - mesh_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -); - -/** - * Draws a mesh. - * - * @param mesh The mesh to draw. - * @param vertexOffset The offset in the vertex array to start drawing from. - * @param vertexCount The number of vertices to draw. If -1, draws all vertices. - * @return An error indicating success or failure. - */ -errorret_t meshDraw( - const mesh_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -); - -/** - * Gets the axis-aligned bounding box of a mesh. - * - * @param mesh The mesh to get the bounds of. - * @param outMin Output parameter for the minimum corner of the bounding box. - * @param outMax Output parameter for the maximum corner of the bounding box. - */ -void meshGetBounds( - const mesh_t *mesh, - vec3 outMin, - vec3 outMax -); - -/** - * Gets the vertex count of a mesh. - * - * @param mesh The mesh to get the vertex count from. - * @return The vertex count of the mesh. - */ -int32_t meshGetVertexCount(const mesh_t *mesh); - -/** - * Disposes a mesh. - * - * @param mesh The mesh to dispose. - * @return An error indicating success or failure. - */ -errorret_t meshDispose(mesh_t *mesh); \ No newline at end of file diff --git a/src/dusk/display/mesh/meshvertex.h b/src/dusk/display/mesh/meshvertex.h deleted file mode 100644 index e301d9b5..00000000 --- a/src/dusk/display/mesh/meshvertex.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#define MESH_VERTEX_UV_SIZE 2 -#define MESH_VERTEX_POS_SIZE 3 - -typedef struct { - float_t uv[MESH_VERTEX_UV_SIZE]; - float_t pos[MESH_VERTEX_POS_SIZE]; -} meshvertex_t; diff --git a/src/dusk/display/mesh/plane.c b/src/dusk/display/mesh/plane.c deleted file mode 100644 index df3d135c..00000000 --- a/src/dusk/display/mesh/plane.c +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "plane.h" -#include "assert/assert.h" - -mesh_t PLANE_MESH_SIMPLE; -meshvertex_t PLANE_MESH_SIMPLE_VERTICES[PLANE_VERTEX_COUNT]; - -errorret_t planeInit() { - vec3 min = { 0.0f, 0.0f, 0.0f }; - vec3 max = { 1.0f, 0.0f, 1.0f }; - vec2 uvMin = { 0.0f, 0.0f }; - vec2 uvMax = { 1.0f, 1.0f }; - planeBuffer( - PLANE_MESH_SIMPLE_VERTICES, - PLANE_AXIS_XZ, - min, - max, - uvMin, - uvMax - ); - errorChain(meshInit( - &PLANE_MESH_SIMPLE, - PLANE_PRIMITIVE_TYPE, - PLANE_VERTEX_COUNT, - PLANE_MESH_SIMPLE_VERTICES - )); - errorOk(); -} - -#define PLANE_VERT(i, px, py, pz, u, v) \ - vertices[i].pos[0] = (px); \ - vertices[i].pos[1] = (py); \ - vertices[i].pos[2] = (pz); \ - vertices[i].uv[0] = (u); \ - vertices[i].uv[1] = (v); - -void planeBuffer( - meshvertex_t *vertices, - const planeaxis_t axis, - const vec3 min, - const vec3 max, - const vec2 uvMin, - const vec2 uvMax -) { - assertNotNull(vertices, "Vertices cannot be NULL"); - assertNotNull(min, "Min vector cannot be NULL"); - assertNotNull(max, "Max vector cannot be NULL"); - assertNotNull(uvMin, "uvMin cannot be NULL"); - assertNotNull(uvMax, "uvMax cannot be NULL"); - - const float_t u0 = uvMin[0], u1 = uvMax[0]; - const float_t v0 = uvMin[1], v1 = uvMax[1]; - - switch(axis) { - case PLANE_AXIS_XY: { - /* Flat in XY at z = min[2]; spans X and Y. */ - const float_t z = min[2]; - PLANE_VERT(0, min[0], min[1], z, u0, v0) - PLANE_VERT(1, max[0], min[1], z, u1, v0) - PLANE_VERT(2, max[0], max[1], z, u1, v1) - PLANE_VERT(3, min[0], min[1], z, u0, v0) - PLANE_VERT(4, max[0], max[1], z, u1, v1) - PLANE_VERT(5, min[0], max[1], z, u0, v1) - break; - } - - case PLANE_AXIS_XZ: { - /* Flat in XZ at y = min[1]; spans X and Z. */ - const float_t y = min[1]; - PLANE_VERT(0, min[0], y, min[2], u0, v0) - PLANE_VERT(1, max[0], y, min[2], u1, v0) - PLANE_VERT(2, max[0], y, max[2], u1, v1) - PLANE_VERT(3, min[0], y, min[2], u0, v0) - PLANE_VERT(4, max[0], y, max[2], u1, v1) - PLANE_VERT(5, min[0], y, max[2], u0, v1) - break; - } - - case PLANE_AXIS_YZ: { - /* Flat in YZ at x = min[0]; spans Y and Z. */ - const float_t x = min[0]; - PLANE_VERT(0, x, min[1], min[2], u0, v0) - PLANE_VERT(1, x, max[1], min[2], u1, v0) - PLANE_VERT(2, x, max[1], max[2], u1, v1) - PLANE_VERT(3, x, min[1], min[2], u0, v0) - PLANE_VERT(4, x, max[1], max[2], u1, v1) - PLANE_VERT(5, x, min[1], max[2], u0, v1) - break; - } - } - - #undef PLANE_VERT -} diff --git a/src/dusk/display/mesh/plane.h b/src/dusk/display/mesh/plane.h deleted file mode 100644 index 0351d2b1..00000000 --- a/src/dusk/display/mesh/plane.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/mesh/mesh.h" - -#define PLANE_VERTEX_COUNT 6 -#define PLANE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES - -/** Which axis the plane's normal points along. */ -typedef enum { - PLANE_AXIS_XY = 0, /**< Flat in XY, normal along +Z (billboard/wall face). */ - PLANE_AXIS_XZ = 1, /**< Flat in XZ, normal along +Y (ground/floor plane). */ - PLANE_AXIS_YZ = 2, /**< Flat in YZ, normal along +X (side wall). */ -} planeaxis_t; - -extern mesh_t PLANE_MESH_SIMPLE; -extern meshvertex_t PLANE_MESH_SIMPLE_VERTICES[PLANE_VERTEX_COUNT]; - -/** - * Initializes the simple unit XZ plane mesh (ground plane) from (0,0,0) to - * (1,0,1). - * - * @return Error for initialization of the plane mesh. - */ -errorret_t planeInit(); - -/** - * Buffers an axis-aligned plane into the provided vertex array. - * Writes PLANE_VERTEX_COUNT (6) vertices (two triangles, CCW winding). - * - * The min/max corners fully describe the plane in 3D space. The axis enum - * controls which dimension is treated as the "depth" (normal) axis: - * PLANE_AXIS_XY: spans X and Y, depth from min[2]/max[2] (uses min[2]) - * PLANE_AXIS_XZ: spans X and Z, depth from min[1]/max[1] (uses min[1]) - * PLANE_AXIS_YZ: spans Y and Z, depth from min[0]/max[0] (uses min[0]) - * - * @param vertices Vertex array to write into (must hold PLANE_VERTEX_COUNT). - * @param axis Which axis the plane's normal points along. - * @param min Minimum XYZ corner. - * @param max Maximum XYZ corner. - * @param color Color applied to all vertices. - * @param uvMin Minimum UV coordinates. - * @param uvMax Maximum UV coordinates. - */ -void planeBuffer( - meshvertex_t *vertices, - const planeaxis_t axis, - const vec3 min, - const vec3 max, - const vec2 uvMin, - const vec2 uvMax -); diff --git a/src/dusk/display/mesh/quad.c b/src/dusk/display/mesh/quad.c deleted file mode 100644 index eff1dee0..00000000 --- a/src/dusk/display/mesh/quad.c +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "quad.h" -#include "assert/assert.h" - -mesh_t QUAD_MESH_SIMPLE; -meshvertex_t QUAD_MESH_SIMPLE_VERTICES[QUAD_VERTEX_COUNT] = { - { .uv = { 0.0f, 0.0f }, .pos = { 0.0f, 0.0f, 0.0f } }, - { .uv = { 1.0f, 0.0f }, .pos = { 1.0f, 0.0f, 0.0f } }, - { .uv = { 1.0f, 1.0f }, .pos = { 1.0f, 1.0f, 0.0f } }, - { .uv = { 0.0f, 0.0f }, .pos = { 0.0f, 0.0f, 0.0f } }, - { .uv = { 1.0f, 1.0f }, .pos = { 1.0f, 1.0f, 0.0f } }, - { .uv = { 0.0f, 1.0f }, .pos = { 0.0f, 1.0f, 0.0f } } -}; - -errorret_t quadInit() { - errorChain(meshInit( - &QUAD_MESH_SIMPLE, - QUAD_PRIMITIVE_TYPE, - QUAD_VERTEX_COUNT, - QUAD_MESH_SIMPLE_VERTICES - )); - errorOk(); -} - -void quadBuffer( - meshvertex_t *vertices, - const float_t minX, - const float_t minY, - const float_t maxX, - const float_t maxY, - const float_t u0, - const float_t v0, - const float_t u1, - const float_t v1 -) { - const float_t z = 0.0f; - assertNotNull(vertices, "Vertices cannot be NULL"); - - vertices[0].uv[0] = u0; vertices[0].uv[1] = v1; - vertices[0].pos[0] = minX; vertices[0].pos[1] = maxY; vertices[0].pos[2] = z; - - vertices[1].uv[0] = u1; vertices[1].uv[1] = v0; - vertices[1].pos[0] = maxX; vertices[1].pos[1] = minY; vertices[1].pos[2] = z; - - vertices[2].uv[0] = u0; vertices[2].uv[1] = v0; - vertices[2].pos[0] = minX; vertices[2].pos[1] = minY; vertices[2].pos[2] = z; - - vertices[3].uv[0] = u0; vertices[3].uv[1] = v1; - vertices[3].pos[0] = minX; vertices[3].pos[1] = maxY; vertices[3].pos[2] = z; - - vertices[4].uv[0] = u1; vertices[4].uv[1] = v1; - vertices[4].pos[0] = maxX; vertices[4].pos[1] = maxY; vertices[4].pos[2] = z; - - vertices[5].uv[0] = u1; vertices[5].uv[1] = v0; - vertices[5].pos[0] = maxX; vertices[5].pos[1] = minY; vertices[5].pos[2] = z; -} - -void quadBuffer3D( - meshvertex_t *vertices, - const vec3 min, - const vec3 max, - const vec2 uvMin, - const vec2 uvMax -) { - assertNotNull(vertices, "Vertices cannot be NULL"); - assertNotNull(min, "Min vector cannot be NULL"); - assertNotNull(max, "Max vector cannot be NULL"); - assertNotNull(uvMin, "UV Min vector cannot be NULL"); - assertNotNull(uvMax, "UV Max vector cannot be NULL"); - - vertices[0].uv[0] = uvMin[0]; vertices[0].uv[1] = uvMin[1]; - vertices[0].pos[0] = min[0]; vertices[0].pos[1] = min[1]; - vertices[0].pos[2] = min[2]; - - vertices[1].uv[0] = uvMax[0]; vertices[1].uv[1] = uvMin[1]; - vertices[1].pos[0] = max[0]; vertices[1].pos[1] = min[1]; - vertices[1].pos[2] = min[2]; - - vertices[2].uv[0] = uvMax[0]; vertices[2].uv[1] = uvMax[1]; - vertices[2].pos[0] = max[0]; vertices[2].pos[1] = max[1]; - vertices[2].pos[2] = min[2]; - - vertices[3].uv[0] = uvMin[0]; vertices[3].uv[1] = uvMin[1]; - vertices[3].pos[0] = min[0]; vertices[3].pos[1] = min[1]; - vertices[3].pos[2] = min[2]; - - vertices[4].uv[0] = uvMax[0]; vertices[4].uv[1] = uvMax[1]; - vertices[4].pos[0] = max[0]; vertices[4].pos[1] = max[1]; - vertices[4].pos[2] = min[2]; - - vertices[5].uv[0] = uvMin[0]; vertices[5].uv[1] = uvMax[1]; - vertices[5].pos[0] = min[0]; vertices[5].pos[1] = max[1]; - vertices[5].pos[2] = min[2]; -} \ No newline at end of file diff --git a/src/dusk/display/mesh/quad.h b/src/dusk/display/mesh/quad.h deleted file mode 100644 index b91df13f..00000000 --- a/src/dusk/display/mesh/quad.h +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "mesh.h" - -#define QUAD_VERTEX_COUNT 6 -#define QUAD_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES - -extern mesh_t QUAD_MESH_SIMPLE; -extern meshvertex_t QUAD_MESH_SIMPLE_VERTICES[QUAD_VERTEX_COUNT]; - -/** - * Initializes the quad mesh. - * - * @return Error for initialization of the quad mesh. - */ -errorret_t quadInit(); - -/** - * Buffers a quad into the provided vertex array. - * - * @param vertices The vertex array to buffer into. - * @param minX The minimum X coordinate of the quad. - * @param minY The minimum Y coordinate of the quad. - * @param maxX The maximum X coordinate of the quad. - * @param maxY The maximum Y coordinate of the quad. - * @param color The color of the quad. - * @param u0 The U texture coordinate for the first vertex. - * @param v0 The V texture coordinate for the first vertex. - * @param u1 The U texture coordinate for the second vertex. - * @param v1 The V texture coordinate for the second vertex. - */ -void quadBuffer( - meshvertex_t *vertices, - const float_t minX, - const float_t minY, - const float_t maxX, - const float_t maxY, - const float_t u0, - const float_t v0, - const float_t u1, - const float_t v1 -); - -/** - * Buffers a 3D quad into the provided vertex array. - * - * @param vertices The vertex array to buffer into. - * @param min The minimum XYZ coordinates of the quad. - * @param max The maximum XYZ coordinates of the quad. - * @param uvMin The minimum UV coordinates of the quad. - * @param uvMax The maximum UV coordinates of the quad. - */ -void quadBuffer3D( - meshvertex_t *vertices, - const vec3 min, - const vec3 max, - const vec2 uvMin, - const vec2 uvMax -); \ No newline at end of file diff --git a/src/dusk/display/mesh/sphere.c b/src/dusk/display/mesh/sphere.c deleted file mode 100644 index f794ced1..00000000 --- a/src/dusk/display/mesh/sphere.c +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "sphere.h" -#include "assert/assert.h" - -mesh_t SPHERE_MESH_SIMPLE; -meshvertex_t SPHERE_MESH_SIMPLE_VERTICES[SPHERE_VERTEX_COUNT]; - -errorret_t sphereInit() { - vec3 center = { 0.0f, 0.0f, 0.0f }; - sphereBuffer( - SPHERE_MESH_SIMPLE_VERTICES, - center, - 0.5f, - SPHERE_STACKS, - SPHERE_SECTORS - ); - errorChain(meshInit( - &SPHERE_MESH_SIMPLE, - SPHERE_PRIMITIVE_TYPE, - SPHERE_VERTEX_COUNT, - SPHERE_MESH_SIMPLE_VERTICES - )); - errorOk(); -} - -void sphereBuffer( - meshvertex_t *vertices, - const vec3 center, - const float_t radius, - const int32_t stacks, - const int32_t sectors -) { - assertNotNull(vertices, "Vertices cannot be NULL"); - assertNotNull(center, "Center vector cannot be NULL"); - - const float_t stackStep = (float_t)GLM_PI / (float_t)stacks; - const float_t sectorStep = 2.0f * (float_t)GLM_PI / (float_t)sectors; - int32_t vi = 0; - - for(int32_t i = 0; i < stacks; i++) { - /* Latitude angles: top of band -> bottom of band */ - 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 y1 = radius * sinf(phi1); - const float_t y2 = radius * sinf(phi2); - const float_t xz1 = radius * cosf(phi1); - const float_t xz2 = radius * cosf(phi2); - - const float_t v1 = 1.0f - (float_t)i / (float_t)stacks; - const float_t v2 = 1.0f - (float_t)(i + 1) / (float_t)stacks; - - for(int32_t j = 0; j < sectors; j++) { - const float_t theta1 = (float_t)j * sectorStep; - const float_t theta2 = (float_t)(j + 1) * sectorStep; - - const float_t x11 = xz1 * cosf(theta1); - const float_t z11 = xz1 * sinf(theta1); - const float_t x12 = xz1 * cosf(theta2); - const float_t z12 = xz1 * sinf(theta2); - - const float_t x21 = xz2 * cosf(theta1); - const float_t z21 = xz2 * sinf(theta1); - const float_t x22 = xz2 * cosf(theta2); - const float_t z22 = xz2 * sinf(theta2); - - const float_t u1 = (float_t)j / (float_t)sectors; - const float_t u2 = (float_t)(j + 1) / (float_t)sectors; - - vertices[vi].pos[0] = center[0] + x11; - vertices[vi].pos[1] = center[1] + y1; - vertices[vi].pos[2] = center[2] + z11; - vertices[vi].uv[0] = u1; vertices[vi].uv[1] = v1; vi++; - - vertices[vi].pos[0] = center[0] + x21; - vertices[vi].pos[1] = center[1] + y2; - vertices[vi].pos[2] = center[2] + z21; - vertices[vi].uv[0] = u1; vertices[vi].uv[1] = v2; vi++; - - vertices[vi].pos[0] = center[0] + x12; - vertices[vi].pos[1] = center[1] + y1; - vertices[vi].pos[2] = center[2] + z12; - vertices[vi].uv[0] = u2; vertices[vi].uv[1] = v1; vi++; - - vertices[vi].pos[0] = center[0] + x12; - vertices[vi].pos[1] = center[1] + y1; - vertices[vi].pos[2] = center[2] + z12; - vertices[vi].uv[0] = u2; vertices[vi].uv[1] = v1; vi++; - - vertices[vi].pos[0] = center[0] + x21; - vertices[vi].pos[1] = center[1] + y2; - vertices[vi].pos[2] = center[2] + z21; - vertices[vi].uv[0] = u1; vertices[vi].uv[1] = v2; vi++; - - vertices[vi].pos[0] = center[0] + x22; - vertices[vi].pos[1] = center[1] + y2; - vertices[vi].pos[2] = center[2] + z22; - vertices[vi].uv[0] = u2; vertices[vi].uv[1] = v2; vi++; - } - } -} diff --git a/src/dusk/display/mesh/sphere.h b/src/dusk/display/mesh/sphere.h deleted file mode 100644 index e90354cf..00000000 --- a/src/dusk/display/mesh/sphere.h +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/mesh/mesh.h" - -#define SPHERE_STACKS 8 -#define SPHERE_SECTORS 16 -#define SPHERE_VERTEX_COUNT (SPHERE_STACKS * SPHERE_SECTORS * 6) -#define SPHERE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES - -extern mesh_t SPHERE_MESH_SIMPLE; -extern meshvertex_t SPHERE_MESH_SIMPLE_VERTICES[SPHERE_VERTEX_COUNT]; - -/** - * Initializes the simple unit sphere mesh centered at (0,0,0) with radius 0.5. - * - * @return Error for initialization of the sphere mesh. - */ -errorret_t sphereInit(); - -/** - * Buffers a UV sphere into the provided vertex array. - * Writes stacks * sectors * 6 vertices (CCW winding). - * - * @param vertices Vertex array to write into (must hold stacks*sectors*6). - * @param center Center position of the sphere. - * @param radius Radius of the sphere. - * @param stacks Number of horizontal rings (latitude bands). - * @param sectors Number of vertical segments (longitude slices). - * @param color Color applied to all vertices. - */ -void sphereBuffer( - meshvertex_t *vertices, - const vec3 center, - const float_t radius, - const int32_t stacks, - const int32_t sectors -); diff --git a/src/dusk/display/mesh/triprism.c b/src/dusk/display/mesh/triprism.c deleted file mode 100644 index 79f9b8f1..00000000 --- a/src/dusk/display/mesh/triprism.c +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "triprism.h" -#include "assert/assert.h" - -mesh_t TRIPRISM_MESH_SIMPLE; -meshvertex_t TRIPRISM_MESH_SIMPLE_VERTICES[TRIPRISM_VERTEX_COUNT]; - -errorret_t triPrismInit() { - triPrismBuffer( - TRIPRISM_MESH_SIMPLE_VERTICES, - 0.0f, 0.0f, - 1.0f, 0.0f, - 0.5f, 1.0f, - 0.0f, 1.0f - ); - errorChain(meshInit( - &TRIPRISM_MESH_SIMPLE, - TRIPRISM_PRIMITIVE_TYPE, - TRIPRISM_VERTEX_COUNT, - TRIPRISM_MESH_SIMPLE_VERTICES - )); - errorOk(); -} - -void triPrismBuffer( - meshvertex_t *vertices, - const float_t x0, const float_t y0, - const float_t x1, const float_t y1, - const float_t x2, const float_t y2, - const float_t minZ, - const float_t maxZ -) { - assertNotNull(vertices, "Vertices cannot be NULL"); - - int32_t vi = 0; - #define PRISM_VERT(px, py, pz, u, v) \ - vertices[vi].pos[0] = (px); \ - vertices[vi].pos[1] = (py); \ - vertices[vi].pos[2] = (pz); \ - vertices[vi].uv[0] = (u); \ - vertices[vi].uv[1] = (v); \ - vi++; - - /* --- Front face (z = maxZ), CCW from +Z --- */ - PRISM_VERT(x0, y0, maxZ, 0.0f, 0.0f) - PRISM_VERT(x1, y1, maxZ, 1.0f, 0.0f) - PRISM_VERT(x2, y2, maxZ, 0.5f, 1.0f) - - /* --- Back face (z = minZ), CCW from -Z (reverse winding) --- */ - PRISM_VERT(x2, y2, minZ, 0.5f, 1.0f) - PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f) - PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f) - - /* --- Side face 0: edge p0->p1 --- */ - PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f) - PRISM_VERT(x1, y1, minZ, 1.0f, 0.0f) - PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f) - - PRISM_VERT(x0, y0, minZ, 0.0f, 0.0f) - PRISM_VERT(x1, y1, maxZ, 1.0f, 1.0f) - PRISM_VERT(x0, y0, maxZ, 0.0f, 1.0f) - - /* --- Side face 1: edge p1->p2 --- */ - PRISM_VERT(x1, y1, minZ, 0.0f, 0.0f) - PRISM_VERT(x2, y2, minZ, 1.0f, 0.0f) - PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f) - - PRISM_VERT(x1, y1, minZ, 0.0f, 0.0f) - PRISM_VERT(x2, y2, maxZ, 1.0f, 1.0f) - PRISM_VERT(x1, y1, maxZ, 0.0f, 1.0f) - - /* --- Side face 2: edge p2->p0 --- */ - PRISM_VERT(x2, y2, minZ, 0.0f, 0.0f) - PRISM_VERT(x0, y0, minZ, 1.0f, 0.0f) - PRISM_VERT(x0, y0, maxZ, 1.0f, 1.0f) - - PRISM_VERT(x2, y2, minZ, 0.0f, 0.0f) - PRISM_VERT(x0, y0, maxZ, 1.0f, 1.0f) - PRISM_VERT(x2, y2, maxZ, 0.0f, 1.0f) - - #undef PRISM_VERT -} diff --git a/src/dusk/display/mesh/triprism.h b/src/dusk/display/mesh/triprism.h deleted file mode 100644 index ef86d591..00000000 --- a/src/dusk/display/mesh/triprism.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/mesh/mesh.h" - -#define TRIPRISM_VERTEX_COUNT 24 -#define TRIPRISM_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES - -extern mesh_t TRIPRISM_MESH_SIMPLE; -extern meshvertex_t TRIPRISM_MESH_SIMPLE_VERTICES[TRIPRISM_VERTEX_COUNT]; - -/** - * Initializes the simple unit triangular prism. The cross-section triangle has - * vertices (0,0), (1,0), (0.5,1) in XY, extruded from z=0 to z=1. - * - * @return Error for initialization of the triangular prism mesh. - */ -errorret_t triPrismInit(); - -/** - * Buffers a triangular prism into the provided vertex array. - * The triangular cross-section is defined by three 2D points in the XY plane; - * the prism is extruded along the Z axis between minZ and maxZ. - * Writes TRIPRISM_VERTEX_COUNT (24) vertices (CCW winding). - * - * @param vertices Vertex array to write into. - * @param x0,y0 First triangle vertex (XY). - * @param x1,y1 Second triangle vertex (XY). - * @param x2,y2 Third triangle vertex (XY). - * @param minZ Near Z extent of the prism. - * @param maxZ Far Z extent of the prism. - * @param color Color applied to all vertices. - */ -void triPrismBuffer( - meshvertex_t *vertices, - const float_t x0, const float_t y0, - const float_t x1, const float_t y1, - const float_t x2, const float_t y2, - const float_t minZ, - const float_t maxZ -); diff --git a/src/dusk/display/screen/CMakeLists.txt b/src/dusk/display/screen/CMakeLists.txt deleted file mode 100644 index 916cdc34..00000000 --- a/src/dusk/display/screen/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - screen.c -) \ No newline at end of file diff --git a/src/dusk/display/screen/screen.c b/src/dusk/display/screen/screen.c deleted file mode 100644 index 333cbeeb..00000000 --- a/src/dusk/display/screen/screen.c +++ /dev/null @@ -1,465 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "screen.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/mesh/quad.h" -#include "display/shader/shaderunlit.h" - -screen_t SCREEN; - -const screencropaspectinfo_t SCREEN_CROP_ASPECTS[SCREEN_CROP_ASPECT_COUNT] = { - [SCREEN_CROP_ASPECT_4_3] = { .ratio = 4.0f / 3.0f }, - [SCREEN_CROP_ASPECT_3_2] = { .ratio = 3.0f / 2.0f }, - [SCREEN_CROP_ASPECT_16_10] = { .ratio = 16.0f / 10.0f }, - [SCREEN_CROP_ASPECT_16_9] = { .ratio = 16.0f / 9.0f }, -}; - -errorret_t screenInit() { - memoryZero(&SCREEN, sizeof(screen_t)); - - SCREEN.scaleUi = DUSK_DISPLAY_SCALE_UI; - SCREEN.scale3d = DUSK_DISPLAY_SCALE_3D; - SCREEN.background = COLOR_CORNFLOWER_BLUE; - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - SCREEN.mode = SCREEN_MODE_FIXED_VIEWPORT_HEIGHT; - SCREEN.fixedHeight.height = DUSK_DISPLAY_SCREEN_HEIGHT; - - quadBuffer( - SCREEN.frameBufferMeshVertices, - 0.0f, 0.0f, - 1.0f, 1.0f, - 0.0f, 0.0f, - 1.0f, 1.0f - ); - errorChain(meshInit( - &SCREEN.frameBufferMesh, - QUAD_PRIMITIVE_TYPE, - QUAD_VERTEX_COUNT, - SCREEN.frameBufferMeshVertices - )); - #endif - - // Init screen to backbuffer mode by default - errorChain(screenBind()); - errorOk(); -} - -errorret_t screenBind() { - // Assume backbuffer is currently bound. - switch(SCREEN.mode) { - case SCREEN_MODE_BACKBUFFER: { - #if defined(DUSK_DISPLAY_WIDTH) && defined(DUSK_DISPLAY_HEIGHT) - SCREEN.height = DUSK_DISPLAY_HEIGHT; - SCREEN.aspect = frameBufferGetAspect(FRAMEBUFFER_BOUND); - SCREEN.width = SCREEN.height * SCREEN.aspect; - #else - SCREEN.width = frameBufferGetWidth(FRAMEBUFFER_BOUND); - SCREEN.height = frameBufferGetHeight(FRAMEBUFFER_BOUND); - SCREEN.aspect = frameBufferGetAspect(FRAMEBUFFER_BOUND); - #endif - screenUpdateScan(); - - // No needd for a framebuffer. - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - if(SCREEN.framebufferReady) { - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - #endif - break; - } - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - case SCREEN_MODE_FIXED_SIZE: { - SCREEN.width = SCREEN.fixedSize.width; - SCREEN.height = SCREEN.fixedSize.height; - SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - screenUpdateScan(); - - if(SCREEN.framebufferReady) { - // Is current framebuffer the correct size? - int32_t curFbWidth, curFbHeight; - curFbWidth = frameBufferGetWidth(&SCREEN.framebuffer); - curFbHeight = frameBufferGetHeight(&SCREEN.framebuffer); - if(curFbWidth == SCREEN.width && curFbHeight == SCREEN.height) { - // Correct size, nothing to do. - errorChain(frameBufferBind(&SCREEN.framebuffer)); - errorOk(); - } - - // Need a new framebuffer. - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - - // Create new framebuffer - errorChain(frameBufferInit( - &SCREEN.framebuffer, SCREEN.width, SCREEN.height - )); - SCREEN.framebufferReady = true; - errorChain(frameBufferBind(&SCREEN.framebuffer)); - break; - } - - case SCREEN_MODE_ASPECT_RATIO: { - // Aspect ratio mode, requires a framebuffer. - int32_t fbWidth, fbHeight; - fbWidth = frameBufferGetWidth(FRAMEBUFFER_BOUND); - fbHeight = frameBufferGetHeight(FRAMEBUFFER_BOUND); - float_t currentAspect = frameBufferGetAspect(FRAMEBUFFER_BOUND); - if(currentAspect == SCREEN.aspectRatio.ratio) { - // No need to use framebuffer. - SCREEN.width = fbWidth; - SCREEN.height = fbHeight; - SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - screenUpdateScan(); - - if(SCREEN.framebufferReady) { - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - errorOk(); - } - - int32_t newFbWidth, newFbHeight; - if(currentAspect > SCREEN.aspectRatio.ratio) { - // Wider than target aspect, limit by height - newFbWidth = (int32_t)floorf(fbHeight * SCREEN.aspectRatio.ratio); - newFbHeight = (int32_t)fbHeight; - } else { - // Taller than target aspect, limit by width - newFbHeight = (int32_t)floorf(fbWidth / SCREEN.aspectRatio.ratio); - newFbWidth = (int32_t)fbWidth; - } - - if(SCREEN.framebufferReady) { - // Is current framebuffer the correct size? - int32_t curFbWidth, curFbHeight; - float_t curFbAspect = frameBufferGetAspect(&SCREEN.framebuffer); - curFbWidth = frameBufferGetWidth(&SCREEN.framebuffer); - curFbHeight = frameBufferGetHeight(&SCREEN.framebuffer); - if(curFbWidth == newFbWidth && curFbHeight == newFbHeight) { - // Correct size, nothing to do. - SCREEN.width = newFbWidth; - SCREEN.height = newFbHeight; - SCREEN.aspect = curFbAspect; - screenUpdateScan(); - errorChain(frameBufferBind(&SCREEN.framebuffer)); - errorOk(); - } - - // Need a new framebuffer. - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - - // Create new framebuffer - errorChain(frameBufferInit( - &SCREEN.framebuffer, newFbWidth, newFbHeight - )); - SCREEN.width = newFbWidth; - SCREEN.height = newFbHeight; - SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - screenUpdateScan(); - SCREEN.framebufferReady = true; - - // Bind FB - errorChain(frameBufferBind(&SCREEN.framebuffer)); - break; - } - - case SCREEN_MODE_FIXED_HEIGHT: { - float_t fbWidth = (float_t)frameBufferGetWidth(FRAMEBUFFER_BOUND); - float_t fbHeight = (float_t)frameBufferGetHeight(FRAMEBUFFER_BOUND); - float_t fbAspect = fbWidth / fbHeight; - - int32_t newFbWidth, newFbHeight; - newFbHeight = SCREEN.fixedHeight.height; - newFbWidth = (int32_t)floorf(newFbHeight * fbAspect); - - SCREEN.width = newFbWidth; - SCREEN.height = newFbHeight; - SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - screenUpdateScan(); - - if(fbWidth == newFbWidth && fbHeight == newFbHeight) { - // No need to use framebuffer. - if(SCREEN.framebufferReady) { - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - errorOk(); - } - - if(SCREEN.framebufferReady) { - // Is current framebuffer the correct size? - int32_t curFbWidth, curFbHeight; - curFbWidth = frameBufferGetWidth(&SCREEN.framebuffer); - curFbHeight = frameBufferGetHeight(&SCREEN.framebuffer); - if(curFbWidth == newFbWidth && curFbHeight == newFbHeight) { - errorChain(frameBufferBind(&SCREEN.framebuffer)); - errorOk(); - } - - // Need a new framebuffer. - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - - // Create a new framebuffer. - errorChain(frameBufferInit( - &SCREEN.framebuffer, newFbWidth, newFbHeight - )); - SCREEN.framebufferReady = true; - errorChain(frameBufferBind(&SCREEN.framebuffer)); - break; - } - - case SCREEN_MODE_FIXED_WIDTH: { - float_t fbWidth = (float_t)frameBufferGetWidth(FRAMEBUFFER_BOUND); - float_t fbHeight = (float_t)frameBufferGetHeight(FRAMEBUFFER_BOUND); - float_t fbAspect = fbWidth / fbHeight; - - int32_t newFbWidth, newFbHeight; - newFbWidth = SCREEN.fixedWidth.width; - newFbHeight = (int32_t)floorf(newFbWidth / fbAspect); - - SCREEN.width = newFbWidth; - SCREEN.height = newFbHeight; - SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - screenUpdateScan(); - - if(fbWidth == newFbWidth && fbHeight == newFbHeight) { - // No need to use framebuffer. - if(SCREEN.framebufferReady) { - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - errorOk(); - } - - if(SCREEN.framebufferReady) { - // Is current framebuffer the correct size? - int32_t curFbWidth, curFbHeight; - curFbWidth = frameBufferGetWidth(&SCREEN.framebuffer); - curFbHeight = frameBufferGetHeight(&SCREEN.framebuffer); - if(curFbWidth == newFbWidth && curFbHeight == newFbHeight) { - errorChain(frameBufferBind(&SCREEN.framebuffer)); - errorOk(); - } - - // Need a new framebuffer. - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - - // Create a new framebuffer. - errorChain(frameBufferInit( - &SCREEN.framebuffer, newFbWidth, newFbHeight - )); - SCREEN.framebufferReady = true; - errorChain(frameBufferBind(&SCREEN.framebuffer)); - break; - } - - case SCREEN_MODE_FIXED_VIEWPORT_HEIGHT: { - SCREEN.height = SCREEN.fixedViewportHeight.height; - float_t fbWidth = (float_t)frameBufferGetWidth(FRAMEBUFFER_BOUND); - float_t fbHeight = (float_t)frameBufferGetHeight(FRAMEBUFFER_BOUND); - float_t fbAspect = fbWidth / fbHeight; - SCREEN.width = (int32_t)floorf(SCREEN.height * fbAspect); - SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - screenUpdateScan(); - break; - } - - #endif - - default: { - assertUnreachable("Invalid screen mode."); - break; - } - } - - errorOk(); -} - -errorret_t screenUnbind() { - switch(SCREEN.mode) { - // Nothing to do here. - case SCREEN_MODE_BACKBUFFER: - break; - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - case SCREEN_MODE_ASPECT_RATIO: - case SCREEN_MODE_FIXED_HEIGHT: - case SCREEN_MODE_FIXED_SIZE: - case SCREEN_MODE_FIXED_WIDTH: - if(SCREEN.framebufferReady) { - errorChain(frameBufferBind(NULL)); - } - break; - - case SCREEN_MODE_FIXED_VIEWPORT_HEIGHT: - break; - #endif - - default: - assertUnreachable("Invalid screen mode."); - break; - } - - errorOk(); -} - -errorret_t screenRender() { - if(SCREEN.mode == SCREEN_MODE_BACKBUFFER) { - errorOk(); - } - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - if(SCREEN.mode == SCREEN_MODE_FIXED_VIEWPORT_HEIGHT) { - glViewport(0, 0, SCREEN.width, SCREEN.height); - errorOk(); - } - - if( - SCREEN.mode == SCREEN_MODE_ASPECT_RATIO || - SCREEN.mode == SCREEN_MODE_FIXED_HEIGHT || - SCREEN.mode == SCREEN_MODE_FIXED_SIZE || - SCREEN.mode == SCREEN_MODE_FIXED_WIDTH - ) { - if(!SCREEN.framebufferReady) { - // Nothing to do here. - errorOk(); - } - - float_t bbWidth, bbHeight; - bbWidth = (float_t)frameBufferGetWidth(FRAMEBUFFER_BOUND); - bbHeight = (float_t)frameBufferGetHeight(FRAMEBUFFER_BOUND); - - float_t backBufferAspect = bbWidth / bbHeight; - - // Determine framebuffer centering - float_t fbWidth, fbHeight, fbAspect; - float_t fbX, fbY; - - fbWidth = frameBufferGetWidth(&SCREEN.framebuffer); - fbHeight = frameBufferGetHeight(&SCREEN.framebuffer); - fbAspect = fbWidth / fbHeight; - - if(backBufferAspect > fbAspect) { - fbHeight = bbHeight; - fbWidth = fbHeight * fbAspect; - fbX = (bbWidth - fbWidth) * 0.5f; - fbY = 0.0f; - } else { - fbWidth = bbWidth; - fbHeight = fbWidth / fbAspect; - fbX = 0.0f; - fbY = (bbHeight - fbHeight) * 0.5f; - } - - // Determine back buffer matricies - float_t centerX = bbWidth * 0.5f; - float_t centerY = bbHeight * 0.5f; - mat4 view, proj, model; - glm_ortho( - 0.0f, bbWidth, bbHeight, 0.0f, 0.01f, 1.0f, - proj - ); - glm_mat4_identity(view); - glm_mat4_identity(model); - - quadBuffer( - SCREEN.frameBufferMeshVertices, - centerX - fbWidth * 0.5f, centerY + fbHeight * 0.5f, - centerX + fbWidth * 0.5f, centerY - fbHeight * 0.5f, - 0.0f, 0.0f, - 1.0f, 1.0f - ); - - frameBufferClear( - FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH, - COLOR_BLACK - ); - - shaderBind(&SHADER_UNLIT); - shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj); - shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view); - shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model); - - // errorChain(textureBind(&SCREEN.framebuffer.texture)); - errorChain(meshDraw(&SCREEN.frameBufferMesh, 0, -1)); - - errorOk(); - }; - #endif - - assertUnreachable("Invalid screen mode."); - errorThrow("Invalid screen mode."); -} - -void screenUpdateScan(void) { - #ifdef DUSK_DISPLAY_OVERSCAN - SCREEN.scanX = DUSK_DISPLAY_OVERSCAN; - SCREEN.scanY = DUSK_DISPLAY_OVERSCAN; - #else - SCREEN.scanX = 0; - SCREEN.scanY = 0; - #endif - - SCREEN.scanWidth = SCREEN.width - (SCREEN.scanX * 2); - SCREEN.scanHeight = SCREEN.height - (SCREEN.scanY * 2); - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - // Find the smallest standard ratio >= the screen aspect. - float_t targetRatio = -1.0f; - for(int32_t i = 0; i < SCREEN_CROP_ASPECT_COUNT; i++) { - if(SCREEN_CROP_ASPECTS[i].ratio >= SCREEN.aspect) { - targetRatio = SCREEN_CROP_ASPECTS[i].ratio; - break; - } - } - if(targetRatio < 0.0f) { - // Wider than all entries: cap at last (16:9). - targetRatio = SCREEN_CROP_ASPECTS[ - SCREEN_CROP_ASPECT_COUNT - 1 - ].ratio; - } - - if(targetRatio > SCREEN.aspect) { - // Letterbox: target is wider, restrict height. - int32_t targetH = (int32_t)floorf( - (float_t)SCREEN.width / targetRatio - ); - SCREEN.scanY = (SCREEN.height - targetH) / 2; - SCREEN.scanHeight = targetH; - } else if(targetRatio < SCREEN.aspect) { - // Pillarbox: target is narrower, restrict width. - int32_t targetW = (int32_t)floorf( - (float_t)SCREEN.height * targetRatio - ); - SCREEN.scanX = (SCREEN.width - targetW) / 2; - SCREEN.scanWidth = targetW; - } - #endif -} - -errorret_t screenDispose() { - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - if(SCREEN.framebufferReady) { - errorChain(frameBufferDispose(&SCREEN.framebuffer)); - SCREEN.framebufferReady = false; - } - #endif - - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/display/screen/screen.h b/src/dusk/display/screen/screen.h deleted file mode 100644 index 8d28536f..00000000 --- a/src/dusk/display/screen/screen.h +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "display/display.h" -#include "display/framebuffer/framebuffer.h" -#include "display/mesh/quad.h" -#include "display/color.h" - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - #ifndef DUSK_DISPLAY_SCREEN_HEIGHT - #error "DUSK_DISPLAY_SCREEN_HEIGHT must be defined" - #endif -#endif - -typedef enum { - SCREEN_CROP_ASPECT_4_3, - SCREEN_CROP_ASPECT_3_2, - SCREEN_CROP_ASPECT_16_10, - SCREEN_CROP_ASPECT_16_9, - SCREEN_CROP_ASPECT_COUNT -} screencropaspect_t; - -typedef struct { - float_t ratio; -} screencropaspectinfo_t; - -/** - * Standard crop aspect ratios, sorted narrowest to widest. - * screenUpdateScan() selects the smallest ratio >= the current - * screen aspect; screens wider than the widest entry are capped - * at the last entry (16:9). - */ -extern const screencropaspectinfo_t - SCREEN_CROP_ASPECTS[SCREEN_CROP_ASPECT_COUNT]; - -typedef enum { - SCREEN_MODE_BACKBUFFER, - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - SCREEN_MODE_FIXED_SIZE, - SCREEN_MODE_ASPECT_RATIO,// Maintains aspect at all cost - SCREEN_MODE_FIXED_HEIGHT, // Fixed height, width expands/contracts as needed - SCREEN_MODE_FIXED_WIDTH, // Fixed width, height expands/contracts as needed - // Fixed viewport height. Fixed height but higher resolution. - SCREEN_MODE_FIXED_VIEWPORT_HEIGHT, - #endif -} screenmode_t; - -// typedef enum { -// SCREEN_SCALE_MODE_FILL, -// SCREEN_SCALE_MODE_INTEGER, -// SCREEN_SCALE_MODE_INEGER_OVERFLOW -// } screenscalemode_t; - -typedef struct { - int32_t scaleUi; - int32_t scale3d; - screenmode_t mode; - // screenscalemode_t scaleMode; - - // Calculated dimensions of the viewport, to be used by the camera - int32_t width; - int32_t height; - float_t aspect; - - // Overscan-safe area; defaults to full viewport each frame. - // Platforms with TV overscan set these after screenBind(). - int32_t scanX; - int32_t scanY; - int32_t scanWidth; - int32_t scanHeight; - - color_t background; - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - framebuffer_t framebuffer; - bool_t framebufferReady; - // camera_t framebufferCamera; - mesh_t frameBufferMesh; - meshvertex_t frameBufferMeshVertices[QUAD_VERTEX_COUNT]; - #endif - - union { - struct { - int32_t width; - int32_t height; - } fixedSize; - - struct { - float_t ratio; - } aspectRatio; - - struct { - int32_t height; - } fixedHeight; - - struct { - int32_t width; - } fixedWidth; - - struct { - int32_t height; - } fixedViewportHeight; - }; -} screen_t; - -extern screen_t SCREEN; - -/** - * Initializes the screen system. - * - * @return Error code and state, if error occurs. - */ -errorret_t screenInit(); - -/** - * Binds the screen, this is done before rendering game content. - * - * @return Error code and state, if error occurs. - */ -errorret_t screenBind(); - -/** - * Unbinds the screen, does nothing for now. - * - * @return Error code and state, if error occurs. - */ -errorret_t screenUnbind(); - -/** - * Renders the screen to the current framebuffer. - * - * @return Error code and state, if error occurs. - */ -errorret_t screenRender(); - -/** - * Updates the scan area for the current screen dimensions. - * Defaults to the full viewport. On dynamic displays, finds the - * smallest SCREEN_CROP_ASPECTS entry whose ratio is >= the - * screen aspect and centers a crop window of that ratio. - * Screens narrower than the target are letterboxed; screens - * wider than the target are pillarboxed. Screens wider than - * all entries are pillarboxed to the last entry (16:9). - */ -void screenUpdateScan(void); - -/** - * Disposes the screen system. - * - * @return Error code and state, if error occurs. - */ -errorret_t screenDispose(); \ No newline at end of file diff --git a/src/dusk/display/shader/CMakeLists.txt b/src/dusk/display/shader/CMakeLists.txt deleted file mode 100644 index 3ec11731..00000000 --- a/src/dusk/display/shader/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - shader.c - shaderlist.c - shaderunlit.c -) \ No newline at end of file diff --git a/src/dusk/display/shader/shader.c b/src/dusk/display/shader/shader.c deleted file mode 100644 index 92c6fb09..00000000 --- a/src/dusk/display/shader/shader.c +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "shader.h" -#include "shadermaterial.h" -#include "assert/assert.h" -#include "log/log.h" - -shader_t *bound = NULL; - -errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) { - assertNotNull(shader, "Shader cannot be null"); - errorChain(shaderInitPlatform(shader, def)); - bound = NULL; - errorOk(); -} - -errorret_t shaderBind(shader_t *shader) { - assertNotNull(shader, "Shader cannot be null"); - errorChain(shaderBindPlatform(shader)); - bound = shader; - errorOk(); -} - -errorret_t shaderSetMatrix( - shader_t *shader, - const char_t *name, - mat4 matrix -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - assertNotNull(matrix, "Matrix cannot be null"); - assertTrue(bound == shader, "Shader must be bound."); - errorChain(shaderSetMatrixPlatform(shader, name, matrix)); - errorOk(); -} - -errorret_t shaderSetTexture( - shader_t *shader, - const char_t *name, - texture_t *texture -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - assertTrue(bound == shader, "Shader must be bound."); - errorChain(shaderSetTexturePlatform(shader, name, texture)); - errorOk(); -} - -errorret_t shaderSetColor( - shader_t *shader, - const char_t *name, - color_t color -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - assertTrue(bound == shader, "Shader must be bound."); - errorChain(shaderSetColorPlatform(shader, name, color)); - errorOk(); -} - -errorret_t shaderSetMaterial( - shader_t *shader, - const shadermaterial_t *material -) { - assertNotNull(shader, "Shader cannot be null"); - assertNotNull(material, "Material cannot be null"); - assertTrue(bound == shader, "Shader must be bound."); - assertNotNull(shader->definition, "Shader definition cannot be null"); - assertNotNull(shader->definition->setMaterial, "Def lacks setMaterial"); - return shader->definition->setMaterial(shader, material); -} - -errorret_t shaderDispose(shader_t *shader) { - assertNotNull(shader, "Shader cannot be null"); - bound = NULL; - errorChain(shaderDisposePlatform(shader)); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/display/shader/shader.h b/src/dusk/display/shader/shader.h deleted file mode 100644 index bb94e3a1..00000000 --- a/src/dusk/display/shader/shader.h +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/texture/texture.h" -#include "display/shader/shaderplatform.h" - -#ifndef shaderInitPlatform - #error "shaderInitPlatform must be defined to use shader.h" -#endif -#ifndef shaderBindPlatform - #error "shaderBindPlatform must be defined to use shader.h" -#endif -#ifndef shaderSetMatrixPlatform - #error "shaderSetMatrixPlatform must be defined to use shader.h" -#endif -#ifndef shaderDisposePlatform - #error "shaderDisposePlatform must be defined to use shader.h" -#endif - -typedef union shadermaterial_u shadermaterial_t; -typedef shaderplatform_t shader_t; -typedef shaderdefinitionplatform_t shaderdefinition_t; - -/** - * Initializes a shader. This is platform dependant. - * - * @param shader Shader to initialize - * @param def Definition of the shader to initialize with. - * @return Error if failure, otherwise errorOk - */ -errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def); - -/** - * Binds a shader. This is platform dependant. - * - * @param shader Shader to bind - * @return Error if failure, otherwise errorOk - */ -errorret_t shaderBind(shader_t *shader); - -/** - * Sets a matrix uniform in the shader. This is platform dependant. - * - * @param shader Shader to set the matrix in - * @param name Name of the uniform to set - * @param matrix Matrix to set - * @return Error if failure, otherwise errorOk - */ -errorret_t shaderSetMatrix( - shader_t *shader, - const char_t *name, - mat4 matrix -); - -/** - * Sets a texture uniform in the shader. This is platform dependant. - * - * @param shader Shader to set the texture in - * @param name Name of the uniform to set - * @param texture Texture to set - * @return Error if failure, otherwise errorOk - */ -errorret_t shaderSetTexture( - shader_t *shader, - const char_t *name, - texture_t *texture -); - -/** - * Sets a color uniform in the shader. This is platform dependant. - * - * @param shader Shader to set the color in - * @param name Name of the uniform to set - * @param color Color to set - * @return Error if failure, otherwise errorOk - */ -errorret_t shaderSetColor( - shader_t *shader, - const char_t *name, - color_t color -); - -/** - * Sets a material's properties in the shader. This is platform dependant. - * the definition's upload function pointer. - * - * @param shader The shader to upload material properties to. - * @param material The material data to upload. - * @return Error if failure, otherwise errorOk. - */ -errorret_t shaderSetMaterial( - shader_t *shader, - const shadermaterial_t *material -); - -/** - * Disposes of a shader. This is platform dependant. - * - * @param shader Shader to dispose - * @return Error if failure, otherwise errorOk - */ -errorret_t shaderDispose(shader_t *shader); \ No newline at end of file diff --git a/src/dusk/display/shader/shaderlist.c b/src/dusk/display/shader/shaderlist.c deleted file mode 100644 index 9a816298..00000000 --- a/src/dusk/display/shader/shaderlist.c +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "shaderlist.h" -#include "display/screen/screen.h" -#include "assert/assert.h" - -shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT] = { - [SHADER_LIST_SHADER_UNLIT] = { - .shader = &SHADER_UNLIT, - .definition = &SHADER_UNLIT_DEFINITION - }, -}; - -errorret_t shaderListInit() { - mat4 view, proj, model; - glm_lookat( - (vec3){ 0.0f, 0.0f, 1.0f }, - (vec3){ 0.0f, 0.0f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f }, - view - ); - - glm_perspective( - glm_rad(45.0f), - SCREEN.aspect, - 0.1f, - 100.0f, - proj - ); - - glm_mat4_identity(model); - - for(shaderlistshader_t i = 0; i < SHADER_LIST_SHADER_COUNT; i++) { - if(i == SHADER_LIST_SHADER_NULL) { - continue; - } - - assertNotNull( - SHADER_LIST_DEFS[i].shader, "Shader cannot be null" - ); - assertNotNull( - SHADER_LIST_DEFS[i].definition, "Shader definition cannot be null" - ); - - errorChain(shaderInit( - SHADER_LIST_DEFS[i].shader, SHADER_LIST_DEFS[i].definition - )); - errorChain(shaderBind(SHADER_LIST_DEFS[i].shader)); - errorChain(shaderSetMatrix( - SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_PROJECTION, proj - )); - errorChain(shaderSetMatrix( - SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_VIEW, view - )); - errorChain(shaderSetMatrix( - SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_MODEL, model - )); - errorChain(shaderSetTexture( - SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_TEXTURE, NULL - )); - errorChain(shaderSetColor( - SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_COLOR, COLOR_WHITE - )); - } - - errorOk(); -} - -errorret_t shaderListDispose(void) { - for(shaderlistshader_t i = 0; i < SHADER_LIST_SHADER_COUNT; i++) { - if(i == SHADER_LIST_SHADER_NULL) { - continue; - } - - assertNotNull( - SHADER_LIST_DEFS[i].shader, "Shader cannot be null" - ); - - errorChain(shaderDispose(SHADER_LIST_DEFS[i].shader)); - } - errorOk(); -} diff --git a/src/dusk/display/shader/shaderlist.h b/src/dusk/display/shader/shaderlist.h deleted file mode 100644 index e7556362..00000000 --- a/src/dusk/display/shader/shaderlist.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/shader/shader.h" -#include "display/shader/shaderunlit.h" - -typedef enum { - SHADER_LIST_SHADER_NULL, - - SHADER_LIST_SHADER_UNLIT, - - SHADER_LIST_SHADER_COUNT -} shaderlistshader_t; - -typedef struct { - shader_t *shader; - shaderdefinition_t *definition; -} shaderlistdef_t; - -extern shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT]; - -/** - * Initializes all default shaders and uploads the initial view, projection, - * and model matrices to each. - * - * @return Error state. - */ -errorret_t shaderListInit(); - -/** - * Disposes all default shaders. - * - * @return Error state. - */ -errorret_t shaderListDispose(void); diff --git a/src/dusk/display/shader/shadermaterial.h b/src/dusk/display/shader/shadermaterial.h deleted file mode 100644 index e01e0cc4..00000000 --- a/src/dusk/display/shader/shadermaterial.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/shader/shaderlist.h" - -typedef union shadermaterial_u { - shaderunlitmaterial_t unlit; -} shadermaterial_t; \ No newline at end of file diff --git a/src/dusk/display/shader/shaderunlit.c b/src/dusk/display/shader/shaderunlit.c deleted file mode 100644 index a6d04649..00000000 --- a/src/dusk/display/shader/shaderunlit.c +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "shaderunlit.h" -#include "display/shader/shadermaterial.h" - -shader_t SHADER_UNLIT = { - .definition = &SHADER_UNLIT_DEFINITION -}; - -errorret_t shaderUnlitSetMaterial( - shader_t *shader, - const shadermaterial_t *material -) { - errorChain(shaderSetTexture( - shader, - SHADER_UNLIT_TEXTURE, - material->unlit.texture - )); - - errorChain(shaderSetColor( - shader, - SHADER_UNLIT_COLOR, - material->unlit.color - )); - - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/display/shader/shaderunlit.h b/src/dusk/display/shader/shaderunlit.h deleted file mode 100644 index 3a04d613..00000000 --- a/src/dusk/display/shader/shaderunlit.h +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "shader.h" - -#define SHADER_UNLIT_PROJECTION "u_Proj" -#define SHADER_UNLIT_VIEW "u_View" -#define SHADER_UNLIT_MODEL "u_Model" -#define SHADER_UNLIT_TEXTURE "u_Texture" -#define SHADER_UNLIT_COLOR "u_Color" - -typedef struct { - color_t color; - texture_t *texture; -} shaderunlitmaterial_t; - -extern shaderdefinition_t SHADER_UNLIT_DEFINITION; -extern shader_t SHADER_UNLIT; - -/** - * Uploads the unlit material properties to the shader. - * - * @param shader The shader to upload to. - * @param material The material data to upload. - * @return Error if failure, otherwise errorOk. - */ -errorret_t shaderUnlitSetMaterial( - shader_t *shader, - const shadermaterial_t *material -); \ No newline at end of file diff --git a/src/dusk/display/spritebatch/CMakeLists.txt b/src/dusk/display/spritebatch/CMakeLists.txt deleted file mode 100644 index 20ba6f23..00000000 --- a/src/dusk/display/spritebatch/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - spritebatch.c - spritebatchsprite.c -) \ No newline at end of file diff --git a/src/dusk/display/spritebatch/spritebatch.c b/src/dusk/display/spritebatch/spritebatch.c deleted file mode 100644 index 32cbad9a..00000000 --- a/src/dusk/display/spritebatch/spritebatch.c +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "spritebatch.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include "display/shader/shadermaterial.h" - -meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; -spritebatch_t SPRITEBATCH; - -errorret_t spriteBatchInit() { - memoryZero(&SPRITEBATCH, sizeof(spritebatch_t)); - errorChain(meshInit( - &SPRITEBATCH.mesh, - QUAD_PRIMITIVE_TYPE, - SPRITEBATCH_VERTEX_COUNT, - SPRITEBATCH_VERTICES - )); - errorOk(); -} - -errorret_t spriteBatchBuffer( - const spritebatchsprite_t *sprites, - const uint32_t count, - shader_t *shader, - const shadermaterial_t material -) { - assertNotNull(sprites, "Sprites cannot be null"); - assertTrue(count > 0, "Count must be greater than zero"); - assertNotNull(shader, "Shader cannot be null"); - - // Did the shader or material data change? - if(shader != SPRITEBATCH.shader) { - errorChain(spriteBatchFlush()); - SPRITEBATCH.shader = shader; - SPRITEBATCH.material = material; - } else if(memoryCompare( - &material, &SPRITEBATCH.material, sizeof(shadermaterial_t) - ) != 0) { - // Did the material data change? - errorChain(spriteBatchFlush()); - SPRITEBATCH.shader = shader; - SPRITEBATCH.material = material; - } - - // Buffer the vertices. - uint32_t remaining = count; - do { - uint32_t spritesBeforeFlush = ( - SPRITEBATCH_SPRITES_MAX_PER_FLUSH - SPRITEBATCH.spriteCount - ); - - if(spritesBeforeFlush == 0) { - // Flush if we have no capacity before flushing. - errorChain(spriteBatchFlush()); - spritesBeforeFlush = SPRITEBATCH_SPRITES_MAX_PER_FLUSH; - } - - // Many we buffering? - const uint32_t batchCount = mathMin( - remaining, - spritesBeforeFlush - ); - - // Destination - meshvertex_t *v = &SPRITEBATCH_VERTICES[ - (SPRITEBATCH.spriteCount + (SPRITEBATCH.spriteFlush * - SPRITEBATCH_SPRITES_MAX_PER_FLUSH)) * QUAD_VERTEX_COUNT - ]; - - // Buffer to the mesh vertices. - spriteBatchBufferToMesh( - sprites + (count - remaining), batchCount, v, - batchCount * QUAD_VERTEX_COUNT - ); - SPRITEBATCH.spriteCount += batchCount; - remaining -= batchCount; - } while(remaining > 0); - - errorOk(); -} - -void spriteBatchBufferToMesh( - const spritebatchsprite_t *sprites, - const uint32_t count, - meshvertex_t *vertices, - const uint32_t verticesSize -) { - assertNotNull(sprites, "Sprites cannot be null"); - assertTrue(count > 0, "Count must be greater than zero"); - assertNotNull(vertices, "Vertices cannot be null"); - assertTrue( - verticesSize >= count * QUAD_VERTEX_COUNT, "Vertices array too small" - ); - - for(uint32_t i = 0; i < count; i++ ){ - spritebatchsprite_t sprite = sprites[i]; - meshvertex_t *v = &vertices[i * QUAD_VERTEX_COUNT]; - - // Buffer the quad - v[0].pos[0] = sprite.min[0]; - v[0].pos[1] = sprite.min[1]; - v[0].pos[2] = sprite.min[2]; - v[0].uv[0] = sprite.uvMin[0]; - v[0].uv[1] = sprite.uvMin[1]; - - v[1].pos[0] = sprite.max[0]; - v[1].pos[1] = sprite.min[1]; - v[1].pos[2] = sprite.min[2]; - v[1].uv[0] = sprite.uvMax[0]; - v[1].uv[1] = sprite.uvMin[1]; - - v[2].pos[0] = sprite.max[0]; - v[2].pos[1] = sprite.max[1]; - v[2].pos[2] = sprite.max[2]; - v[2].uv[0] = sprite.uvMax[0]; - v[2].uv[1] = sprite.uvMax[1]; - - v[3].pos[0] = sprite.min[0]; - v[3].pos[1] = sprite.min[1]; - v[3].pos[2] = sprite.min[2]; - v[3].uv[0] = sprite.uvMin[0]; - v[3].uv[1] = sprite.uvMin[1]; - - v[4].pos[0] = sprite.max[0]; - v[4].pos[1] = sprite.max[1]; - v[4].pos[2] = sprite.max[2]; - v[4].uv[0] = sprite.uvMax[0]; - v[4].uv[1] = sprite.uvMax[1]; - - v[5].pos[0] = sprite.min[0]; - v[5].pos[1] = sprite.max[1]; - v[5].pos[2] = sprite.max[2]; - v[5].uv[0] = sprite.uvMin[0]; - v[5].uv[1] = sprite.uvMax[1]; - } -} - -void spriteBatchClear() { - SPRITEBATCH.spriteCount = 0; - SPRITEBATCH.spriteFlush = 0; - SPRITEBATCH.shader = NULL; - memoryZero(&SPRITEBATCH.material, sizeof(shadermaterial_t)); -} - -errorret_t spriteBatchFlush() { - if(SPRITEBATCH.spriteCount == 0) { - errorOk(); - } - - size_t vertexCount = QUAD_VERTEX_COUNT * SPRITEBATCH.spriteCount; - size_t vertexOffset = ( - SPRITEBATCH.spriteFlush * SPRITEBATCH_SPRITES_MAX_PER_FLUSH * - QUAD_VERTEX_COUNT - ); - - errorChain(shaderBind(SPRITEBATCH.shader)); - errorChain(shaderSetMaterial(SPRITEBATCH.shader, &SPRITEBATCH.material)); - errorChain(meshFlush(&SPRITEBATCH.mesh, vertexOffset, vertexCount)); - errorChain(meshDraw(&SPRITEBATCH.mesh, vertexOffset, vertexCount)); - - SPRITEBATCH.spriteFlush++; - if(SPRITEBATCH.spriteFlush >= SPRITEBATCH_FLUSH_COUNT) { - SPRITEBATCH.spriteFlush = 0; - } - SPRITEBATCH.spriteCount = 0; - - errorOk(); -} - -errorret_t spriteBatchDispose() { - errorChain(meshDispose(&SPRITEBATCH.mesh)); - errorOk(); -} diff --git a/src/dusk/display/spritebatch/spritebatch.h b/src/dusk/display/spritebatch/spritebatch.h deleted file mode 100644 index ee816aa5..00000000 --- a/src/dusk/display/spritebatch/spritebatch.h +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "spritebatchsprite.h" -#include "display/mesh/quad.h" -#include "display/texture/texture.h" -#include "display/shader/shadermaterial.h" - -#define SPRITEBATCH_SPRITES_MAX 512 -#define SPRITEBATCH_VERTEX_COUNT (SPRITEBATCH_SPRITES_MAX * QUAD_VERTEX_COUNT) -#define SPRITEBATCH_FLUSH_COUNT 16 -#define SPRITEBATCH_SPRITES_MAX_PER_FLUSH (\ - SPRITEBATCH_SPRITES_MAX / SPRITEBATCH_FLUSH_COUNT \ -) - -typedef struct { - mesh_t mesh; - int32_t spriteCount; - int32_t spriteFlush; - - shader_t *shader; - shadermaterial_t material; -} spritebatch_t; - -// Have to define these separately because of alignment on certain platforms. -extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; -extern spritebatch_t SPRITEBATCH; - -/** - * Initializes the global sprite batch and its internal mesh buffer. - * - * @return Error state. - */ -errorret_t spriteBatchInit(); - -/** - * Lowest-level buffer function. Writes sprites into the internal vertex buffer. - * Flushes automatically when the per-flush capacity is reached. Does not - * modify material state - call spriteBatchSetState or use a high-level push - * function before buffering. - * - * @param sprites Pointer to the sprite array. - * @param count Number of sprites to buffer. - * @param shader Shader to use when flushing. - * @param material Material information passed to the shader when flushing. - * @return Error state. - */ -errorret_t spriteBatchBuffer( - const spritebatchsprite_t *sprites, - const uint32_t count, - shader_t *shader, - const shadermaterial_t material -); - -/** - * Buffers an array of sprites to a given array of mesh vertices. This is the - * internal method that is used to buffer to the internal spritebatch mesh, but - * you can use it to achieve sprite buffering to a mesh you own. - * - * verticesSize is the size of the vertices array, we use this to ensure no - * buffer overflows. - * - * @param sprites Pointer to the sprite array. - * @param count Number of sprites to buffer. - * @param vertices Pointer to the vertex array to write to. - * @param verticesSize Size of the vertex array, in number of vertices. - */ -void spriteBatchBufferToMesh( - const spritebatchsprite_t *sprites, - const uint32_t count, - meshvertex_t *vertices, - const uint32_t verticesSize -); - -/** - * Resets sprite and flush counters and clears the current material state. - * Calling spriteBatchFlush after this renders nothing. - */ -void spriteBatchClear(); - -/** - * Uploads and draws all buffered sprites. If a material type has been set via - * spriteBatchSetState or spriteBatchCheckState, the shader is bound and the - * material is applied first. If matType is NULL the caller is responsible for - * having the correct shader already bound. Does nothing if the buffer is empty. - * - * @return Error state. - */ -errorret_t spriteBatchFlush(); - -/** - * Disposes of the sprite batch and frees its internal mesh buffer. - * - * @return Error state. - */ -errorret_t spriteBatchDispose(); diff --git a/src/dusk/display/spritebatch/spritebatchsprite.c b/src/dusk/display/spritebatch/spritebatchsprite.c deleted file mode 100644 index bc0437db..00000000 --- a/src/dusk/display/spritebatch/spritebatchsprite.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "spritebatchsprite.h" -#include "util/memory.h" - -spritebatchsprite_t spriteBatchSpriteTilesetPosition( - const tileset_t *tileset, - const uint16_t column, - const uint16_t row, - const float_t x, - const float_t y, - const float_t width, - const float_t height -) { - spritebatchsprite_t sprite; - if(width == 0 || height == 0) { - memoryZero(sprite.min, sizeof(vec3)); - memoryZero(sprite.max, sizeof(vec3)); - return sprite; - } - - vec4 uv; - tilesetPositionGetUV(tileset, column, row, uv); - - sprite.min[0] = x; - sprite.min[1] = y; - sprite.min[2] = 0.0f; - sprite.max[0] = x + width; - sprite.max[1] = y + height; - sprite.max[2] = 0.0f; - sprite.uvMin[0] = uv[0]; - sprite.uvMin[1] = uv[1]; - sprite.uvMax[0] = uv[2]; - sprite.uvMax[1] = uv[3]; - return sprite; -} diff --git a/src/dusk/display/spritebatch/spritebatchsprite.h b/src/dusk/display/spritebatch/spritebatchsprite.h deleted file mode 100644 index ef7e2924..00000000 --- a/src/dusk/display/spritebatch/spritebatchsprite.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "display/texture/tileset.h" - -typedef struct { - vec3 min; - vec3 max; - vec2 uvMin; - vec2 uvMax; -} spritebatchsprite_t; - -/** - * Builds a sprite from a tileset tile at a given column/row, positioned and - * sized in screen space. - * - * @param tileset Tileset to read UV coordinates from. - * @param column Column of the tile within the tileset. - * @param row Row of the tile within the tileset. - * @param x Screen x 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 height Height of the sprite in screen space. - * @returns The populated sprite. - */ -spritebatchsprite_t spriteBatchSpriteTilesetPosition( - const tileset_t *tileset, - const uint16_t column, - const uint16_t row, - const float_t x, - const float_t y, - const float_t width, - const float_t height -); diff --git a/src/dusk/display/text/CMakeLists.txt b/src/dusk/display/text/CMakeLists.txt deleted file mode 100644 index faca5a9a..00000000 --- a/src/dusk/display/text/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - text.c - font.c -) \ No newline at end of file diff --git a/src/dusk/display/text/font.c b/src/dusk/display/text/font.c deleted file mode 100644 index 9772e64d..00000000 --- a/src/dusk/display/text/font.c +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "font.h" -#include "util/memory.h" -#include "util/math.h" -#include "display/color.h" - -font_t FONT_DEFAULT; -static texture_t FONT_DEFAULT_TEXTURE; -static tileset_t FONT_DEFAULT_TILESET; - -const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][ - FONT_DEFAULT_TILE_HEIGHT -] = { - { 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // ! - { 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // " - { 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // # - { 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $ - { 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // % - { 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // & - { 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ' - { 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // ( - { 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // ) - { 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // * - { 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // + - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // , - { 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // - - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // . - { 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // / - { 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0 - { 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1 - { 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2 - { 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3 - { 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4 - { 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5 - { 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6 - { 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7 - { 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8 - { 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9 - { 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // : - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ; - { 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // < - { 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // = - { 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // > - { 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ? - { 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @ - { 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A - { 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B - { 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C - { 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D - { 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E - { 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F - { 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G - { 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H - { 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I - { 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J - { 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K - { 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L - { 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M - { 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N - { 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O - { 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P - { 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q - { 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R - { 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S - { 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T - { 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U - { 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V - { 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W - { 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X - { 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y - { 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z - { 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [ - // Custom icon glyph, not a real backslash - a backspace symbol: Caps - // Lock's up arrow (see FONT_ICON_CAPSLOCK) rotated 270 degrees to - // point left instead, adapted to this font's fixed 6x10 tile (each of - // Caps Lock's row widths becomes a column height here, centered - // vertically). Backslash was never drawn anyway, and isn't a key this - // virtual keyboard can type - see FONT_ICON_BACKSPACE. - // FONT_ICON_BACKSPACE - { 0x00, 0x00, 0x08, 0x1F, 0x3F, 0x3F, 0x1F, 0x08, 0x00, 0x00 }, - { 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ] - // ^ (not drawn in source font) - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E }, // _ - // Custom icon glyph, not a real backtick - an up arrow for Shift, same - // head as Caps Lock's (see FONT_ICON_CAPSLOCK) but with a shaft 2px - // thinner, plus one empty row near the bottom of it, so it reads as a - // lighter/broken version of Caps Lock's thicker uninterrupted one. - // Backtick was never drawn anyway, and isn't a key this virtual - // keyboard can type - see FONT_ICON_SHIFT. - // FONT_ICON_SHIFT - { 0x0C, 0x1E, 0x3F, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x0C, 0x00 }, - { 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a - { 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b - { 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c - { 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d - { 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e - { 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f - { 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g - { 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h - { 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i - { 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j - { 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k - { 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l - { 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m - { 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n - { 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o - { 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p - { 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q - { 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r - { 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s - { 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t - { 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u - { 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v - { 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w - { 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x - { 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y - { 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z - { 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // { - // Custom icon glyph, not a real pipe - a "return" arrow for the - // keyboard's newline key: a vertical riser down the right side that - // hooks left into a leftward-pointing arrowhead, i.e. "<-|" rotated - // into an L. Pipe was never drawn anyway, and isn't a key this - // virtual keyboard can type - see FONT_ICON_NEWLINE. - // FONT_ICON_NEWLINE - { 0x02, 0x02, 0x02, 0x02, 0x0E, 0x1E, 0x08, 0x00, 0x00, 0x00 }, - { 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // } - // Custom icon glyph, not a real tilde - a spacebar symbol for the - // keyboard's space key: an underscore with a tick at each end, like - // "|___|". Tilde was never drawn anyway, and was explicitly dropped - // from this virtual keyboard's own key set - see FONT_ICON_SPACE. - // FONT_ICON_SPACE - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x3F }, - // Custom icon glyph, not a real ASCII character - a thick, solid, - // uninterrupted up arrow for Caps Lock (contrast FONT_ICON_SHIFT's - // same head but a thinner, gapped shaft). Assigned to char code 127 - // (DEL) since that codepoint is never legitimately typed text and - // (unlike 128+) is still a positive value regardless of whether this - // platform's plain `char` is signed - see FONT_ICON_CAPSLOCK. - // FONT_ICON_CAPSLOCK - { 0x0C, 0x1E, 0x3F, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x00 }, - // unused trailing tile - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, -}; - -errorret_t fontDefaultInit(void) { - const int32_t width = (int32_t)mathNextPowTwo( - FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH - ); - const int32_t height = (int32_t)mathNextPowTwo( - FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT - ); - - color_t *pixels = memoryAllocate(sizeof(color_t) * width * height); - memoryZero(pixels, sizeof(color_t) * width * height); - - for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) { - const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH; - const uint16_t tileY = - (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT; - - for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) { - const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row]; - - for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) { - if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue; - pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE; - } - } - } - - FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH; - FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT; - FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS; - FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS; - FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT; - FONT_DEFAULT_TILESET.uv[0] = - (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width; - FONT_DEFAULT_TILESET.uv[1] = - (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height; - - const texturedata_t data = { .rgbaColors = pixels }; - errorret_t textureResult = textureInit( - &FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data - ); - memoryFree(pixels); - errorChain(textureResult); - - FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE; - FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET; - errorOk(); -} - -errorret_t fontDefaultDispose(void) { - errorChain(textureDispose(&FONT_DEFAULT_TEXTURE)); - FONT_DEFAULT.texture = NULL; - FONT_DEFAULT.tileset = NULL; - errorOk(); -} diff --git a/src/dusk/display/text/font.h b/src/dusk/display/text/font.h deleted file mode 100644 index 35913b36..00000000 --- a/src/dusk/display/text/font.h +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/texture/texture.h" -#include "display/texture/tileset.h" - -typedef struct { - texture_t *texture; - tileset_t *tileset; -} font_t; - -/** - * Pixel width/height of a single default-font glyph tile. - */ -#define FONT_DEFAULT_TILE_WIDTH 6 -#define FONT_DEFAULT_TILE_HEIGHT 10 - -/** Grid layout of the generated default-font texture, in tiles. */ -#define FONT_DEFAULT_COLUMNS 16 -#define FONT_DEFAULT_ROWS 6 - -/** - * Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS * - * FONT_DEFAULT_ROWS), covering the printable ASCII range starting at - * TEXT_CHAR_START ('!'), one custom icon glyph in a trailing unused tile - * (see FONT_ICON_CAPSLOCK), plus one more genuinely unused trailing tile. - * FONT_ICON_SHIFT/FONT_ICON_NEWLINE reuse existing-but-blank slots within - * the printable range instead of more trailing tiles - see their own - * comments. - */ -#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS) - -/** - * Custom (non-ASCII-meaning) icon glyphs baked into FONT_DEFAULT_GLYPHS - * at otherwise-unused codepoints within the range this font covers - - * safe to use anywhere a char_t string is expected, e.g. a uibutton_t - * label. Kept below 128: char_t is a plain `char`, whose signedness - * varies by platform, so a codepoint of 128 or above isn't safely - * representable everywhere this engine targets. - */ -// Thick, solid, uninterrupted up arrow - Caps Lock. A trailing tile -// (char code 127/DEL) that was never a real character to begin with. -#define FONT_ICON_CAPSLOCK "\x7F" -// Same up arrow, but with a thinner and gapped shaft - Shift. Reuses -// the backtick's glyph slot: backtick was never drawn by this font -// anyway, and isn't a key this engine's virtual keyboard can type. -#define FONT_ICON_SHIFT "`" -// A "return" arrow (down then left, with a leftward arrowhead) - the -// keyboard's newline key. Reuses the pipe's glyph slot, for the same -// reason as FONT_ICON_SHIFT. -#define FONT_ICON_NEWLINE "|" -// An underscore with a tick at each end ("|___|") - the keyboard's -// space key. Reuses the tilde's glyph slot: tilde was never drawn by -// this font, and was explicitly dropped from this engine's virtual -// keyboard's own key set. -#define FONT_ICON_SPACE "~" -// Caps Lock's arrow rotated to point left - the keyboard's backspace -// key. Reuses the backslash's glyph slot, for the same reason as -// FONT_ICON_SHIFT/FONT_ICON_NEWLINE/FONT_ICON_SPACE. Last free reused -// slot below 128 - one more custom icon after this needs -// FONT_DEFAULT_COLUMNS/ROWS grown to make room. -#define FONT_ICON_BACKSPACE "\\" - -extern font_t FONT_DEFAULT; - -/** - * Hard coded bitmap data for the built-in default font. Indexed - * [glyph][row], where glyph 0 corresponds to TEXT_CHAR_START ('!') and - * glyphs run consecutively through the printable ASCII range. Each row - * byte holds FONT_DEFAULT_TILE_WIDTH bit flags, one per pixel column: - * bit (FONT_DEFAULT_TILE_WIDTH - 1) is the leftmost pixel and bit 0 is - * the rightmost; 1 means the pixel is set, 0 means it is not. - */ -extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][ - FONT_DEFAULT_TILE_HEIGHT -]; - -/** - * Builds the default font's texture + tileset directly from - * FONT_DEFAULT_GLYPHS, without going through the asset system - so the - * engine always has a usable font to render with regardless of whether - * asset loading (e.g. the packed .dsk archive) succeeds. - * - * @return Either an error or success result. - */ -errorret_t fontDefaultInit(void); - -/** - * Disposes of the default font created by fontDefaultInit(). - * - * @return Either an error or success result. - */ -errorret_t fontDefaultDispose(void); diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c deleted file mode 100644 index fb83bc3d..00000000 --- a/src/dusk/display/text/text.c +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "text.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" - -errorret_t textInit(void) { - errorChain(fontDefaultInit()); - errorOk(); -} - -errorret_t textDispose(void) { - errorChain(fontDefaultDispose()); - errorOk(); -} - -spritebatchsprite_t textGetSprite( - const vec2 pos, const char_t c, const font_t *font -) { - assertNotNull(font, "Font cannot be NULL"); - - // Change char from ASCII to a tile index. - int32_t tileIndex = (int32_t)(c) - TEXT_CHAR_START; - if(tileIndex < 0 || tileIndex >= font->tileset->tileCount) { - tileIndex = ((int32_t)'@') - TEXT_CHAR_START; - } - assertTrue( - tileIndex >= 0 && tileIndex <= font->tileset->tileCount, - "Character is out of bounds for font tiles" - ); - - // Create sprite. - vec4 uv; - tilesetTileGetUV(font->tileset, tileIndex, uv); - - spritebatchsprite_t sprite; - sprite.min[0] = pos[0]; - sprite.min[1] = pos[1]; - sprite.min[2] = 0.0f; - sprite.max[0] = pos[0] + font->tileset->tileWidth; - sprite.max[1] = pos[1] + font->tileset->tileHeight; - sprite.max[2] = 0.0f; - sprite.uvMin[0] = uv[0]; - sprite.uvMin[1] = uv[1]; - sprite.uvMax[0] = uv[2]; - sprite.uvMax[1] = uv[3]; - return sprite; -} - -int32_t textBuffer( - const float_t x, - const float_t y, - const char_t *text, - font_t *font, - spritebatchsprite_t *outSprites, - const int32_t maxSprites, - int32_t *charIndex, - float_t *posX, - float_t *posY -) { - assertNotNull(text, "Text cannot be NULL"); - - if(outSprites == NULL) { - int32_t count = 0; - char_t c; - int32_t i = 0; - while((c = text[i++]) != '\0') { - if(c != ' ' && c != '\n') count++; - } - return count; - } - - assertNotNull(font, "Font cannot be NULL"); - assertTrue(maxSprites > 0, "Max sprites must be greater than zero"); - assertNotNull(posX, "Output posX cannot be NULL"); - assertNotNull(posY, "Output posY cannot be NULL"); - assertNotNull(charIndex, "Output charIndex cannot be NULL"); - - int32_t spriteIndex = 0; - char_t c; - for(;;) { - c = text[*charIndex]; - if(c == '\0') break; - (*charIndex)++; - - if(c == '\n') { - *posX = x; - *posY += font->tileset->tileHeight; - continue; - } - - if(c == ' ') { - *posX += font->tileset->tileWidth; - continue; - } - - outSprites[spriteIndex++] = textGetSprite((vec2){*posX, *posY}, c, font); - *posX += font->tileset->tileWidth; - - if(spriteIndex >= maxSprites) break; - } - - return spriteIndex; -} - -errorret_t textDraw( - const float_t x, - const float_t y, - const char_t *text, - const color_t color, - font_t *font -) { - assertNotNull(text, "Text cannot be NULL"); - int32_t length = strlen(text); - if(length == 0) errorOk(); - - if(font == NULL) font = &FONT_DEFAULT; - - shadermaterial_t material = { - .unlit = { - .color = color, - .texture = font->texture - } - }; - - spritebatchsprite_t sprites[32]; - float_t posX = x; - float_t posY = y; - int32_t buffered = 0; - int32_t charIndex = 0; - do { - buffered = textBuffer( - x, y, text, font, - sprites, - sizeof(sprites) / sizeof(spritebatchsprite_t), - &charIndex, &posX, &posY - ); - errorChain(spriteBatchBuffer(sprites, buffered, &SHADER_UNLIT, material)); - } while(charIndex < length); - - errorOk(); -} - -int32_t textMeasure( - const char_t *text, - const font_t *font, - int32_t *outWidth, - int32_t *outHeight -) { - assertNotNull(text, "Text cannot be NULL"); - assertNotNull(outWidth, "Output width pointer cannot be NULL"); - assertNotNull(outHeight, "Output height pointer cannot be NULL"); - - int32_t width = 0; - int32_t height = font->tileset->tileHeight; - int32_t lineWidth = 0; - int32_t spriteCount = 0; - - char_t c; - int32_t i = 0; - while((c = text[i++]) != '\0') { - if(c == '\n') { - if(lineWidth > width) width = lineWidth; - lineWidth = 0; - height += font->tileset->tileHeight; - continue; - } - - lineWidth += font->tileset->tileWidth; - - if(c != ' ') { - spriteCount++; - } - } - - if(lineWidth > width) width = lineWidth; - - *outWidth = width; - *outHeight = height; - - return spriteCount; -} - -void textWrap(char_t *text, const font_t *font, const float_t maxWidth) { - assertNotNull(text, "Text cannot be NULL"); - assertNotNull(font, "Font cannot be NULL"); - - float_t fontWidth = (float_t)font->tileset->tileWidth; - if(fontWidth <= 0.0f) return; - - int32_t charsPerLine = (int32_t)(maxWidth / fontWidth); - if(charsPerLine <= 0) return; - - int32_t lineWidth = 0; - int32_t lastSpace = -1; - - for(int32_t i = 0; text[i] != '\0'; i++) { - if(text[i] == '\n') { - lineWidth = 0; - lastSpace = -1; - continue; - } - - if(text[i] == ' ') { - lastSpace = i; - } - - lineWidth++; - - if(lineWidth > charsPerLine && lastSpace != -1) { - text[lastSpace] = '\n'; - lineWidth = i - lastSpace; - lastSpace = -1; - } - } -} diff --git a/src/dusk/display/text/text.h b/src/dusk/display/text/text.h deleted file mode 100644 index d62c9e6b..00000000 --- a/src/dusk/display/text/text.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/asset.h" -#include "display/text/font.h" -#include "display/spritebatch/spritebatch.h" - -#define TEXT_CHAR_START '!' - -/** - * Initializes the text system. - * - * @return Either an error or success result. - */ -errorret_t textInit(void); - -/** - * Disposes of the text system. - * - * @return Either an error or success result. - */ -errorret_t textDispose(void); - -/** - * Builds a sprite for a single character at the given position. - * - * @param pos The (x, y) position of the character in screen/world space. - * @param c The character to build a sprite for. - * @param font Font to use for tile lookup. - * @return The populated sprite ready for spriteBatchBuffer. - */ -spritebatchsprite_t textGetSprite( - const vec2 pos, - const char_t c, - const font_t *font -); - -/** - * Buffers a string into sprites for rendering. If outSprites is NULL then - * the function will only return the count of sprites necessary for the buffer. - * - * posX and posY are updated whilst buffering characters, if you need to do - * buffering in sets then these will be reusable between buffer commands. Start - * by setting these to x and y initially. - * - * @param x The x-coordinate to start buffering the text at. - * @param y The y-coordinate to start buffering the text at. - * @param text The null-terminated string of text to buffer. - * @param font Font to use for rendering. - * @param outSprites Pointer to an array of spritebatchsprite_t. - * @param maxSprites The maximum number of sprites in outSprites. - * @param charIndex Pointer to an int32_t to store the character indexed. - * @param posX Pointer to a float_t to store the final x position. - * @param posY Pointer to a float_t to store the final y position. - * @return The count of sprites buffered. - */ -int32_t textBuffer( - const float_t x, - const float_t y, - const char_t *text, - font_t *font, - spritebatchsprite_t *outSprites, - const int32_t maxSprites, - int32_t *charIndex, - float_t *posX, - float_t *posY -); - -/** - * Draws a string of text at the specified position. - * - * @param x The x-coordinate to draw the text at. - * @param y The y-coordinate to draw the text at. - * @param text The null-terminated string of text to draw. - * @param color The color to draw the text in. - * @param font Font to use for rendering. - * @return Either an error or success result. - */ -errorret_t textDraw( - const float_t x, - const float_t y, - const char_t *text, - const color_t color, - font_t *font -); - -/** - * Measures the width and height of the given text string when rendered. - * - * @param text The null-terminated string of text to measure. - * @param font Font to use for measurement. - * @param outWidth Pointer to store the measured width in pixels. - * @param outHeight Pointer to store the measured height in pixels. - * @return The count of sprites that will be rendered for the given text. - */ -int32_t textMeasure( - const char_t *text, - const font_t *font, - int32_t *outWidth, - int32_t *outHeight -); - -/** - * Word-wraps text in place for display at up to maxWidth pixels wide, - * by replacing the space nearest each overflow point with a newline. - * Length is unchanged - this only ever swaps existing spaces for - * newlines, never inserts characters - so it's always safe to call on a - * fixed-size buffer. A single word wider than maxWidth on its own is - * left unbroken. - * - * @param text Null-terminated, caller-owned buffer to wrap in place. - * @param font Font to measure character width with. - * @param maxWidth Maximum line width, in pixels. - */ -void textWrap(char_t *text, const font_t *font, const float_t maxWidth); diff --git a/src/dusk/display/texture/CMakeLists.txt b/src/dusk/display/texture/CMakeLists.txt deleted file mode 100644 index 23ab16b3..00000000 --- a/src/dusk/display/texture/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - tileset.c - texture.c - palette.c -) \ No newline at end of file diff --git a/src/dusk/display/texture/palette.c b/src/dusk/display/texture/palette.c deleted file mode 100644 index e5e0b6e0..00000000 --- a/src/dusk/display/texture/palette.c +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "palette.h" - -palette_t PALETTES[PALETTE_COUNT]; \ No newline at end of file diff --git a/src/dusk/display/texture/palette.h b/src/dusk/display/texture/palette.h deleted file mode 100644 index fbc325c6..00000000 --- a/src/dusk/display/texture/palette.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/color.h" - -#define PALETTE_COLOR_COUNT 0xFF -#define PALETTE_COUNT 6 - -typedef struct { - color_t colors[PALETTE_COLOR_COUNT]; - uint8_t count; -} palette_t; - -extern palette_t PALETTES[PALETTE_COUNT]; \ No newline at end of file diff --git a/src/dusk/display/texture/texture.c b/src/dusk/display/texture/texture.c deleted file mode 100644 index 2cfdff28..00000000 --- a/src/dusk/display/texture/texture.c +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "texture.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include "display/display.h" - -texture_t TEXTURE_WHITE; -color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = { - COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, - COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, - COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, - COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, -}; - -texture_t TEXTURE_TEST; -color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = { - COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, - COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, - COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, - COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, -}; - -errorret_t textureInit( - texture_t *texture, - const int32_t width, - const int32_t height, - const textureformat_t format, - const texturedata_t data -) { - assertNotNull(texture, "Texture cannot be NULL"); - assertTrue(width > 0 && height > 0, "width/height must be greater than 0"); - assertTrue(width == mathNextPowTwo(width), "Width must be a power of 2."); - assertTrue(height == mathNextPowTwo(height), "Height must be a power of 2."); - - if(texture->format == TEXTURE_FORMAT_RGBA) { - assertNotNull(data.rgbaColors, "RGBA color data cannot be NULL"); - } else if(texture->format == TEXTURE_FORMAT_PALETTE) { - assertNotNull(data.paletted.indices, "Palette indices cannot be NULL"); - assertNotNull(data.paletted.palette, "Palette colors cannot be NULL"); - assertTrue( - data.paletted.palette->count == - mathNextPowTwo(data.paletted.palette->count), - "Palette color count must be a power of 2" - ); - } - - memoryZero(texture, sizeof(texture_t)); - texture->width = width; - texture->height = height; - texture->format = format; - - errorChain(textureInitPlatform(texture, width, height, format, data)); - errorOk(); -} - -errorret_t textureDispose(texture_t *texture) { - assertNotNull(texture, "Texture cannot be NULL"); - - errorChain(textureDisposePlatform(texture)); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/display/texture/texture.h b/src/dusk/display/texture/texture.h deleted file mode 100644 index 65df2d59..00000000 --- a/src/dusk/display/texture/texture.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/texture/palette.h" -#include "display/texture/textureplatform.h" - -#ifndef textureInitPlatform - #error "textureInitPlatform should not be defined." -#endif -#ifndef textureDisposePlatform - #error "textureDisposePlatform should not be defined." -#endif - -#define TEXTURE_FIXED_WIDTH 4 -#define TEXTURE_FIXED_HEIGHT 4 - -typedef textureformatplatform_t textureformat_t; -typedef textureplatform_t texture_t; - -typedef union texturedata_u { - struct { - uint8_t *indices; - palette_t *palette; - } paletted; - color_t *rgbaColors; -} texturedata_t; - -extern texture_t TEXTURE_WHITE; -extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT]; -extern texture_t TEXTURE_TEST; -extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT]; - -/** - * Initializes a texture. - * - * @param texture The texture to initialize. - * @param width The width of the texture. - * @param height The height of the texture. - * @param format The format of the texture (e.g., GL_RGBA, GL_ALPHA). - * @param data The data for the texture, the format changes per format. - */ -errorret_t textureInit( - texture_t *texture, - const int32_t width, - const int32_t height, - const textureformat_t format, - const texturedata_t data -); - -/** - * Disposes a texture. - * - * @param texture The texture to dispose. - */ -errorret_t textureDispose(texture_t *texture); \ No newline at end of file diff --git a/src/dusk/display/texture/tileset.c b/src/dusk/display/texture/tileset.c deleted file mode 100644 index 47bb0e12..00000000 --- a/src/dusk/display/texture/tileset.c +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "tileset.h" -#include "assert/assert.h" -#include "util/string.h" - -void tilesetTileGetUV( - const tileset_t *tileset, - const uint16_t tileIndex, - vec4 outUV -) { - const uint16_t column = tileIndex % tileset->columns; - const uint16_t row = tileIndex / tileset->columns; - tilesetPositionGetUV(tileset, column, row, outUV); -} - -void tilesetPositionGetUV( - const tileset_t *tileset, - const uint16_t column, - const uint16_t row, - vec4 outUV -) { - assertNotNull(tileset, "Tileset cannot be NULL"); - assertTrue(column < tileset->columns, "Column index out of bounds"); - assertTrue(row < tileset->rows, "Row index out of bounds"); - - outUV[0] = ((float_t)column) * tileset->uv[0]; - outUV[1] = ((float_t)row) * tileset->uv[1]; - outUV[2] = outUV[0] + tileset->uv[0]; - outUV[3] = outUV[1] + tileset->uv[1]; -} \ No newline at end of file diff --git a/src/dusk/display/texture/tileset.h b/src/dusk/display/texture/tileset.h deleted file mode 100644 index bd4a54e0..00000000 --- a/src/dusk/display/texture/tileset.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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 tileset_s { - uint16_t tileWidth; - uint16_t tileHeight; - uint16_t tileCount; - uint16_t columns; - uint16_t rows; - vec2 uv; -} tileset_t; - -/** - * Gets the UV coordinates for a tile index in the tileset. - * - * @param tileset The tileset to get the UV coordinates from. - * @param tileIndex The index of the tile to get the UV coordinates for. - * @param outUV The output UV coordinates (vec4). - */ -void tilesetTileGetUV( - const tileset_t *tileset, - const uint16_t tileIndex, - vec4 outUV -); - -/** - * Gets the UV coordinates for a tile position in the tileset. - * - * @param tileset The tileset to get the UV coordinates from. - * @param column The column of the tile to get the UV coordinates for. - * @param row The row of the tile to get the UV coordinates for. - * @param outUV The output UV coordinates (vec4). - */ -void tilesetPositionGetUV( - const tileset_t *tileset, - const uint16_t column, - const uint16_t row, - vec4 outUV -); \ No newline at end of file diff --git a/src/dusk/dusk.h b/src/dusk/dusk.h deleted file mode 100644 index c004dc33..00000000 --- a/src/dusk/dusk.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "duskplatform.h" - -typedef bool bool_t; -typedef int int_t; -typedef float float_t; -typedef char char_t; \ No newline at end of file diff --git a/src/dusk/engine/CMakeLists.txt b/src/dusk/engine/CMakeLists.txt deleted file mode 100644 index 84851ec0..00000000 --- a/src/dusk/engine/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - engine.c -) \ No newline at end of file diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c deleted file mode 100644 index f1420c4a..00000000 --- a/src/dusk/engine/engine.c +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "engine.h" -#include "util/memory.h" -#include "perf/perf.h" -#include "time/time.h" -#include "input/input.h" -#include "locale/localemanager.h" -#include "rpg/rpg.h" -#include "display/display.h" -#include "scene/scene.h" -#include "asset/asset.h" -#include "ui/ui.h" -#include "assert/assert.h" -#ifdef DUSK_NETWORK - #include "network/network.h" -#endif -#include "system/system.h" -#include "console/console.h" -#include "save/save.h" -#include "audio/audio.h" -#include "audio/mixer/audiomixer.h" - -engine_t ENGINE; - -errorret_t engineInit(const int32_t argc, const char_t **argv) { - assertInit(); - memoryZero(&ENGINE, sizeof(engine_t)); - ENGINE.running = true; - ENGINE.argc = argc; - ENGINE.argv = argv; - ENGINE.version = DUSK_VERSION; - - // Init systems. Order is important. - perfInit(); - timeInit(); - consoleInit(); - errorChain(systemInit()); - errorChain(inputInit()); - errorChain(assetInit()); - errorChain(saveInit()); - errorChain(localeManagerInit()); - errorChain(displayInit()); - errorChain(audioInit()); - errorChain(rpgInit()); - errorChain(uiInit()); - #ifdef DUSK_NETWORK - errorChain(networkInit()); - #endif - errorChain(sceneInit()); - - consolePrint("Engine initialized"); - - #ifdef DUSK_ASSERTIONS_FAKED - consolePrint("Assertions faked"); - #else - consolePrint("Assertions real"); - #endif - - sceneSet(SCENE_TYPE_INITIAL); - - errorOk(); -} - -errorret_t engineUpdate(void) { - // Order here is important. - #ifdef DUSK_NETWORK - errorChain(networkUpdate()); - #endif - errorChain(saveUpdate()); - timeUpdate(); - inputUpdate(); - consoleUpdate(); - errorChain(audioUpdateEarly()); - errorChain(rpgUpdate()); - errorChain(sceneUpdate()); - errorChain(assetUpdate()); - errorChain(uiUpdate()); - - // Render - errorChain(displayUpdate()); - errorChain(audioUpdateLate()); - if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; - errorOk(); -} - -void engineExit(void) { - ENGINE.running = false; -} - -errorret_t engineDispose(void) { - errorChain(sceneDispose()); - #ifdef DUSK_NETWORK - errorChain(networkDispose()); - #endif - errorChain(rpgDispose()); - localeManagerDispose(); - errorChain(uiDispose()); - errorChain(audioDispose()); - consoleDispose(); - errorChain(displayDispose()); - errorChain(saveDispose()); - errorChain(assetDispose()); - perfDispose(); - - memoryCheckUnfreed(); - - errorOk(); -} diff --git a/src/dusk/engine/engine.h b/src/dusk/engine/engine.h deleted file mode 100644 index c1de0118..00000000 --- a/src/dusk/engine/engine.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -// Important to be included first: -#include "display/display.h" -#include "error/error.h" - -typedef struct { - bool_t running; - int32_t argc; - const char_t **argv; - const char_t *version; -} engine_t; - -extern engine_t ENGINE; - -/** - * Initializes the engine. - * - * @param argc The argument count from main(). - * @param argv The argument vector from main(). - */ -errorret_t engineInit(const int32_t argc, const char_t **argv); - -/** - * Updates the engine. - */ -errorret_t engineUpdate(void); - -/** - * Shuts down the engine. - */ -errorret_t engineDispose(void); - diff --git a/src/dusk/error/CMakeLists.txt b/src/dusk/error/CMakeLists.txt deleted file mode 100644 index e6839225..00000000 --- a/src/dusk/error/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - error.c -) \ No newline at end of file diff --git a/src/dusk/error/error.c b/src/dusk/error/error.c deleted file mode 100644 index 804b8013..00000000 --- a/src/dusk/error/error.c +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assert/assert.h" -#include "error.h" -#include "util/memory.h" -#include "util/string.h" -#include "log/log.h" - -THREAD_LOCAL errorstate_t ERROR_STATE = { 0 }; - -errorret_t errorThrowImpl( - errorstate_t *state, - errorcode_t code, - const char_t *file, - const char_t *function, - const int32_t line, - const char_t *message, - ... -) { - assertNotNull(state, "Error state cannot be NULL"); - assertTrue(code != ERROR_OK, "Error code must not be OK"); - assertNotNull(file, "File cannot be NULL"); - assertNotNull(function, "Function cannot be NULL"); - assertTrue(line >= 0, "File pointer must be valid"); - assertNotNull(message, "Message cannot be NULL"); - - memoryZero(state, sizeof(errorstate_t)); - state->code = code; - - // Format args. - va_list args; - va_start(args, message); - - // Get length of formatted message - va_list argsCopy; - va_copy(argsCopy, args); - int32_t len = stringFormatVA(NULL, 0, message, argsCopy); - va_end(argsCopy); - - // Create string to hold the formatted message - state->message = (char_t *)memoryAllocate(len + 1); - stringFormatVA(state->message, len + 1, message, args); - va_end(args); - - // Format lines - len = stringFormat(NULL, 0, ERROR_LINE_FORMAT, file, line, function); - assertTrue(len >= 0, "Line formatting failed"); - state->lines = (char_t *)memoryAllocate(len + 1); - stringFormat(state->lines, len + 1, ERROR_LINE_FORMAT, file, line, function); - - return (errorret_t) { - .code = code, - .state = state - }; -} - -errorret_t errorOkImpl() { - assertTrue( - ERROR_STATE.code == ERROR_OK, - "Global error state is not OK (Likely missing errorCatch)" - ); - - return (errorret_t) { - .code = ERROR_OK, - .state = NULL - }; -} - -errorret_t errorChainImpl( - const errorret_t retval, - const char_t *file, - const char_t *function, - const int32_t line -) { - if(errorIsOk(retval)) return retval; - - assertNotNull(retval.state, "Error state NULL (Likely missing errorOk)"); - assertNotNull(retval.state->message, "Message cannot be NULL"); - - // Create a new line string. - int32_t newLineLen = snprintf( - NULL, 0, ERROR_LINE_FORMAT, file, line, function - ); - assertTrue(newLineLen >= 0, "Line formatting failed"); - char_t *newLine = (char_t *)memoryAllocate(newLineLen + 1); - snprintf(newLine, newLineLen + 1, ERROR_LINE_FORMAT, file, line, function); - - // Resize the existing lines to accommodate the new line - size_t existingLen = strlen(retval.state->lines); - memoryResize( - (void**)&retval.state->lines, - existingLen, - existingLen + newLineLen + 1 - ); - - // Now append the new line to the existing lines - memoryCopy( - retval.state->lines + existingLen, - newLine, - newLineLen + 1 - ); - - // Cleanup the temporary new line - memoryFree(newLine); - return retval; -} - -void errorCatch(errorret_t retval) { - if(errorIsOk(retval)) return; - - assertNotNull(retval.state, "Error state cannot be NULL"); - assertNotNull(retval.state->message, "Message cannot be NULL"); - - // Clear the error state - memoryFree((void*)retval.state->message); - memoryFree((void*)retval.state->lines); - retval.state->code = ERROR_OK; -} - -errorret_t errorPrint(const errorret_t retval) { - if(errorIsOk(retval)) return retval; - - assertNotNull(retval.state, "Error state cannot be NULL"); - assertNotNull(retval.state->message, "Message cannot be NULL"); - - logError( - ERROR_PRINT_FORMAT, - retval.state->code, - retval.state->message, - retval.state->lines - ); - return retval; -} \ No newline at end of file diff --git a/src/dusk/error/error.h b/src/dusk/error/error.h deleted file mode 100644 index 68a8a387..00000000 --- a/src/dusk/error/error.h +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "thread/threadlocal.h" - -typedef uint8_t errorcode_t; - -typedef struct { - errorcode_t code; - char_t *message; - char_t *lines; -} errorstate_t; - -typedef struct { - errorcode_t code; - errorstate_t *state; -} errorret_t; - -static const errorcode_t ERROR_OK = 0; -static const errorcode_t ERROR_NOT_OK = 1; - -static const char_t *ERROR_PRINT_FORMAT = "Error (%d): %s\n%s"; -static const char_t *ERROR_LINE_FORMAT = " at %s:%d in function %s\n"; -extern THREAD_LOCAL errorstate_t ERROR_STATE; - -/** - * Sets the error state with the provided code and message. - * - * @param state The error state to initialize. - * @param code The error code to set. - * @param file The file where the error occurred. - * @param function The function where the error occurred. - * @param line The line number where the error occurred. - * @param message The error message. - * @param args The arguments for the error message. - * @return The error code. - */ -errorret_t errorThrowImpl( - errorstate_t *state, - errorcode_t code, - const char_t *file, - const char_t *function, - const int32_t line, - const char_t *message, - ... -); - -/** - * Returns an error state with no error. - * - * @return An error state with code ERROR_OK. - */ -errorret_t errorOkImpl(); - -/** - * Chains an error state, allowing for error propagation. - * - * @param retval The return value containing the error state. - * @param file The file where the error occurred. - * @param function The function where the error occurred. - * @param line The line number where the error occurred. - * @return The error code if an error occurred, otherwise continues execution. - */ -errorret_t errorChainImpl( - const errorret_t retval, - const char_t *file, - const char_t *function, - const int32_t line -); - -/** - * Catches an error and handles it. - * - * @param retval The return value containing the error state. - */ -void errorCatch(errorret_t retval); - -/** - * Prints the error state to the console. - * - * @param retval The return value containing the error state. - * @return Passed retval for chaining. - */ -errorret_t errorPrint(const errorret_t retval); - -/** - * Throws an error with a formatted message. - * - * @param code The error code to throw. - * @param message The format string for the error message. - * @param ... Additional arguments for the format string. - * @return The error code. - */ -#define errorThrowWithCode(code, message, ... ) \ - return errorThrowImpl(\ - &ERROR_STATE, (code), __FILE__, __func__, __LINE__, (message), \ - __VA_ARGS__ \ - ) - -/** - * Throws an error with a default error code of ERROR_NOT_OK. - * - * @param message The format string for the error message. - * @param ... Additional arguments for the format string. - * @return The error code. - */ -#define errorThrow(message, ...) \ - return errorThrowImpl(\ - &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \ - ##__VA_ARGS__ \ - ) - -/** - * Checks if a child method errored, and if it did, then send it up the chain. - * @param retval The return value containing the error state. - * @return The error code if an error occurred, otherwise continues execution. - */ -#define errorChain(retval) { \ - errorret_t errorChainRetval = (retval); \ - if(errorChainRetval.code != ERROR_OK) { \ - return errorChainImpl(errorChainRetval, __FILE__, __func__, __LINE__); \ - } \ -} - -/** - * Returns without an error. - * - * @return An error state with code ERROR_OK. - */ -#define errorOk() \ - return errorOkImpl() - -/** - * Returns non-zero if retval indicates success. - * - * @param retval errorret_t to test. - */ -#define errorIsOk(retval) ((retval).code == ERROR_OK) - -/** - * Returns non-zero if retval indicates failure. - * - * @param retval errorret_t to test. - */ -#define errorIsNotOk(retval) ((retval).code != ERROR_OK) - -// EOF \ No newline at end of file diff --git a/src/dusk/input/CMakeLists.txt b/src/dusk/input/CMakeLists.txt deleted file mode 100644 index 6cf2100f..00000000 --- a/src/dusk/input/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - input.c - inputbutton.c - inputaction.c -) - -# Input Action Definitions -dusk_run_python( - dusk_input_csv_defs - tools.input - --csv ${CMAKE_CURRENT_SOURCE_DIR}/input.csv - --output ${DUSK_GENERATED_HEADERS_DIR}/input/inputactiondefs.h -) -add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_input_csv_defs) \ No newline at end of file diff --git a/src/dusk/input/input.c b/src/dusk/input/input.c deleted file mode 100644 index 5ef56e81..00000000 --- a/src/dusk/input/input.c +++ /dev/null @@ -1,256 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "input.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "util/math.h" -#include "time/time.h" - -input_t INPUT; - -errorret_t inputInit(void) { - memoryZero(&INPUT, sizeof(input_t)); - - for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) { - INPUT.actions[i].action = (inputaction_t)i; - INPUT.actions[i].lastValue = 0.0f; - INPUT.actions[i].currentValue = 0.0f; - } - - #ifdef inputInitPlatform - errorChain(inputInitPlatform()); - #endif - - errorOk(); -} - -void inputUpdate(void) { - #ifdef inputUpdatePlatform - inputUpdatePlatform(); - #endif - - // Reset all actions - inputactiondata_t *action = &INPUT.actions[0]; - do { - #ifdef DUSK_TIME_DYNAMIC - action->lastDynamicValue = action->currentDynamicValue; - action->currentDynamicValue = 0.0f; - if(!TIME.dynamicUpdate) { - action->lastValue = action->currentValue; - action->currentValue = 0.0f; - } - #else - action->lastValue = action->currentValue; - action->currentValue = 0.0f; - #endif - - action++; - } while(action < &INPUT.actions[INPUT_ACTION_COUNT]); - - // For each button... - inputbuttondata_t *cur = &INPUT_BUTTON_DATA[0]; - while(cur->name) { - cur->lastVal = cur->curVal; - cur->curVal = inputButtonGetValue(cur->button); - - if(cur->curVal == 0.0f) { - cur++; - continue; - } - - // Update current val. - #ifdef DUSK_TIME_DYNAMIC - INPUT.actions[cur->action].currentDynamicValue = mathMax( - cur->curVal, INPUT.actions[cur->action].currentDynamicValue - ); - if(!TIME.dynamicUpdate) { - INPUT.actions[cur->action].currentValue = mathMax( - cur->curVal, INPUT.actions[cur->action].currentValue - ); - } - #else - INPUT.actions[cur->action].currentValue = mathMax( - cur->curVal, INPUT.actions[cur->action].currentValue - ); - #endif - - cur++; - } - - #ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) return; - #endif - - for(uint8_t i = INPUT_ACTION_NULL + 1; i < INPUT_ACTION_COUNT; i++) { - inputactiondata_t *act = &INPUT.actions[i]; - bool_t isDown = act->currentValue > 0.0f; - bool_t wasDown = act->lastValue > 0.0f; - } -} - -float_t inputGetCurrentValue(const inputaction_t action) { - #ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) return inputGetCurrentValueDynamic(action); - #endif - - assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds"); - return INPUT.actions[action].currentValue; -} - -float_t inputGetLastValue(const inputaction_t action) { - #ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) return inputGetLastValueDynamic(action); - #endif - - assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds"); - return INPUT.actions[action].lastValue; -} - -#ifdef DUSK_TIME_DYNAMIC - float_t inputGetCurrentValueDynamic(const inputaction_t action) { - assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds"); - return INPUT.actions[action].currentDynamicValue; - } - - float_t inputGetLastValueDynamic(const inputaction_t action) { - assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds"); - return INPUT.actions[action].lastDynamicValue; - } - - bool_t inputIsDownDynamic(const inputaction_t action) { - return inputGetCurrentValueDynamic(action) > 0.0f; - } - - bool_t inputWasDownDynamic(const inputaction_t action) { - return inputGetLastValueDynamic(action) > 0.0f; - } - - bool_t inputPressedDynamic(const inputaction_t action) { - return inputIsDownDynamic(action) && !inputWasDownDynamic(action); - } - - bool_t inputReleasedDynamic(const inputaction_t action) { - return !inputIsDownDynamic(action) && inputWasDownDynamic(action); - } - - float_t inputAxisDynamic( - const inputaction_t neg, - const inputaction_t pos - ) { - return inputGetCurrentValueDynamic(pos) - - inputGetCurrentValueDynamic(neg); - } - - void inputAxis2DDynamic( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result - ) { - assertNotNull(result, "Result vector cannot be null"); - result[0] = inputAxisDynamic(negX, posX); - result[1] = inputAxisDynamic(negY, posY); - } - - void inputAngle2DDynamic( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result - ) { - assertNotNull(result, "Result vector cannot be null"); - float_t x = inputAxisDynamic(negX, posX); - float_t y = inputAxisDynamic(negY, posY); - float_t mag = sqrtf(x * x + y * y); - if(mag <= 0.0f) { - result[0] = 0.0f; - result[1] = 0.0f; - return; - } - if(mag > 1.0f) { - x /= mag; - y /= mag; - } - result[0] = x; - result[1] = y; - } -#endif - -bool_t inputIsDown(const inputaction_t action) { - return inputGetCurrentValue(action) > 0.0f; -} - -bool_t inputWasDown(const inputaction_t action) { - return inputGetLastValue(action) > 0.0f; -} - -bool_t inputPressed(const inputaction_t action) { - return inputIsDown(action) && !inputWasDown(action); -} - -bool_t inputReleased(const inputaction_t action) { - return !inputIsDown(action) && inputWasDown(action); -} - -float_t inputAxis(const inputaction_t neg, const inputaction_t pos) { - assertTrue(neg < INPUT_ACTION_COUNT, "Negative input action out of bounds"); - assertTrue(pos < INPUT_ACTION_COUNT, "Positive input action out of bounds"); - - return inputGetCurrentValue(pos) - inputGetCurrentValue(neg); -} - -void inputAxis2D( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result -) { - assertNotNull(result, "Result vector cannot be null"); - result[0] = inputAxis(negX, posX); - result[1] = inputAxis(negY, posY); -} - -void inputAngle2D( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result -) { - assertNotNull(result, "Result vector cannot be null"); - float_t x = inputAxis(negX, posX); - float_t y = inputAxis(negY, posY); - float_t mag = sqrtf(x * x + y * y); - if(mag <= 0.0f) { - result[0] = 0.0f; - result[1] = 0.0f; - return; - } - if(mag > 1.0f) { - x /= mag; - y /= mag; - } - result[0] = x; - result[1] = y; -} - -void inputBind(const inputbutton_t button, const inputaction_t act) { - assertTrue( - act < INPUT_ACTION_COUNT, - "Invalid input action" - ); - assertTrue(act != INPUT_ACTION_NULL, "Cannot bind to NULL action"); - - // Get the button data for this button. - inputbuttondata_t *data = inputButtonGetData(button); - assertNotNull(data, "Input button not found"); - - // Bind the action. - data->action = act; -} - -float_t inputDeadzone(const float_t rawValue, const float_t deadzone) { - if(rawValue < deadzone) return 0.0f; - return (rawValue - deadzone) / (1.0f - deadzone); -} \ No newline at end of file diff --git a/src/dusk/input/input.csv b/src/dusk/input/input.csv deleted file mode 100644 index 2fa6a46e..00000000 --- a/src/dusk/input/input.csv +++ /dev/null @@ -1,12 +0,0 @@ -id, -UP, -DOWN, -LEFT, -RIGHT, -ACCEPT, -CANCEL, -PAUSE, -RAGEQUIT, -CONSOLE, -POINTERX, -POINTERY, \ No newline at end of file diff --git a/src/dusk/input/input.h b/src/dusk/input/input.h deleted file mode 100644 index 739b7e99..00000000 --- a/src/dusk/input/input.h +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "inputbutton.h" -#include "inputaction.h" - -typedef struct { - inputactiondata_t actions[INPUT_ACTION_COUNT]; - inputplatform_t platform; -} input_t; - -extern input_t INPUT; - -/** - * Initialize the input system. - * - * @return An error code if initialization fails. - */ -errorret_t inputInit(void); - -/** - * Updates the input state. - */ -void inputUpdate(void); - -/** - * Gets the current value of a specific input action. - * - * @param action The input action to get the value for. - * @return The current value of the action (0.0f to 1.0f). - */ -float_t inputGetCurrentValue(const inputaction_t action); - -/** - * Gets the last value of a specific input action. - * - * @param action The input action to get the value for. - * @return The last value of the action (0.0f to 1.0f). - */ -float_t inputGetLastValue(const inputaction_t action); - -#ifdef DUSK_TIME_DYNAMIC - /** - * Gets the current value of a specific input action (dynamic timestep). - * - * @param action The input action to get the value for. - * @return The current value of the action (0.0f to 1.0f). - */ - float_t inputGetCurrentValueDynamic(const inputaction_t action); - - /** - * Gets the last value of a specific input action (dynamic timestep). - * - * @param action The input action to get the value for. - * @return The last value of the action (0.0f to 1.0f). - */ - float_t inputGetLastValueDynamic(const inputaction_t action); - - /** - * Checks if an action is currently down on the dynamic timestep. - * - * @param action The input action to check. - * @return true if the action is currently down. - */ - bool_t inputIsDownDynamic(const inputaction_t action); - - /** - * Checks if an action was down on the previous dynamic frame. - * - * @param action The input action to check. - * @return true if the action was down last dynamic frame. - */ - bool_t inputWasDownDynamic(const inputaction_t action); - - /** - * Checks if an action was pressed this dynamic frame (down now, not before). - * - * @param action The input action to check. - * @return true if the action was just pressed this dynamic frame. - */ - bool_t inputPressedDynamic(const inputaction_t action); - - /** - * Checks if an action was released this dynamic frame (up now, down before). - * - * @param action The input action to check. - * @return true if the action was just released this dynamic frame. - */ - bool_t inputReleasedDynamic(const inputaction_t action); - - /** - * Gets the value of an input axis on the dynamic timestep. - * - * @param neg The action representing the negative direction. - * @param pos The action representing the positive direction. - * @return The current axis value (-1.0f to 1.0f). - */ - float_t inputAxisDynamic( - const inputaction_t neg, - const inputaction_t pos - ); - - /** - * Gets the values of a 2D input axis on the dynamic timestep. - * - * @param negX Negative X action. - * @param posX Positive X action. - * @param negY Negative Y action. - * @param posY Positive Y action. - * @param result vec2 to store the result (-1.0f to 1.0f per axis). - */ - void inputAxis2DDynamic( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result - ); - - /** - * Gets an angled 2D unit vector from four actions on the dynamic timestep. - * - * @param negX Negative X action. - * @param posX Positive X action. - * @param negY Negative Y action. - * @param posY Positive Y action. - * @param result vec2 to store the result. - */ - void inputAngle2DDynamic( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result - ); -#endif - -/** - * Checks if a specific input action is currently pressed. - * - * @param action The input action to check. - * @return true if the action is currently pressed, false otherwise. - */ -bool_t inputIsDown(const inputaction_t action); - -/** - * Checks if a specific input action was pressed in the last update. - * - * @param action The input action to check. - * @return true if the action was pressed in the last update, false otherwise. - */ -bool_t inputWasDown(const inputaction_t action); - -/** - * Checks if a specific input action was down this frame but not in the the - * previous frame. - * - * @param action The input action to check. - * @return true if the action is currently pressed, false otherwise. - */ -bool_t inputPressed(const inputaction_t action); - -/** - * Checks if a specific input action was released this frame. - * - * @param action The input action to check. - * @return true if the action was released this frame, false otherwise. - */ -bool_t inputReleased(const inputaction_t action); - -/** - * Gets the value of an input axis, defined by two actions (negative and - * positive). - * - * @param neg The action representing the negative direction of the axis. - * @param pos The action representing the positive direction of the axis. - * @return The current value of the axis (-1.0f to 1.0f). - */ -float_t inputAxis(const inputaction_t neg, const inputaction_t pos); - -/** - * Gets the values of a 2D input axis, defined by two pairs of actions (negative - * and positive for each axis). - * - * @param negX The action representing the negative direction of the X axis. - * @param posX The action representing the positive direction of the X axis. - * @param negY The action representing the negative direction of the Y axis. - * @param posY The action representing the positive direction of the Y axis. - * @param result A vec2 to store the resulting axis values (-1.0f to 1.0f). - */ -void inputAxis2D( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result -); - -/** - * Returns an angled 2D vector based on the input of 4 actions. Functionally - * using atan2 to get the angle of the input then multiplying by a unit vector. - * - * @param negX The action representing the negative direction of the X axis. - * @param posX The action representing the positive direction of the X axis. - * @param negY The action representing the negative direction of the Y axis. - * @param posY The action representing the positive direction of the Y axis. - * @param result A vec2 to store the resulting axis values (-1.0f to 1.0f). - */ -void inputAngle2D( - const inputaction_t negX, const inputaction_t posX, - const inputaction_t negY, const inputaction_t posY, - vec2 result -); - -/** - * Binds an input button to an action. - * - * @param button The input button to bind. - * @param action The input action to bind the button to. - */ -void inputBind(const inputbutton_t button, const inputaction_t act); - -/** - * Applies deadzone formula to a raw input value. This will; - * return 0 if input is less than the deadzone. - * re-map the range between 0 and 1 in deadzone-space. - * - * @param rawValue The raw input value to apply the deadzone to. - * @param deadzone The deadzone threshold (0.0f to 1.0f). - * @return The input value after applying the deadzone. - */ -float_t inputDeadzone(const float_t rawValue, const float_t deadzone); - -#ifdef DUSK_TIME_DYNAMIC - #define inputPressedIfDynamic(action) inputPressedDynamic(action) - #define inputReleasedIfDynamic(action) inputReleasedDynamic(action) - #define inputDownIfDynamic(action) inputIsDownDynamic(action) -#else - #define inputPressedIfDynamic(action) inputPressed(action) - #define inputReleasedIfDynamic(action) inputReleased(action) - #define inputDownIfDynamic(action) inputIsDown(action) -#endif \ No newline at end of file diff --git a/src/dusk/input/inputaction.c b/src/dusk/input/inputaction.c deleted file mode 100644 index d2a34697..00000000 --- a/src/dusk/input/inputaction.c +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "inputaction.h" -#include "assert/assert.h" -#include "util/string.h" - -inputaction_t inputActionGetByName(const char_t *name) { - assertNotNull(name, "name must not be NULL"); - - for(inputaction_t i = 0; i < INPUT_ACTION_COUNT; i++) { - if(INPUT_ACTION_IDS[i] == NULL) continue; - if(stringCompareInsensitive(INPUT_ACTION_IDS[i], name) != 0) continue; - return i; - } - - return INPUT_ACTION_COUNT; -} \ No newline at end of file diff --git a/src/dusk/input/inputaction.h b/src/dusk/input/inputaction.h deleted file mode 100644 index 9f28c9c4..00000000 --- a/src/dusk/input/inputaction.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "time/time.h" -#include "input/inputactiondefs.h" - -#define INPUT_ACTION_CALLBACK_COUNT_MAX 4 - -typedef struct { - inputaction_t action; - float_t lastValue; - float_t currentValue; - - #ifdef DUSK_TIME_DYNAMIC - float_t lastDynamicValue; - float_t currentDynamicValue; - #endif -} inputactiondata_t; - -/** - * Gets an input action by its name. - * - * @param name The name of the input action. - * @return The input action, or INPUT_ACTION_COUNT if not found. - */ -inputaction_t inputActionGetByName(const char_t *name); \ No newline at end of file diff --git a/src/dusk/input/inputbutton.c b/src/dusk/input/inputbutton.c deleted file mode 100644 index 32aa310e..00000000 --- a/src/dusk/input/inputbutton.c +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "inputbutton.h" -#include "input.h" -#include "assert/assert.h" -#include "util/string.h" -#include "util/memory.h" - -inputbutton_t inputButtonGetByName(const char_t *name) { - assertNotNull(name, "name must not be NULL"); - - inputbuttondata_t *data = INPUT_BUTTON_DATA; - while(data->name != NULL) { - if(stringCompareInsensitive(data->name, name) == 0) { - return data->button; - } - data++; - } - - return (inputbutton_t){ .type = INPUT_BUTTON_TYPE_NONE }; -} - -float_t inputButtonGetValue(const inputbutton_t button) { - return inputButtonGetValuePlatform(button); -} -inputbuttondata_t * inputButtonGetData(const inputbutton_t button) { - inputbuttondata_t *data = INPUT_BUTTON_DATA; - - do { - if(memoryCompare(&data->button, &button, sizeof(inputbutton_t)) == 0) { - return data; - } - data++; - } while(data->name != NULL); - - return NULL; -} \ No newline at end of file diff --git a/src/dusk/input/inputbutton.h b/src/dusk/input/inputbutton.h deleted file mode 100644 index 10199c40..00000000 --- a/src/dusk/input/inputbutton.h +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "inputaction.h" -#include "input/inputplatform.h" - -#ifndef inputButtonGetValuePlatform - #error "inputButtonGetValuePlatform is not defined" -#endif - -#ifdef DUSK_INPUT_KEYBOARD - typedef inputscancodeplatform_t inputscancode_t; -#endif - -#ifdef DUSK_INPUT_GAMEPAD - typedef inputgamepadbuttonplatform_t inputgamepadbutton_t; - typedef inputgamepadaxissplatform_t inputgamepadaxis_t; -#endif - -#ifdef DUSK_INPUT_POINTER - typedef inputpointeraxisplatform_t inputpointeraxis_t; -#endif - -typedef enum inputbuttontype_e { - INPUT_BUTTON_TYPE_NONE, - - #ifdef DUSK_INPUT_KEYBOARD - INPUT_BUTTON_TYPE_KEYBOARD, - #endif - - #ifdef DUSK_INPUT_POINTER - INPUT_BUTTON_TYPE_POINTER, - #endif - - #ifdef DUSK_INPUT_TOUCH - INPUT_BUTTON_TYPE_TOUCH, - #endif - - #ifdef DUSK_INPUT_GAMEPAD - INPUT_BUTTON_TYPE_GAMEPAD, - INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - #endif -} inputbuttontype_t; - -typedef struct inputbutton_s { - inputbuttontype_t type; - - union { - #ifdef DUSK_INPUT_KEYBOARD - inputscancode_t scancode; - #endif - - #ifdef DUSK_INPUT_GAMEPAD - inputgamepadbutton_t gpButton; - struct { - inputgamepadaxis_t axis; - bool_t positive; - } gpAxis; - #endif - - #ifdef DUSK_INPUT_POINTER - inputpointeraxis_t pointerAxis; - #endif - - // Unusued incase all input types are disabled. - uint8_t unused; - }; -} inputbutton_t; - -typedef struct { - const char_t *name; - inputbutton_t button; - float_t curVal; - float_t lastVal; - inputaction_t action; -} inputbuttondata_t; - -extern inputbuttondata_t INPUT_BUTTON_DATA[]; - -/** - * Gets an input button by its name. - * - * @param name The name of the input button. - * @return The input button, or .type = INPUT_BUTTON_TYPE_NONE if not found. - */ -inputbutton_t inputButtonGetByName(const char_t *name); - -/** - * Gets the current value of an input button. - * - * @param button The input button. - * @return The current value of the input button (0.0f to 1.0f). - */ -float_t inputButtonGetValue(const inputbutton_t button); - -/** - * Gets the button data for a specific input button. - * - * @param button The input button to get the data for. - * @return The button data, or NULL if not found. - */ -inputbuttondata_t * inputButtonGetData(const inputbutton_t button); \ No newline at end of file diff --git a/src/dusk/locale/CMakeLists.txt b/src/dusk/locale/CMakeLists.txt deleted file mode 100644 index 0feecf4f..00000000 --- a/src/dusk/locale/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - localemanager.c -) \ No newline at end of file diff --git a/src/dusk/locale/localeinfo.h b/src/dusk/locale/localeinfo.h deleted file mode 100644 index db935b68..00000000 --- a/src/dusk/locale/localeinfo.h +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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 *name; - const char_t *file; -} localeinfo_t; - -static const localeinfo_t LOCALE_INFO_EN_US = { - .name = "en-US", - .file = "locale/en_US.po", -}; - -static const localeinfo_t LOCALE_INFO_JP_JP = { - .name = "ja-JP", - .file = "locale/jp_JP.po", -}; - -static const localeinfo_t LOCALE_INFO_ES_MX = { - .name = "es-MX", - .file = "locale/es_MX.po", -}; - -static const localeinfo_t * const LOCALE_INFO_LIST[] = { - &LOCALE_INFO_EN_US, - &LOCALE_INFO_JP_JP, - &LOCALE_INFO_ES_MX -}; - -#define LOCALE_INFO_LIST_COUNT ( \ - sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \ -) - -#define LOCALE_DEFAULT LOCALE_INFO_EN_US - -// EOF diff --git a/src/dusk/locale/localemanager.c b/src/dusk/locale/localemanager.c deleted file mode 100644 index 878a7e53..00000000 --- a/src/dusk/locale/localemanager.c +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "localemanager.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assert.h" -#include "ui/ui.h" -#include "system/system.h" -#include "console/console.h" - -localemanager_t LOCALE; - -errorret_t localeManagerInit() { - memoryZero(&LOCALE, sizeof(localemanager_t)); - - // TODO: Set locale based on system locale. - const localeinfo_t *locale = systemGetLocale(); - errorChain(localeManagerSetLocale(locale)); - consolePrint("Locale set to: %s", locale->name); - - errorOk(); -} - -errorret_t localeManagerSetLocale(const localeinfo_t *locale) { - assertNotNull(locale, "Locale cannot be NULL"); - - if(LOCALE.entry != NULL) { - assetEntryUnlock(LOCALE.entry); - LOCALE.entry = NULL; - } - - LOCALE.locale = locale; - LOCALE.entry = assetGetEntry(locale->file, ASSET_LOADER_TYPE_LOCALE, NULL); - assetEntryLock(LOCALE.entry); - errorChain(assetRequireLoaded(LOCALE.entry)); - - // TODO : Trigger UI update. - errorChain(uiUpdateTranslations()); - - errorOk(); -} - -void localeManagerDispose() { - if(LOCALE.entry != NULL) { - assetEntryUnlock(LOCALE.entry); - LOCALE.entry = NULL; - } - - LOCALE.locale = NULL; -} \ No newline at end of file diff --git a/src/dusk/locale/localemanager.h b/src/dusk/locale/localemanager.h deleted file mode 100644 index 5951336d..00000000 --- a/src/dusk/locale/localemanager.h +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "locale/localemanager.h" -#include "locale/localeinfo.h" -#include "asset/asset.h" - -typedef struct { - const localeinfo_t *locale; - assetentry_t *entry; -} localemanager_t; - -extern localemanager_t LOCALE; - -/** - * Initialize the locale system. - * - * @return An error code if a failure occurs. - */ -errorret_t localeManagerInit(); - -/** - * Set the current locale. - * - * @param locale The locale to set. - * @return An error code if a failure occurs. - */ -errorret_t localeManagerSetLocale(const localeinfo_t *locale); - -/** - * Get a localized string for the given message ID verbatim, with no - * printf-style formatting applied - use this (rather than - * localeManagerGetText with no variadic args) whenever the raw string - * is displayed as-is, or cached to be formatted later (e.g. a - * "_format" message ID whose %-specifiers are filled in by the caller - * itself via stringFormat once the actual values are known). Passing a - * "_format" string through localeManagerGetText/GetTextArgs instead - * would consume it immediately as a format string with zero args, - * which is wrong for this use, and would also mishandle any literal - * '%' in a plain display string. - * - * @param id The message ID to retrieve. - * @param buffer Buffer to write the retrieved string to. - * @param bufferSize Size of the buffer. - * @param plural Plural index to retrieve. - * @return An error code if a failure occurs. - */ -#define localeManagerGetString(id, buffer, bufferSize, plural) \ - assetLocaleGetString( \ - &LOCALE.entry->data.locale, \ - id, \ - plural, \ - buffer, \ - bufferSize \ - ) - -/** - * Get a localized string for the given message ID, formatted - * printf-style with the given variadic arguments. - * - * @param id The message ID to retrieve. - * @param buffer Buffer to write the retrieved string to. - * @param bufferSize Size of the buffer. - * @param plural Plural index to retrieve. - * @param ... Arguments for formatting the string - must not be empty, - * since the retrieved string is always run through vsnprintf; see - * localeManagerGetString for a message ID with no format args at all. - * @return An error code if a failure occurs. - */ -#define localeManagerGetText(id, buffer, bufferSize, plural, ...) \ - assetLocaleGetStringWithVA( \ - &LOCALE.entry->data.locale, \ - id, \ - plural, \ - buffer, \ - bufferSize, \ - __VA_ARGS__ \ - ) - -/** - * Get a localized string for the given message ID with a list of arguments. - * - * @param id The message ID to retrieve. - * @param buffer Buffer to write the retrieved string to. - * @param bufferSize Size of the buffer. - * @param plural Plural index to retrieve. - * @param args List of arguments for formatting the string. - * @param argCount Number of arguments in the list. - * @return An error code if a failure occurs. - */ -#define localeManagerGetTextArgs( \ - id, buffer, bufferSize, plural, args, argCount \ -) \ - assetLocaleGetStringWithArgs( \ - &LOCALE.entry->data.locale, \ - id, \ - plural, \ - buffer, \ - bufferSize, \ - args, \ - argCount \ - ) - -/** - * Dispose of the locale system. - */ -void localeManagerDispose(); \ No newline at end of file diff --git a/src/dusk/log/CMakeLists.txt b/src/dusk/log/CMakeLists.txt deleted file mode 100644 index 57c6ef05..00000000 --- a/src/dusk/log/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC -) \ No newline at end of file diff --git a/src/dusk/log/log.h b/src/dusk/log/log.h deleted file mode 100644 index 60180752..00000000 --- a/src/dusk/log/log.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Prints a debug message to the debug console. - * - * @param message The message format string. - * @param ... Additional arguments for the format string. - */ -void logDebug(const char_t *message, ...); - -/** - * Prints a message to the error console. For some platforms this will pause - * the program. - * - * @param message The message format string. - * @param ... Additional arguments for the format string. - */ -void logError(const char_t *message, ...); \ No newline at end of file diff --git a/src/dusk/main.c b/src/dusk/main.c deleted file mode 100644 index 54f4d0bf..00000000 --- a/src/dusk/main.c +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "engine/engine.h" -#include "util/string.h" -#include "input/input.h" - -int main(int argc, char **argv) { - // Main applet - errorret_t ret; - - // Init engine - ret = engineInit(argc, (const char_t **)argv); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - return ret.code; - } - - // Begin main loop - do { - ret = engineUpdate(); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - return ret.code; - } - } while(ENGINE.running); - - ret = engineDispose(); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - return ret.code; - } - - return 0; -} \ No newline at end of file diff --git a/src/dusk/null.c b/src/dusk/null.c deleted file mode 100644 index d649128f..00000000 --- a/src/dusk/null.c +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -// Empty C file for annoying platforms. -void nothing(void) { - -} \ No newline at end of file diff --git a/src/dusk/perf/CMakeLists.txt b/src/dusk/perf/CMakeLists.txt deleted file mode 100644 index bc198e9e..00000000 --- a/src/dusk/perf/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 -) diff --git a/src/dusk/perf/perf.c b/src/dusk/perf/perf.c deleted file mode 100644 index 1b83095f..00000000 --- a/src/dusk/perf/perf.c +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 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 -#elif defined(DUSK_PSP) - #include -#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 - ); -} diff --git a/src/dusk/perf/perf.h b/src/dusk/perf/perf.h deleted file mode 100644 index 82619a2f..00000000 --- a/src/dusk/perf/perf.h +++ /dev/null @@ -1,89 +0,0 @@ -/** - * 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() diff --git a/src/dusk/perf/perfitem.h b/src/dusk/perf/perfitem.h deleted file mode 100644 index c9975758..00000000 --- a/src/dusk/perf/perfitem.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 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; \ No newline at end of file diff --git a/src/dusk/rpg/CMakeLists.txt b/src/dusk/rpg/CMakeLists.txt deleted file mode 100644 index 51db8064..00000000 --- a/src/dusk/rpg/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - rpg.c - rpgcamera.c -) - -# Subdirs -add_subdirectory(battle) -add_subdirectory(cutscene) -add_subdirectory(entity) -add_subdirectory(overworld) - -add_subdirectory(story) -add_subdirectory(item) \ No newline at end of file diff --git a/src/dusk/rpg/battle/CMakeLists.txt b/src/dusk/rpg/battle/CMakeLists.txt deleted file mode 100644 index 49363600..00000000 --- a/src/dusk/rpg/battle/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# 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 - battle.c - party.c -) - -add_subdirectory(fighter) -add_subdirectory(move) -add_subdirectory(state) diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c deleted file mode 100644 index f9e2d47a..00000000 --- a/src/dusk/rpg/battle/battle.c +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battle.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "scene/scene.h" - -battle_t BATTLE; - -void battleInit(void) { - memoryZero(&BATTLE, sizeof(battle_t)); -} - -void battleStart( - battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] -) { - assertNotNull(fighters, "Fighters cannot be NULL"); - - #if DUSK_ASSERTIONS - // Must be at least two non null fighter present, may need to ensure they - // are from seperate teams also. - uint8_t nonNullCount = 0; - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - if(fighters[i] != NULL) nonNullCount++; - } - assertTrue(nonNullCount >= 2, "Must have at least two fighters"); - #endif - - - // TODO: I may not need to zero entire battle. - memoryZero(&BATTLE, sizeof(battle_t)); - memoryCopy( - BATTLE.fighters, - fighters, - sizeof(battlefighter_t *) * BATTLE_FIGHTER_POS_COUNT - ); - - // Enter the opening state. - battleSetState(BATTLE_STATE_OPENING); - - // Change scene - sceneSet(SCENE_TYPE_BATTLE); -} - -void battleSetState(const battlestate_t next) { - assertTrue(next < BATTLE_STATE_COUNT, "Invalid battle state"); - assertNotNull(BATTLE_STATE_FUNCTIONS[next].init, "Missing state init function"); - assertNotNull(BATTLE_STATE_FUNCTIONS[next].update, "Missing state update function"); - assertNotNull(BATTLE_STATE_FUNCTIONS[next].dispose, "Missing state dispose function"); - - if(BATTLE.state != BATTLE_STATE_NULL) { - BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose(); - } - BATTLE.state = next; - BATTLE_STATE_FUNCTIONS[BATTLE.state].init(); -} - -void battleUpdate(void) { - if(BATTLE.state == BATTLE_STATE_NULL) return; - assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state"); - assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].update, "Missing state update function"); - - BATTLE_STATE_FUNCTIONS[BATTLE.state].update(); -} - -void battleDispose(void) { - if(BATTLE.state == BATTLE_STATE_NULL) return; - assertTrue(BATTLE.state < BATTLE_STATE_COUNT, "Invalid battle state"); - assertNotNull(BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose, "Missing state dispose function"); - - BATTLE_STATE_FUNCTIONS[BATTLE.state].dispose(); - memoryZero(&BATTLE, sizeof(battle_t)); -} \ No newline at end of file diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h deleted file mode 100644 index 108a0742..00000000 --- a/src/dusk/rpg/battle/battle.h +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/fighter/battlefighter.h" -#include "rpg/battle/move/battlemove.h" -#include "rpg/battle/move/battlemoveaction.h" -#include "rpg/battle/fighter/battlefighterpos.h" -#include "rpg/battle/state/battlestate.h" - -typedef enum { - BATTLE_RESULT_NONE, - BATTLE_RESULT_WIN, - BATTLE_RESULT_LOSS, - BATTLE_RESULT_FLED, - BATTLE_RESULT_COUNT -} battleresult_t; - -typedef struct { - battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT]; - - // Order of who gets to act. - uint8_t fighterOrder[BATTLE_FIGHTER_POS_COUNT]; - uint8_t fighterOrderCount; - - // Actions to be executed. - battlemoveaction_t actionsPending[BATTLE_FIGHTER_POS_COUNT]; - uint8_t actionsPendingCount; - - battlestate_t state; - battlestatedata_t stateData; - - // Set once a side has been wiped out (see cutsceneBattlePostMoveStart) - // or the player has fled (see battleStateSelectionPlayerFlee). Stays - // BATTLE_RESULT_NONE for the rest of the battle otherwise. - battleresult_t result; -} battle_t; - -extern battle_t BATTLE; - -/** - * Initializes the battle system. Marks it as inactive with no fighters. - */ -void battleInit(void); - -/** - * Starts a battle. - * - * @param fighters Fighters in the battle. - */ -void battleStart( - battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] -); - -/** - * Transitions the battle to a new state: disposes the current state (if - * any) and initializes next. - * - * @param next The state to transition to. - */ -void battleSetState(const battlestate_t next); - -/** - * Updates the battle system. - */ -void battleUpdate(void); - -/** - * Disposes of the battle, clearing all fighters and marking it inactive. - */ -void battleDispose(void); \ No newline at end of file diff --git a/src/dusk/rpg/battle/fighter/CMakeLists.txt b/src/dusk/rpg/battle/fighter/CMakeLists.txt deleted file mode 100644 index bc721bd2..00000000 --- a/src/dusk/rpg/battle/fighter/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - battlefighter.c -) diff --git a/src/dusk/rpg/battle/fighter/battlefighter.c b/src/dusk/rpg/battle/fighter/battlefighter.c deleted file mode 100644 index 27ebe06f..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighter.c +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlefighter.h" -#include "assert/assert.h" -#include "util/memory.h" - -errorret_t battleFighterParseStatField( - yyjson_val *obj, - const char_t *key, - uint16_t *out -) { - yyjson_val *val = yyjson_obj_get(obj, key); - if(!yyjson_is_int(val)) { - errorThrow("Fighter: missing/invalid '%s'", key); - } - - const int64_t num = yyjson_get_sint(val); - if(num < 0 || num > UINT16_MAX) { - errorThrow("Fighter: '%s' must be between 0 and %u", key, UINT16_MAX); - } - - *out = (uint16_t)num; - errorOk(); -} - -errorret_t battleFighterParseOptionalIntField( - yyjson_val *obj, - const char_t *key, - const int32_t defaultValue, - int32_t *out -) { - yyjson_val *val = yyjson_obj_get(obj, key); - if(val == NULL) { - *out = defaultValue; - errorOk(); - } - - if(!yyjson_is_int(val)) errorThrow("Fighter: invalid '%s'", key); - *out = (int32_t)yyjson_get_sint(val); - errorOk(); -} - -errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj) { - assertNotNull(fighter, "Fighter cannot be NULL"); - assertNotNull(obj, "JSON object cannot be NULL"); - - int32_t teamNum; - errorChain(battleFighterParseOptionalIntField( - obj, "team", BATTLE_FIGHTER_TEAM_ALLY, &teamNum - )); - if(teamNum < 0 || teamNum >= BATTLE_FIGHTER_TEAM_COUNT) { - errorThrow("Fighter: invalid 'team'"); - } - - int32_t controllerNum; - errorChain(battleFighterParseOptionalIntField( - obj, "controller", BATTLE_FIGHTER_CONTROLLER_PLAYER, &controllerNum - )); - if(controllerNum < 0 || controllerNum >= BATTLE_FIGHTER_CONTROLLER_COUNT) { - errorThrow("Fighter: invalid 'controller'"); - } - - battlefighterstats_t stats; - errorChain(battleFighterParseStatField(obj, "attack", &stats.attack)); - errorChain(battleFighterParseStatField(obj, "defense", &stats.defense)); - errorChain(battleFighterParseStatField(obj, "magic", &stats.magic)); - errorChain(battleFighterParseStatField(obj, "speed", &stats.speed)); - errorChain(battleFighterParseStatField(obj, "luck", &stats.luck)); - errorChain(battleFighterParseStatField(obj, "healthMax", &stats.healthMax)); - errorChain(battleFighterParseStatField(obj, "mpMax", &stats.mpMax)); - - battleFighterInit( - fighter, (battlefighterteam_t)teamNum, - (battlefightercontroller_t)controllerNum, stats - ); - - int32_t statusNum; - errorChain(battleFighterParseOptionalIntField( - obj, "status", BATTLE_FIGHTER_STATUS_NORMAL, &statusNum - )); - if(statusNum < 0 || statusNum >= BATTLE_FIGHTER_STATUS_COUNT) { - errorThrow("Fighter: invalid 'status'"); - } - fighter->status = (battlefighterstatus_t)statusNum; - - int32_t health; - errorChain( - battleFighterParseOptionalIntField(obj, "health", stats.healthMax, &health) - ); - if(health < 0 || health > stats.healthMax) { - errorThrow("Fighter: 'health' out of range"); - } - fighter->stats.health = (uint16_t)health; - - int32_t mp; - errorChain(battleFighterParseOptionalIntField(obj, "mp", stats.mpMax, &mp)); - if(mp < 0 || mp > stats.mpMax) { - errorThrow("Fighter: 'mp' out of range"); - } - fighter->stats.mp = (uint16_t)mp; - - yyjson_val *abilities = yyjson_obj_get(obj, "abilities"); - if(abilities != NULL) { - if( - !yyjson_is_arr(abilities) || - yyjson_arr_size(abilities) > BATTLE_FIGHTER_ABILITY_COUNT_MAX - ) { - errorThrow( - "Fighter: 'abilities' must have 0-%u entries", - BATTLE_FIGHTER_ABILITY_COUNT_MAX - ); - } - - yyjson_arr_iter iter = yyjson_arr_iter_with(abilities); - yyjson_val *abilityVal; - while((abilityVal = yyjson_arr_iter_next(&iter)) != NULL) { - const char_t *name = yyjson_get_str(abilityVal); - battlemoveid_t ability = - name == NULL ? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name); - if(ability == BATTLE_MOVE_ID_NULL) { - errorThrow("Fighter: invalid entry in 'abilities'"); - } - battleFighterAbilityAdd(fighter, ability); - } - } - - errorOk(); -} - -yyjson_mut_val *battleFighterSerialize( - const battlefighter_t *fighter, - yyjson_mut_doc *doc -) { - assertNotNull(fighter, "Fighter cannot be NULL"); - assertNotNull(doc, "Doc cannot be NULL"); - - yyjson_mut_val *obj = yyjson_mut_obj(doc); - - yyjson_mut_obj_add_uint(doc, obj, "team", fighter->team); - yyjson_mut_obj_add_uint(doc, obj, "controller", fighter->controller); - yyjson_mut_obj_add_uint(doc, obj, "status", fighter->status); - - yyjson_mut_obj_add_uint(doc, obj, "attack", fighter->stats.attack); - yyjson_mut_obj_add_uint(doc, obj, "defense", fighter->stats.defense); - yyjson_mut_obj_add_uint(doc, obj, "magic", fighter->stats.magic); - yyjson_mut_obj_add_uint(doc, obj, "speed", fighter->stats.speed); - yyjson_mut_obj_add_uint(doc, obj, "luck", fighter->stats.luck); - yyjson_mut_obj_add_uint(doc, obj, "healthMax", fighter->stats.healthMax); - yyjson_mut_obj_add_uint(doc, obj, "mpMax", fighter->stats.mpMax); - yyjson_mut_obj_add_uint(doc, obj, "health", fighter->stats.health); - yyjson_mut_obj_add_uint(doc, obj, "mp", fighter->stats.mp); - - yyjson_mut_val *abilities = yyjson_mut_arr(doc); - for(uint8_t i = 0; i < fighter->abilityCount; i++) { - yyjson_mut_arr_add_str(doc, abilities, BATTLE_MOVES[fighter->abilities[i]].name); - } - yyjson_mut_obj_add_val(doc, obj, "abilities", abilities); - - return obj; -} - -void battleFighterInit( - battlefighter_t *fighter, - const battlefighterteam_t team, - const battlefightercontroller_t controller, - const battlefighterstats_t stats -) { - assertNotNull(fighter, "Fighter pointer cannot be NULL"); - assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team"); - assertTrue( - controller < BATTLE_FIGHTER_CONTROLLER_COUNT, - "Invalid fighter controller" - ); - - const uint8_t id = fighter->id; - memoryZero(fighter, sizeof(battlefighter_t)); - fighter->id = id; - fighter->status = BATTLE_FIGHTER_STATUS_NORMAL; - fighter->team = team; - fighter->controller = controller; - fighter->stats = stats; - fighter->stats.health = stats.healthMax; - fighter->stats.mp = stats.mpMax; -} - -bool_t battleFighterIsAlive(const battlefighter_t *fighter) { - assertNotNull(fighter, "Fighter pointer cannot be NULL"); - return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL; -} - -void battleFighterAbilityAdd( - battlefighter_t *fighter, - const battlemoveid_t ability -) { - assertNotNull(fighter, "Fighter pointer cannot be NULL"); - assertTrue( - fighter->abilityCount < BATTLE_FIGHTER_ABILITY_COUNT_MAX, - "Fighter already has the maximum number of abilities" - ); - - fighter->abilities[fighter->abilityCount++] = ability; -} - -void battleFighterDamage( - battlefighter_t *fighter, - const uint16_t amount -) { - assertNotNull(fighter, "Fighter pointer cannot be NULL"); - - fighter->stats.health = - amount >= fighter->stats.health ? 0 : fighter->stats.health - amount; - if(fighter->stats.health == 0) fighter->status = BATTLE_FIGHTER_STATUS_DEAD; -} - -void battleFighterHeal( - battlefighter_t *fighter, - const uint16_t amount -) { - assertNotNull(fighter, "Fighter pointer cannot be NULL"); - if(!battleFighterIsAlive(fighter)) return; - - const uint32_t healed = (uint32_t)fighter->stats.health + amount; - fighter->stats.health = healed > fighter->stats.healthMax - ? fighter->stats.healthMax : (uint16_t)healed; -} diff --git a/src/dusk/rpg/battle/fighter/battlefighter.h b/src/dusk/rpg/battle/fighter/battlefighter.h deleted file mode 100644 index 447f9772..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighter.h +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "yyjson.h" -#include "rpg/battle/move/battlemove.h" -#include "rpg/battle/fighter/battlefighterstatus.h" -#include "rpg/battle/fighter/battlefighterteam.h" -#include "rpg/battle/fighter/battlefightercontroller.h" -#include "rpg/battle/fighter/battlefighterstats.h" -#include "rpg/battle/fighter/battlefighterpos.h" - -#define BATTLE_FIGHTER_ABILITY_COUNT_MAX 8 - -typedef struct battlefighter_s { - uint8_t id; - battlefighterstatus_t status; - battlefighterteam_t team; - battlefightercontroller_t controller; - - - battlefighterstats_t stats; - - battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX]; - battlemoveid_t abilityCount; -} battlefighter_t; - -/** - * Initializes a battle fighter in place, filling health/mp to the maximum - * values carried on stats and setting its status to normal. - * - * @param fighter Pointer to the fighter to initialize. - * @param team The team the fighter belongs to. - * @param controller Who makes decisions for the fighter. - * @param stats The fighter's base combat stats, including healthMax/mpMax. - */ -void battleFighterInit( - battlefighter_t *fighter, - const battlefighterteam_t team, - const battlefightercontroller_t controller, - const battlefighterstats_t stats -); - -/** - * Generic battlefighter_t parser, shared by every place that builds a - * fighter from JSON (party.jsonc roster members, BATTLE_START enemy - * definitions, save-file party members) - see battleFighterSerialize for - * the inverse. Reads 'team'/'controller'/'status' as ints (each optional, - * defaulting to ALLY/PLAYER/NORMAL respectively - a caller that needs a - * different team/controller, e.g. BATTLE_START's enemies, sets those - * fields on fighter after this returns), 'attack'/'defense'/'magic'/ - * 'speed'/'luck'/'healthMax'/'mpMax' as required ints in [0, UINT16_MAX], - * 'health'/'mp' as optional ints defaulting to healthMax/mpMax (a fresh - * fighter), and an optional 'abilities' array of move name strings. Does - * not touch fighter->id - set it (or leave it) before calling, same as - * battleFighterInit. - * - * @param fighter Pointer to the fighter to fill in. - * @param obj The JSON object to read fields from. - * @return Error if a required field is missing/invalid, or an abilities - * entry doesn't name a real move. - */ -errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj); - -/** - * Inverse of battleFighterParse: builds a new JSON object (not yet - * attached anywhere - the caller attaches it, e.g. via - * yyjson_mut_arr_add_val/yyjson_mut_obj_add_val) with every field - * battleFighterParse reads except id (see its doc comment for why). - * - * @param fighter The fighter to serialize. - * @param doc The mutable document to allocate the object in. - * @return The newly built object. - */ -yyjson_mut_val *battleFighterSerialize( - const battlefighter_t *fighter, - yyjson_mut_doc *doc -); - -/** - * Returns true if the fighter is in a state where it can still act (i.e. - * is not dead). - * - * @param fighter Pointer to the fighter to check. - * @returns True if the fighter can act. - */ -bool_t battleFighterIsAlive(const battlefighter_t *fighter); - -/** - * Appends an ability to the fighter's ability list. - * - * @param fighter Pointer to the fighter to add the ability to. - * @param ability The ability to add. - */ -void battleFighterAbilityAdd( - battlefighter_t *fighter, - const battlemoveid_t ability -); - -/** - * Subtracts amount from the fighter's health (floored at 0), marking it - * dead once health reaches 0. - * - * @param fighter Pointer to the fighter to damage. - * @param amount The amount of damage to apply. - */ -void battleFighterDamage( - battlefighter_t *fighter, - const uint16_t amount -); - -/** - * Adds amount to the fighter's health (capped at healthMax). No-op if the - * fighter isn't alive -- healing can't revive a dead fighter. - * - * @param fighter Pointer to the fighter to heal. - * @param amount The amount to heal. - */ -void battleFighterHeal( - battlefighter_t *fighter, - const uint16_t amount -); diff --git a/src/dusk/rpg/battle/fighter/battlefightercontroller.h b/src/dusk/rpg/battle/fighter/battlefightercontroller.h deleted file mode 100644 index 5e0389d0..00000000 --- a/src/dusk/rpg/battle/fighter/battlefightercontroller.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef enum { - BATTLE_FIGHTER_CONTROLLER_NONE, - - BATTLE_FIGHTER_CONTROLLER_PLAYER, - BATTLE_FIGHTER_CONTROLLER_AI, - - BATTLE_FIGHTER_CONTROLLER_COUNT -} battlefightercontroller_t; diff --git a/src/dusk/rpg/battle/fighter/battlefighterpos.h b/src/dusk/rpg/battle/fighter/battlefighterpos.h deleted file mode 100644 index e7f237b0..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighterpos.h +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef uint8_t battlefighterpos_t; - -#define BATTLE_FIGHTER_POS(x) ((battlefighterpos_t)(x)) - -// Left (Enemy side) -#define BATTLE_FIGHTER_POS_0 BATTLE_FIGHTER_POS(0) -#define BATTLE_FIGHTER_POS_1 BATTLE_FIGHTER_POS(1) -#define BATTLE_FIGHTER_POS_2 BATTLE_FIGHTER_POS(2) -#define BATTLE_FIGHTER_POS_3 BATTLE_FIGHTER_POS(3) - -// Right (Ally side) -#define BATTLE_FIGHTER_POS_4 BATTLE_FIGHTER_POS(4) -#define BATTLE_FIGHTER_POS_5 BATTLE_FIGHTER_POS(5) -#define BATTLE_FIGHTER_POS_6 BATTLE_FIGHTER_POS(6) -#define BATTLE_FIGHTER_POS_7 BATTLE_FIGHTER_POS(7) - -#define BATTLE_FIGHTER_POS_COUNT (BATTLE_FIGHTER_POS_7 + 1) - -// Sentinels resolved by battleStateExecutingInit against the currently -// resolving action, for cutscene items authored inside a move's own -// cutscene (e.g. cutscenes/battle/moves/slash.jsonc) that need to refer -// to "whoever is being hit"/"whoever is attacking" without knowing their -// concrete BATTLE.fighters index ahead of time. -#define BATTLE_FIGHTER_POS_TARGET BATTLE_FIGHTER_POS(0xFE) -#define BATTLE_FIGHTER_POS_USER BATTLE_FIGHTER_POS(0xFD) \ No newline at end of file diff --git a/src/dusk/rpg/battle/fighter/battlefighterspecies.h b/src/dusk/rpg/battle/fighter/battlefighterspecies.h deleted file mode 100644 index 92162b98..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighterspecies.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/battle/fighter/battlefighterstats.h" - -#define BATTLE_FIGHTER_SPECIES_NAME_MAX 16 -#define BATTLE_FIGHTER_SPECIES_COUNT_MAX 250 - -typedef uint8_t battlespeciesid_t; - -#define BATTLE_SPECIES_ID_NULL ((battlespeciesid_t)0) - -typedef struct { - char_t name[BATTLE_FIGHTER_SPECIES_NAME_MAX]; - battlefighterstats_t baseStats; -} battlefighterspecies_t; diff --git a/src/dusk/rpg/battle/fighter/battlefighterstats.h b/src/dusk/rpg/battle/fighter/battlefighterstats.h deleted file mode 100644 index f4b731bb..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighterstats.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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 { - uint16_t health; - uint16_t mp; - - uint16_t healthMax; - uint16_t mpMax; - uint16_t attack; - uint16_t defense; - uint16_t magic; - uint16_t speed; - uint16_t luck; -} battlefighterstats_t; diff --git a/src/dusk/rpg/battle/fighter/battlefighterstatus.h b/src/dusk/rpg/battle/fighter/battlefighterstatus.h deleted file mode 100644 index 3c73af2d..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighterstatus.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef enum { - BATTLE_FIGHTER_STATUS_NULL, - - BATTLE_FIGHTER_STATUS_NORMAL, - BATTLE_FIGHTER_STATUS_DEAD, - - // Poisoned, burned, confused, etc. - - BATTLE_FIGHTER_STATUS_COUNT -} battlefighterstatus_t; diff --git a/src/dusk/rpg/battle/fighter/battlefighterteam.h b/src/dusk/rpg/battle/fighter/battlefighterteam.h deleted file mode 100644 index 85abda07..00000000 --- a/src/dusk/rpg/battle/fighter/battlefighterteam.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef enum { - BATTLE_FIGHTER_TEAM_ALLY, - BATTLE_FIGHTER_TEAM_ENEMY, - - BATTLE_FIGHTER_TEAM_COUNT -} battlefighterteam_t; diff --git a/src/dusk/rpg/battle/move/CMakeLists.txt b/src/dusk/rpg/battle/move/CMakeLists.txt deleted file mode 100644 index cdbe40d2..00000000 --- a/src/dusk/rpg/battle/move/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - battlemove.c -) diff --git a/src/dusk/rpg/battle/move/battlemove.c b/src/dusk/rpg/battle/move/battlemove.c deleted file mode 100644 index 389c5ee0..00000000 --- a/src/dusk/rpg/battle/move/battlemove.c +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlemove.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include - -battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX]; -uint8_t BATTLE_MOVE_COUNT; - -errorret_t battleMoveInit(void) { - memoryZero(BATTLE_MOVES, sizeof(BATTLE_MOVES)); - BATTLE_MOVE_COUNT = BATTLE_MOVE_ID_NULL + 1;// Reserve NULL. - - // Get moves JSON. - assetentry_t *entry = assetLock( - "battle/moves.jsonc", ASSET_LOADER_TYPE_JSON, NULL - ); - errorret_t ret = assetRequireLoaded(entry); - if(errorIsNotOk(ret)) { - assetUnlockEntry(entry); - errorChain(ret); - } - - yyjson_val *root = yyjson_doc_get_root(entry->data.json); - yyjson_val *moves = yyjson_obj_get(root, "moves"); - yyjson_obj_iter iter = yyjson_obj_iter_with(moves); - yyjson_val *key; - while((key = yyjson_obj_iter_next(&iter))) { - if(BATTLE_MOVE_COUNT >= BATTLE_MOVE_COUNT_MAX) { - assetUnlockEntry(entry); - errorThrow( - "Battle move: too many moves defined (max %u)", - (uint32_t)BATTLE_MOVE_COUNT_MAX - ); - } - - battlemoveid_t id = BATTLE_MOVE_COUNT++; - battleability_t *move = &BATTLE_MOVES[id]; - - // Name - if(!yyjson_is_str(key)) { - assetUnlockEntry(entry); - errorThrow("Battle move: keys in 'moves' must be strings"); - } - const char_t *name = yyjson_get_str(key); - stringCopy(move->name, name, BATTLE_MOVE_NAME_MAX); - - if(strlen(name) >= BATTLE_MOVE_NAME_MAX) { - assetUnlockEntry(entry); - errorThrow( - "Battle move '%s': name must be less than %u characters", - name, (uint32_t)BATTLE_MOVE_NAME_MAX - ); - } - - // Must me object - yyjson_val *val = yyjson_obj_iter_get_val(key); - if(!yyjson_is_obj(val)) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': value must be an object", name); - } - - // Valid targets - yyjson_val *enemies = yyjson_obj_get(val, "enemies"); - if(enemies != NULL && !yyjson_is_bool(enemies)) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': 'enemies' must be a boolean", name); - } - yyjson_val *allies = yyjson_obj_get(val, "allies"); - if(allies != NULL && !yyjson_is_bool(allies)) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': 'allies' must be a boolean", name); - } - yyjson_val *multiple = yyjson_obj_get(val, "multiple"); - if(multiple != NULL && !yyjson_is_bool(multiple)) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': 'multiple' must be a boolean", name); - } - yyjson_val *self = yyjson_obj_get(val, "self"); - if(self != NULL && !yyjson_is_bool(self)) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': 'self' must be a boolean", name); - } - - battletarget_t target = 0; - if(yyjson_get_bool(enemies)) target |= BATTLE_TARGET_ENEMIES; - if(yyjson_get_bool(allies)) target |= BATTLE_TARGET_ALLIES; - if(yyjson_get_bool(multiple)) target |= BATTLE_TARGET_MULTIPLE; - if(yyjson_get_bool(self)) target |= BATTLE_TARGET_SELF; - move->target = target; - if(move->target == 0) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': missing target type", name); - } - - // Default target (optional) - yyjson_val *defaultVal = yyjson_obj_get(val, "default"); - if(defaultVal != NULL && !yyjson_is_str(defaultVal)) { - assetUnlockEntry(entry); - errorThrow("Battle move '%s': 'default' must be a string", name); - } - - const char_t *defaultTarget = yyjson_get_str(defaultVal); - if(defaultTarget == NULL) { - move->targetDefault = 0; - } else if(stringCompareInsensitive(defaultTarget, "enemy") == 0) { - move->targetDefault = BATTLE_TARGET_ENEMIES; - } else if(stringCompareInsensitive(defaultTarget, "ally") == 0) { - move->targetDefault = BATTLE_TARGET_ALLIES; - } else if(stringCompareInsensitive(defaultTarget, "self") == 0) { - move->targetDefault = BATTLE_TARGET_SELF; - } else { - assetUnlockEntry(entry); - errorThrow("Battle move %s has invalid default target.", name); - } - } - - assetUnlockEntry(entry); - errorOk(); -} - -battlemoveid_t battleMoveGetIdByName(const char_t *name) { - assertNotNull(name, "name must not be NULL"); - - for(battlemoveid_t id = BATTLE_MOVE_ID_NULL + 1; id < BATTLE_MOVE_COUNT; id++) { - if(stringEquals(BATTLE_MOVES[id].name, name)) return id; - } - - return BATTLE_MOVE_ID_NULL; -} diff --git a/src/dusk/rpg/battle/move/battlemove.h b/src/dusk/rpg/battle/move/battlemove.h deleted file mode 100644 index 3c11b2d2..00000000 --- a/src/dusk/rpg/battle/move/battlemove.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/asset.h" -#include "battletarget.h" - -typedef uint8_t battlemoveid_t; - -#define BATTLE_MOVE_NAME_MAX 16 -#define BATTLE_MOVE_COUNT_MAX 250 - -#define BATTLE_MOVE_ID_NULL ((battlemoveid_t)0) - -extern uint8_t BATTLE_MOVE_COUNT; - -typedef struct { - char_t name[BATTLE_MOVE_NAME_MAX]; - battletarget_t target; - battletarget_t targetDefault; -} battleability_t; - -extern battleability_t BATTLE_MOVES[BATTLE_MOVE_COUNT_MAX]; - -/** - * Initializes the battle move subsystem, loading assets/battle/moves.jsonc. - * - * @return Any error that occurs. - */ -errorret_t battleMoveInit(void); - -/** - * Finds a move's id by name (exact match). - * - * @param name The move name to search for. - * @return The matching move's id, or BATTLE_MOVE_ID_NULL if not found. - */ -battlemoveid_t battleMoveGetIdByName(const char_t *name); diff --git a/src/dusk/rpg/battle/move/battlemoveaction.h b/src/dusk/rpg/battle/move/battlemoveaction.h deleted file mode 100644 index f39f8852..00000000 --- a/src/dusk/rpg/battle/move/battlemoveaction.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/move/battlemove.h" -#include "rpg/battle/fighter/battlefighterpos.h" - -typedef struct battlefighter_s battlefighter_t; - -typedef struct { - battlemoveid_t move; - battlefighterpos_t targets[BATTLE_FIGHTER_POS_COUNT]; - battlefighter_t *user; -} battlemoveaction_t; diff --git a/src/dusk/rpg/battle/move/battletarget.h b/src/dusk/rpg/battle/move/battletarget.h deleted file mode 100644 index 73abfb38..00000000 --- a/src/dusk/rpg/battle/move/battletarget.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef uint8_t battletarget_t; - -#define BATTLE_TARGET_ENEMIES (1 << 0) -#define BATTLE_TARGET_ALLIES (1 << 1) -#define BATTLE_TARGET_MULTIPLE (1 << 2) -#define BATTLE_TARGET_SELF (1 << 3) \ No newline at end of file diff --git a/src/dusk/rpg/battle/party.c b/src/dusk/rpg/battle/party.c deleted file mode 100644 index 016e4ca3..00000000 --- a/src/dusk/rpg/battle/party.c +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "party.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "save/save.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" - -errorret_t partyInit(party_t *party) { - assertNotNull(party, "Party cannot be NULL"); - - memoryZero(party, sizeof(party_t)); - for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { - party->order[i] = PARTY_ORDER_EMPTY; - } - - assetentry_t *entry = assetLock("party.jsonc", ASSET_LOADER_TYPE_JSON, NULL); - errorret_t result = assetRequireLoaded(entry); - if(errorIsNotOk(result)) { - assetUnlockEntry(entry); - errorChain(result); - } - - yyjson_val *root = yyjson_doc_get_root(entry->data.json); - yyjson_val *members = yyjson_obj_get(root, "members"); - if(!yyjson_is_obj(members)) { - assetUnlockEntry(entry); - errorThrow("party.jsonc: missing/invalid 'members' object"); - } - - const size_t memberCount = yyjson_obj_size(members); - if(memberCount > PARTY_MEMBER_COUNT_MAX) { - assetUnlockEntry(entry); - errorThrow( - "party.jsonc: defines more than PARTY_MEMBER_COUNT_MAX (%u) members", - PARTY_MEMBER_COUNT_MAX - ); - } - - uint8_t idx = 0; - yyjson_val *key; - yyjson_val *memberObj; - yyjson_obj_iter iter = yyjson_obj_iter_with(members); - while((key = yyjson_obj_iter_next(&iter))) { - memberObj = yyjson_obj_iter_get_val(key); - - if(yyjson_get_len(key) >= PARTY_KEY_LENGTH_MAX) { - assetUnlockEntry(entry); - errorThrow( - "party.jsonc: member key '%s' exceeds PARTY_KEY_LENGTH_MAX (%u)", - yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - ); - } - - stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1); - - battlefighter_t *member = &party->members[idx]; - member->id = idx; - errorret_t parseResult = battleFighterParse(member, memberObj); - if(errorIsNotOk(parseResult)) { - assetUnlockEntry(entry); - errorChain(parseResult); - } - - idx++; - } - - assetUnlockEntry(entry); - - // TEST: only the first defined member starts active, in the first party - // position - replace once there's a real starting-party/character- - // creation flow. Deliberately separate from the roster seeding above: - // order (not a member's presence in the roster) is what determines who's - // currently in the party. - if(memberCount > 0) party->order[0] = 0; - - errorOk(); -} - -uint8_t partyGetMemberIndexByKey(const char_t *key) { - assertNotNull(key, "Key cannot be NULL"); - - const party_t *party = &SAVE.slot.party; - for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) { - if(stringEquals(party->keys[i], key)) return i; - } - - return PARTY_MEMBER_NOT_FOUND; -} - -const char_t *partyGetMemberKey(const uint8_t memberIndex) { - assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index"); - return SAVE.slot.party.keys[memberIndex]; -} - -bool_t partyIsMemberActive(const uint8_t memberIndex) { - assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index"); - - const party_t *party = &SAVE.slot.party; - for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { - if(party->order[i] == memberIndex) return true; - } - - return false; -} - -void partyAddMember(const uint8_t memberIndex) { - assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index"); - - party_t *party = &SAVE.slot.party; - assertTrue( - party->members[memberIndex].status != BATTLE_FIGHTER_STATUS_NULL, - "Member does not exist in the roster" - ); - - if(partyIsMemberActive(memberIndex)) return; - - for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { - if(party->order[i] != PARTY_ORDER_EMPTY) continue; - party->order[i] = memberIndex; - return; - } -} - -battlefighter_t *partyGetOrderMember(const uint8_t slot) { - assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot"); - - party_t *party = &SAVE.slot.party; - const uint8_t index = party->order[slot]; - if(index == PARTY_ORDER_EMPTY) return NULL; - return &party->members[index]; -} - -void partySetOrder(const uint8_t slot, const uint8_t memberIndex) { - assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot"); - assertTrue( - memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX, - "Invalid party member index" - ); - - SAVE.slot.party.order[slot] = memberIndex; -} - -void partyRemoveMember(const uint8_t memberIndex) { - assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index"); - - party_t *party = &SAVE.slot.party; - - uint8_t removedSlot = PARTY_ORDER_EMPTY; - for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { - if(party->order[i] != memberIndex) continue; - removedSlot = i; - break; - } - if(removedSlot == PARTY_ORDER_EMPTY) return; - - for(uint8_t i = removedSlot; i < PARTY_ACTIVE_SIZE_MAX - 1; i++) { - party->order[i] = party->order[i + 1]; - } - party->order[PARTY_ACTIVE_SIZE_MAX - 1] = PARTY_ORDER_EMPTY; -} - -void partyHealMember(const uint8_t memberIndex) { - assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index"); - - battlefighter_t *member = &SAVE.slot.party.members[memberIndex]; - if(member->status == BATTLE_FIGHTER_STATUS_NULL) return; - - member->status = BATTLE_FIGHTER_STATUS_NORMAL; - member->stats.health = member->stats.healthMax; - member->stats.mp = member->stats.mpMax; -} - -void partyHealAll(void) { - const party_t *party = &SAVE.slot.party; - - for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) { - if(party->members[i].status == BATTLE_FIGHTER_STATUS_NULL) continue; - partyHealMember(i); - } -} diff --git a/src/dusk/rpg/battle/party.h b/src/dusk/rpg/battle/party.h deleted file mode 100644 index 8feedaef..00000000 --- a/src/dusk/rpg/battle/party.h +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/fighter/battlefighter.h" -#include "yyjson.h" - -#define PARTY_MEMBER_COUNT_MAX 10 -#define PARTY_ACTIVE_SIZE_MAX 4 -#define PARTY_KEY_LENGTH_MAX 32 -#define PARTY_ORDER_EMPTY 0xFF -#define PARTY_MEMBER_NOT_FOUND 0xFF - -typedef struct { - battlefighter_t members[PARTY_MEMBER_COUNT_MAX]; - char_t keys[PARTY_MEMBER_COUNT_MAX][PARTY_KEY_LENGTH_MAX]; - uint8_t order[PARTY_ACTIVE_SIZE_MAX]; -} party_t; - -/** - * Initializes party with an empty order, then seeds every roster slot - * directly from party.jsonc's "members" object (each key/value pair fills - * the next members[]/keys[] slot in the object's iteration order, key - * copied into keys[] and value parsed via battleFighterParse) - unlike - * partyAddMember, this never touches order itself except for one - * placeholder default: if party.jsonc defines at least one member, - * order[0] is set to 0 (the first member starts active in the first party - * position). Replace this default once there's a real starting-party/ - * character-creation flow. - * - * @param party The party to initialize. - * @return Error if party.jsonc fails to load/parse, defines more members - * than PARTY_MEMBER_COUNT_MAX has room for, or a key exceeds - * PARTY_KEY_LENGTH_MAX. - */ -errorret_t partyInit(party_t *party); - -/** - * Looks up a roster member's index by its party.jsonc key (see - * party_t.keys). - * - * @param key The member's key to search for. - * @return The matching roster index, or PARTY_MEMBER_NOT_FOUND if no - * member has that key. - */ -uint8_t partyGetMemberIndexByKey(const char_t *key); - -/** - * Gets a roster member's party.jsonc key by index (see party_t.keys). - * - * @param memberIndex The roster member index to query. - * @return The member's key - "" if memberIndex is past however many - * members party.jsonc actually defines. - */ -const char_t *partyGetMemberKey(const uint8_t memberIndex); - -/** - * Checks whether memberIndex currently occupies a slot in the current save - * slot's party order (i.e. is actively in the party right now). - * - * @param memberIndex The roster member index to check. - * @return True if memberIndex appears anywhere in order. - */ -bool_t partyIsMemberActive(const uint8_t memberIndex); - -/** - * Adds an existing roster member (see partyInit - every roster slot - * always exists once initialized) to the current save slot's active party - * order, in the first free order slot. No-op if memberIndex is already - * active, or if there is no free order slot. - * - * @param memberIndex The roster member index to activate. - */ -void partyAddMember(const uint8_t memberIndex); - -/** - * Gets the roster member currently occupying an active order slot in the - * current save slot's party. - * - * @param slot The active order slot to query. - * @return Pointer to the member in that slot, or NULL if the slot is - * empty. - */ -battlefighter_t *partyGetOrderMember(const uint8_t slot); - -/** - * Assigns a roster member to an active order slot in the current save - * slot's party, replacing whatever was there. Use PARTY_ORDER_EMPTY to - * clear a slot. - * - * @param slot The active order slot to assign. - * @param memberIndex The roster member index to place there, or - * PARTY_ORDER_EMPTY to clear the slot. - */ -void partySetOrder(const uint8_t slot, const uint8_t memberIndex); - -/** - * Removes memberIndex from the current save slot's active party order - - * the roster member itself is untouched (it still exists, per partyInit; - * it's simply no longer active) - shifting every later order entry down - * one slot to close the gap, so order stays packed with no holes between - * active members. No-op if memberIndex isn't currently active. - * - * @param memberIndex The roster member index to deactivate. - */ -void partyRemoveMember(const uint8_t memberIndex); - -/** - * Fully restores a roster member's health/mp to their max and clears a - * dead status back to normal - unlike battleFighterHeal, this can revive - * a dead member (there's no combat context here to keep them down). No-op - * if the slot is empty. - * - * @param memberIndex The roster member index to heal. - */ -void partyHealMember(const uint8_t memberIndex); - -/** - * Calls partyHealMember for every occupied slot in the current save - * slot's party roster. - */ -void partyHealAll(void); diff --git a/src/dusk/rpg/battle/state/CMakeLists.txt b/src/dusk/rpg/battle/state/CMakeLists.txt deleted file mode 100644 index 603227d0..00000000 --- a/src/dusk/rpg/battle/state/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# 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 - battlestate.c - battlestateopening.c - battlestatepreround.c - battlestateselection.c - battlestateexecuting.c - battlestatepostround.c - battlestatelost.c - battlestatewon.c - battlestatefled.c - battlestateending.c -) diff --git a/src/dusk/rpg/battle/state/battlestate.c b/src/dusk/rpg/battle/state/battlestate.c deleted file mode 100644 index d5387658..00000000 --- a/src/dusk/rpg/battle/state/battlestate.c +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestate.h" - -battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT] = { - [BATTLE_STATE_NULL] = { 0 }, - - [BATTLE_STATE_OPENING] = { - .init = battleStateOpeningInit, - .update = battleStateOpeningUpdate, - .dispose = battleStateOpeningDispose - }, - - [BATTLE_STATE_PRE_ROUND] = { - .init = battleStatePreRoundInit, - .update = battleStatePreRoundUpdate, - .dispose = battleStatePreRoundDispose - }, - - [BATTLE_STATE_SELECTION] = { - .init = battleStateSelectionInit, - .update = battleStateSelectionUpdate, - .dispose = battleStateSelectionDispose - }, - - [BATTLE_STATE_EXECUTING] = { - .init = battleStateExecutingInit, - .update = battleStateExecutingUpdate, - .dispose = battleStateExecutingDispose - }, - - [BATTLE_STATE_POST_ROUND] = { - .init = battleStatePostRoundInit, - .update = battleStatePostRoundUpdate, - .dispose = battleStatePostRoundDispose - }, - - [BATTLE_STATE_LOST] = { - .init = battleStateLostInit, - .update = battleStateLostUpdate, - .dispose = battleStateLostDispose - }, - - [BATTLE_STATE_WON] = { - .init = battleStateWonInit, - .update = battleStateWonUpdate, - .dispose = battleStateWonDispose - }, - - [BATTLE_STATE_FLED] = { - .init = battleStateFledInit, - .update = battleStateFledUpdate, - .dispose = battleStateFledDispose - }, - - [BATTLE_STATE_ENDING] = { - .init = battleStateEndingInit, - .update = battleStateEndingUpdate, - .dispose = battleStateEndingDispose - } -}; diff --git a/src/dusk/rpg/battle/state/battlestate.h b/src/dusk/rpg/battle/state/battlestate.h deleted file mode 100644 index 00eddd5e..00000000 --- a/src/dusk/rpg/battle/state/battlestate.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "battlestateopening.h" -#include "battlestatepreround.h" -#include "battlestateselection.h" -#include "battlestateexecuting.h" -#include "battlestatepostround.h" -#include "battlestatelost.h" -#include "battlestatewon.h" -#include "battlestatefled.h" -#include "battlestateending.h" - -typedef enum { - // No active battle. - BATTLE_STATE_NULL, - - // Battle hasn't actually started yet, equiv to trainer intro in Pokemon. - BATTLE_STATE_OPENING, - // Pre round, anything that happens before AI or Player can act. - BATTLE_STATE_PRE_ROUND, - // Player is deciding what to do, waiting for their input - BATTLE_STATE_SELECTION, - // Currently executing the moves. - BATTLE_STATE_EXECUTING, - // Post round, anything that happens after AI or Player has acted, loops back - // after this point - BATTLE_STATE_POST_ROUND, - // Battle has ended with a player loss - BATTLE_STATE_LOST, - // Battle has ended with a player win - BATTLE_STATE_WON, - // Battle has ended with a player fleeing - BATTLE_STATE_FLED, - // Battle state is done, this is fade out or whatever. - BATTLE_STATE_ENDING, - - BATTLE_STATE_COUNT -} battlestate_t; - -typedef union { - battlestateopening_t opening; - battlestatepreround_t preRound; - battlestateselection_t selection; - battlestateexecuting_t executing; - battlestatepostround_t postRound; - battlestatelost_t lost; - battlestatewon_t won; - battlestatefled_t fled; - battlestateending_t ending; -} battlestatedata_t; - -typedef struct { - void (*init)(); - void (*update)(); - void (*dispose)(); -} battlestatefunctions_t; - -extern battlestatefunctions_t BATTLE_STATE_FUNCTIONS[BATTLE_STATE_COUNT]; \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestateending.c b/src/dusk/rpg/battle/state/battlestateending.c deleted file mode 100644 index 2a195cbc..00000000 --- a/src/dusk/rpg/battle/state/battlestateending.c +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestateending.h" -#include "rpg/battle/battle.h" -#include "scene/scene.h" - -void battleStateEndingInit(void) { - // Tear down the battle and hand control back to the overworld. The - // overworld scene keeps no state of its own in scenedata_t (player/map - // state lives in the separate global entity/map systems, untouched by - // switching to/from SCENE_TYPE_BATTLE), so there's nothing else to - // restore here. - battleDispose(); - sceneSet(SCENE_TYPE_OVERWORLD); -} - -void battleStateEndingUpdate(void) { - -} - -void battleStateEndingDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestateending.h b/src/dusk/rpg/battle/state/battlestateending.h deleted file mode 100644 index 7b4fc581..00000000 --- a/src/dusk/rpg/battle/state/battlestateending.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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 { - -} battlestateending_t; - -/** - * Called when the battle enters BATTLE_STATE_ENDING. Disposes the battle - * (see battleDispose) and switches back to SCENE_TYPE_OVERWORLD. - */ -void battleStateEndingInit(void); - -/** - * Updates BATTLE_STATE_ENDING for one frame. - */ -void battleStateEndingUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_ENDING. - */ -void battleStateEndingDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.c b/src/dusk/rpg/battle/state/battlestateexecuting.c deleted file mode 100644 index d0045a38..00000000 --- a/src/dusk/rpg/battle/state/battlestateexecuting.c +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestateexecuting.h" -#include "rpg/battle/battle.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/cutscene.h" -#include "asset/assetfile.h" -#include "ui/overlay/uifatalerror.h" -#include "assert/assert.h" -#include "util/string.h" - - -void battleStateExecutingResolveFighterIndex( - battlefighterpos_t *fighterIndex, - const uint8_t userIndex, - const battlemoveaction_t *action -) { - if(*fighterIndex == BATTLE_FIGHTER_POS_TARGET) { - *fighterIndex = action->targets[0]; - } else if(*fighterIndex == BATTLE_FIGHTER_POS_USER) { - *fighterIndex = userIndex; - } -} - -void battleStateExecutingInit(void) { - cutsceneitem_t items[BATTLE_STATE_EXECUTING_ITEMS_MAX]; - uint8_t itemsCount = 0; - - // For each action (just queued during selection). actionsPending is - // indexed by fighter position, not sequentially, so scan every position - // and stop early once every pending action has been processed. - uint8_t processed = 0; - for( - uint8_t i = 0; - i < BATTLE_FIGHTER_POS_COUNT && processed < BATTLE.actionsPendingCount; - i++ - ) { - // Get action. - battlemoveaction_t *action = &BATTLE.actionsPending[i]; - if(action->move == BATTLE_MOVE_ID_NULL) continue; - processed++; - - assertTrue(action->move < BATTLE_MOVE_COUNT, "Invalid move id"); - - // Get ability. - battleability_t *ability = &BATTLE_MOVES[action->move]; - assertStrLenMin(ability->name, 1, "Invalid move name"); - - // Determine cutscene path - char_t path[ASSET_FILE_NAME_MAX]; - stringFormat( - path, ASSET_FILE_NAME_MAX - 1, - "cutscenes/battle/moves/%s.jsonc", ability->name - ); - - // Load and parse the move's cutscene straight into the tail of items. - const uint8_t moveItemsStart = itemsCount; - cutscene_t scene; - errorret_t result = cutsceneLoadParse( - path, &scene, &items[itemsCount], - BATTLE_STATE_EXECUTING_ITEMS_MAX - itemsCount - ); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - uiFatalErrorOpen(NULL); - continue; - } - itemsCount += scene.itemCount; - - // Resolve BATTLE_FIGHTER_POS_TARGET/USER sentinels against this - // action, now that its user/target fighters are known. - for(uint8_t j = moveItemsStart; j < itemsCount; j++) { - cutsceneitem_t *moveItem = &items[j]; - if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE) { - battleStateExecutingResolveFighterIndex( - &moveItem->battleFighterDamage.fighterIndex, i, action - ); - } else if(moveItem->type == CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL) { - battleStateExecutingResolveFighterIndex( - &moveItem->battleFighterHeal.fighterIndex, i, action - ); - } - } - - // Insert end of attack cutscene item. - items[itemsCount++] = (cutsceneitem_t){ - .type = CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE - }; - - // This action has now been resolved - clear it so it isn't - // re-executed (and doesn't block re-selection) next round. - action->move = BATTLE_MOVE_ID_NULL; - } - BATTLE.actionsPendingCount = 0; - - // After all moves completed, transition to pre-round for next round. - items[itemsCount++] = (cutsceneitem_t){ - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_PRE_ROUND } - }; - cutsceneSystemInsertItemsNext(items, itemsCount); -} - -void battleStateExecutingUpdate(void) { - -} - -void battleStateExecutingDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestateexecuting.h b/src/dusk/rpg/battle/state/battlestateexecuting.h deleted file mode 100644 index 8e51d3e9..00000000 --- a/src/dusk/rpg/battle/state/battlestateexecuting.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/battle/fighter/battlefighterpos.h" -#include "rpg/battle/move/battlemoveaction.h" - -typedef struct { - -} battlestateexecuting_t; - -#define BATTLE_STATE_EXECUTING_ITEMS_MAX 64 - -/** - * Called when the battle enters BATTLE_STATE_EXECUTING. - */ -void battleStateExecutingInit(void); - -/** - * Resolves a BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER sentinel - * (as parsed onto a BATTLE_FIGHTER_DAMAGE/BATTLE_FIGHTER_HEAL item inside - * a move's own cutscene) against the action currently being resolved - * into cutscene items. Leaves fighterIndex untouched if it's already a - * concrete BATTLE.fighters index. - * - * @param fighterIndex The field to resolve in place. - * @param userIndex Index into BATTLE.fighters of the fighter performing - * the action. - * @param action The action being resolved. - */ -void battleStateExecutingResolveFighterIndex( - battlefighterpos_t *fighterIndex, - const uint8_t userIndex, - const battlemoveaction_t *action -); - -/** - * Updates BATTLE_STATE_EXECUTING for one frame. - */ -void battleStateExecutingUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_EXECUTING. - */ -void battleStateExecutingDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestatefled.c b/src/dusk/rpg/battle/state/battlestatefled.c deleted file mode 100644 index 441072a2..00000000 --- a/src/dusk/rpg/battle/state/battlestatefled.c +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestatefled.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void battleStateFledInit(void) { - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_TEXT, - .text = { .text = "Got away safely!" } - }, - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_ENDING } - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); -} - -void battleStateFledUpdate(void) { - -} - -void battleStateFledDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestatefled.h b/src/dusk/rpg/battle/state/battlestatefled.h deleted file mode 100644 index f482b6be..00000000 --- a/src/dusk/rpg/battle/state/battlestatefled.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 { - -} battlestatefled_t; - -/** - * Called when the battle enters BATTLE_STATE_FLED. Queues a textbox - * announcing the escape, then transitions to BATTLE_STATE_ENDING once - * dismissed. - */ -void battleStateFledInit(void); - -/** - * Updates BATTLE_STATE_FLED for one frame. - */ -void battleStateFledUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_FLED. - */ -void battleStateFledDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestatelost.c b/src/dusk/rpg/battle/state/battlestatelost.c deleted file mode 100644 index 334d8d87..00000000 --- a/src/dusk/rpg/battle/state/battlestatelost.c +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestatelost.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void battleStateLostInit(void) { - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_TEXT, - .text = { .text = "You were defeated..." } - }, - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_ENDING } - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); -} - -void battleStateLostUpdate(void) { - -} - -void battleStateLostDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestatelost.h b/src/dusk/rpg/battle/state/battlestatelost.h deleted file mode 100644 index 7a20dca7..00000000 --- a/src/dusk/rpg/battle/state/battlestatelost.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 { - -} battlestatelost_t; - -/** - * Called when the battle enters BATTLE_STATE_LOST. Queues a textbox - * announcing the loss, then transitions to BATTLE_STATE_ENDING once - * dismissed. - */ -void battleStateLostInit(void); - -/** - * Updates BATTLE_STATE_LOST for one frame. - */ -void battleStateLostUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_LOST. - */ -void battleStateLostDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestateopening.c b/src/dusk/rpg/battle/state/battlestateopening.c deleted file mode 100644 index eb2a2752..00000000 --- a/src/dusk/rpg/battle/state/battlestateopening.c +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestateopening.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void battleStateOpeningInit(void) { - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_PRE_ROUND } - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); -} - -void battleStateOpeningUpdate(void) { - -} - -void battleStateOpeningDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestateopening.h b/src/dusk/rpg/battle/state/battlestateopening.h deleted file mode 100644 index 946c4d06..00000000 --- a/src/dusk/rpg/battle/state/battlestateopening.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 { - -} battlestateopening_t; - -/** - * Called when the battle enters BATTLE_STATE_OPENING. - */ -void battleStateOpeningInit(void); - -/** - * Updates BATTLE_STATE_OPENING for one frame. - */ -void battleStateOpeningUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_OPENING. - */ -void battleStateOpeningDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestatepostround.c b/src/dusk/rpg/battle/state/battlestatepostround.c deleted file mode 100644 index 2e713610..00000000 --- a/src/dusk/rpg/battle/state/battlestatepostround.c +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestatepostround.h" - -void battleStatePostRoundInit(void) { - -} - -void battleStatePostRoundUpdate(void) { - -} - -void battleStatePostRoundDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestatepostround.h b/src/dusk/rpg/battle/state/battlestatepostround.h deleted file mode 100644 index cba3a38f..00000000 --- a/src/dusk/rpg/battle/state/battlestatepostround.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 { - -} battlestatepostround_t; - -/** - * Called when the battle enters BATTLE_STATE_POST_ROUND. - */ -void battleStatePostRoundInit(void); - -/** - * Updates BATTLE_STATE_POST_ROUND for one frame. - */ -void battleStatePostRoundUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_POST_ROUND. - */ -void battleStatePostRoundDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestatepreround.c b/src/dusk/rpg/battle/state/battlestatepreround.c deleted file mode 100644 index 7b85ece7..00000000 --- a/src/dusk/rpg/battle/state/battlestatepreround.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestatepreround.h" -#include "rpg/battle/battle.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void battleStatePreRoundInit(void) { - // TODO: Determine selection order and who gets to act. - BATTLE.fighterOrderCount = 0; - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - if(BATTLE.fighters[i] == NULL) continue; - if(!battleFighterIsAlive(BATTLE.fighters[i])) continue; - BATTLE.fighterOrder[BATTLE.fighterOrderCount++] = i; - } - - // Queue up the set state and wait for selection to stop cutscene ending. - cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_SELECTION } - }, - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); -} - -void battleStatePreRoundUpdate(void) { - -} - -void battleStatePreRoundDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestatepreround.h b/src/dusk/rpg/battle/state/battlestatepreround.h deleted file mode 100644 index 0f26afe9..00000000 --- a/src/dusk/rpg/battle/state/battlestatepreround.h +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 { - -} battlestatepreround_t; - -/** - * Called when the battle enters BATTLE_STATE_PRE_ROUND. - */ -void battleStatePreRoundInit(void); - -/** - * Updates BATTLE_STATE_PRE_ROUND for one frame. - */ -void battleStatePreRoundUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_PRE_ROUND. - */ -void battleStatePreRoundDispose(void); diff --git a/src/dusk/rpg/battle/state/battlestateselection.c b/src/dusk/rpg/battle/state/battlestateselection.c deleted file mode 100644 index 8bd2e2f8..00000000 --- a/src/dusk/rpg/battle/state/battlestateselection.c +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestateselection.h" -#include "rpg/battle/battle.h" -#include "assert/assert.h" - -void battleStateSelectionInit(void) { - BATTLE.stateData.selection.selectionIndex = 0; -} - -bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB"); - return BATTLE.actionsPending[fighterIndex].move != BATTLE_MOVE_ID_NULL; -} - -bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB"); - - battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter == NULL) return false; - if(!battleFighterIsAlive(fighter)) return false; - - return !battleStateSelectionFighterHasDecided(fighterIndex); -} - -bool_t battleStateSelectionIsComplete(void) { - return BATTLE.stateData.selection.selectionIndex >= BATTLE.fighterOrderCount; -} - -void battleStateSelectionAiDecide(const uint8_t fighterIndex) { - battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - const battlefighterteam_t enemyTeam = - fighter->team == BATTLE_FIGHTER_TEAM_ALLY ? - BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; - - // Simple placeholder AI: attack the first living fighter on the - // opposing team. No move variety or target scoring yet. - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - battlefighter_t *candidate = BATTLE.fighters[i]; - if(candidate == NULL) continue; - if(candidate->team != enemyTeam) continue; - if(!battleFighterIsAlive(candidate)) continue; - - battleStateSelectionQueueAttack(fighterIndex, i); - return; - } -} - -void battleStateSelectionUpdate(void) { - battlestateselection_t *state = &BATTLE.stateData.selection; - - // Are we still waiting for a selection? - while(state->selectionIndex < BATTLE.fighterOrderCount) { - const uint8_t fighterIndex = BATTLE.fighterOrder[state->selectionIndex]; - - // Is this fighter still owed a decision? - if(!battleStateSelectionFighterNeedsDecision(fighterIndex)) { - state->selectionIndex++; - continue; - } - - // Players will act on their own. - battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_AI) return; - - battleStateSelectionAiDecide(fighterIndex); - state->selectionIndex++; - } -} - -void battleStateSelectionDispose(void) { - -} - -void battleStateSelectionQueueAttack( - const uint8_t fighterIndex, - const uint8_t targetIndex -) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "Fighter OOB"); - assertTrue(targetIndex < BATTLE_FIGHTER_POS_COUNT, "Target OOB"); - assertNotNull(BATTLE.fighters[fighterIndex], "Fighter cannot be NULL"); - assertNotNull(BATTLE.fighters[targetIndex], "Target cannot be NULL"); - - const battlemoveid_t move = battleMoveGetIdByName("slash"); - assertTrue(move != BATTLE_MOVE_ID_NULL, "Missing 'slash' battle move"); - - battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex]; - if(action->move == BATTLE_MOVE_ID_NULL) BATTLE.actionsPendingCount++; - action->move = move; - action->targets[0] = targetIndex; - action->user = BATTLE.fighters[fighterIndex]; -} - -battlefighter_t *battleStateSelectionGetCurrentFighter(void) { - if(BATTLE.state != BATTLE_STATE_SELECTION) return NULL; - - const uint8_t index = BATTLE.stateData.selection.selectionIndex; - if(index >= BATTLE.fighterOrderCount) return NULL; - - return BATTLE.fighters[BATTLE.fighterOrder[index]]; -} - -void battleStateSelectionPlayerAttack(const uint8_t targetIndex) { - battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); - if(fighter == NULL) return; - - const uint8_t selectionIndex = BATTLE.stateData.selection.selectionIndex; - const uint8_t fighterIndex = BATTLE.fighterOrder[selectionIndex]; - battleStateSelectionQueueAttack(fighterIndex, targetIndex); - BATTLE.stateData.selection.selectionIndex++; -} - -void battleStateSelectionGoBack(void) { - battlestateselection_t *state = &BATTLE.stateData.selection; - uint8_t index = state->selectionIndex; - - while(index > 0) { - index--; - - const uint8_t fighterIndex = BATTLE.fighterOrder[index]; - battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter == NULL) continue; - if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) continue; - if(!battleStateSelectionFighterHasDecided(fighterIndex)) continue; - - battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex]; - action->move = BATTLE_MOVE_ID_NULL; - BATTLE.actionsPendingCount--; - - state->selectionIndex = index; - return; - } -} - -void battleStateSelectionPlayerFlee(void) { - battlefighter_t *fighter = battleStateSelectionGetCurrentFighter(); - if(fighter == NULL) return; - - BATTLE.result = BATTLE_RESULT_FLED; - battleSetState(BATTLE_STATE_FLED); -} \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestateselection.h b/src/dusk/rpg/battle/state/battlestateselection.h deleted file mode 100644 index 3a43439e..00000000 --- a/src/dusk/rpg/battle/state/battlestateselection.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/fighter/battlefighter.h" - -typedef struct { - // Cursor into BATTLE.fighters (by position index) for the fighter - // currently deciding a move this round. - uint8_t selectionIndex; -} battlestateselection_t; - -/** - * Called when the battle enters BATTLE_STATE_SELECTION. - */ -void battleStateSelectionInit(void); - -/** - * Updates BATTLE_STATE_SELECTION for one frame: auto-decides (see - * battleStateSelectionAiDecide) for every AI-controlled fighter the - * cursor passes, stopping once it reaches a player-controlled fighter - * awaiting input (see battleStateSelectionGetCurrentFighter). Once every - * position has decided, BATTLE_WAIT_SELECTION (see - * cutsceneBattleWaitSelectionUpdate) queues the move to - * BATTLE_STATE_EXECUTING. - */ -void battleStateSelectionUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_SELECTION. - */ -void battleStateSelectionDispose(void); - -/** - * Checks whether the fighter at fighterIndex has already queued a move - * this round. - * - * @param fighterIndex Index into BATTLE.fighters to check. - * @return True if an action is already pending for this fighter. - */ -bool_t battleStateSelectionFighterHasDecided(const uint8_t fighterIndex); - -/** - * Checks whether the fighter at fighterIndex is a live, undecided - * candidate that BATTLE_STATE_SELECTION should still be deciding a move - * for. - * - * @param fighterIndex Index into BATTLE.fighters to check. - * @return True if the fighter is alive and has no move queued yet. - */ -bool_t battleStateSelectionFighterNeedsDecision(const uint8_t fighterIndex); - -/** - * Decides and queues a move for an AI-controlled fighter: currently - * always a basic attack against the first living fighter on the - * opposing team (see battleStateSelectionQueueAttack). No-ops if there's - * no living target (the battle should already be over by then via - * BATTLE_POST_MOVE). - * - * @param fighterIndex Index into BATTLE.fighters of the deciding fighter. - */ -void battleStateSelectionAiDecide(const uint8_t fighterIndex); - -/** - * Checks whether every fighter in BATTLE.fighterOrder has decided a move - * this round. - * - * @return True once the selection cursor has passed every fighter. - */ -bool_t battleStateSelectionIsComplete(void); - -/** - * Queues a basic attack (the "slash" move) from fighterIndex against - * targetIndex. - * - * @param fighterIndex Index into BATTLE.fighters of the deciding fighter. - * @param targetIndex Index into BATTLE.fighters of the target. - */ -void battleStateSelectionQueueAttack( - const uint8_t fighterIndex, - const uint8_t targetIndex -); - -/** - * Returns the fighter currently awaiting a decision in - * BATTLE_STATE_SELECTION. - * - * @return Pointer to the fighter awaiting a decision, or NULL if the - * battle isn't in BATTLE_STATE_SELECTION or every position has - * already decided. - */ -battlefighter_t *battleStateSelectionGetCurrentFighter(void); - -/** - * Submits the currently-selecting fighter's attack against a target, if - * the battle is in BATTLE_STATE_SELECTION and awaiting a decision. Queues - * the move and advances the selection cursor. - * - * @param targetIndex Index into BATTLE.fighters of the target. - */ -void battleStateSelectionPlayerAttack(const uint8_t targetIndex); - -/** - * Ends the battle immediately with BATTLE_STATE_FLED, if the battle is in - * BATTLE_STATE_SELECTION and awaiting a decision. - */ -void battleStateSelectionPlayerFlee(void); - -/** - * Rewinds the selection cursor to the nearest earlier player-controlled - * fighter that has already decided a move this round, clearing its - * pending action so it can be re-decided. AI-decided fighters along the - * way are left alone (skipped over, not rewound onto). No-ops if there's - * no such fighter (e.g. already at the first decision of the round). - */ -void battleStateSelectionGoBack(void); \ No newline at end of file diff --git a/src/dusk/rpg/battle/state/battlestatewon.c b/src/dusk/rpg/battle/state/battlestatewon.c deleted file mode 100644 index 95d09f06..00000000 --- a/src/dusk/rpg/battle/state/battlestatewon.c +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "battlestatewon.h" -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void battleStateWonInit(void) { - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_TEXT, - .text = { .text = "You won the battle!" } - }, - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_ENDING } - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); -} - -void battleStateWonUpdate(void) { - -} - -void battleStateWonDispose(void) { - -} diff --git a/src/dusk/rpg/battle/state/battlestatewon.h b/src/dusk/rpg/battle/state/battlestatewon.h deleted file mode 100644 index d9c72dec..00000000 --- a/src/dusk/rpg/battle/state/battlestatewon.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 { - -} battlestatewon_t; - -/** - * Called when the battle enters BATTLE_STATE_WON. Queues a textbox - * announcing the win, then transitions to BATTLE_STATE_ENDING once - * dismissed. - */ -void battleStateWonInit(void); - -/** - * Updates BATTLE_STATE_WON for one frame. - */ -void battleStateWonUpdate(void); - -/** - * Called when the battle leaves BATTLE_STATE_WON. - */ -void battleStateWonDispose(void); diff --git a/src/dusk/rpg/cutscene/CMakeLists.txt b/src/dusk/rpg/cutscene/CMakeLists.txt deleted file mode 100644 index 7655eb25..00000000 --- a/src/dusk/rpg/cutscene/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscene.c - cutsceneref.c - cutscenesystem.c -) - -# Subdirs -add_subdirectory(item) \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/cutscene.c b/src/dusk/rpg/cutscene/cutscene.c deleted file mode 100644 index 0b03d15e..00000000 --- a/src/dusk/rpg/cutscene/cutscene.c +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscene.h" -#include "item/json/cutscenejsonpauseflags.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" - -errorret_t cutsceneParseItem( - yyjson_val *itemObj, - cutsceneitem_t *item, - const uint8_t index -) { - assertNotNull(itemObj, "Item object cannot be NULL"); - assertNotNull(item, "Item cannot be NULL"); - - if(!yyjson_is_obj(itemObj)) { - errorThrow("Cutscene item %u is not an object", (uint32_t)index); - } - - const char_t *typeName = yyjson_get_str(yyjson_obj_get(itemObj, "type")); - if(!cutsceneItemLookupType(typeName, &item->type)) { - errorThrow( - "Cutscene item %u: missing/unsupported item type %s", - (uint32_t)index, typeName - ); - } - - cutsceneitemloadcallback_t *load = CUTSCENE_ITEM_CALLBACKS[item->type].load; - if(load == NULL) { - errorThrow( - "Cutscene item %u: unhandled item type %u", - (uint32_t)index, (uint32_t)item->type - ); - } - - errorret_t result = load(itemObj, item); - if(errorIsNotOk(result)) errorChain(result); - - errorOk(); -} - -size_t cutsceneCountItems(yyjson_doc *doc) { - assertNotNull(doc, "Doc cannot be NULL"); - - yyjson_val *root = yyjson_doc_get_root(doc); - yyjson_val *itemsArr = yyjson_obj_get(root, "items"); - if(!yyjson_is_arr(itemsArr)) return 0; - return yyjson_arr_size(itemsArr); -} - -errorret_t cutsceneParseDoc( - yyjson_doc *doc, - cutscene_t *scene, - cutsceneitem_t *items, - size_t itemsMax -) { - assertNotNull(doc, "Doc cannot be NULL"); - assertNotNull(scene, "Scene cannot be NULL"); - assertNotNull(items, "Items cannot be NULL"); - assertTrue(itemsMax > 0, "Items max cannot be 0"); - - yyjson_val *root = yyjson_doc_get_root(doc); - yyjson_val *itemsArr = yyjson_obj_get(root, "items"); - if(!yyjson_is_arr(itemsArr)) { - errorThrow("Cutscene JSON is missing an 'items' array"); - } - - size_t itemCount = yyjson_arr_size(itemsArr); - if(itemCount > itemsMax) { - errorThrow("Cutscene has more items than its target buffer allows"); - } - - cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT; - yyjson_val *pauseArr = yyjson_obj_get(root, "pause"); - if(yyjson_is_arr(pauseArr)) { - if(!cutsceneJsonParsePauseFlags(pauseArr, &pause)) { - errorThrow("Cutscene JSON has an invalid 'pause' flag"); - } - } - - memoryZero(items, itemCount * sizeof(cutsceneitem_t)); - - yyjson_arr_iter itemIter = yyjson_arr_iter_with(itemsArr); - yyjson_val *itemVal; - uint8_t index = 0; - while((itemVal = yyjson_arr_iter_next(&itemIter)) != NULL) { - errorret_t itemResult = cutsceneParseItem(itemVal, &items[index], index); - if(errorIsNotOk(itemResult)) errorChain(itemResult); - index++; - } - - scene->itemCount = (uint8_t)itemCount; - scene->itemsMax = itemsMax; - scene->pause = pause; - - errorOk(); -} - -void cutsceneAppendAt( - cutscene_t *scene, - const uint8_t atIndex, - const cutsceneitem_t *items, - const uint8_t count -) { - assertNotNull(scene, "Scene cannot be NULL"); - assertNotNull(items, "Items cannot be NULL"); - assertTrue(count > 0, "Count cannot be 0"); - assertTrue(atIndex <= scene->itemCount, "atIndex out of bounds"); - assertTrue( - (size_t)scene->itemCount + count <= scene->itemsMax, - "Cutscene item append exceeds itemsMax" - ); - - const size_t tailCount = (size_t)scene->itemCount - atIndex; - if(tailCount > 0) { - memoryMove( - &scene->items[atIndex + count], - &scene->items[atIndex], - tailCount * sizeof(cutsceneitem_t) - ); - } - memoryCopy(&scene->items[atIndex], items, count * sizeof(cutsceneitem_t)); - scene->itemCount += count; -} - -void cutsceneAppend( - cutscene_t *scene, - const cutsceneitem_t *items, - const uint8_t count -) { - cutsceneAppendAt(scene, scene->itemCount, items, count); -} - -void cutsceneAppendNext( - cutscene_t *scene, - const uint8_t currentIndex, - const cutsceneitem_t *items, - const uint8_t count -) { - cutsceneAppendAt(scene, (uint8_t)(currentIndex + 1), items, count); -} - -errorret_t cutsceneLoadParse( - const char_t *path, - cutscene_t *scene, - cutsceneitem_t *items, - const size_t itemsMax -) { - assertNotNull(path, "Path cannot be NULL"); - assertNotNull(scene, "Scene cannot be NULL"); - assertNotNull(items, "Items cannot be NULL"); - - // Lock the asset, require it loaded. - assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_JSON, NULL); - errorret_t result = assetRequireLoaded(entry); - if(errorIsNotOk(result)) { - assetUnlockEntry(entry); - errorChain(result); - } - - // Parse the loaded JSON - errorret_t parseResult = cutsceneParseDoc( - entry->data.json, scene, items, itemsMax - ); - - // Free up the asset entry. - assetUnlockEntry(entry); - errorChain(parseResult); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h deleted file mode 100644 index 07939299..00000000 --- a/src/dusk/rpg/cutscene/cutscene.h +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenepause.h" - -typedef struct cutscene_s { - cutsceneitem_t *items; - uint8_t itemCount; - // Capacity of items - see cutsceneAppend/cutsceneAppendAt/ - // cutsceneAppendNext, which assert against this rather than taking a - // capacity parameter of their own. Set once, either by cutsceneParseDoc - // (to whatever itemsMax it was given) or by whoever declares a - // C-authored cutscene_t directly (to items' true array capacity, which - // may be larger than itemCount to leave room for a later append/insert). - size_t itemsMax; - cutscenepause_t pause; -} cutscene_t; - -/** - * Parses one cutscene item object into item. Every field/enum name is - * validated as real (untrusted, authored) file content - failures are - * real errors, not asserts. Dispatches to the matching type's Load - * callback (see CUTSCENE_ITEM_CALLBACKS in item/cutsceneitem.h) - the - * per-type parsing logic itself lives there, colocated with that type's - * runtime behavior. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @param index Index of this item within the cutscene, for error messages. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneParseItem( - yyjson_val *itemObj, - cutsceneitem_t *item, - const uint8_t index -); - -/** - * Counts how many entries are in doc's top-level "items" array, without - * parsing any of them - e.g. so a caller that wants to malloc its own - * items array instead of using a fixed one could size it first (we don't - * do that ourselves, but the option stays open). - * - * @param doc Already-parsed JSON document. - * @return Number of entries in "items", or 0 if it's missing/not an array. - */ -size_t cutsceneCountItems(yyjson_doc *doc); - -/** - * Parses doc's "items" (and optional "pause") into items, and updates - * scene's itemCount/itemsMax/pause - scene->items is left untouched, since - * the caller already points it at items. Purely a parser: doesn't touch - * the asset system, doesn't lock/load anything, and doesn't take - * ownership of doc - the caller is responsible for both loading doc - * beforehand and for its lifetime afterward (though as of cutscene items - * owning their own string fields by value, nothing parsed here actually - * needs doc to survive past this call returning). - * - * @param doc Already-parsed JSON document to read from. - * @param scene Destination scene - itemCount/itemsMax/pause are - * overwritten. - * @param items Destination item array, capacity itemsMax. - * @param itemsMax Capacity of items. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneParseDoc( - yyjson_doc *doc, - cutscene_t *scene, - cutsceneitem_t *items, - size_t itemsMax -); - -/** - * Inserts items into scene->items starting at atIndex, growing - * scene->itemCount. Every existing item from atIndex onward shifts right - * by count to make room. Asserts if this would exceed scene->itemsMax. - * - * @param scene Cutscene to insert into. - * @param atIndex Index the first inserted item should end up at - must be - * <= scene->itemCount. - * @param items Items to insert, copied in order. - * @param count Number of entries in items. - */ -void cutsceneAppendAt( - cutscene_t *scene, - const uint8_t atIndex, - const cutsceneitem_t *items, - const uint8_t count -); - -/** - * Same as cutsceneAppendAt, but always inserts at the end of - * scene->items. - * - * @param scene Cutscene to append to. - * @param items Items to append, copied in order. - * @param count Number of entries in items. - */ -void cutsceneAppend( - cutscene_t *scene, - const cutsceneitem_t *items, - const uint8_t count -); - -/** - * Same as cutsceneAppendAt, but inserts right after currentIndex (i.e. at - * currentIndex + 1) instead of at a literal index - e.g. pass - * CUTSCENE_SYSTEM.currentItem to make these items run immediately after - * the currently-executing one. currentIndex may be 0xFF (nothing has run - * yet), which wraps to insert at index 0, matching that sentinel's usual - * meaning elsewhere in the cutscene system. - * - * @param scene Cutscene to insert into. - * @param currentIndex Index to insert after. - * @param items Items to insert, copied in order. - * @param count Number of entries in items. - */ -void cutsceneAppendNext( - cutscene_t *scene, - const uint8_t currentIndex, - const cutsceneitem_t *items, - const uint8_t count -); - -/** - * Loads a cutscene JSON asset by path and parses it into scene/items (see - * cutsceneParseDoc) - the common "lock the asset, require it loaded, - * parse its 'items' array, unlock it again" sequence shared by every - * place that resolves a cutscene by file path (cutsceneSystemLoad, - * cutsceneCutsceneResolve, cutsceneInsertResolveAndSplice, - * battleStateExecutingInit, cutsceneSystemGoTo's reload). Purely propagates the - * error on failure (like cutsceneParseDoc) - this is core cutscene logic, - * not UI, so it doesn't reach for uiFatalErrorOpen itself; each caller - * decides how to surface the failure (typically errorCatch(errorPrint()) - * + uiFatalErrorOpen(NULL), same as any other asset load failure). - * - * @param path Asset path of the cutscene JSON file to load. - * @param scene Destination scene - itemCount/itemsMax/pause overwritten. - * @param items Destination item array, capacity itemsMax. - * @param itemsMax Capacity of items. - * @return Error code indicating success or failure of the load/parse. - */ -errorret_t cutsceneLoadParse( - const char_t *path, - cutscene_t *scene, - cutsceneitem_t *items, - const size_t itemsMax -); diff --git a/src/dusk/rpg/cutscene/cutscenepause.h b/src/dusk/rpg/cutscene/cutscenepause.h deleted file mode 100644 index e9aad3fd..00000000 --- a/src/dusk/rpg/cutscene/cutscenepause.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef uint8_t cutscenepause_t; - -#define CUTSCENE_PAUSE_NONE ((cutscenepause_t)0) -#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0)) -#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1)) -#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2)) -#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3)) - -#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \ - CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \ -)) - -#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \ - CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \ - CUTSCENE_PAUSE_BATTLE \ -)) diff --git a/src/dusk/rpg/cutscene/cutsceneref.c b/src/dusk/rpg/cutscene/cutsceneref.c deleted file mode 100644 index 095965e9..00000000 --- a/src/dusk/rpg/cutscene/cutsceneref.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutsceneref.h" -#include "assert/assert.h" - -void cutsceneRefReset( - cutsceneref_t *refs, - entity_t *interact, - entity_t *interacted -) { - assertNotNull(refs, "Refs cannot be null"); - - refs->entityInteract = interact; - refs->entityInteracted = interacted; - refs->entityLastCreated = NULL; - refs->entityLastRef = NULL; - refs->areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED; - refs->textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED; -} diff --git a/src/dusk/rpg/cutscene/cutsceneref.h b/src/dusk/rpg/cutscene/cutsceneref.h deleted file mode 100644 index 7197460c..00000000 --- a/src/dusk/rpg/cutscene/cutsceneref.h +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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 entity_s entity_t; - -#define CUTSCENE_REF_AREA_LAST_CREATED ((uint8_t)0xFF) -#define CUTSCENE_REF_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA) - -typedef struct { - entity_t *entityInteract; - entity_t *entityInteracted; - entity_t *entityLastCreated; - entity_t *entityLastRef; - uint8_t areaLastCreated; - uint8_t textMiniLastCreated; -} cutsceneref_t; - -/** - * Resets refs to an empty state. - * - * @param refs Refs to reset. - * @param interact The entity that initiated the interaction, or NULL. - * @param interacted The entity being interacted with, or NULL. - */ -void cutsceneRefReset( - cutsceneref_t *refs, - entity_t *interact, - entity_t *interacted -); diff --git a/src/dusk/rpg/cutscene/cutscenesystem.c b/src/dusk/rpg/cutscene/cutscenesystem.c deleted file mode 100644 index a68c8cdf..00000000 --- a/src/dusk/rpg/cutscene/cutscenesystem.c +++ /dev/null @@ -1,358 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assert.h" -#include "ui/overlay/uifatalerror.h" -#include "save/slot/saveslot.h" - -cutscenesystem_t CUTSCENE_SYSTEM; - -void cutsceneSystemInit() { - memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t)); - CUTSCENE_SYSTEM.loadedScene.items = CUTSCENE_SYSTEM.loadedItems; - CUTSCENE_SYSTEM.loadedScene.itemsMax = CUTSCENE_LOADED_ITEMS_MAX; -} - -void cutsceneSystemUpdate() { - if(CUTSCENE_SYSTEM.scene == NULL) return; - - const cutsceneitem_t *item = cutsceneSystemGetCurrentItem(); - if(cutsceneItemUpdate(item, &CUTSCENE_SYSTEM.data)) cutsceneSystemNext(); -} - -void cutsceneSystemDispose() { - CUTSCENE_SYSTEM.scene = NULL; - CUTSCENE_SYSTEM.currentItem = CUTSCENE_CURRENT_ITEM_NONE; - CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; - cutsceneRefReset(&CUTSCENE_SYSTEM.refs, NULL, NULL); - CUTSCENE_SYSTEM.textCache[0] = '\0'; - CUTSCENE_SYSTEM.onComplete = NULL; - CUTSCENE_SYSTEM.loadedFile[0] = '\0'; -} - -void cutsceneSystemPrepare( - cutscene_t *cutscene, - entity_t *interact, - entity_t *interacted -) { - assertNotNull(cutscene, "Cutscene cannot be NULL"); - - CUTSCENE_SYSTEM.scene = cutscene; - CUTSCENE_SYSTEM.pause = cutscene->pause; - cutsceneRefReset(&CUTSCENE_SYSTEM.refs, interact, interacted); - CUTSCENE_SYSTEM.textCache[0] = '\0'; - CUTSCENE_SYSTEM.onComplete = NULL; - - // cutsceneNext will cause overflow to index 0 (intentional). - CUTSCENE_SYSTEM.currentItem = CUTSCENE_CURRENT_ITEM_NONE; - - // Clear loaded file if cutscene not running from file. - if(cutscene != &CUTSCENE_SYSTEM.loadedScene) { - CUTSCENE_SYSTEM.loadedFile[0] = '\0'; - } -} - -void cutsceneSystemStartCutscene(cutscene_t *cutscene) { - cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL); -} - -void cutsceneSystemStartCutsceneWith( - cutscene_t *cutscene, - entity_t *interact, - entity_t *interacted -) { - assertNotNull(cutscene, "Cutscene cannot be NULL"); - assertTrue( - cutscene->itemCount <= cutscene->itemsMax, - "Cutscene itemCount exceeds itemsMax" - ); - - // Prep cutscene, save it to the save slot, and advance, aka overflow to 0. - cutsceneSystemPrepare(cutscene, interact, interacted); - saveSlotSetCurrentCutscene(CUTSCENE_SYSTEM.loadedFile); - cutsceneSystemNext(); -} - -void cutsceneSystemStartCutsceneAndGoToMarker( - cutscene_t *cutscene, - const char_t *marker -) { - assertNotNull(marker, "Marker cannot be NULL"); - - // Restarts the cutscene - cutsceneSystemPrepare(cutscene, NULL, NULL); - cutsceneSystemGoTo(marker); -} - -void cutsceneSystemInsertItemsNext( - const cutsceneitem_t *items, - const size_t itemCount -) { - assertNotNull(items, "Items cannot be NULL"); - assertNotNull( - CUTSCENE_SYSTEM.scene, - "cutsceneSystemInsertItemsNext called with no cutscene running" - ); - cutsceneAppendNext( - CUTSCENE_SYSTEM.scene, CUTSCENE_SYSTEM.currentItem, items, itemCount - ); -} - -void cutsceneSystemInsertItems( - const cutsceneitem_t *items, - const size_t itemCount -) { - cutsceneSystemInsertItemsNext(items, itemCount); - cutsceneSystemNext(); -} - -void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) { - assertNotNull(cutscene, "Cutscene cannot be NULL"); - cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount); -} - -void cutsceneSystemClearAfter(const uint8_t index) { - assertNotNull( - CUTSCENE_SYSTEM.scene, - "cutsceneSystemClearAfter called with no cutscene running" - ); - assertTrue( - index < CUTSCENE_SYSTEM.scene->itemCount, - "cutsceneSystemClearAfter index out of range" - ); - - CUTSCENE_SYSTEM.scene->itemCount = index + 1; -} - -void cutsceneSystemLoad(const char_t *file) { - assertNotNull(file, "File cannot be NULL"); - - // Take a copy of file, as it may be a part of the cutscene itself. - char_t fileCopy[ASSET_FILE_NAME_MAX]; - stringCopy(fileCopy, file, ASSET_FILE_NAME_MAX - 1); - - // Load the cutscene asset, parse it, and start it. - errorret_t result = cutsceneLoadParse( - fileCopy, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems, - CUTSCENE_LOADED_ITEMS_MAX - ); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - uiFatalErrorOpen(NULL); - return; - } - - // Store the loaded file so we can restart if needed. - stringCopy(CUTSCENE_SYSTEM.loadedFile, fileCopy, ASSET_FILE_NAME_MAX - 1); - cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene); -} - -void cutsceneRestart(void) { - assertNotNull( - CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running" - ); - assertTrue( - CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene, - "cutsceneRestart called with a cutscene not loaded from file" - ); - assertTrue( - CUTSCENE_SYSTEM.loadedFile[0] != '\0', - "cutsceneRestart called with no loaded file" - ); - - // Copy the refs and callbacks - cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete; - cutsceneref_t refs = CUTSCENE_SYSTEM.refs; - - // Reload the file. - char_t file[ASSET_FILE_NAME_MAX]; - stringCopy(file, CUTSCENE_SYSTEM.loadedFile, ASSET_FILE_NAME_MAX - 1); - cutsceneSystemLoad(file); - - // Restore the refs and callbacks. - CUTSCENE_SYSTEM.onComplete = onComplete; - CUTSCENE_SYSTEM.refs = refs; -} - -void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) { - assertNotNull( - CUTSCENE_SYSTEM.scene, - "cutsceneSystemSetOnComplete called with no cutscene running" - ); - CUTSCENE_SYSTEM.onComplete = onComplete; -} - -entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) { - entity_t *entity; - - if(entityIndex == CUTSCENE_ENTITY_INTERACT) { - assertNotNull( - CUTSCENE_SYSTEM.refs.entityInteract, - "CUTSCENE_ENTITY_INTERACT used but no interact entity is set" - ); - entity = CUTSCENE_SYSTEM.refs.entityInteract; - } else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) { - assertNotNull( - CUTSCENE_SYSTEM.refs.entityInteracted, - "CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set" - ); - entity = CUTSCENE_SYSTEM.refs.entityInteracted; - } else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) { - assertNotNull( - CUTSCENE_SYSTEM.refs.entityLastCreated, - "CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created" - ); - entity = CUTSCENE_SYSTEM.refs.entityLastCreated; - } else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) { - assertNotNull( - CUTSCENE_SYSTEM.refs.entityLastRef, - "CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced" - ); - entity = CUTSCENE_SYSTEM.refs.entityLastRef; - } else { - assertTrue( - entityIndex < ENTITY_COUNT, - "Entity index is out of range" - ); - entity = &ENTITIES[entityIndex]; - } - - CUTSCENE_SYSTEM.refs.entityLastRef = entity; - return entity; -} - -uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) { - if(areaId == CUTSCENE_REF_AREA_LAST_CREATED) { - assertTrue( - CUTSCENE_SYSTEM.refs.areaLastCreated != CUTSCENE_REF_AREA_LAST_CREATED, - "CUTSCENE_REF_AREA_LAST_CREATED used but no map area has been created" - ); - return CUTSCENE_SYSTEM.refs.areaLastCreated; - } - return areaId; -} - -uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) { - if(index == CUTSCENE_REF_TEXT_MINI_LAST_CREATED) { - assertTrue( - CUTSCENE_SYSTEM.refs.textMiniLastCreated != - CUTSCENE_REF_TEXT_MINI_LAST_CREATED, - "CUTSCENE_REF_TEXT_MINI_LAST_CREATED used but no mini textbox has been " - "shown" - ); - return CUTSCENE_SYSTEM.refs.textMiniLastCreated; - } - return index; -} - -const char_t * cutsceneSystemGetTextCache(void) { - return CUTSCENE_SYSTEM.textCache; -} - -void cutsceneSystemSetTextCache(const char_t *text) { - assertNotNull(text, "Text cannot be NULL"); - - stringCopy( - CUTSCENE_SYSTEM.textCache, text, CUTSCENE_TEXT_CACHE_MAX - 1 - ); -} - -void cutsceneSystemNext() { - if(CUTSCENE_SYSTEM.scene == NULL) return; - - // Pop the just-finished item off the front. - if(CUTSCENE_SYSTEM.currentItem == CUTSCENE_CURRENT_ITEM_NONE) { - CUTSCENE_SYSTEM.currentItem = 0; - } else { - cutscene_t *scene = CUTSCENE_SYSTEM.scene; - const size_t tailCount = (size_t)scene->itemCount - 1; - if(tailCount > 0) { - memoryMove( - &scene->items[0], &scene->items[1], tailCount * sizeof(cutsceneitem_t) - ); - } - scene->itemCount--; - } - - // End of the cutscene? - if(CUTSCENE_SYSTEM.scene->itemCount == 0) { - cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete; - - CUTSCENE_SYSTEM.scene = NULL; - CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; - - if(onComplete != NULL) onComplete(NULL); - return; - } - - // Start next (now current) item - const cutsceneitem_t *item = cutsceneSystemGetCurrentItem(); - memset(&CUTSCENE_SYSTEM.data, 0, sizeof(CUTSCENE_SYSTEM.data)); - cutsceneItemStart(item, &CUTSCENE_SYSTEM.data); -} - -void cutsceneSystemGoTo(const char_t *name) { - assertNotNull(name, "Name cannot be NULL"); - assertNotNull( - CUTSCENE_SYSTEM.scene, "cutsceneSystemGoTo called with no cutscene running" - ); - assertTrue( - CUTSCENE_SYSTEM.scene == &CUTSCENE_SYSTEM.loadedScene, - "cutsceneSystemGoTo requires the running cutscene file reference (1)" - ); - assertTrue( - CUTSCENE_SYSTEM.loadedFile[0] != '\0', - "cutsceneSystemGoTo requires the running cutscene file reference (2)" - ); - - cutscene_t freshScene = { .items = CUTSCENE_SYSTEM.gotoScratchItems }; - errorret_t result = cutsceneLoadParse( - CUTSCENE_SYSTEM.loadedFile, &freshScene, CUTSCENE_SYSTEM.gotoScratchItems, - CUTSCENE_LOADED_ITEMS_MAX - ); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - uiFatalErrorOpen(NULL); - return; - } - - // Search for the marker with the given name... - for(uint8_t i = 0; i < freshScene.itemCount; i++) { - const cutsceneitem_t *item = &freshScene.items[i]; - if( - item->type != CUTSCENE_ITEM_TYPE_MARKER || - !stringEquals(item->marker.name, name) - ) continue; - - // Found, so splice the cutscene at that point and start it, the remaining - // items being the rest of the cutscene after the marker. - const uint8_t remaining = freshScene.itemCount - i; - memoryCopy( - CUTSCENE_SYSTEM.loadedItems, &freshScene.items[i], - remaining * sizeof(cutsceneitem_t) - ); - CUTSCENE_SYSTEM.loadedScene.itemCount = remaining; - CUTSCENE_SYSTEM.currentItem = 0; - - memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data)); - cutsceneItemStart( - &CUTSCENE_SYSTEM.loadedItems[0], &CUTSCENE_SYSTEM.data - ); - return; - } - - assertTrue(false, "cutsceneSystemGoTo: no marker found with that name"); -} - -const cutsceneitem_t * cutsceneSystemGetCurrentItem() { - if(CUTSCENE_SYSTEM.scene == NULL) return NULL; - return &CUTSCENE_SYSTEM.scene->items[0]; -} diff --git a/src/dusk/rpg/cutscene/cutscenesystem.h b/src/dusk/rpg/cutscene/cutscenesystem.h deleted file mode 100644 index 3921d549..00000000 --- a/src/dusk/rpg/cutscene/cutscenesystem.h +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "cutscene.h" -#include "cutsceneref.h" - -#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE) -#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD) -#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC) -#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB) -#define CUTSCENE_CURRENT_ITEM_NONE ((uint8_t)0xFF) - -#define CUTSCENE_TEXT_CACHE_MAX 64 -#define CUTSCENE_LOADED_ITEMS_MAX 128 -#define CUTSCENE_INSERT_ITEMS_MAX 16 - -typedef struct { - cutscene_t *scene; - uint8_t currentItem; - cutscenepause_t pause; - - cutsceneref_t refs; - - // Free-form text cache for the running cutscene - see - // cutsceneSystemGetTextCache/cutsceneSystemSetTextCache. - char_t textCache[CUTSCENE_TEXT_CACHE_MAX]; - - // Runtime data for the current item. - cutsceneitemdata_t data; - - // See cutsceneSystemSetOnComplete. - cutscenecallback_t onComplete; - - // Cache and storage used for the dynamically loaded cutscenes. - cutsceneitem_t loadedItems[CUTSCENE_LOADED_ITEMS_MAX]; - cutscene_t loadedScene; - char_t loadedFile[ASSET_FILE_NAME_MAX]; - - // Cutscene items scratch, mostly used for CUTSCENE_INSERT items. - cutsceneitem_t gotoScratchItems[CUTSCENE_LOADED_ITEMS_MAX]; -} cutscenesystem_t; - -extern cutscenesystem_t CUTSCENE_SYSTEM; - -/** - * Initialize the cutscene system. - */ -void cutsceneSystemInit(); - -/** - * Disposes of the cutscene system, stopping any active cutscene. - */ -void cutsceneSystemDispose(); - -/** - * Resets CUTSCENE_SYSTEM to run cutscene from its first item, binding - * interact/interacted entities. Does not itself advance to the first item. - * - * @param cutscene Pointer to the cutscene to prepare. - * @param interact The entity that initiated the interaction (player), or - * NULL. - * @param interacted The entity being interacted with (NPC), or NULL. - */ -void cutsceneSystemPrepare( - cutscene_t *cutscene, - entity_t *interact, - entity_t *interacted -); - -/** - * Start a cutscene with no bound entities. - * - * @param cutscene Pointer to the cutscene to start. - */ -void cutsceneSystemStartCutscene(cutscene_t *cutscene); - -/** - * Start a cutscene with the two entities that triggered it. - * - * @param cutscene Pointer to the cutscene to start. - * @param interact The entity that initiated the interaction (player). - * @param interacted The entity that was interacted with (NPC). - */ -void cutsceneSystemStartCutsceneWith( - cutscene_t *cutscene, - entity_t *interact, - entity_t *interacted -); - -/** - * Starts a cutscene with no bound entities, jumping straight to the - * marker with the given name instead of running from the first item. - * Asserts if no marker with that name exists. - * - * @param cutscene Pointer to the cutscene to start. - * @param marker Marker name to jump to. - */ -void cutsceneSystemStartCutsceneAndGoToMarker( - cutscene_t *cutscene, - const char_t *marker -); - -/** - * Splices items into the running cutscene so that items[0] becomes the - * very next item, without otherwise disturbing the currently-running - * item's progress. - * - * @param items Items to splice in. - * @param itemCount Number of items in items. - */ -void cutsceneSystemInsertItemsNext( - const cutsceneitem_t *items, - const size_t itemCount -); - -/** - * Splices items into the running cutscene right after the current item - * (see cutsceneSystemInsertItemsNext), then immediately advances into the - * first spliced item. - * - * @param items Items to splice in. - * @param itemCount Number of items in items. - */ -void cutsceneSystemInsertItems( - const cutsceneitem_t *items, - const size_t itemCount -); - -/** - * Splices cutscene's items into the running cutscene. - * - * @param cutscene The cutscene whose items to splice in. - */ -void cutsceneSystemInsertCutscene(const cutscene_t *cutscene); - -/** - * Discards every item queued after index (e.g. index 0 keeps only the - * currently-running item and drops everything spliced in behind it), - * without disturbing index's own progress - e.g. so a callback that - * decides the rest of a batch-spliced sequence is now moot (a battle - * ending mid-round, with other fighters' still-queued move cutscenes and - * the round's trailing state transition left behind) can drop it before - * splicing in its own follow-up items, instead of leaving it to run - * afterward against torn-down state. - * - * @param index Index to keep - everything after it is discarded. Must be - * < the running cutscene's current item count. - */ -void cutsceneSystemClearAfter(const uint8_t index); - -/** - * Loads and immediately starts a cutscene asset by file name. - * - * @param file Cutscene file name. - */ -void cutsceneSystemLoad(const char_t *file); - -/** - * Restarts the currently running cutscene. Will perform a load if the cutscene - * was initially from a file. - */ -void cutsceneRestart(void); - -/** - * Sets a native callback to fire once when the currently running cutscene - * finishes. - * - * @param onComplete Callback to fire on completion. - */ -void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete); - -/** - * Resolves a raw entity index (or sentinel) to an entity pointer. - * - * @param entityIndex Raw entity index or sentinel value. - * @returns Pointer to the resolved entity. - */ -entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex); - -/** - * Resolves a raw map area ID (or CUTSCENE_REF_AREA_LAST_CREATED sentinel) to - * a concrete map area ID. - * - * @param areaId Raw map area ID or sentinel value. - * @returns The resolved map area ID. - */ -uint8_t cutsceneSystemGetAreaId(const uint8_t areaId); - -/** - * Resolves a raw mini textbox slot index (or CUTSCENE_REF_TEXT_MINI_LAST_CREATED - * sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index. - * - * @param index Raw slot index or sentinel value. - * @returns The resolved slot index. - */ -uint8_t cutsceneSystemGetTextMiniId(const uint8_t index); - -/** - * Returns the running cutscene's cached text - see - * cutsceneSystemSetTextCache. Empty ("") if nothing has been cached yet. - * - * @returns The cached text. - */ -const char_t * cutsceneSystemGetTextCache(void); - -/** - * Overwrites the running cutscene's cached text. - * - * @param text The text to cache; copied internally. - */ -void cutsceneSystemSetTextCache(const char_t *text); - -/** - * Advance to the next item in the cutscene. - */ -void cutsceneSystemNext(); - -/** - * Jumps to a cutscene marker directly. If the cutscene is a loaded cutscene, - * this will result in it being reloaded (due to unshift nature of cutscenes). - * - * @param name Marker name to search for. - */ -void cutsceneSystemGoTo(const char_t *name); - -/** - * Update the cutscene system for one frame. - */ -void cutsceneSystemUpdate(); - -/** - * Get the current cutscene item. - * - * @return Pointer to the current cutscene item. - */ -const cutsceneitem_t * cutsceneSystemGetCurrentItem(); diff --git a/src/dusk/rpg/cutscene/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/CMakeLists.txt deleted file mode 100755 index bffc8bf0..00000000 --- a/src/dusk/rpg/cutscene/item/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutsceneitem.c - cutsceneitembase.c - cutscenecallback.c - cutsceneprint.c -) - -add_subdirectory(control) -add_subdirectory(entity) -add_subdirectory(item) -add_subdirectory(maparea) -add_subdirectory(ui) -add_subdirectory(battle) -add_subdirectory(save) -add_subdirectory(audio) -add_subdirectory(json) -add_subdirectory(story) -add_subdirectory(party) diff --git a/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt b/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt deleted file mode 100644 index 2fdb6a5b..00000000 --- a/src/dusk/rpg/cutscene/item/audio/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutsceneaudioplay.c - cutsceneaudiostop.c - cutsceneaudiopause.c - cutsceneaudioresume.c - cutsceneaudiofade.c - cutsceneaudiofadewait.c - cutsceneaudiosetpan.c - cutsceneaudiosetloop.c - cutsceneaudioset.c -) diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c deleted file mode 100644 index fbacb232..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.c +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" -#include "rpg/cutscene/item/json/cutscenejsoneasing.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneAudioFadeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerFadeTo( - item->audioFade.channel, - item->audioFade.from, - item->audioFade.to, - item->audioFade.duration, - item->audioFade.easing - ); -} - -bool_t cutsceneAudioFadeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioFadeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - audiomixerchannel_t channelValue; - if(!cutsceneJsonLookupAudioChannel( - yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue - )) { - errorThrow("Cutscene item (AUDIO_FADE): invalid 'channel'"); - } - item->audioFade.channel = channelValue; - item->audioFade.from = cutsceneJsonGetFloat(itemObj, "from", 0.0f); - item->audioFade.to = cutsceneJsonGetFloat(itemObj, "to", 0.0f); - item->audioFade.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f); - - const char_t *easingName = - yyjson_get_str(yyjson_obj_get(itemObj, "easing")); - easingtype_t easingValue = EASING_LINEAR; - if(easingName != NULL && - !cutsceneJsonLookupEasing(easingName, &easingValue)) { - errorThrow("Cutscene item (AUDIO_FADE): invalid 'easing'"); - } - item->audioFade.easing = easingValue; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h deleted file mode 100644 index d6845c85..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofade.h +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "animation/easing.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - audiomixerchannel_t channel; - float_t from; - float_t to; - float_t duration; - easingtype_t easing; -} cutsceneaudiofade_t; - -/** - * Starts an audio fade step (starts the fade via audioMixerFadeTo() - - * doesn't wait for it; pair with CUTSCENE_AUDIO_FADE_WAIT to block on it). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioFadeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio fade step (always completes immediately - the fade - * itself keeps running independently of cutscene progression). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioFadeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_FADE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioFadeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c deleted file mode 100644 index fd8a0912..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" - -bool_t cutsceneAudioFadeWaitUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return !audioMixerIsFading(item->audioFadeWaitChannel); -} - -errorret_t cutsceneAudioFadeWaitLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioFadeWaitChannel); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h deleted file mode 100644 index 68a3ada7..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiofadewait.h +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Updates an audio fade-wait step, completing once the watched channel's - * fade (started by a CUTSCENE_AUDIO_FADE elsewhere) finishes. Has no Start - * callback -- there's nothing to do until the fade is actually done. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the channel is no longer fading. - */ -bool_t cutsceneAudioFadeWaitUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_FADE_WAIT cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioFadeWaitLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c deleted file mode 100644 index 7b098c3c..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" - -void cutsceneAudioPauseStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerPause(item->audioPauseChannel); -} - -bool_t cutsceneAudioPauseUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioPauseLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioPauseChannel); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h deleted file mode 100644 index 787f1498..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiopause.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts an audio pause step (pauses the channel immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioPauseStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio pause step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioPauseUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_PAUSE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioPauseLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c deleted file mode 100644 index e742a20b..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.c +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneAudioPlayStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerQueuePlay( - item->audioPlay.file, - item->audioPlay.channel, - item->audioPlay.volume, - item->audioPlay.pan, - item->audioPlay.looping, - item->audioPlay.loopCount, - item->audioPlay.loopStart, - item->audioPlay.loopTo - ); -} - -bool_t cutsceneAudioPlayUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioPlayLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonCopyString( - itemObj, "file", NULL, item->audioPlay.file, AUDIO_PATH_MAX - )) { - errorThrow("Cutscene item (AUDIO_PLAY): missing/too-long 'file'"); - } - audiomixerchannel_t channelValue; - if(!cutsceneJsonLookupAudioChannel( - yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue - )) { - errorThrow("Cutscene item (AUDIO_PLAY): invalid 'channel'"); - } - item->audioPlay.channel = channelValue; - - item->audioPlay.volume = cutsceneJsonGetFloat(itemObj, "volume", 1.0f); - item->audioPlay.pan = cutsceneJsonGetFloat(itemObj, "pan", 0.0f); - item->audioPlay.looping = cutsceneJsonGetBool(itemObj, "looping", false); - item->audioPlay.loopCount = - (uint8_t)cutsceneJsonGetInt(itemObj, "loopCount", 0); - item->audioPlay.loopStart = - cutsceneJsonGetFloat(itemObj, "loopStart", -1.0f); - item->audioPlay.loopTo = cutsceneJsonGetFloat(itemObj, "loopTo", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h deleted file mode 100644 index 5348af96..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioplay.h +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - char_t file[AUDIO_PATH_MAX]; - audiomixerchannel_t channel; - float_t volume; - float_t pan; - bool_t looping; - uint8_t loopCount; - float_t loopStart; - float_t loopTo; -} cutsceneaudioplay_t; - -/** - * Starts an audio play step (queues the sound via audioMixerQueuePlay() - - * see its own comment on when it actually starts). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioPlayStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio play step (always completes immediately - the sound - * keeps loading/playing independently of cutscene progression). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioPlayUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_PLAY cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioPlayLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c deleted file mode 100644 index c6ab25dc..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" - -void cutsceneAudioResumeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerResume(item->audioResumeChannel); -} - -bool_t cutsceneAudioResumeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioResumeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioResumeChannel); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h deleted file mode 100644 index 91740eb2..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioresume.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts an audio resume step (resumes the channel immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioResumeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio resume step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioResumeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_RESUME cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioResumeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c deleted file mode 100644 index 2e6ef12a..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.c +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneAudioSetStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerSetPan(item->audioSet.channel, item->audioSet.pan); - audioMixerSetLoop( - item->audioSet.channel, - item->audioSet.looping, - item->audioSet.loopCount, - item->audioSet.loopStart, - item->audioSet.loopTo - ); -} - -bool_t cutsceneAudioSetUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioSetLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - audiomixerchannel_t channelValue; - if(!cutsceneJsonLookupAudioChannel( - yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue - )) { - errorThrow("Cutscene item (AUDIO_SET): invalid 'channel'"); - } - item->audioSet.channel = channelValue; - item->audioSet.pan = cutsceneJsonGetFloat(itemObj, "pan", 0.0f); - item->audioSet.looping = cutsceneJsonGetBool(itemObj, "looping", true); - item->audioSet.loopCount = - (uint8_t)cutsceneJsonGetInt(itemObj, "loopCount", 0); - item->audioSet.loopStart = - cutsceneJsonGetFloat(itemObj, "loopStart", -1.0f); - item->audioSet.loopTo = cutsceneJsonGetFloat(itemObj, "loopTo", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h deleted file mode 100644 index 648dfad2..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudioset.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - audiomixerchannel_t channel; - float_t pan; - bool_t looping; - uint8_t loopCount; - float_t loopStart; - float_t loopTo; -} cutsceneaudioset_t; - -/** - * Starts an audio set step - applies pan and looping behaviour together in - * one item (via audioMixerSetPan()/audioMixerSetLoop()), for when you want - * to change both at once rather than using CUTSCENE_AUDIO_SET_PAN/ - * CUTSCENE_AUDIO_SET_LOOP separately. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioSetStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio set step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioSetUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_SET cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioSetLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c deleted file mode 100644 index 101c563b..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.c +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneAudioSetLoopStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerSetLoop( - item->audioSetLoop.channel, - item->audioSetLoop.looping, - item->audioSetLoop.loopCount, - item->audioSetLoop.loopStart, - item->audioSetLoop.loopTo - ); -} - -bool_t cutsceneAudioSetLoopUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioSetLoopLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - audiomixerchannel_t channelValue; - if(!cutsceneJsonLookupAudioChannel( - yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue - )) { - errorThrow("Cutscene item (AUDIO_SET_LOOP): invalid 'channel'"); - } - item->audioSetLoop.channel = channelValue; - item->audioSetLoop.looping = cutsceneJsonGetBool(itemObj, "looping", true); - item->audioSetLoop.loopCount = - (uint8_t)cutsceneJsonGetInt(itemObj, "loopCount", 0); - item->audioSetLoop.loopStart = - cutsceneJsonGetFloat(itemObj, "loopStart", -1.0f); - item->audioSetLoop.loopTo = cutsceneJsonGetFloat(itemObj, "loopTo", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h deleted file mode 100644 index 5f964719..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetloop.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - audiomixerchannel_t channel; - bool_t looping; - uint8_t loopCount; - float_t loopStart; - float_t loopTo; -} cutsceneaudiosetloop_t; - -/** - * Starts an audio set-loop step (applies the new looping behaviour - * immediately, via audioMixerSetLoop()). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioSetLoopStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio set-loop step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioSetLoopUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_SET_LOOP cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioSetLoopLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c deleted file mode 100644 index 04cef26d..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneAudioSetPanStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerSetPan(item->audioSetPan.channel, item->audioSetPan.pan); -} - -bool_t cutsceneAudioSetPanUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioSetPanLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - audiomixerchannel_t channelValue; - if(!cutsceneJsonLookupAudioChannel( - yyjson_get_str(yyjson_obj_get(itemObj, "channel")), &channelValue - )) { - errorThrow("Cutscene item (AUDIO_SET_PAN): invalid 'channel'"); - } - item->audioSetPan.channel = channelValue; - item->audioSetPan.pan = cutsceneJsonGetFloat(itemObj, "pan", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h deleted file mode 100644 index bbc62cd7..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiosetpan.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - audiomixerchannel_t channel; - float_t pan; -} cutsceneaudiosetpan_t; - -/** - * Starts an audio set-pan step (applies the new pan immediately, via - * audioMixerSetPan()). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioSetPanStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio set-pan step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioSetPanUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_SET_PAN cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioSetPanLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c deleted file mode 100644 index 0666a89d..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "audio/mixer/audiomixer.h" -#include "rpg/cutscene/item/json/cutscenejsonaudiochannel.h" - -void cutsceneAudioStopStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - audioMixerStop(item->audioStopChannel); -} - -bool_t cutsceneAudioStopUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneAudioStopLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - return cutsceneJsonLoadAudioChannelOnly(itemObj, &item->audioStopChannel); -} diff --git a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h b/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h deleted file mode 100644 index 1423a98d..00000000 --- a/src/dusk/rpg/cutscene/item/audio/cutsceneaudiostop.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audio/mixer/audiomixerchannel.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts an audio stop step (stops the channel immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneAudioStopStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an audio stop step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneAudioStopUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an AUDIO_STOP cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneAudioStopLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt b/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt deleted file mode 100644 index 55c2fdd3..00000000 --- a/src/dusk/rpg/cutscene/item/battle/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenebattlestart.c - cutscenebattlesetstate.c - cutscenebattlewaitselection.c - cutscenebattlepostmove.c - cutscenebattlefighterdamage.c - cutscenebattlefighterheal.c -) diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c deleted file mode 100644 index 36a9bdbf..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.c +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/battle.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" -#include "assert/assert.h" -#include "util/string.h" - -#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16 - -void cutsceneBattleFighterDamageStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - const cutscenebattlefighterdamage_t *damage = &item->battleFighterDamage; - assertTrue( - damage->fighterIndex < BATTLE_FIGHTER_POS_COUNT, - "fighterIndex sentinel was never resolved" - ); - battlefighter_t *fighter = BATTLE.fighters[damage->fighterIndex]; - assertNotNull(fighter, "Fighter cannot be NULL"); - - battleFighterDamage(fighter, damage->amount); -} - -bool_t cutsceneBattleFighterDamageUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneBattleFighterDamageLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX]; - if(!cutsceneJsonCopyString( - itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX - )) { - errorThrow("Cutscene item (BATTLE_FIGHTER_DAMAGE): missing 'fighter'"); - } - if(stringEquals(fighterRef, "target")) { - item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_TARGET; - } else if(stringEquals(fighterRef, "user")) { - item->battleFighterDamage.fighterIndex = BATTLE_FIGHTER_POS_USER; - } else { - errorThrow( - "Cutscene item (BATTLE_FIGHTER_DAMAGE): 'fighter' must be " - "'target' or 'user'" - ); - } - - const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1); - if(amount < 0 || amount > UINT16_MAX) { - errorThrow( - "Cutscene item (BATTLE_FIGHTER_DAMAGE): invalid/missing 'amount'" - ); - } - item->battleFighterDamage.amount = (uint16_t)amount; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h deleted file mode 100644 index 7d592732..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterdamage.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/battle/fighter/battlefighterpos.h" - -typedef struct { - // Index into BATTLE.fighters of the fighter to damage, or one of - // BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against - // the currently-executing action by battleStateExecutingInit before - // this item ever runs. - battlefighterpos_t fighterIndex; - uint16_t amount; -} cutscenebattlefighterdamage_t; - -/** - * Starts a BATTLE_FIGHTER_DAMAGE item, applying the configured damage to - * BATTLE.fighters[fighterIndex] immediately. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneBattleFighterDamageStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a BATTLE_FIGHTER_DAMAGE item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneBattleFighterDamageUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a BATTLE_FIGHTER_DAMAGE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneBattleFighterDamageLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c deleted file mode 100644 index ee288541..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.c +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/battle.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" -#include "assert/assert.h" -#include "util/string.h" - -#define CUTSCENE_BATTLE_FIGHTER_REF_MAX 16 - -void cutsceneBattleFighterHealStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - const cutscenebattlefighterheal_t *heal = &item->battleFighterHeal; - assertTrue( - heal->fighterIndex < BATTLE_FIGHTER_POS_COUNT, - "fighterIndex sentinel was never resolved" - ); - battlefighter_t *fighter = BATTLE.fighters[heal->fighterIndex]; - assertNotNull(fighter, "Fighter cannot be NULL"); - - battleFighterHeal(fighter, heal->amount); -} - -bool_t cutsceneBattleFighterHealUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneBattleFighterHealLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - char_t fighterRef[CUTSCENE_BATTLE_FIGHTER_REF_MAX]; - if(!cutsceneJsonCopyString( - itemObj, "fighter", NULL, fighterRef, CUTSCENE_BATTLE_FIGHTER_REF_MAX - )) { - errorThrow("Cutscene item (BATTLE_FIGHTER_HEAL): missing 'fighter'"); - } - if(stringEquals(fighterRef, "target")) { - item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_TARGET; - } else if(stringEquals(fighterRef, "user")) { - item->battleFighterHeal.fighterIndex = BATTLE_FIGHTER_POS_USER; - } else { - errorThrow( - "Cutscene item (BATTLE_FIGHTER_HEAL): 'fighter' must be " - "'target' or 'user'" - ); - } - - const int32_t amount = cutsceneJsonGetInt(itemObj, "amount", -1); - if(amount < 0 || amount > UINT16_MAX) { - errorThrow( - "Cutscene item (BATTLE_FIGHTER_HEAL): invalid/missing 'amount'" - ); - } - item->battleFighterHeal.amount = (uint16_t)amount; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h deleted file mode 100644 index ae414b9d..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlefighterheal.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/battle/fighter/battlefighterpos.h" - -typedef struct { - // Index into BATTLE.fighters of the fighter to heal, or one of - // BATTLE_FIGHTER_POS_TARGET/BATTLE_FIGHTER_POS_USER, resolved against - // the currently-executing action by battleStateExecutingInit before - // this item ever runs. - battlefighterpos_t fighterIndex; - uint16_t amount; -} cutscenebattlefighterheal_t; - -/** - * Starts a BATTLE_FIGHTER_HEAL item, applying the configured heal to - * BATTLE.fighters[fighterIndex] immediately. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneBattleFighterHealStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a BATTLE_FIGHTER_HEAL item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneBattleFighterHealUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a BATTLE_FIGHTER_HEAL cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneBattleFighterHealLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c deleted file mode 100644 index ba7e1b26..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.c +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/battle.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "console/console.h" - -void cutsceneBattlePostMoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - consolePrint("BATTLE_POST_MOVE\n"); - - // Once a result has already been decided (e.g. the player fled), there - // is nothing left to check. - if(BATTLE.result != BATTLE_RESULT_NONE) return; - - bool_t allyAlive = false; - bool_t enemyAlive = false; - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - battlefighter_t *fighter = BATTLE.fighters[i]; - if(fighter == NULL) continue; - if(!battleFighterIsAlive(fighter)) continue; - - if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) allyAlive = true; - else enemyAlive = true; - } - - if(allyAlive && enemyAlive) return; - - BATTLE.result = allyAlive ? BATTLE_RESULT_WIN : BATTLE_RESULT_LOSS; - - // Drop the rest of this round's already-spliced items (other fighters' - // still-pending move cutscenes, the trailing BATTLE_SET_STATE(PRE_ROUND) - - // see battleStateExecutingInit) - the battle is over, so none of that - // should run against the WON/LOST/ENDING transition about to replace it. - cutsceneSystemClearAfter(CUTSCENE_SYSTEM.currentItem); - - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { - .state = allyAlive ? BATTLE_STATE_WON : BATTLE_STATE_LOST - } - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); -} - -bool_t cutsceneBattlePostMoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h deleted file mode 100644 index 58b62943..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlepostmove.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts a BATTLE_POST_MOVE item. Runs after each queued move's cutscene - * finishes: checks whether every ally or every enemy fighter has been - * knocked out and, if so, sets BATTLE.result and splices in a - * BATTLE_SET_STATE to BATTLE_STATE_WON/LOST. No-ops the health check if - * BATTLE.result is already set (e.g. the player fled). Not - * JSON-authorable -- constructed dynamically by battleStateExecutingInit. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneBattlePostMoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a BATTLE_POST_MOVE item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneBattlePostMoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c deleted file mode 100644 index 5b715c4c..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/battle.h" -#include "rpg/cutscene/item/json/cutscenejsonbattlestate.h" -#include "assert/assert.h" - -void cutsceneBattleSetStateStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - assertNotNull(item, "item cannot be NULL"); - battleSetState(item->battleSetState.state); -} - -bool_t cutsceneBattleSetStateUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneBattleSetStateLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - battlestate_t state; - if(!cutsceneJsonLookupBattleState( - yyjson_get_str(yyjson_obj_get(itemObj, "state")), &state - )) { - errorThrow("Cutscene item (BATTLE_SET_STATE): invalid 'state'"); - } - item->battleSetState.state = state; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h deleted file mode 100644 index 2d4753bf..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlesetstate.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/state/battlestate.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - battlestate_t state; -} cutscenebattlesetstate_t; - -/** - * Starts a BATTLE_SET_STATE item, transitioning the battle to the - * configured state immediately. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneBattleSetStateStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a BATTLE_SET_STATE item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneBattleSetStateUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a BATTLE_SET_STATE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneBattleSetStateLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c deleted file mode 100644 index d136db47..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.c +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/battle.h" -#include "rpg/battle/party.h" -#include "assert/assert.h" -#include "util/memory.h" - -battlefighter_t CUTSCENE_BATTLE_START_ENEMY_POOL[ - CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX -]; - -void cutsceneBattleStartStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - assertNotNull(item, "item cannot be NULL"); - - const cutscenebattlestart_t *config = &item->battleStart; - assertTrue( - config->enemyCount <= CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX, - "enemyCount out of bounds" - ); - - battlefighter_t *fighters[BATTLE_FIGHTER_POS_COUNT] = { 0 }; - - memoryZero( - CUTSCENE_BATTLE_START_ENEMY_POOL, - sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL) - ); - for(uint8_t i = 0; i < config->enemyCount; i++) { - battlefighter_t *enemy = &CUTSCENE_BATTLE_START_ENEMY_POOL[i]; - *enemy = config->enemies[i].fighter; - fighters[i] = enemy; - } - - for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { - battlefighter_t *member = partyGetOrderMember(i); - if(member == NULL) continue; - - fighters[BATTLE_FIGHTER_POS_4 + i] = member; - } - - battleStart(fighters); -} - -bool_t cutsceneBattleStartUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneBattleStartLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - yyjson_val *enemies = yyjson_obj_get(itemObj, "enemies"); - size_t enemyCount = yyjson_arr_size(enemies); - if( - !yyjson_is_arr(enemies) || enemyCount == 0 || - enemyCount > CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX - ) { - errorThrow( - "Cutscene item (BATTLE_START): 'enemies' must have 1-%u entries", - (uint32_t)CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX - ); - } - item->battleStart.enemyCount = (uint8_t)enemyCount; - - yyjson_arr_iter iter = yyjson_arr_iter_with(enemies); - yyjson_val *enemyVal; - size_t e = 0; - while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) { - cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e]; - - errorret_t parseResult = battleFighterParse(&enemy->fighter, enemyVal); - if(errorIsNotOk(parseResult)) errorChain(parseResult); - - // Enemy JSON never specifies team/controller (battleFighterParse just - // defaulted them to ALLY/PLAYER) - BATTLE_START enemies are always - // ENEMY/AI. - enemy->fighter.team = BATTLE_FIGHTER_TEAM_ENEMY; - enemy->fighter.controller = BATTLE_FIGHTER_CONTROLLER_AI; - - e++; - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h deleted file mode 100644 index c6024903..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlestart.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/fighter/battlefighter.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -// Enemies always occupy the left-side positions (BATTLE_FIGHTER_POS_0-3). -#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4 - -typedef struct { - // Fully parsed at Load time via battleFighterParse (team/controller - // forced to ENEMY/AI right after, since enemy JSON only ever supplies - // stats/abilities) - Start splices this directly into - // CUTSCENE_BATTLE_START_ENEMY_POOL, no further init needed. - battlefighter_t fighter; -} cutscenebattlestartenemy_t; - -typedef struct { - cutscenebattlestartenemy_t enemies[CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX]; - uint8_t enemyCount; -} cutscenebattlestart_t; - -/** - * Starts a BATTLE_START item: spawns the configured enemies, fills the - * remaining positions with the active party (via partyGetOrderMember), and - * calls battleStart. Always completes immediately - the battle scene takes - * over from here. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneBattleStartStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a BATTLE_START item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneBattleStartUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a BATTLE_START cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneBattleStartLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c deleted file mode 100644 index 763dbbff..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.c +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/battle/battle.h" -#include "rpg/battle/state/battlestateselection.h" - -bool_t cutsceneBattleWaitSelectionUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - if(BATTLE.state != BATTLE_STATE_SELECTION) return true; - if(!battleStateSelectionIsComplete()) return false; - - // Every fighter has decided - queue the move to BATTLE_STATE_EXECUTING. - const cutsceneitem_t items[] = { - { - .type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - .battleSetState = { .state = BATTLE_STATE_EXECUTING } - } - }; - cutsceneSystemInsertItemsNext(items, sizeof(items) / sizeof(cutsceneitem_t)); - return true; -} diff --git a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h b/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h deleted file mode 100644 index c5235734..00000000 --- a/src/dusk/rpg/cutscene/item/battle/cutscenebattlewaitselection.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Updates a BATTLE_WAIT_SELECTION step, completing once every fighter has - * decided a move this round (letting the queued BATTLE_SET_STATE step - * that follows move the battle on to BATTLE_STATE_EXECUTING) or once the - * selecting fighter has fled (moving on to BATTLE_STATE_FLED). Has no - * Start callback -- there's nothing to do until selection actually - * finishes. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once selection has completed or the battle has left - * BATTLE_STATE_SELECTION. - */ -bool_t cutsceneBattleWaitSelectionUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); diff --git a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt b/src/dusk/rpg/cutscene/item/control/CMakeLists.txt deleted file mode 100644 index 43f8e9dd..00000000 --- a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenewait.c - cutscenesetpause.c - cutsceneconcurrent.c - cutscenemarker.c - cutscenerestart.c - cutscenequitgame.c - cutscenescene.c - cutsceneidle.c -) diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c b/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c deleted file mode 100644 index 7145b383..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.c +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "assert/assert.h" - -void cutsceneConcurrentStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - assertTrue( - item->concurrent.count <= CUTSCENE_CONCURRENT_MAX, - "Too many items in CUTSCENE_CONCURRENT" - ); - for(uint8_t i = 0; i < item->concurrent.count; i++) { - assertTrue( - item->concurrent.items[i].type != CUTSCENE_ITEM_TYPE_CONCURRENT, - "Concurrent items cannot be nested" - ); - cutsceneItemStart( - &item->concurrent.items[i], - (cutsceneitemdata_t *)&data->concurrent.childData[i] - ); - } -} - -bool_t cutsceneConcurrentUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - for(uint8_t i = 0; i < item->concurrent.count; i++) { - if(data->concurrent.doneMask & (1u << i)) continue; - if(cutsceneItemUpdate( - &item->concurrent.items[i], - (cutsceneitemdata_t *)&data->concurrent.childData[i] - )) { - data->concurrent.doneMask |= (uint8_t)(1u << i); - } - } - uint8_t allDone = (uint8_t)((1u << item->concurrent.count) - 1u); - return data->concurrent.doneMask == allDone; -} diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h b/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h deleted file mode 100644 index 71b0ba1e..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutsceneconcurrent.h +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "cutscenewait.h" -#include "rpg/cutscene/item/entity/cutsceneentitywalkto.h" - -/** Maximum number of items that may run inside a CUTSCENE_CONCURRENT. */ -#define CUTSCENE_CONCURRENT_MAX 8 - -/** - * Static (const) data for a concurrent cutscene item. - */ -typedef struct { - const cutsceneitem_t *items; - uint8_t count; -} cutsceneconcurrent_t; - -/** - * Runtime data for one non-concurrent child item. - * Concurrent items cannot be nested. - */ -typedef union { - cutscenewaitdata_t wait; - cutsceneentitywalktodata_t entityWalkTo; -} cutsceneconcurrentchilddata_t; - -/** Runtime data for a running concurrent item. */ -typedef struct { - cutsceneconcurrentchilddata_t childData[CUTSCENE_CONCURRENT_MAX]; - uint8_t doneMask; -} cutsceneconcurrentdata_t; - -/** - * Starts a concurrent item (starts all child items simultaneously). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneConcurrentStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a concurrent item (ticks all unfinished children). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once every child has completed. - */ -bool_t cutsceneConcurrentUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneidle.c b/src/dusk/rpg/cutscene/item/control/cutsceneidle.c deleted file mode 100644 index fcadce3e..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutsceneidle.c +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" - -bool_t cutsceneIdleUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} diff --git a/src/dusk/rpg/cutscene/item/control/cutsceneidle.h b/src/dusk/rpg/cutscene/item/control/cutsceneidle.h deleted file mode 100644 index b230e186..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutsceneidle.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Updates an idle item. Never completes on its own - the cutscene blocks - * here indefinitely until something external (e.g. a UI callback) calls - * cutsceneSystemGoTo to jump elsewhere. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneIdleUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -// IDLE has no fields of its own to parse - see cutsceneItemLoadNoop -// (cutsceneitembase.h), used directly as its .load callback. diff --git a/src/dusk/rpg/cutscene/item/control/cutscenemarker.c b/src/dusk/rpg/cutscene/item/control/cutscenemarker.c deleted file mode 100644 index 0c210f5c..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenemarker.c +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneMarkerStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { -} - -bool_t cutsceneMarkerUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneMarkerLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonCopyString( - itemObj, "name", NULL, item->marker.name, CUTSCENE_MARKER_NAME_MAX - )) { - errorThrow("Cutscene item (MARKER): missing/too-long 'name'"); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenemarker.h b/src/dusk/rpg/cutscene/item/control/cutscenemarker.h deleted file mode 100644 index 0dc10e16..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenemarker.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -// Also the max length of a marker name referenced (not declared) by other -// item types - see cutscenesavedevicecheck_t/cutscenesaveloadallslots_t/ -// cutscenemodaloptionsmarkers_t. -#define CUTSCENE_MARKER_NAME_MAX 32 - -typedef struct { - char_t name[CUTSCENE_MARKER_NAME_MAX]; -} cutscenemarker_t; - -/** - * Starts a marker item. A marker does nothing on its own - it exists - * purely as a named position for cutsceneSystemGoTo to jump to. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneMarkerStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a marker item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneMarkerUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a MARKER cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneMarkerLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c deleted file mode 100644 index b07a5e8e..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "engine/engine.h" - -void cutsceneQuitGameStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - ENGINE.running = false; -} - -bool_t cutsceneQuitGameUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h deleted file mode 100644 index 9848a84f..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts a quit-game item (sets ENGINE.running to false, ending the - * process's main loop). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneQuitGameStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a quit-game item. The engine is shutting down by the time this - * would run, so this always reports incomplete (mirrors - * cutsceneRestartUpdate). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneQuitGameUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -// QUIT_GAME has no fields of its own to parse - see cutsceneItemLoadNoop -// (cutsceneitembase.h), used directly as its .load callback. diff --git a/src/dusk/rpg/cutscene/item/control/cutscenerestart.c b/src/dusk/rpg/cutscene/item/control/cutscenerestart.c deleted file mode 100644 index 160a35ba..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenerestart.c +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void cutsceneRestartStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - cutsceneRestart(); -} - -bool_t cutsceneRestartUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenerestart.h b/src/dusk/rpg/cutscene/item/control/cutscenerestart.h deleted file mode 100644 index a1e05dd4..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenerestart.h +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts a restart item (restarts the currently running cutscene from - * its first item via cutsceneRestart). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneRestartStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a restart item. By the time this would run, the cutscene has - * already restarted from its first item, so this always reports - * incomplete (mirrors cutsceneCutsceneUpdate). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneRestartUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -// RESTART has no fields of its own to parse - see cutsceneItemLoadNoop -// (cutsceneitembase.h), used directly as its .load callback. diff --git a/src/dusk/rpg/cutscene/item/control/cutscenescene.c b/src/dusk/rpg/cutscene/item/control/cutscenescene.c deleted file mode 100644 index 57c92249..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenescene.c +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "scene/scene.h" -#include "rpg/cutscene/item/json/cutscenejsonscenetype.h" - -void cutsceneSceneStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - sceneSet(item->sceneChange.type); -} - -bool_t cutsceneSceneUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneSceneLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - scenetype_t sceneValue; - if(!cutsceneJsonLookupSceneType( - yyjson_get_str(yyjson_obj_get(itemObj, "sceneType")), &sceneValue - )) { - errorThrow("Cutscene item (SCENE): invalid 'sceneType'"); - } - item->sceneChange.type = sceneValue; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenescene.h b/src/dusk/rpg/cutscene/item/control/cutscenescene.h deleted file mode 100644 index f3e2d23b..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenescene.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "scene/scenetype.h" - -typedef struct { - scenetype_t type; -} cutscenescene_t; - -/** - * Starts a scene item (requests a switch to the given scene via - * sceneSet). The switch itself doesn't happen until the next - * sceneUpdate() tick, so the rest of the current frame - including - * whatever else this cutscene does after this item - still runs - * against the scene that's being left. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneSceneStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a scene item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneSceneUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a SCENE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneSceneLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.c b/src/dusk/rpg/cutscene/item/control/cutscenesetpause.c deleted file mode 100644 index e434a73d..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/item/json/cutscenejsonpauseflags.h" - -void cutsceneSetPauseStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - CUTSCENE_SYSTEM.pause = item->setPause; -} - -bool_t cutsceneSetPauseUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneSetPauseLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - yyjson_val *flags = yyjson_obj_get(itemObj, "flags"); - cutscenepause_t value; - if(!cutsceneJsonParsePauseFlags(flags, &value)) { - errorThrow( - "Cutscene item (SET_PAUSE): 'flags' must be an array of valid pause " - "flags" - ); - } - item->setPause = value; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.h b/src/dusk/rpg/cutscene/item/control/cutscenesetpause.h deleted file mode 100644 index 8636a50f..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenesetpause.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/cutscenepause.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts a set-pause item (applies the new pause flags immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneSetPauseStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a set-pause item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneSetPauseUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a SET_PAUSE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneSetPauseLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/control/cutscenewait.c b/src/dusk/rpg/cutscene/item/control/cutscenewait.c deleted file mode 100644 index be5b84f6..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenewait.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" -#include "time/time.h" - -void cutsceneWaitStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - data->wait = item->wait; -} - -bool_t cutsceneWaitUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - data->wait -= TIME.delta; - return data->wait <= 0; -} - -errorret_t cutsceneWaitLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - item->wait = cutsceneJsonGetFloat(itemObj, "seconds", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenewait.h b/src/dusk/rpg/cutscene/item/control/cutscenewait.h deleted file mode 100644 index 64ccd7cb..00000000 --- a/src/dusk/rpg/cutscene/item/control/cutscenewait.h +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef float_t cutscenewait_t; -typedef float_t cutscenewaitdata_t; - -/** - * Starts a wait item (stores the duration in data). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneWaitStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a wait item. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true when the wait has elapsed. - */ -bool_t cutsceneWaitUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a WAIT cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneWaitLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/item/cutscenecallback.c b/src/dusk/rpg/cutscene/item/cutscenecallback.c deleted file mode 100644 index 9f5c0743..00000000 --- a/src/dusk/rpg/cutscene/item/cutscenecallback.c +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void cutsceneCallbackStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - if(item->callback != NULL) item->callback(NULL); -} - -bool_t cutsceneCallbackUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} diff --git a/src/dusk/rpg/cutscene/item/cutscenecallback.h b/src/dusk/rpg/cutscene/item/cutscenecallback.h deleted file mode 100644 index 41029607..00000000 --- a/src/dusk/rpg/cutscene/item/cutscenecallback.h +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef void (*cutscenecallback_t)(void *userData); - -/** - * Starts a callback item (invokes the callback immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneCallbackStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a callback item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneCallbackUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c deleted file mode 100644 index 558b810a..00000000 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ /dev/null @@ -1,562 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "util/memory.h" -#include "util/string.h" -#include "ui/overlay/uifatalerror.h" - -cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { - [CUTSCENE_ITEM_TYPE_NULL] = { 0 }, - - // Control - [CUTSCENE_ITEM_TYPE_CALLBACK] = { - .init = cutsceneCallbackStart, - .update = cutsceneCallbackUpdate - }, - - [CUTSCENE_ITEM_TYPE_WAIT] = { - .init = cutsceneWaitStart, - .update = cutsceneWaitUpdate, - .jsonName = "WAIT", - .load = cutsceneWaitLoad - }, - - [CUTSCENE_ITEM_TYPE_CUTSCENE] = { - .init = cutsceneCutsceneStart, - .update = cutsceneCutsceneUpdate, - .jsonName = "CUTSCENE", - .load = cutsceneCutsceneLoad - }, - - [CUTSCENE_ITEM_TYPE_INSERT] = { - .init = cutsceneInsertStart, - .update = cutsceneInsertUpdate, - .jsonName = "INSERT", - .load = cutsceneInsertLoad - }, - - [CUTSCENE_ITEM_TYPE_SET_PAUSE] = { - .init = cutsceneSetPauseStart, - .update = cutsceneSetPauseUpdate, - .jsonName = "SET_PAUSE", - .load = cutsceneSetPauseLoad - }, - - [CUTSCENE_ITEM_TYPE_CONCURRENT] = { - .init = cutsceneConcurrentStart, - .update = cutsceneConcurrentUpdate - }, - - [CUTSCENE_ITEM_TYPE_MARKER] = { - .init = cutsceneMarkerStart, - .update = cutsceneMarkerUpdate, - .jsonName = "MARKER", - .load = cutsceneMarkerLoad - }, - - [CUTSCENE_ITEM_TYPE_RESTART] = { - .init = cutsceneRestartStart, - .update = cutsceneRestartUpdate, - .jsonName = "RESTART", - .load = cutsceneItemLoadNoop - }, - - [CUTSCENE_ITEM_TYPE_QUIT_GAME] = { - .init = cutsceneQuitGameStart, - .update = cutsceneQuitGameUpdate, - .jsonName = "QUIT_GAME", - .load = cutsceneItemLoadNoop - }, - - [CUTSCENE_ITEM_TYPE_SCENE] = { - .init = cutsceneSceneStart, - .update = cutsceneSceneUpdate, - .jsonName = "SCENE", - .load = cutsceneSceneLoad - }, - - [CUTSCENE_ITEM_TYPE_IDLE] = { - .update = cutsceneIdleUpdate, - .jsonName = "IDLE", - .load = cutsceneItemLoadNoop - }, - - // Entity - [CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = { - .init = cutsceneEntityTeleportStart, - .update = cutsceneEntityTeleportUpdate, - .jsonName = "ENTITY_TELEPORT", - .load = cutsceneEntityTeleportLoad - }, - - [CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = { - .init = cutsceneEntityWalkToStart, - .update = cutsceneEntityWalkToUpdate, - .jsonName = "ENTITY_WALK_TO", - .load = cutsceneEntityWalkToLoad - }, - - [CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = { - .init = cutsceneEntityWalkToEntityStart, - .update = cutsceneEntityWalkToEntityUpdate, - .jsonName = "ENTITY_WALK_TO_ENTITY", - .load = cutsceneEntityWalkToEntityLoad - }, - - [CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = { - .init = cutsceneEntityRemoveStart, - .update = cutsceneEntityRemoveUpdate, - .jsonName = "ENTITY_REMOVE", - .load = cutsceneEntityRemoveLoad - }, - - [CUTSCENE_ITEM_TYPE_ENTITY_ADD] = { - .init = cutsceneEntityAddStart, - .update = cutsceneEntityAddUpdate, - .jsonName = "ENTITY_ADD", - .load = cutsceneEntityAddLoad - }, - - [CUTSCENE_ITEM_TYPE_ENTITY_TURN] = { - .init = cutsceneEntityTurnStart, - .update = cutsceneEntityTurnUpdate, - .jsonName = "ENTITY_TURN", - .load = cutsceneEntityTurnLoad - }, - - // UI - [CUTSCENE_ITEM_TYPE_TEXT] = { - .init = cutsceneTextStart, - .update = cutsceneTextUpdate, - .jsonName = "TEXT", - .load = cutsceneTextLoad - }, - - [CUTSCENE_ITEM_TYPE_TEXT_MINI] = { - .init = cutsceneTextMiniStart, - .update = cutsceneTextMiniUpdate, - .jsonName = "TEXT_MINI", - .load = cutsceneTextMiniLoad - }, - - [CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = { - .init = cutsceneTextMiniHideStart, - .update = cutsceneTextMiniHideUpdate, - .jsonName = "TEXT_MINI_HIDE", - .load = cutsceneTextMiniHideLoad - }, - - [CUTSCENE_ITEM_TYPE_PRINT] = { - .init = cutscenePrintStart, - .update = cutscenePrintUpdate, - .jsonName = "PRINT", - .load = cutscenePrintLoad - }, - - [CUTSCENE_ITEM_TYPE_FADE] = { - .init = cutsceneFadeStart, - .update = cutsceneFadeUpdate, - .jsonName = "FADE", - .load = cutsceneFadeLoad - }, - - [CUTSCENE_ITEM_TYPE_EMOJI] = { - .init = cutsceneEmojiStart, - .update = cutsceneEmojiUpdate, - .jsonName = "EMOJI", - .load = cutsceneEmojiLoad - }, - - [CUTSCENE_ITEM_TYPE_SHAKE] = { - .init = cutsceneShakeStart, - .update = cutsceneShakeUpdate, - .jsonName = "SHAKE", - .load = cutsceneShakeLoad - }, - - [CUTSCENE_ITEM_TYPE_MODAL] = { - .init = cutsceneModalStart, - .update = cutsceneModalUpdate, - .jsonName = "MODAL", - .load = cutsceneModalLoad - }, - - [CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = { - .init = cutsceneModalOptionsMarkersStart, - .update = cutsceneModalOptionsMarkersUpdate, - .jsonName = "MODAL_OPTIONS_MARKERS", - .load = cutsceneModalOptionsMarkersLoad - }, - - [CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = { - .init = cutsceneModalCloseStart, - .update = cutsceneModalCloseUpdate, - .jsonName = "MODAL_CLOSE", - .load = cutsceneItemLoadNoop - }, - - [CUTSCENE_ITEM_TYPE_KEYBOARD] = { - .init = cutsceneKeyboardStart, - .update = cutsceneKeyboardUpdate - }, - - [CUTSCENE_ITEM_TYPE_UI_SHOW] = { - .init = cutsceneUIShowStart, - .update = cutsceneUIShowUpdate, - .jsonName = "UI_SHOW", - .load = cutsceneUIShowLoad - }, - - // Item - [CUTSCENE_ITEM_TYPE_ITEM_GIVE] = { - .init = cutsceneItemGiveStart, - .update = cutsceneItemGiveUpdate, - .jsonName = "ITEM_GIVE", - .load = cutsceneItemGiveLoad - }, - - // Map area - [CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = { - .init = cutsceneMapAreaAddStart, - .update = cutsceneMapAreaAddUpdate - }, - - [CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = { - .init = cutsceneMapAreaRemoveStart, - .update = cutsceneMapAreaRemoveUpdate, - .jsonName = "MAP_AREA_REMOVE", - .load = cutsceneMapAreaRemoveLoad - }, - - [CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = { - .init = cutsceneMapAreaWaitStart, - .update = cutsceneMapAreaWaitUpdate, - .jsonName = "MAP_AREA_WAIT", - .load = cutsceneMapAreaWaitLoad - }, - - // Battle - [CUTSCENE_ITEM_TYPE_BATTLE_START] = { - .init = cutsceneBattleStartStart, - .update = cutsceneBattleStartUpdate, - .jsonName = "BATTLE_START", - .load = cutsceneBattleStartLoad - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE] = { - .init = cutsceneBattleSetStateStart, - .update = cutsceneBattleSetStateUpdate, - .jsonName = "BATTLE_SET_STATE", - .load = cutsceneBattleSetStateLoad - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION] = { - .update = cutsceneBattleWaitSelectionUpdate, - .jsonName = "BATTLE_WAIT_SELECTION", - .load = cutsceneItemLoadNoop - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE] = { - .init = cutsceneBattlePostMoveStart, - .update = cutsceneBattlePostMoveUpdate - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE] = { - .init = cutsceneBattleFighterDamageStart, - .update = cutsceneBattleFighterDamageUpdate, - .jsonName = "BATTLE_FIGHTER_DAMAGE", - .load = cutsceneBattleFighterDamageLoad - }, - - [CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL] = { - .init = cutsceneBattleFighterHealStart, - .update = cutsceneBattleFighterHealUpdate, - .jsonName = "BATTLE_FIGHTER_HEAL", - .load = cutsceneBattleFighterHealLoad - }, - - // Save - [CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = { - .init = cutsceneSaveDeviceCheckStart, - .update = cutsceneSaveDeviceCheckUpdate, - .jsonName = "SAVE_DEVICE_CHECK", - .load = cutsceneSaveDeviceCheckLoad - }, - - [CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS] = { - .init = cutsceneSaveLoadAllSlotsStart, - .update = cutsceneSaveLoadAllSlotsUpdate, - .jsonName = "SAVE_LOAD_ALL_SLOTS", - .load = cutsceneSaveLoadAllSlotsLoad - }, - - // Audio - [CUTSCENE_ITEM_TYPE_AUDIO_PLAY] = { - .init = cutsceneAudioPlayStart, - .update = cutsceneAudioPlayUpdate, - .jsonName = "AUDIO_PLAY", - .load = cutsceneAudioPlayLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_STOP] = { - .init = cutsceneAudioStopStart, - .update = cutsceneAudioStopUpdate, - .jsonName = "AUDIO_STOP", - .load = cutsceneAudioStopLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_PAUSE] = { - .init = cutsceneAudioPauseStart, - .update = cutsceneAudioPauseUpdate, - .jsonName = "AUDIO_PAUSE", - .load = cutsceneAudioPauseLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_RESUME] = { - .init = cutsceneAudioResumeStart, - .update = cutsceneAudioResumeUpdate, - .jsonName = "AUDIO_RESUME", - .load = cutsceneAudioResumeLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_FADE] = { - .init = cutsceneAudioFadeStart, - .update = cutsceneAudioFadeUpdate, - .jsonName = "AUDIO_FADE", - .load = cutsceneAudioFadeLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT] = { - .update = cutsceneAudioFadeWaitUpdate, - .jsonName = "AUDIO_FADE_WAIT", - .load = cutsceneAudioFadeWaitLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN] = { - .init = cutsceneAudioSetPanStart, - .update = cutsceneAudioSetPanUpdate, - .jsonName = "AUDIO_SET_PAN", - .load = cutsceneAudioSetPanLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP] = { - .init = cutsceneAudioSetLoopStart, - .update = cutsceneAudioSetLoopUpdate, - .jsonName = "AUDIO_SET_LOOP", - .load = cutsceneAudioSetLoopLoad - }, - - [CUTSCENE_ITEM_TYPE_AUDIO_SET] = { - .init = cutsceneAudioSetStart, - .update = cutsceneAudioSetUpdate, - .jsonName = "AUDIO_SET", - .load = cutsceneAudioSetLoad - }, - - // Story - [CUTSCENE_ITEM_TYPE_STORY_SET_FLAG] = { - .init = cutsceneStorySetFlagStart, - .update = cutsceneStorySetFlagUpdate, - .jsonName = "STORY_SET_FLAG", - .load = cutsceneStorySetFlagLoad - }, - - [CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG] = { - .update = cutsceneStoryWaitFlagUpdate, - .jsonName = "STORY_WAIT_FLAG", - .load = cutsceneStoryWaitFlagLoad - }, - - // Party - [CUTSCENE_ITEM_TYPE_PARTY_ADD_MEMBER] = { - .init = cutscenePartyAddMemberStart, - .update = cutscenePartyAddMemberUpdate, - .jsonName = "PARTY_ADD_MEMBER", - .load = cutscenePartyAddMemberLoad - }, - - [CUTSCENE_ITEM_TYPE_PARTY_REMOVE_MEMBER] = { - .init = cutscenePartyRemoveMemberStart, - .update = cutscenePartyRemoveMemberUpdate, - .jsonName = "PARTY_REMOVE_MEMBER", - .load = cutscenePartyRemoveMemberLoad - }, - - [CUTSCENE_ITEM_TYPE_PARTY_HEAL_MEMBER] = { - .init = cutscenePartyHealMemberStart, - .update = cutscenePartyHealMemberUpdate, - .jsonName = "PARTY_HEAL_MEMBER", - .load = cutscenePartyHealMemberLoad - }, - - [CUTSCENE_ITEM_TYPE_PARTY_HEAL_ALL] = { - .init = cutscenePartyHealAllStart, - .update = cutscenePartyHealAllUpdate, - .jsonName = "PARTY_HEAL_ALL", - .load = cutsceneItemLoadNoop - } -}; - -bool_t cutsceneItemLookupType( - const char_t *name, - cutsceneitemtype_t *outType -) { - if(name == NULL) return false; - for(size_t i = 0; i < CUTSCENE_ITEM_TYPE_COUNT; i++) { - const char_t *jsonName = CUTSCENE_ITEM_CALLBACKS[i].jsonName; - if(jsonName != NULL && stringEquals(jsonName, name)) { - *outType = (cutsceneitemtype_t)i; - return true; - } - } - return false; -} - -void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) { - cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init; - if(init != NULL) init(item, data); -} - -bool_t cutsceneItemUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - cutsceneitemupdatecallback_t *update = - CUTSCENE_ITEM_CALLBACKS[item->type].update; - if(update == NULL) return false; - - return update(item, data); -} - -void cutsceneCutsceneStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - cutscene_t *cutscene = item->cutsceneRef.cutscene; - if(cutscene == NULL && item->cutsceneRef.name[0] != '\0') { - // Not cached back into item - with only one shared, fixed load buffer - // (see cutscenesystem.h's CUTSCENE_LOADED_ITEMS_MAX doc comment), - // every JSON-authored reference resolves to the same fixed address - // every time anyway, and item itself lives inside that very buffer - - // caching a pointer into it here would go stale (or, worse, get - // clobbered mid-resolve) the moment a later load overwrites it. - cutscene = cutsceneCutsceneResolve(item->cutsceneRef.name); - } - if(cutscene != NULL) cutsceneSystemStartCutscene(cutscene); -} - -bool_t cutsceneCutsceneUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} - -cutscene_t * cutsceneCutsceneResolve(const char_t *name) { - // Copy name out before it can be invalidated: this is commonly called - // with a pointer into CUTSCENE_SYSTEM.loadedItems itself (a running - // CUTSCENE/INSERT item's own cutsceneRef.name/insert.name field, when - // the running cutscene IS loadedScene, e.g. initial.jsonc's closing - // CUTSCENE item into main_menu.jsonc) - cutsceneLoadParse below parses - // straight into that same shared loadedItems buffer, overwriting the - // very memory name points into with the target cutscene's own first - // item before this function is done reading name. - char_t nameCopy[ASSET_FILE_NAME_MAX]; - stringCopy(nameCopy, name, ASSET_FILE_NAME_MAX - 1); - - errorret_t result = cutsceneLoadParse( - nameCopy, &CUTSCENE_SYSTEM.loadedScene, CUTSCENE_SYSTEM.loadedItems, - CUTSCENE_LOADED_ITEMS_MAX - ); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - uiFatalErrorOpen(NULL); - return NULL; - } - - // Shares loadedScene/loadedItems with cutsceneSystemLoad, so track the - // source the same way it does - cutsceneSystemGoTo needs this to reload the - // untouched original when jumping to a marker behind the current - // position (see cutsceneSystemGoTo's doc comment). - stringCopy(CUTSCENE_SYSTEM.loadedFile, nameCopy, ASSET_FILE_NAME_MAX - 1); - - return &CUTSCENE_SYSTEM.loadedScene; -} - -errorret_t cutsceneCutsceneLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - return cutsceneCutsceneRefLoad(itemObj, &item->cutsceneRef, "CUTSCENE"); -} - -errorret_t cutsceneCutsceneRefLoad( - yyjson_val *itemObj, - cutscenecutsceneref_t *outRef, - const char_t *itemTypeLabel -) { - const char_t *name = yyjson_get_str(yyjson_obj_get(itemObj, "name")); - if(name == NULL) { - errorThrow("Cutscene item (%s): missing 'name'", itemTypeLabel); - } - - // Only validate that name itself would fit - the reference itself is - // not resolved/locked until this item actually runs (see - // cutsceneCutsceneStart/cutsceneCutsceneResolve). - int32_t nameLen = stringFormat(NULL, 0, "%s", name); - if(nameLen < 0 || (size_t)nameLen >= CUTSCENE_CUTSCENE_REF_NAME_MAX) { - errorThrow("Cutscene item (%s): 'name' too long", itemTypeLabel); - } - - outRef->cutscene = NULL; - stringCopy(outRef->name, name, CUTSCENE_CUTSCENE_REF_NAME_MAX); - - errorOk(); -} - -void cutsceneInsertStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - if(item->insert.cutscene != NULL) { - cutsceneSystemInsertCutscene(item->insert.cutscene); - } else if(item->insert.name[0] != '\0') { - cutsceneInsertResolveAndSplice(item->insert.name); - } -} - -bool_t cutsceneInsertUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} - -void cutsceneInsertResolveAndSplice(const char_t *name) { - // Local scratch, not a shared buffer - this target is spliced straight - // into the running scene below and never referenced again afterward, so - // it only needs to live for the rest of this call. - cutsceneitem_t items[CUTSCENE_INSERT_ITEMS_MAX]; - cutscene_t scene = { .items = items }; - errorret_t result = cutsceneLoadParse(name, &scene, items, CUTSCENE_INSERT_ITEMS_MAX); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - uiFatalErrorOpen(NULL); - return; - } - - cutsceneSystemInsertCutscene(&scene); -} - -errorret_t cutsceneInsertLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - return cutsceneCutsceneRefLoad(itemObj, &item->insert, "INSERT"); -} diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h deleted file mode 100644 index 84b09277..00000000 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "yyjson.h" -#include "asset/assetfile.h" -#include "cutsceneitembase.h" -#include "cutscenecallback.h" -#include "cutsceneprint.h" -#include "control/cutscenewait.h" -#include "control/cutscenesetpause.h" -#include "control/cutsceneconcurrent.h" -#include "control/cutscenemarker.h" -#include "control/cutscenerestart.h" -#include "control/cutscenequitgame.h" -#include "control/cutscenescene.h" -#include "control/cutsceneidle.h" -#include "entity/cutsceneentityteleport.h" -#include "entity/cutsceneentitywalkto.h" -#include "entity/cutsceneentityremove.h" -#include "entity/cutsceneentityadd.h" -#include "entity/cutsceneentityturn.h" -#include "entity/cutsceneentitywalktoentity.h" -#include "ui/cutscenetext.h" -#include "ui/cutscenetextmini.h" -#include "ui/cutscenetextminihide.h" -#include "ui/cutscenefade.h" -#include "ui/cutsceneemoji.h" -#include "ui/cutsceneshake.h" -#include "ui/cutscenemodal.h" -#include "ui/cutscenemodaloptionsmarkers.h" -#include "ui/cutscenekeyboard.h" -#include "ui/cutsceneuishow.h" -#include "item/cutsceneitemgive.h" -#include "maparea/cutscenemapareaadd.h" -#include "maparea/cutscenemaparearemove.h" -#include "maparea/cutscenemapareawait.h" -#include "battle/cutscenebattlestart.h" -#include "battle/cutscenebattlesetstate.h" -#include "battle/cutscenebattlewaitselection.h" -#include "battle/cutscenebattlepostmove.h" -#include "battle/cutscenebattlefighterdamage.h" -#include "battle/cutscenebattlefighterheal.h" -#include "save/cutscenesavedevicecheck.h" -#include "save/cutscenesaveloadallslots.h" -#include "audio/cutsceneaudioplay.h" -#include "audio/cutsceneaudiostop.h" -#include "audio/cutsceneaudiopause.h" -#include "audio/cutsceneaudioresume.h" -#include "audio/cutsceneaudiofade.h" -#include "audio/cutsceneaudiofadewait.h" -#include "audio/cutsceneaudiosetpan.h" -#include "audio/cutsceneaudiosetloop.h" -#include "audio/cutsceneaudioset.h" -#include "story/cutscenestorysetflag.h" -#include "story/cutscenestorywaitflag.h" -#include "party/cutscenepartyaddmember.h" -#include "party/cutscenepartyremovemember.h" -#include "party/cutscenepartyhealmember.h" -#include "party/cutscenepartyhealall.h" - -typedef struct cutscene_s cutscene_t; - -// Max length of a CUTSCENE/INSERT item's "name" field - it's the full -// asset path to load, so this is bounded by ASSET_FILE_NAME_MAX directly. -#define CUTSCENE_CUTSCENE_REF_NAME_MAX ASSET_FILE_NAME_MAX - -// A CUTSCENE (or INSERT) item's target. A C-authored cutscene_t* set -// directly (e.g. by a test) leaves name empty ("") - safe to hold onto -// indefinitely, since C-authored cutscenes are compiled directly into -// their own static arrays, never a shared load buffer. JSON-authored -// cutscenes (cutsceneCutsceneLoad/cutsceneInsertLoad) do the opposite - -// only name is set at parse time (copied by value out of the JSON doc, -// so it stays valid regardless of the doc's own lifetime), since -// resolving/locking the referenced asset there would eagerly load every -// cutscene a cutscene merely references, even ones never played. -// cutsceneCutsceneStart resolves name to cutscene the first time this -// item actually runs (via cutsceneCutsceneResolve) but does NOT cache the -// result back into this field - with only one shared, fixed buffer (see -// CUTSCENE_LOADED_ITEMS_MAX in cutscenesystem.h), every JSON-authored -// reference resolves to the same fixed address every time anyway, and -// item itself may live inside that very buffer, so caching a pointer -// into it here would go stale (or, worse, get clobbered mid-resolve) the -// moment a later load overwrites it. cutsceneInsertStart instead resolves -// AND splices name in one step (cutsceneInsertResolveAndSplice) using a -// local scratch buffer that's gone by the time this item's Start -// returns, so there's nothing to cache back for INSERT even in principle. -// Shared by both CUTSCENE (item->cutsceneRef) and INSERT (item->insert), -// since both need exactly this "resolved pointer, or a name to lazily -// resolve" shape. -typedef struct { - cutscene_t *cutscene; - char_t name[CUTSCENE_CUTSCENE_REF_NAME_MAX]; -} cutscenecutsceneref_t; - -struct cutsceneitem_s { - cutsceneitemtype_t type; - - union { - cutscenetext_t text; - cutscenetextmini_t textMini; - cutscenetextminihide_t textMiniHide; - cutscenecallback_t callback; - cutscenewait_t wait; - cutscenecutsceneref_t cutsceneRef; - cutscenecutsceneref_t insert; - cutsceneentityteleport_t entityTeleport; - cutsceneentitywalkto_t entityWalkTo; - cutscenefade_t fade; - cutscenepause_t setPause; - cutsceneconcurrent_t concurrent; - cutsceneitemgive_t itemGive; - cutsceneentityremove_t entityRemove; - cutsceneentityadd_t entityAdd; - cutsceneentityturn_t entityTurn; - cutsceneentitywalktoentity_t entityWalkToEntity; - cutscenemapareaadd_t mapAreaAdd; - cutscenemaparearemove_t mapAreaRemove; - cutscenemapareawait_t mapAreaWait; - cutscenebattlestart_t battleStart; - cutscenebattlesetstate_t battleSetState; - cutscenebattlefighterdamage_t battleFighterDamage; - cutscenebattlefighterheal_t battleFighterHeal; - cutsceneemoji_t emoji; - cutsceneshake_t shake; - cutscenemodal_t modal; - cutscenemodaloptionsmarkers_t modalOptionsMarkers; - cutsceneprint_t print; - cutscenemarker_t marker; - cutscenescene_t sceneChange; - cutscenesavedevicecheck_t saveDeviceCheck; - cutscenesaveloadallslots_t saveLoadAllSlots; - cutscenekeyboard_t keyboard; - cutsceneaudioplay_t audioPlay; - audiomixerchannel_t audioStopChannel; - audiomixerchannel_t audioPauseChannel; - audiomixerchannel_t audioResumeChannel; - cutsceneaudiofade_t audioFade; - audiomixerchannel_t audioFadeWaitChannel; - cutsceneaudiosetpan_t audioSetPan; - cutsceneaudiosetloop_t audioSetLoop; - cutsceneaudioset_t audioSet; - cutsceneuishow_t uiShow; - cutscenestorysetflag_t storySetFlag; - cutscenestorywaitflag_t storyWaitFlag; - cutscenepartyaddmember_t partyAddMember; - cutscenepartyremovemember_t partyRemoveMember; - cutscenepartyhealmember_t partyHealMember; - }; -}; - -typedef union cutsceneitemdata_u { - cutscenewaitdata_t wait; - cutsceneentitywalktodata_t entityWalkTo; - cutsceneconcurrentdata_t concurrent; - cutscenemapareawaitdata_t mapAreaWait; -} cutsceneitemdata_t; - -typedef void (cutsceneiteminitcallback_t)( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -typedef bool_t (cutsceneitemupdatecallback_t)( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -typedef errorret_t (cutsceneitemloadcallback_t)( - yyjson_val *itemObj, - cutsceneitem_t *item -); - -typedef struct { - cutsceneiteminitcallback_t *init; - cutsceneitemupdatecallback_t *update; - const char_t *jsonName; // NULL if not JSON-authorable - cutsceneitemloadcallback_t *load; // NULL if not JSON-authorable -} cutsceneitemcallbacks_t; - -extern cutsceneitemcallbacks_t - CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT]; - -/** - * Start the given cutscene item. - * - * @param item The cutscene item to start. - * @param data Runtime data storage (pre-zeroed by caller). - */ -void cutsceneItemStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Tick the given cutscene item (one frame). - * - * @param item The cutscene item to tick. - * @param data Runtime data storage. - * @returns true if the item is complete and the cutscene should advance. - */ -bool_t cutsceneItemUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Looks up name (exact case) against every JSON-authorable item type's - * jsonName. - * - * @param name Name to search for. - * @param outType Set to the matching item type on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneItemLookupType( - const char_t *name, - cutsceneitemtype_t *outType -); - -/** - * Starts a nested-cutscene item, handing control over to the referenced - * cutscene. If item->cutsceneRef.cutscene hasn't been resolved yet (a - * JSON-authored reference, only known by name so far), resolves it first - * via cutsceneCutsceneResolve (not cached back - see cutscenecutsceneref_t's - * doc comment for why). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneCutsceneStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Resolves a JSON-authored CUTSCENE item's referenced cutscene by name: - * locks+loads name as a plain ASSET_LOADER_TYPE_JSON asset, parses it into - * CUTSCENE_SYSTEM.loadedItems/loadedScene, then unlocks the asset entry - * like any other asset - the parsed items don't reference the doc past - * that (see cutsceneInsertResolveAndSplice's matching comment for why). - * Called lazily from cutsceneCutsceneStart the first time the item - * actually runs, not at parse time, so a cutscene referencing many others - * doesn't load every one of them up front. On failure, opens the fatal - * error overlay (matching cutsceneSystemLoad's runtime-failure handling) - * and returns NULL. - * - * @param name Full asset path to the referenced cutscene's JSON file. - * @return The resolved cutscene, or NULL on failure. - */ -cutscene_t * cutsceneCutsceneResolve(const char_t *name); - -/** - * Updates a nested-cutscene item. By the time this would run, control - * has already moved on to the referenced cutscene, so this always - * reports incomplete. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneCutsceneUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a CUTSCENE cutscene item's JSON fields into item. Kept inline here - * (rather than its own file) since it's tightly bound to the asset/loader - * system itself. Only stores the 'name' string (which points into the - * cutscene asset's still-alive yyjson doc, so no copy is needed) - - * resolving it to the referenced asset is deferred to cutsceneCutsceneStart, - * the first time this item actually runs, so a cutscene referencing many - * others doesn't pay to load every one of them up front. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneCutsceneLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); - -/** - * Shared JSON parsing body for both CUTSCENE and INSERT items - both need - * exactly a 'name' string field, the full asset path to the referenced - * cutscene's JSON file, validated to fit CUTSCENE_CUTSCENE_REF_NAME_MAX. - * Only stores the name (see cutsceneCutsceneLoad for why resolving is - * deferred). - * - * @param itemObj The item's JSON object. - * @param outRef Destination ref, already zeroed by the caller. - * @param itemTypeLabel Item type name used only in the error message on - * failure (e.g. "CUTSCENE" or "INSERT"). - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneCutsceneRefLoad( - yyjson_val *itemObj, - cutscenecutsceneref_t *outRef, - const char_t *itemTypeLabel -); - -/** - * Starts an INSERT item, splicing the referenced cutscene's items into the - * running cutscene in place via cutsceneSystemInsertCutscene - unlike - * CUTSCENE (a one-way jump that replaces the running cutscene outright), - * whatever already followed this item continues normally once the - * inserted items run out, just shifted further down the same array. If - * item->insert.cutscene is set (a compile-time target), splices it - * directly; otherwise (a JSON-authored reference, only known by name so - * far) resolves and splices it via cutsceneInsertResolveAndSplice. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneInsertStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Resolves a JSON-authored INSERT item's referenced cutscene by name and - * splices it into the running cutscene (CUTSCENE_SYSTEM.scene): locks+ - * loads name as a plain ASSET_LOADER_TYPE_JSON asset, parses it into a - * small scratch buffer local to this call (capacity - * CUTSCENE_INSERT_ITEMS_MAX) straight out of the asset entry's own doc, - * then unlocks the entry like any other asset - nothing needs to keep the - * doc itself resident past the parse: every cutscene item field that could - * reference it owns its string data by value (see - * cutscenecutsceneref_t/cutscenemarker_t's doc comments), and - * cutsceneSystemInsertCutscene copies the scratch items into the running - * scene's own array right after. On failure, opens the fatal error - * overlay, matching cutsceneCutsceneResolve. - * - * @param name Full asset path to the referenced cutscene's JSON file. - */ -void cutsceneInsertResolveAndSplice(const char_t *name); - -/** - * Updates an INSERT item. By the time this would run, control has already - * moved on to the inserted items, so this always reports incomplete. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneInsertUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an INSERT cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneInsertLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.c b/src/dusk/rpg/cutscene/item/cutsceneitembase.c deleted file mode 100644 index 11cb848e..00000000 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.c +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutsceneitembase.h" - -errorret_t cutsceneItemLoadNoop( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/cutsceneitembase.h b/src/dusk/rpg/cutscene/item/cutsceneitembase.h deleted file mode 100644 index b53bcbbf..00000000 --- a/src/dusk/rpg/cutscene/item/cutsceneitembase.h +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "error/error.h" -#include "yyjson.h" - -typedef struct cutsceneitem_s cutsceneitem_t; -typedef union cutsceneitemdata_u cutsceneitemdata_t; - -typedef enum { - CUTSCENE_ITEM_TYPE_NULL, - - // Control - CUTSCENE_ITEM_TYPE_CALLBACK, - CUTSCENE_ITEM_TYPE_WAIT, - CUTSCENE_ITEM_TYPE_CUTSCENE, - CUTSCENE_ITEM_TYPE_INSERT, - CUTSCENE_ITEM_TYPE_SET_PAUSE, - CUTSCENE_ITEM_TYPE_CONCURRENT, - CUTSCENE_ITEM_TYPE_MARKER, - CUTSCENE_ITEM_TYPE_RESTART, - CUTSCENE_ITEM_TYPE_QUIT_GAME, - CUTSCENE_ITEM_TYPE_SCENE, - CUTSCENE_ITEM_TYPE_IDLE, - - // Entity - CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, - CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, - CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, - CUTSCENE_ITEM_TYPE_ENTITY_REMOVE, - CUTSCENE_ITEM_TYPE_ENTITY_ADD, - CUTSCENE_ITEM_TYPE_ENTITY_TURN, - - // UI - CUTSCENE_ITEM_TYPE_TEXT, - CUTSCENE_ITEM_TYPE_TEXT_MINI, - CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, - CUTSCENE_ITEM_TYPE_PRINT, - CUTSCENE_ITEM_TYPE_FADE, - CUTSCENE_ITEM_TYPE_EMOJI, - CUTSCENE_ITEM_TYPE_SHAKE, - CUTSCENE_ITEM_TYPE_MODAL, - CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, - CUTSCENE_ITEM_TYPE_MODAL_CLOSE, - CUTSCENE_ITEM_TYPE_KEYBOARD, - CUTSCENE_ITEM_TYPE_UI_SHOW, - - // Item - CUTSCENE_ITEM_TYPE_ITEM_GIVE, - - // Map area - CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, - CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, - CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, - - // Battle - CUTSCENE_ITEM_TYPE_BATTLE_START, - CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE, - CUTSCENE_ITEM_TYPE_BATTLE_WAIT_SELECTION, - CUTSCENE_ITEM_TYPE_BATTLE_POST_MOVE, - CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_DAMAGE, - CUTSCENE_ITEM_TYPE_BATTLE_FIGHTER_HEAL, - - // Save - CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, - CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, - - // Audio - CUTSCENE_ITEM_TYPE_AUDIO_PLAY, - CUTSCENE_ITEM_TYPE_AUDIO_STOP, - CUTSCENE_ITEM_TYPE_AUDIO_PAUSE, - CUTSCENE_ITEM_TYPE_AUDIO_RESUME, - CUTSCENE_ITEM_TYPE_AUDIO_FADE, - CUTSCENE_ITEM_TYPE_AUDIO_FADE_WAIT, - CUTSCENE_ITEM_TYPE_AUDIO_SET_PAN, - CUTSCENE_ITEM_TYPE_AUDIO_SET_LOOP, - CUTSCENE_ITEM_TYPE_AUDIO_SET, - - // Story - CUTSCENE_ITEM_TYPE_STORY_SET_FLAG, - CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG, - - // Party - CUTSCENE_ITEM_TYPE_PARTY_ADD_MEMBER, - CUTSCENE_ITEM_TYPE_PARTY_REMOVE_MEMBER, - CUTSCENE_ITEM_TYPE_PARTY_HEAL_MEMBER, - CUTSCENE_ITEM_TYPE_PARTY_HEAL_ALL, - - CUTSCENE_ITEM_TYPE_COUNT -} cutsceneitemtype_t; - -/** - * Shared no-op Load for item types with no JSON fields of their own to - * parse (IDLE, RESTART, MODAL_CLOSE) - always succeeds without touching - * item. - * - * @param itemObj The item's JSON object (unused). - * @param item Destination item (unused). - * @return Always errorOk(). - */ -errorret_t cutsceneItemLoadNoop( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/cutsceneprint.c b/src/dusk/rpg/cutscene/item/cutsceneprint.c deleted file mode 100644 index 45b084cf..00000000 --- a/src/dusk/rpg/cutscene/item/cutsceneprint.c +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutsceneitem.h" -#include "console/console.h" -#include "json/cutscenejsonfield.h" - -void cutscenePrintStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - consolePrint("%s", item->print.text); -} - -bool_t cutscenePrintUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutscenePrintLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonCopyString( - itemObj, "text", NULL, item->print.text, CUTSCENE_PRINT_MAX_CHARS - )) { - errorThrow("Cutscene item (PRINT): missing/too-long 'text'"); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/cutsceneprint.h b/src/dusk/rpg/cutscene/item/cutsceneprint.h deleted file mode 100644 index 640dab3e..00000000 --- a/src/dusk/rpg/cutscene/item/cutsceneprint.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -#define CUTSCENE_PRINT_MAX_CHARS 128 - -typedef struct { - char_t text[CUTSCENE_PRINT_MAX_CHARS]; -} cutsceneprint_t; - -/** - * Starts a print item (prints the item's text to the console). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutscenePrintStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a print item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutscenePrintUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a PRINT cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutscenePrintLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/entity/CMakeLists.txt b/src/dusk/rpg/cutscene/item/entity/CMakeLists.txt deleted file mode 100644 index e6dfbd2d..00000000 --- a/src/dusk/rpg/cutscene/item/entity/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutsceneentityteleport.c - cutsceneentitywalkto.c - cutsceneentityremove.c - cutsceneentityadd.c - cutsceneentityturn.c - cutsceneentitywalktoentity.c -) diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c deleted file mode 100644 index af8cca35..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.c +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "assert/assert.h" -#include "rpg/cutscene/item/json/cutscenejsonentitytype.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneEntityAddStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uint8_t entIndex = entityGetAvailable(); - assertTrue( - entIndex != 0xFF, "No available entity slots for CUTSCENE_ENTITY_ADD" - ); - - entity_t *entity = &ENTITIES[entIndex]; - entityInit(entity, item->entityAdd.entityType); - entityPositionSet(entity, item->entityAdd.position);// Also assigns chunk. - - CUTSCENE_SYSTEM.refs.entityLastCreated = entity; - CUTSCENE_SYSTEM.refs.entityLastRef = entity; -} - -bool_t cutsceneEntityAddUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneEntityAddLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - entitytype_t entityTypeValue; - if(!cutsceneJsonLookupEntityType( - yyjson_get_str(yyjson_obj_get(itemObj, "entityType")), &entityTypeValue - )) { - errorThrow("Cutscene item (ENTITY_ADD): invalid 'entityType'"); - } - item->entityAdd.entityType = entityTypeValue; - if(!cutsceneJsonReadWorldPos( - yyjson_obj_get(itemObj, "position"), &item->entityAdd.position - )) { - errorThrow("Cutscene item (ENTITY_ADD): 'position' must be [x, y, z]"); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h deleted file mode 100644 index 440cd415..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityadd.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/entity/entitytype.h" -#include "rpg/overworld/worldpos.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - entitytype_t entityType; - worldpos_t position; -} cutsceneentityadd_t; - -/** - * Starts an entity add step (spawns the entity into the world immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEntityAddStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an entity add step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneEntityAddUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ENTITY_ADD cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEntityAddLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c deleted file mode 100644 index 3a1a02c2..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "rpg/cutscene/item/json/cutscenejsonentityindex.h" - -void cutsceneEntityRemoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - entity_t *entity = - cutsceneSystemGetEntity(item->entityRemove.entityIndex); - entitySetChunk(entity, CHUNK_INDEX_INVALID); - entity->type = ENTITY_TYPE_NULL; -} - -bool_t cutsceneEntityRemoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneEntityRemoveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "entityIndex"), &item->entityRemove.entityIndex - )) { - errorThrow("Cutscene item (ENTITY_REMOVE): invalid 'entityIndex'"); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h deleted file mode 100644 index 26eb81d1..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityremove.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t entityIndex; -} cutsceneentityremove_t; - -/** - * Starts an entity remove step (removes the entity from the world immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEntityRemoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an entity remove step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneEntityRemoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ENTITY_REMOVE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEntityRemoveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c deleted file mode 100644 index b63608fb..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.c +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "rpg/cutscene/item/json/cutscenejsonentityindex.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneEntityTeleportStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - entityPositionSet( - cutsceneSystemGetEntity(item->entityTeleport.entityIndex), - item->entityTeleport.target - ); -} - -bool_t cutsceneEntityTeleportUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneEntityTeleportLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "entityIndex"), - &item->entityTeleport.entityIndex - )) { - errorThrow("Cutscene item (ENTITY_TELEPORT): invalid 'entityIndex'"); - } - if(!cutsceneJsonReadWorldPos( - yyjson_obj_get(itemObj, "target"), &item->entityTeleport.target - )) { - errorThrow( - "Cutscene item (ENTITY_TELEPORT): 'target' must be [x, y, z]" - ); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h deleted file mode 100644 index 029771d9..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityteleport.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/worldpos.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t entityIndex; - worldpos_t target; -} cutsceneentityteleport_t; - -/** - * Starts an entity teleport item (teleports the entity immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEntityTeleportStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an entity teleport item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneEntityTeleportUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ENTITY_TELEPORT cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEntityTeleportLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c deleted file mode 100644 index 00b7f7cb..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.c +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "rpg/cutscene/item/json/cutscenejsonentityindex.h" -#include "rpg/cutscene/item/json/cutscenejsonentitydir.h" - -void cutsceneEntityTurnStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { -} - -bool_t cutsceneEntityTurnUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - entity_t *entity = cutsceneSystemGetEntity(item->entityTurn.entityIndex); - if( - entity->direction == item->entityTurn.direction && - entity->animation == ENTITY_ANIM_IDLE - ) return true; - - entityTurn(entity, item->entityTurn.direction); - return false; -} - -errorret_t cutsceneEntityTurnLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "entityIndex"), &item->entityTurn.entityIndex - )) { - errorThrow("Cutscene item (ENTITY_TURN): invalid 'entityIndex'"); - } - entitydir_t dirValue; - if(!cutsceneJsonLookupEntityDir( - yyjson_get_str(yyjson_obj_get(itemObj, "direction")), &dirValue - )) { - errorThrow("Cutscene item (ENTITY_TURN): invalid 'direction'"); - } - item->entityTurn.direction = dirValue; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h deleted file mode 100644 index 164deaf7..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentityturn.h +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/entity/entitydir.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t entityIndex; - entitydir_t direction; -} cutsceneentityturn_t; - -/** - * Starts an entity turn step. The turn itself is driven from Update, since - * the entity may still be finishing a previous action. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEntityTurnStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an entity turn step, retrying entityTurn until it takes effect - * and its animation completes. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the entity is idle and facing the target direction. - */ -bool_t cutsceneEntityTurnUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ENTITY_TURN cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEntityTurnLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c deleted file mode 100644 index 16a82a08..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.c +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entitypathstep.h" -#include "rpg/cutscene/item/json/cutscenejsonentityindex.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" -#include "assert/assert.h" - -void cutsceneEntityWalkToStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - assertTrue( - item->entityWalkTo.count <= CUTSCENE_ENTITY_WALK_TO_MAX, - "Too many waypoints in CUTSCENE_ENTITY_WALK_TO" - ); - data->entityWalkTo.currentIndex = 0; -} - -bool_t cutsceneEntityWalkToUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uint8_t i = data->entityWalkTo.currentIndex; - entity_t *e = cutsceneSystemGetEntity(item->entityWalkTo.entityIndex); - if(!entityPathStep( - e, - item->entityWalkTo.positions[i], - item->entityWalkTo.walkAround - )) return false; - i++; - if(i < item->entityWalkTo.count) { - data->entityWalkTo.currentIndex = i; - return false; - } - return true; -} - -errorret_t cutsceneEntityWalkToLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "entityIndex"), &item->entityWalkTo.entityIndex - )) { - errorThrow("Cutscene item (ENTITY_WALK_TO): invalid 'entityIndex'"); - } - item->entityWalkTo.walkAround = - cutsceneJsonGetBool(itemObj, "walkAround", true); - - yyjson_val *positions = yyjson_obj_get(itemObj, "positions"); - size_t count = yyjson_arr_size(positions); - if( - !yyjson_is_arr(positions) || count == 0 || - count > CUTSCENE_ENTITY_WALK_TO_MAX - ) { - errorThrow( - "Cutscene item (ENTITY_WALK_TO): 'positions' must have 1-%u entries " - "of [x, y, z]", - (uint32_t)CUTSCENE_ENTITY_WALK_TO_MAX - ); - } - - yyjson_arr_iter iter = yyjson_arr_iter_with(positions); - yyjson_val *posVal; - size_t i = 0; - while((posVal = yyjson_arr_iter_next(&iter)) != NULL) { - if(!cutsceneJsonReadWorldPos(posVal, &item->entityWalkTo.positions[i])) { - errorThrow( - "Cutscene item (ENTITY_WALK_TO): position %u must be [x, y, z]", - (uint32_t)i - ); - } - i++; - } - item->entityWalkTo.count = (uint8_t)count; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h deleted file mode 100644 index 2ad31f09..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalkto.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/worldpos.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -// Maximum number of waypoints a single ENTITY_WALK_TO/ -// CUTSCENE_ENTITY_WALK_PATH may hold - generous relative to every current -// asset/C-authored cutscene (none use more than a couple waypoints). -#define CUTSCENE_ENTITY_WALK_TO_MAX 16 - -typedef struct { - uint8_t entityIndex; - worldpos_t positions[CUTSCENE_ENTITY_WALK_TO_MAX]; - uint8_t count; - bool_t walkAround; -} cutsceneentitywalkto_t; - -typedef struct { - uint8_t currentIndex; -} cutsceneentitywalktodata_t; - -/** - * Starts an entity walk-to item (resets the waypoint index). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEntityWalkToStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an entity walk-to item (steps the entity toward the next waypoint). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once all waypoints have been reached. - */ -bool_t cutsceneEntityWalkToUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ENTITY_WALK_TO cutscene item's JSON fields into item, writing - * its "positions" array directly into item->entityWalkTo.positions (at - * most CUTSCENE_ENTITY_WALK_TO_MAX entries). - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEntityWalkToLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c deleted file mode 100644 index 208beea7..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.c +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "rpg/entity/entitypathstep.h" -#include "rpg/overworld/map.h" -#include "rpg/cutscene/item/json/cutscenejsonentityindex.h" - -void cutsceneEntityWalkToEntityStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { -} - -bool_t cutsceneEntityWalkToEntityUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - entity_t *entity = cutsceneSystemGetEntity( - item->entityWalkToEntity.entityIndex - ); - entity_t *target = cutsceneSystemGetEntity( - item->entityWalkToEntity.targetEntityIndex - ); - - worldpos_t dest = { - .x = (worldunit_t)(target->position.x + item->entityWalkToEntity.offsetX), - .y = (worldunit_t)(target->position.y + item->entityWalkToEntity.offsetY), - .z = target->position.z - }; - - worldunit_t z; - if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z; - - return entityPathStep(entity, dest, true); -} - -errorret_t cutsceneEntityWalkToEntityLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "entityIndex"), - &item->entityWalkToEntity.entityIndex - ) || !cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "targetEntityIndex"), - &item->entityWalkToEntity.targetEntityIndex - )) { - errorThrow( - "Cutscene item (ENTITY_WALK_TO_ENTITY): invalid entity index" - ); - } - item->entityWalkToEntity.offsetX = - (worldunit_t)yyjson_get_int(yyjson_obj_get(itemObj, "offsetX")); - item->entityWalkToEntity.offsetY = - (worldunit_t)yyjson_get_int(yyjson_obj_get(itemObj, "offsetY")); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h b/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h deleted file mode 100644 index 2dc0c254..00000000 --- a/src/dusk/rpg/cutscene/item/entity/cutsceneentitywalktoentity.h +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/worldpos.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t entityIndex; - uint8_t targetEntityIndex; - worldunit_t offsetX; - worldunit_t offsetY; -} cutsceneentitywalktoentity_t; - -/** - * Starts an entity walk-to-entity item. No setup is needed, the destination - * is recomputed from the target's live position every Update. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEntityWalkToEntityStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an entity walk-to-entity item. Re-reads the target entity's - * current position each frame, applies the X/Y offset, resolves the - * destination Z from nearby terrain (to account for ramps), and steps - * the entity toward it. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the entity has reached the target's side. - */ -bool_t cutsceneEntityWalkToEntityUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ENTITY_WALK_TO_ENTITY cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEntityWalkToEntityLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/item/CMakeLists.txt b/src/dusk/rpg/cutscene/item/item/CMakeLists.txt deleted file mode 100644 index df5ebc21..00000000 --- a/src/dusk/rpg/cutscene/item/item/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutsceneitemgive.c -) diff --git a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c b/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c deleted file mode 100644 index 975aada8..00000000 --- a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.c +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/item/itemgive.h" -#include "ui/rpg/textbox/uitextboxmain.h" - -void cutsceneItemGiveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - itemGive(item->itemGive.item, item->itemGive.quantity); -} - -bool_t cutsceneItemGiveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return !uiTextboxMainIsActive(); -} - -errorret_t cutsceneItemGiveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - item->itemGive.item = - (itemid_t)yyjson_get_int(yyjson_obj_get(itemObj, "item")); - item->itemGive.quantity = - (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "quantity")); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h b/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h deleted file mode 100644 index 06762ba6..00000000 --- a/src/dusk/rpg/cutscene/item/item/cutsceneitemgive.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/item/item.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - itemid_t item; - uint8_t quantity; -} cutsceneitemgive_t; - -/** - * Starts a give-item step (adds the item to the player's backpack immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneItemGiveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a give-item step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneItemGiveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an ITEM_GIVE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneItemGiveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt b/src/dusk/rpg/cutscene/item/json/CMakeLists.txt deleted file mode 100644 index 60a8f286..00000000 --- a/src/dusk/rpg/cutscene/item/json/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenejsonenum.c - cutscenejsonfield.c - cutscenejsonentityindex.c - cutscenejsonareaid.c - cutscenejsonpauseflags.c - cutscenejsoneasing.c - cutscenejsonemoji.c - cutscenejsonentitytype.c - cutscenejsonentitydir.c - cutscenejsonbattlestate.c - cutscenejsonscenetype.c - cutscenejsonuiscreentype.c - cutscenejsonaudiochannel.c - cutscenejsonstoryflag.c -) diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c deleted file mode 100644 index 36d1db38..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.c +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonareaid.h" -#include "util/string.h" - -bool_t cutsceneJsonGetAreaId(yyjson_val *val, uint8_t *outAreaId) { - if(yyjson_is_str(val)) { - const char_t *name = yyjson_get_str(val); - if(stringCompareInsensitive(name, "LAST_CREATED") == 0) { - *outAreaId = CUTSCENE_JSON_AREA_SENTINEL_LAST_CREATED; - return true; - } - return false; - } - if(!yyjson_is_int(val)) return false; - *outAreaId = (uint8_t)yyjson_get_int(val); - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h deleted file mode 100644 index d12ee505..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonareaid.h +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "yyjson.h" - -// Sentinel accepted in place of a literal area id - see cutscenesystem.h's -// CUTSCENE_REF_AREA_LAST_CREATED. Kept local since it's a JSON-authoring -// convenience, not part of the item struct definitions themselves. -#define CUTSCENE_JSON_AREA_SENTINEL_LAST_CREATED ((uint8_t)0xFF) - -/** - * Resolves a JSON value that's either the "LAST_CREATED" map area sentinel - * string or a plain integer area id. - * - * @param val JSON value to resolve (a string or a number). - * @param outAreaId Set to the resolved area id on success. - * @return true if val resolved to a valid area id, false otherwise. - */ -bool_t cutsceneJsonGetAreaId(yyjson_val *val, uint8_t *outAreaId); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c deleted file mode 100644 index 065a6245..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.c +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonaudiochannel.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_AUDIO_CHANNEL_TABLE[] = { - { "BGM_0", AUDIO_MIXER_CHANNEL_BGM_0 }, - { "VOICE_0", AUDIO_MIXER_CHANNEL_VOICE_0 }, - { "VOICE_1", AUDIO_MIXER_CHANNEL_VOICE_1 }, - { "SFX_0", AUDIO_MIXER_CHANNEL_SFX_0 }, - { "SFX_1", AUDIO_MIXER_CHANNEL_SFX_1 }, - { "SFX_2", AUDIO_MIXER_CHANNEL_SFX_2 }, - { "SFX_3", AUDIO_MIXER_CHANNEL_SFX_3 }, -}; -#define CUTSCENE_JSON_AUDIO_CHANNEL_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_AUDIO_CHANNEL_TABLE) / \ - sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupAudioChannel( - const char_t *name, - audiomixerchannel_t *out -) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_AUDIO_CHANNEL_TABLE, - CUTSCENE_JSON_AUDIO_CHANNEL_TABLE_COUNT, name, &value - )) { - return false; - } - *out = (audiomixerchannel_t)value; - return true; -} - -errorret_t cutsceneJsonLoadAudioChannelOnly( - yyjson_val *itemObj, - audiomixerchannel_t *outChannel -) { - if(!cutsceneJsonLookupAudioChannel( - yyjson_get_str(yyjson_obj_get(itemObj, "channel")), outChannel - )) { - errorThrow("Cutscene item: invalid 'channel'"); - } - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h deleted file mode 100644 index c6e2234f..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonaudiochannel.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "error/error.h" -#include "yyjson.h" -#include "audio/mixer/audiomixerchannel.h" - -/** - * Looks up name (case-insensitively) against the audio mixer channel - * table. - * - * @param name Name to search for. - * @param out Set to the matching channel on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupAudioChannel( - const char_t *name, - audiomixerchannel_t *out -); - -/** - * Reads itemObj's "channel" field via cutsceneJsonLookupAudioChannel, - * erroring if missing/invalid - shared by every channel-only item type - * (AUDIO_STOP/PAUSE/RESUME/FADE_WAIT), which differ only in which item - * union field they store the result in. - * - * @param itemObj The item's JSON object. - * @param outChannel Set to the parsed channel on success. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneJsonLoadAudioChannelOnly( - yyjson_val *itemObj, - audiomixerchannel_t *outChannel -); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c deleted file mode 100644 index 60993646..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.c +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonbattlestate.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_BATTLE_STATE_TABLE[] = { - { "OPENING", BATTLE_STATE_OPENING }, - { "PRE_ROUND", BATTLE_STATE_PRE_ROUND }, - { "SELECTION", BATTLE_STATE_SELECTION }, - { "EXECUTING", BATTLE_STATE_EXECUTING }, - { "POST_ROUND", BATTLE_STATE_POST_ROUND }, - { "LOST", BATTLE_STATE_LOST }, - { "WON", BATTLE_STATE_WON }, - { "FLED", BATTLE_STATE_FLED }, - { "ENDING", BATTLE_STATE_ENDING }, -}; -#define CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_BATTLE_STATE_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupBattleState( - const char_t *name, - battlestate_t *out -) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_BATTLE_STATE_TABLE, - CUTSCENE_JSON_BATTLE_STATE_TABLE_COUNT, name, &value - )) { - return false; - } - *out = (battlestate_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h deleted file mode 100644 index 1cf302f2..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonbattlestate.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/battle/state/battlestate.h" - -/** - * Looks up name (case-insensitive) against every battlestate_t name. - * - * @param name Name to search for. - * @param out Set to the matching state on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupBattleState( - const char_t *name, - battlestate_t *out -); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c b/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c deleted file mode 100644 index b70df9be..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.c +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsoneasing.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_EASING_TABLE[] = { - { "LINEAR", EASING_LINEAR }, - { "IN_SINE", EASING_IN_SINE }, - { "OUT_SINE", EASING_OUT_SINE }, - { "IN_OUT_SINE", EASING_IN_OUT_SINE }, - { "IN_QUAD", EASING_IN_QUAD }, - { "OUT_QUAD", EASING_OUT_QUAD }, - { "IN_OUT_QUAD", EASING_IN_OUT_QUAD }, - { "IN_CUBIC", EASING_IN_CUBIC }, - { "OUT_CUBIC", EASING_OUT_CUBIC }, - { "IN_OUT_CUBIC", EASING_IN_OUT_CUBIC }, - { "IN_QUART", EASING_IN_QUART }, - { "OUT_QUART", EASING_OUT_QUART }, - { "IN_OUT_QUART", EASING_IN_OUT_QUART }, - { "IN_BACK", EASING_IN_BACK }, - { "OUT_BACK", EASING_OUT_BACK }, - { "IN_OUT_BACK", EASING_IN_OUT_BACK }, -}; -#define CUTSCENE_JSON_EASING_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_EASING_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupEasing(const char_t *name, easingtype_t *out) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_EASING_TABLE, CUTSCENE_JSON_EASING_TABLE_COUNT, name, - &value - )) { - return false; - } - *out = (easingtype_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h b/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h deleted file mode 100644 index 26d282e5..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsoneasing.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "animation/easing.h" - -/** - * Looks up name (case-insensitively) against the easing type table. - * - * @param name Name to search for. - * @param out Set to the matching easing type on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupEasing(const char_t *name, easingtype_t *out); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c deleted file mode 100644 index ce878f41..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.c +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonemoji.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_UI_EMOJI_TABLE[] = { - { "NULL", UI_EMOJI_NULL }, - { "QUESTION_MARK", UI_EMOJI_QUESTION_MARK }, - { "EXCLAMATION_MARK", UI_EMOJI_EXCLAMATION_MARK }, -}; -#define CUTSCENE_JSON_UI_EMOJI_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_UI_EMOJI_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupEmoji(const char_t *name, uiemojitype_t *out) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_UI_EMOJI_TABLE, CUTSCENE_JSON_UI_EMOJI_TABLE_COUNT, name, - &value - )) { - return false; - } - *out = (uiemojitype_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h deleted file mode 100644 index 55b483c3..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonemoji.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "ui/rpg/uiemoji.h" - -/** - * Looks up name (case-insensitively) against the emoji type table. - * - * @param name Name to search for. - * @param out Set to the matching emoji type on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupEmoji(const char_t *name, uiemojitype_t *out); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c deleted file mode 100644 index 85ac7baa..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonentitydir.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_ENTITY_DIR_TABLE[] = { - { "NORTH", ENTITY_DIR_NORTH }, - { "EAST", ENTITY_DIR_EAST }, - { "SOUTH", ENTITY_DIR_SOUTH }, - { "WEST", ENTITY_DIR_WEST }, - { "UP", ENTITY_DIR_UP }, - { "RIGHT", ENTITY_DIR_RIGHT }, - { "DOWN", ENTITY_DIR_DOWN }, - { "LEFT", ENTITY_DIR_LEFT }, -}; -#define CUTSCENE_JSON_ENTITY_DIR_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_ENTITY_DIR_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupEntityDir(const char_t *name, entitydir_t *out) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_ENTITY_DIR_TABLE, CUTSCENE_JSON_ENTITY_DIR_TABLE_COUNT, - name, &value - )) { - return false; - } - *out = (entitydir_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h deleted file mode 100644 index b3a3d226..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitydir.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/entity/entitydir.h" - -/** - * Looks up name (case-insensitively) against the entity direction table. - * - * @param name Name to search for. - * @param out Set to the matching direction on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupEntityDir(const char_t *name, entitydir_t *out); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c deleted file mode 100644 index 9cc076bf..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.c +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonentityindex.h" -#include "util/string.h" - -bool_t cutsceneJsonGetEntityIndex(yyjson_val *val, uint8_t *outIndex) { - if(yyjson_is_str(val)) { - const char_t *name = yyjson_get_str(val); - if(stringCompareInsensitive(name, "INTERACT") == 0) { - *outIndex = CUTSCENE_JSON_ENTITY_SENTINEL_INTERACT; - return true; - } - if(stringCompareInsensitive(name, "INTERACTED") == 0) { - *outIndex = CUTSCENE_JSON_ENTITY_SENTINEL_INTERACTED; - return true; - } - return false; - } - if(!yyjson_is_int(val)) return false; - *outIndex = (uint8_t)yyjson_get_int(val); - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h deleted file mode 100644 index e7740d63..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentityindex.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "yyjson.h" - -// Sentinels accepted in place of a literal entity index - see -// cutscenesystem.h's CUTSCENE_ENTITY_INTERACT/INTERACTED. Kept local -// since they're a JSON-authoring convenience, not part of the item -// struct definitions themselves. -#define CUTSCENE_JSON_ENTITY_SENTINEL_INTERACT ((uint8_t)0xFE) -#define CUTSCENE_JSON_ENTITY_SENTINEL_INTERACTED ((uint8_t)0xFD) - -/** - * Resolves a JSON value that's either an entity index sentinel string - * ("INTERACT"/"INTERACTED") or a plain integer entity index. - * - * @param val JSON value to resolve (a string or a number). - * @param outIndex Set to the resolved index on success. - * @return true if val resolved to a valid index, false otherwise. - */ -bool_t cutsceneJsonGetEntityIndex(yyjson_val *val, uint8_t *outIndex); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c deleted file mode 100644 index ac2f9e13..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.c +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonentitytype.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_ENTITY_TYPE_TABLE[] = { - { "NULL", ENTITY_TYPE_NULL }, - { "PLAYER", ENTITY_TYPE_PLAYER }, - { "NPC", ENTITY_TYPE_NPC }, - { "ITEM", ENTITY_TYPE_ITEM }, -}; -#define CUTSCENE_JSON_ENTITY_TYPE_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_ENTITY_TYPE_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupEntityType(const char_t *name, entitytype_t *out) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_ENTITY_TYPE_TABLE, CUTSCENE_JSON_ENTITY_TYPE_TABLE_COUNT, - name, &value - )) { - return false; - } - *out = (entitytype_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h deleted file mode 100644 index f08b45e8..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonentitytype.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/entity/entitytype.h" - -/** - * Looks up name (case-insensitively) against the entity type table. - * - * @param name Name to search for. - * @param out Set to the matching entity type on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupEntityType(const char_t *name, entitytype_t *out); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c deleted file mode 100644 index 3c7a1d67..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonenum.h" -#include "util/string.h" - -bool_t cutsceneJsonLookupEnum( - const cutscenejsonenumentry_t *table, - const size_t tableCount, - const char_t *name, - int32_t *outValue -) { - if(name == NULL) return false; - for(size_t i = 0; i < tableCount; i++) { - if(stringEquals(table[i].name, name)) { - *outValue = table[i].value; - return true; - } - } - return false; -} - -bool_t cutsceneJsonLookupEnumInsensitive( - const cutscenejsonenumentry_t *table, - const size_t tableCount, - const char_t *name, - int32_t *outValue -) { - if(name == NULL) return false; - for(size_t i = 0; i < tableCount; i++) { - if(stringCompareInsensitive(table[i].name, name) == 0) { - *outValue = table[i].value; - return true; - } - } - return false; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h deleted file mode 100644 index 8130007d..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonenum.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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 *name; - int32_t value; -} cutscenejsonenumentry_t; - -/** - * Looks up name in a {name, value} table via stringEquals - exact case, - * used only for the top-level item "type" name (the one enum-ish field - * the original tool's ITEM_TYPE dict looked up without .upper()). - * - * @param table Table to search. - * @param tableCount Number of entries in table. - * @param name Name to search for. - * @param outValue Set to the matching entry's value on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupEnum( - const cutscenejsonenumentry_t *table, - const size_t tableCount, - const char_t *name, - int32_t *outValue -); - -/** - * Looks up name in a {name, value} table via stringCompareInsensitive - - * every other enum-ish field (pause flags, entity/scene/battle/audio - * enums, ...) is authored case-insensitively, matching the original - * tool's ENUM_TABLE[name.upper()] lookups. - * - * @param table Table to search. - * @param tableCount Number of entries in table. - * @param name Name to search for. - * @param outValue Set to the matching entry's value on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupEnumInsensitive( - const cutscenejsonenumentry_t *table, - const size_t tableCount, - const char_t *name, - int32_t *outValue -); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c deleted file mode 100644 index 25b78d86..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.c +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonfield.h" -#include "util/string.h" - -bool_t cutsceneJsonCopyString( - yyjson_val *obj, - const char_t *key, - const char_t *defaultValue, - char_t *dest, - const size_t destCapacity -) { - yyjson_val *val = yyjson_obj_get(obj, key); - const char_t *str = yyjson_get_str(val); - size_t len; - if(str == NULL) { - if(defaultValue == NULL) return false; - str = defaultValue; - len = (size_t)stringFormat(NULL, 0, "%s", str); - } else { - len = yyjson_get_len(val); - } - if(len >= destCapacity) return false; - stringCopy(dest, str, destCapacity); - return true; -} - -bool_t cutsceneJsonReadWorldPos(yyjson_val *arr, worldpos_t *out) { - if(!yyjson_is_arr(arr) || yyjson_arr_size(arr) != 3) return false; - yyjson_val *x = yyjson_arr_get(arr, 0); - yyjson_val *y = yyjson_arr_get(arr, 1); - yyjson_val *z = yyjson_arr_get(arr, 2); - if(!yyjson_is_int(x) || !yyjson_is_int(y) || !yyjson_is_int(z)) return false; - out->x = (worldunit_t)yyjson_get_int(x); - out->y = (worldunit_t)yyjson_get_int(y); - out->z = (worldunit_t)yyjson_get_int(z); - return true; -} - -int32_t cutsceneJsonGetInt( - yyjson_val *obj, - const char_t *key, - const int32_t defaultValue -) { - yyjson_val *val = yyjson_obj_get(obj, key); - return val == NULL ? defaultValue : (int32_t)yyjson_get_int(val); -} - -float_t cutsceneJsonGetFloat( - yyjson_val *obj, - const char_t *key, - const float_t defaultValue -) { - yyjson_val *val = yyjson_obj_get(obj, key); - return val == NULL ? defaultValue : (float_t)yyjson_get_num(val); -} - -bool_t cutsceneJsonGetBool( - yyjson_val *obj, - const char_t *key, - const bool_t defaultValue -) { - yyjson_val *val = yyjson_obj_get(obj, key); - return val == NULL ? defaultValue : yyjson_get_bool(val); -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h deleted file mode 100644 index a73f07fe..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonfield.h +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/overworld/worldpos.h" -#include "yyjson.h" - -/** - * Reads obj[key] as a string (falling back to defaultValue if the key is - * absent and defaultValue is non-NULL) into dest, a fixed-capacity - * char_t[destCapacity] item field. Fails rather than truncating/asserting - * if the string doesn't fit - this is untrusted, authored file content, - * not an in-memory invariant. - * - * @param obj The item's JSON object. - * @param key Field name to read. - * @param defaultValue Value to use if key is absent, or NULL to require it. - * @param dest Destination buffer. - * @param destCapacity Capacity of dest, including the null terminator. - * @return true on success, false if the field is missing (with no - * default) or exceeds destCapacity. - */ -bool_t cutsceneJsonCopyString( - yyjson_val *obj, - const char_t *key, - const char_t *defaultValue, - char_t *dest, - const size_t destCapacity -); - -/** - * Reads a 3-element JSON array of integers into a worldpos_t. - * - * @param arr JSON array value, expected to hold exactly 3 numbers. - * @param out Set to the decoded position on success. - * @return true if arr was a valid 3-element numeric array, false otherwise. - */ -bool_t cutsceneJsonReadWorldPos(yyjson_val *arr, worldpos_t *out); - -/** - * Reads obj[key] as an integer, falling back to defaultValue if the key - * is absent. - * - * @param obj The item's JSON object. - * @param key Field name to read. - * @param defaultValue Value to use if key is absent. - * @return The field's value, or defaultValue if absent. - */ -int32_t cutsceneJsonGetInt( - yyjson_val *obj, - const char_t *key, - const int32_t defaultValue -); - -/** - * Reads obj[key] as a float, falling back to defaultValue if the key is - * absent. - * - * @param obj The item's JSON object. - * @param key Field name to read. - * @param defaultValue Value to use if key is absent. - * @return The field's value, or defaultValue if absent. - */ -float_t cutsceneJsonGetFloat( - yyjson_val *obj, - const char_t *key, - const float_t defaultValue -); - -/** - * Reads obj[key] as a boolean, falling back to defaultValue if the key is - * absent. - * - * @param obj The item's JSON object. - * @param key Field name to read. - * @param defaultValue Value to use if key is absent. - * @return The field's value, or defaultValue if absent. - */ -bool_t cutsceneJsonGetBool( - yyjson_val *obj, - const char_t *key, - const bool_t defaultValue -); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c deleted file mode 100644 index 29eb1b54..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.c +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonpauseflags.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_PAUSE_FLAG_TABLE[] = { - { "NPC", CUTSCENE_PAUSE_NPC }, - { "PLAYER", CUTSCENE_PAUSE_PLAYER }, - { "WORLD", CUTSCENE_PAUSE_WORLD }, - { "BATTLE", CUTSCENE_PAUSE_BATTLE }, -}; -#define CUTSCENE_JSON_PAUSE_FLAG_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_PAUSE_FLAG_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonParsePauseFlags(yyjson_val *arr, cutscenepause_t *out) { - if(!yyjson_is_arr(arr)) return false; - - cutscenepause_t value = CUTSCENE_PAUSE_NONE; - yyjson_arr_iter iter = yyjson_arr_iter_with(arr); - yyjson_val *flagVal; - while((flagVal = yyjson_arr_iter_next(&iter)) != NULL) { - int32_t flagValue; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_PAUSE_FLAG_TABLE, CUTSCENE_JSON_PAUSE_FLAG_TABLE_COUNT, - yyjson_get_str(flagVal), &flagValue - )) { - return false; - } - value |= (cutscenepause_t)flagValue; - } - *out = value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h deleted file mode 100644 index b6bccb6b..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonpauseflags.h +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/cutscene/cutscenepause.h" -#include "yyjson.h" - -/** - * Parses a JSON array of pause flag name strings, OR-ing each into out. - * Shared by the top-level "pause" field (cutsceneParseDoc) and the - * SET_PAUSE item's "flags" field - both authored as the same shape. - * - * @param arr JSON array of pause flag name strings. - * @param out Set to the OR'd-together flags on success (CUTSCENE_PAUSE_NONE - * if arr is empty). - * @return true on success, false if arr is not an array or contains an - * invalid flag name. - */ -bool_t cutsceneJsonParsePauseFlags(yyjson_val *arr, cutscenepause_t *out); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c deleted file mode 100644 index 70a5616f..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.c +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonscenetype.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_SCENE_TYPE_TABLE[] = { - { "NULL", SCENE_TYPE_NULL }, - { "INITIAL", SCENE_TYPE_INITIAL }, - { "OVERWORLD", SCENE_TYPE_OVERWORLD }, - { "BATTLE", SCENE_TYPE_BATTLE }, -}; -#define CUTSCENE_JSON_SCENE_TYPE_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_SCENE_TYPE_TABLE) / sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupSceneType(const char_t *name, scenetype_t *out) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_SCENE_TYPE_TABLE, CUTSCENE_JSON_SCENE_TYPE_TABLE_COUNT, - name, &value - )) { - return false; - } - *out = (scenetype_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h deleted file mode 100644 index 58a38271..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonscenetype.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "scene/scenetype.h" - -/** - * Looks up name (case-insensitively) against the scene type table. - * - * @param name Name to search for. - * @param out Set to the matching scene type on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupSceneType(const char_t *name, scenetype_t *out); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c deleted file mode 100644 index 2eb14384..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.c +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonstoryflag.h" - -bool_t cutsceneJsonGetStoryFlag(yyjson_val *val, storyflag_t *outFlag) { - if(!yyjson_is_str(val)) return false; - - storyflag_t flag = storyGetFlagByName(yyjson_get_str(val)); - if(flag >= STORY_FLAG_COUNT) return false; - - *outFlag = flag; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h deleted file mode 100644 index d5152fde..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonstoryflag.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "yyjson.h" -#include "rpg/story/story.h" - -/** - * Resolves a JSON string value naming a story flag (via storyGetFlagByName) - * into a storyflag_t. - * - * @param val JSON value to resolve (a string). - * @param outFlag Set to the resolved story flag id on success. - * @return true if val resolved to a known story flag, false otherwise. - */ -bool_t cutsceneJsonGetStoryFlag(yyjson_val *val, storyflag_t *outFlag); diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c b/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c deleted file mode 100644 index 4d53a349..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "cutscenejsonuiscreentype.h" -#include "cutscenejsonenum.h" - -static const cutscenejsonenumentry_t CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE[] = { - { "MAIN_MENU", UI_SCREEN_TYPE_MAIN_MENU }, -}; -#define CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE_COUNT \ - (sizeof(CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE) / \ - sizeof(cutscenejsonenumentry_t)) - -bool_t cutsceneJsonLookupUiScreenType( - const char_t *name, - uiscreentype_t *out -) { - int32_t value; - if(!cutsceneJsonLookupEnumInsensitive( - CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE, - CUTSCENE_JSON_UI_SCREEN_TYPE_TABLE_COUNT, name, &value - )) { - return false; - } - *out = (uiscreentype_t)value; - return true; -} diff --git a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h b/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h deleted file mode 100644 index fd1f984d..00000000 --- a/src/dusk/rpg/cutscene/item/json/cutscenejsonuiscreentype.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/cutscene/item/ui/cutsceneuishow.h" - -/** - * Looks up name (case-insensitively) against the UI screen type table. - * - * @param name Name to search for. - * @param out Set to the matching screen type on success. - * @return true if a matching entry was found, false otherwise. - */ -bool_t cutsceneJsonLookupUiScreenType( - const char_t *name, - uiscreentype_t *out -); diff --git a/src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt b/src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt deleted file mode 100644 index cd32d7f6..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenemapareaadd.c - cutscenemaparearemove.c - cutscenemapareawait.c -) diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c deleted file mode 100644 index 8b408a0b..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.c +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" - -void cutsceneMapAreaAddStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - CUTSCENE_SYSTEM.refs.areaLastCreated = mapAreaAdd( - item->mapAreaAdd.min, - item->mapAreaAdd.max, - item->mapAreaAdd.callback, - item->mapAreaAdd.notify, - item->mapAreaAdd.trigger - ); -} - -bool_t cutsceneMapAreaAddUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h deleted file mode 100644 index b4135a65..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareaadd.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/overworld/maparea.h" -#include "rpg/cutscene/item/maparea/cutscenemapareawait.h" -#include "rpg/cutscene/item/maparea/cutscenemaparearemove.h" - -typedef struct { - worldpos_t min; - worldpos_t max; - mapareacallback_t callback; - uint8_t notify; - uint8_t trigger; -} cutscenemapareaadd_t; - -/** - * Starts a map area add step (adds the area immediately, storing its ID - * in CUTSCENE_SYSTEM.refs.areaLastCreated). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneMapAreaAddStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a map area add step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneMapAreaAddUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c b/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c deleted file mode 100644 index 47a0065d..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.c +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/overworld/maparea.h" -#include "rpg/cutscene/item/json/cutscenejsonareaid.h" - -void cutsceneMapAreaRemoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - mapAreaRemove(cutsceneSystemGetAreaId(item->mapAreaRemove.areaId)); -} - -bool_t cutsceneMapAreaRemoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneMapAreaRemoveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetAreaId( - yyjson_obj_get(itemObj, "areaId"), &item->mapAreaRemove.areaId - )) { - errorThrow("Cutscene item (MAP_AREA_REMOVE): invalid 'areaId'"); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h b/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h deleted file mode 100644 index c85ba9be..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/cutscenemaparearemove.h +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t areaId; -} cutscenemaparearemove_t; - -/** - * Starts a map area remove step (removes the area immediately). Accepts - * CUTSCENE_REF_AREA_LAST_CREATED in place of a literal area ID. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneMapAreaRemoveStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a map area remove step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneMapAreaRemoveUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a MAP_AREA_REMOVE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneMapAreaRemoveLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c deleted file mode 100644 index 1acbb963..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.c +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/overworld/maparea.h" -#include "assert/assert.h" -#include "rpg/cutscene/item/json/cutscenejsonareaid.h" - -void cutsceneMapAreaWaitStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - assertTrue( - item->mapAreaWait.count <= CUTSCENE_MAP_AREA_WAIT_MAX, - "Too many areas in CUTSCENE_MAP_AREA_WAIT" - ); - - for(uint8_t i = 0; i < item->mapAreaWait.count; i++) { - uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]); - data->mapAreaWait.baseline[i] = MAP_AREAS[areaId].triggerCount; - } -} - -bool_t cutsceneMapAreaWaitUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - for(uint8_t i = 0; i < item->mapAreaWait.count; i++) { - uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]); - if(MAP_AREAS[areaId].triggerCount != data->mapAreaWait.baseline[i]) { - return true; - } - } - - return false; -} - -errorret_t cutsceneMapAreaWaitLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - yyjson_val *areaIds = yyjson_obj_get(itemObj, "areaIds"); - size_t count = yyjson_arr_size(areaIds); - if( - !yyjson_is_arr(areaIds) || count == 0 || - count > CUTSCENE_MAP_AREA_WAIT_MAX - ) { - errorThrow( - "Cutscene item (MAP_AREA_WAIT): 'areaIds' must have 1-%u entries", - (uint32_t)CUTSCENE_MAP_AREA_WAIT_MAX - ); - } - yyjson_arr_iter iter = yyjson_arr_iter_with(areaIds); - yyjson_val *idVal; - size_t i = 0; - while((idVal = yyjson_arr_iter_next(&iter)) != NULL) { - if(!cutsceneJsonGetAreaId(idVal, &item->mapAreaWait.areaIds[i])) { - errorThrow( - "Cutscene item (MAP_AREA_WAIT): areaIds[%u] is invalid", (uint32_t)i - ); - } - i++; - } - item->mapAreaWait.count = (uint8_t)count; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h b/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h deleted file mode 100644 index 0787170f..00000000 --- a/src/dusk/rpg/cutscene/item/maparea/cutscenemapareawait.h +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** Maximum number of areas a single CUTSCENE_MAP_AREA_WAIT may watch. */ -#define CUTSCENE_MAP_AREA_WAIT_MAX 4 - -typedef struct { - uint8_t areaIds[CUTSCENE_MAP_AREA_WAIT_MAX]; - uint8_t count; -} cutscenemapareawait_t; - -typedef struct { - uint32_t baseline[CUTSCENE_MAP_AREA_WAIT_MAX]; -} cutscenemapareawaitdata_t; - -/** - * Starts a map area wait step, snapshotting each watched area's current - * trigger count. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneMapAreaWaitStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a map area wait step, completing once any watched area's - * trigger count has changed since Start (i.e. its callback fired). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once any watched area has been triggered. - */ -bool_t cutsceneMapAreaWaitUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a MAP_AREA_WAIT cutscene item's JSON fields into item, writing its - * "areaIds" array directly into item->mapAreaWait.areaIds. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneMapAreaWaitLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/party/CMakeLists.txt b/src/dusk/rpg/cutscene/item/party/CMakeLists.txt deleted file mode 100644 index 6f383c3e..00000000 --- a/src/dusk/rpg/cutscene/item/party/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenepartyaddmember.c - cutscenepartyremovemember.c - cutscenepartyhealmember.c - cutscenepartyhealall.c -) diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c deleted file mode 100644 index 9612132e..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.c +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/party.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutscenePartyAddMemberStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - partyAddMember(item->partyAddMember.memberIndex); -} - -bool_t cutscenePartyAddMemberUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutscenePartyAddMemberLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - char_t key[PARTY_KEY_LENGTH_MAX]; - if(!cutsceneJsonCopyString(itemObj, "key", NULL, key, PARTY_KEY_LENGTH_MAX)) { - errorThrow("Cutscene item (PARTY_ADD_MEMBER): missing/invalid 'key'"); - } - - const uint8_t memberIndex = partyGetMemberIndexByKey(key); - if(memberIndex == PARTY_MEMBER_NOT_FOUND) { - errorThrow("Cutscene item (PARTY_ADD_MEMBER): no member with key '%s'", key); - } - item->partyAddMember.memberIndex = memberIndex; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h deleted file mode 100644 index 28768ef1..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyaddmember.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - // Resolved from the JSON 'key' field at load time (see - // partyGetMemberIndexByKey) - not a raw JSON field itself. - uint8_t memberIndex; -} cutscenepartyaddmember_t; - -/** - * Starts a PARTY_ADD_MEMBER item, activating the configured existing - * roster member into the current save slot's active party order (see - * partyAddMember). No-op if already active or there's no free order slot. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutscenePartyAddMemberStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a PARTY_ADD_MEMBER item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutscenePartyAddMemberUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a PARTY_ADD_MEMBER cutscene item's JSON fields into item: - * 'key', a required string matching a party.jsonc member key, resolved to - * its roster index via partyGetMemberIndexByKey. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutscenePartyAddMemberLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c deleted file mode 100644 index 3b3aa111..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.c +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/party.h" - -void cutscenePartyHealAllStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - partyHealAll(); -} - -bool_t cutscenePartyHealAllUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h deleted file mode 100644 index e2d12858..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealall.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -/** - * Starts a PARTY_HEAL_ALL item, fully restoring every occupied slot in - * the current save slot's party roster (see partyHealAll). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutscenePartyHealAllStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a PARTY_HEAL_ALL item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutscenePartyHealAllUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c deleted file mode 100644 index a4091e4e..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.c +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/party.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutscenePartyHealMemberStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - partyHealMember(item->partyHealMember.memberIndex); -} - -bool_t cutscenePartyHealMemberUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutscenePartyHealMemberLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - char_t key[PARTY_KEY_LENGTH_MAX]; - if(!cutsceneJsonCopyString(itemObj, "key", NULL, key, PARTY_KEY_LENGTH_MAX)) { - errorThrow("Cutscene item (PARTY_HEAL_MEMBER): missing/invalid 'key'"); - } - - const uint8_t memberIndex = partyGetMemberIndexByKey(key); - if(memberIndex == PARTY_MEMBER_NOT_FOUND) { - errorThrow("Cutscene item (PARTY_HEAL_MEMBER): no member with key '%s'", key); - } - item->partyHealMember.memberIndex = memberIndex; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h deleted file mode 100644 index 3a36a86c..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyhealmember.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - // Resolved from the JSON 'key' field at load time (see - // partyGetMemberIndexByKey) - not a raw JSON field itself. - uint8_t memberIndex; -} cutscenepartyhealmember_t; - -/** - * Starts a PARTY_HEAL_MEMBER item, fully restoring the configured member's - * health/mp and reviving it if dead (see partyHealMember). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutscenePartyHealMemberStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a PARTY_HEAL_MEMBER item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutscenePartyHealMemberUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a PARTY_HEAL_MEMBER cutscene item's JSON fields into item: - * 'key', a required string matching a party.jsonc member key, resolved to - * its roster index via partyGetMemberIndexByKey. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutscenePartyHealMemberLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c b/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c deleted file mode 100644 index e636d75e..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.c +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/battle/party.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutscenePartyRemoveMemberStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - partyRemoveMember(item->partyRemoveMember.memberIndex); -} - -bool_t cutscenePartyRemoveMemberUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutscenePartyRemoveMemberLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - char_t key[PARTY_KEY_LENGTH_MAX]; - if(!cutsceneJsonCopyString(itemObj, "key", NULL, key, PARTY_KEY_LENGTH_MAX)) { - errorThrow("Cutscene item (PARTY_REMOVE_MEMBER): missing/invalid 'key'"); - } - - const uint8_t memberIndex = partyGetMemberIndexByKey(key); - if(memberIndex == PARTY_MEMBER_NOT_FOUND) { - errorThrow( - "Cutscene item (PARTY_REMOVE_MEMBER): no member with key '%s'", key - ); - } - item->partyRemoveMember.memberIndex = memberIndex; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h b/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h deleted file mode 100644 index 91809aea..00000000 --- a/src/dusk/rpg/cutscene/item/party/cutscenepartyremovemember.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - // Resolved from the JSON 'key' field at load time (see - // partyGetMemberIndexByKey) - not a raw JSON field itself. - uint8_t memberIndex; -} cutscenepartyremovemember_t; - -/** - * Starts a PARTY_REMOVE_MEMBER item, deactivating the configured member - * from the current save slot's active party order (see partyRemoveMember) - * - the roster member itself still exists, just no longer active. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutscenePartyRemoveMemberStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a PARTY_REMOVE_MEMBER item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutscenePartyRemoveMemberUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a PARTY_REMOVE_MEMBER cutscene item's JSON fields into item: - * 'key', a required string matching a party.jsonc member key, resolved to - * its roster index via partyGetMemberIndexByKey. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutscenePartyRemoveMemberLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/save/CMakeLists.txt b/src/dusk/rpg/cutscene/item/save/CMakeLists.txt deleted file mode 100644 index 7ca216ee..00000000 --- a/src/dusk/rpg/cutscene/item/save/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenesavedevicecheck.c - cutscenesaveloadallslots.c -) diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c b/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c deleted file mode 100644 index 76d3a3fa..00000000 --- a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.c +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" -#include "save/save.h" - -void cutsceneSaveDeviceCheckCallback(savedevice_t *device, void *user) { - const cutsceneitem_t *item = cutsceneSystemGetCurrentItem(); - cutsceneSystemGoTo( - device != NULL ? - item->saveDeviceCheck.successMarker : - item->saveDeviceCheck.failureMarker - ); -} - -void cutsceneSaveDeviceCheckStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - saveFindAvailableDevice(cutsceneSaveDeviceCheckCallback, NULL); -} - -bool_t cutsceneSaveDeviceCheckUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} - -errorret_t cutsceneSaveDeviceCheckLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if( - !cutsceneJsonCopyString( - itemObj, "successMarker", NULL, item->saveDeviceCheck.successMarker, - CUTSCENE_MARKER_NAME_MAX - ) || - !cutsceneJsonCopyString( - itemObj, "failureMarker", NULL, item->saveDeviceCheck.failureMarker, - CUTSCENE_MARKER_NAME_MAX - ) - ) { - errorThrow( - "Cutscene item (SAVE_DEVICE_CHECK): needs 'successMarker'/" - "'failureMarker'" - ); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h b/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h deleted file mode 100644 index 3998dd4c..00000000 --- a/src/dusk/rpg/cutscene/item/save/cutscenesavedevicecheck.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/cutscene/item/control/cutscenemarker.h" - -typedef struct { - char_t successMarker[CUTSCENE_MARKER_NAME_MAX]; - char_t failureMarker[CUTSCENE_MARKER_NAME_MAX]; -} cutscenesavedevicecheck_t; - -/** - * Starts a save-device-check item: (re)requests an available save - * device via saveFindAvailableDevice. Never completes on its own - see - * cutsceneSaveDeviceCheckUpdate. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneSaveDeviceCheckStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a save-device-check item. Like a CUTSCENE_MODAL_OPTIONS item, - * this never completes on its own - once saveFindAvailableDevice's - * callback fires, it jumps straight to the item's successMarker or - * failureMarker via cutsceneSystemGoTo. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneSaveDeviceCheckUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a SAVE_DEVICE_CHECK cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneSaveDeviceCheckLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c b/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c deleted file mode 100644 index 1b368723..00000000 --- a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.c +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" -#include "save/save.h" - -void cutsceneSaveLoadAllSlotsStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - errorret_t result = saveLoadAllSlots(); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - cutsceneSystemGoTo(item->saveLoadAllSlots.failureMarker); - return; - } - - cutsceneSystemGoTo(item->saveLoadAllSlots.successMarker); -} - -bool_t cutsceneSaveLoadAllSlotsUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} - -errorret_t cutsceneSaveLoadAllSlotsLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if( - !cutsceneJsonCopyString( - itemObj, "successMarker", NULL, item->saveLoadAllSlots.successMarker, - CUTSCENE_MARKER_NAME_MAX - ) || - !cutsceneJsonCopyString( - itemObj, "failureMarker", NULL, item->saveLoadAllSlots.failureMarker, - CUTSCENE_MARKER_NAME_MAX - ) - ) { - errorThrow( - "Cutscene item (SAVE_LOAD_ALL_SLOTS): needs 'successMarker'/" - "'failureMarker'" - ); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h b/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h deleted file mode 100644 index 18b32b9d..00000000 --- a/src/dusk/rpg/cutscene/item/save/cutscenesaveloadallslots.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/cutscene/item/control/cutscenemarker.h" - -typedef struct { - char_t successMarker[CUTSCENE_MARKER_NAME_MAX]; - char_t failureMarker[CUTSCENE_MARKER_NAME_MAX]; -} cutscenesaveloadallslots_t; - -/** - * Starts a save-load-all-slots item: calls saveLoadAllSlots() and jumps - * straight to the item's successMarker or failureMarker via - * cutsceneSystemGoTo, depending on the result. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneSaveLoadAllSlotsStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a save-load-all-slots item. Never completes on its own - - * Start already jumps to whichever marker applies before this would - * ever run. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneSaveLoadAllSlotsUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a SAVE_LOAD_ALL_SLOTS cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneSaveLoadAllSlotsLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/story/CMakeLists.txt b/src/dusk/rpg/cutscene/item/story/CMakeLists.txt deleted file mode 100644 index c84faff0..00000000 --- a/src/dusk/rpg/cutscene/item/story/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenestorysetflag.c - cutscenestorywaitflag.c -) diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c b/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c deleted file mode 100644 index 0939f7bc..00000000 --- a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.c +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/item/json/cutscenejsonstoryflag.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneStorySetFlagStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - storySetFlagValue(item->storySetFlag.flag, item->storySetFlag.value); -} - -bool_t cutsceneStorySetFlagUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneStorySetFlagLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - yyjson_val *flagVal = yyjson_obj_get(itemObj, "flag"); - if(!cutsceneJsonGetStoryFlag(flagVal, &item->storySetFlag.flag)) { - errorThrow("Cutscene item (STORY_SET_FLAG): invalid or missing 'flag'"); - } - - item->storySetFlag.value = - (storyflagval_t)cutsceneJsonGetInt(itemObj, "value", 1); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h b/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h deleted file mode 100644 index 8fceb9d8..00000000 --- a/src/dusk/rpg/cutscene/item/story/cutscenestorysetflag.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/story/story.h" - -typedef struct { - storyflag_t flag; - storyflagval_t value; -} cutscenestorysetflag_t; - -/** - * Starts a story set-flag step, immediately writing the given value to the - * given story flag. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneStorySetFlagStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a story set-flag step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneStorySetFlagUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a STORY_SET_FLAG cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneStorySetFlagLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c b/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c deleted file mode 100644 index df7f65e7..00000000 --- a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.c +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/item/json/cutscenejsonstoryflag.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -bool_t cutsceneStoryWaitFlagUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return storyGetFlagValue(item->storyWaitFlag.flag) == item->storyWaitFlag.value; -} - -errorret_t cutsceneStoryWaitFlagLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - yyjson_val *flagVal = yyjson_obj_get(itemObj, "flag"); - if(!cutsceneJsonGetStoryFlag(flagVal, &item->storyWaitFlag.flag)) { - errorThrow("Cutscene item (STORY_WAIT_FLAG): invalid or missing 'flag'"); - } - - item->storyWaitFlag.value = - (storyflagval_t)cutsceneJsonGetInt(itemObj, "value", 1); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h b/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h deleted file mode 100644 index d31eeaa0..00000000 --- a/src/dusk/rpg/cutscene/item/story/cutscenestorywaitflag.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/story/story.h" - -typedef struct { - storyflag_t flag; - storyflagval_t value; -} cutscenestorywaitflag_t; - -/** - * Updates a story wait-flag step, completing once the given story flag's - * current value matches the given value. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the flag's current value matches, false otherwise. - */ -bool_t cutsceneStoryWaitFlagUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a STORY_WAIT_FLAG cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneStoryWaitFlagLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt b/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt deleted file mode 100644 index 5702ce7a..00000000 --- a/src/dusk/rpg/cutscene/item/ui/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - cutscenetext.c - cutscenetextmini.c - cutscenetextminihide.c - cutscenefade.c - cutsceneemoji.c - cutsceneshake.c - cutscenemodal.c - cutscenemodaloptionsmarkers.c - cutscenekeyboard.c - cutsceneuishow.c -) diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c b/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c deleted file mode 100644 index 43b4466f..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.c +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "rpg/cutscene/item/json/cutscenejsonentityindex.h" -#include "rpg/cutscene/item/json/cutscenejsonemoji.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneEmojiStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - entity_t *entity = cutsceneSystemGetEntity(item->emoji.entityIndex); - uiEmojiAdd(entity->id, item->emoji.duration, item->emoji.emojiType); -} - -bool_t cutsceneEmojiUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneEmojiLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonGetEntityIndex( - yyjson_obj_get(itemObj, "entityIndex"), &item->emoji.entityIndex - )) { - errorThrow("Cutscene item (EMOJI): invalid 'entityIndex'"); - } - item->emoji.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f); - uiemojitype_t emojiValue; - if(!cutsceneJsonLookupEmoji( - yyjson_get_str(yyjson_obj_get(itemObj, "emojiType")), &emojiValue - )) { - errorThrow("Cutscene item (EMOJI): invalid 'emojiType'"); - } - item->emoji.emojiType = emojiValue; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h b/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h deleted file mode 100644 index 39d5fc5d..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutsceneemoji.h +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "ui/rpg/uiemoji.h" - -typedef struct { - uint8_t entityIndex; - float_t duration; - uiemojitype_t emojiType; -} cutsceneemoji_t; - -/** - * Starts an emoji step (shows an emoji above the entity for the given - * duration, then completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneEmojiStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates an emoji step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneEmojiUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses an EMOJI cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneEmojiLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenefade.c b/src/dusk/rpg/cutscene/item/ui/cutscenefade.c deleted file mode 100644 index 3b796b10..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenefade.c +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "ui/overlay/uifullbox.h" -#include "rpg/cutscene/item/json/cutscenejsoneasing.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneFadeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uiFullboxTransition( - &UI_FULLBOX_OVER, - item->fade.from, - item->fade.to, - item->fade.duration, - item->fade.easing - ); -} - -bool_t cutsceneFadeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return !( - UI_FULLBOX_OVER.duration > 0.0f && - UI_FULLBOX_OVER.time < UI_FULLBOX_OVER.duration - ); -} - -errorret_t cutsceneFadeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - yyjson_val *from = yyjson_obj_get(itemObj, "from"); - yyjson_val *to = yyjson_obj_get(itemObj, "to"); - if(!yyjson_is_arr(from) || yyjson_arr_size(from) != 4 || - !yyjson_is_arr(to) || yyjson_arr_size(to) != 4) { - errorThrow("Cutscene item (FADE): 'from'/'to' must each be [r, g, b, a]"); - } - item->fade.from.r = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 0)); - item->fade.from.g = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 1)); - item->fade.from.b = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 2)); - item->fade.from.a = (uint8_t)yyjson_get_int(yyjson_arr_get(from, 3)); - item->fade.to.r = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 0)); - item->fade.to.g = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 1)); - item->fade.to.b = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 2)); - item->fade.to.a = (uint8_t)yyjson_get_int(yyjson_arr_get(to, 3)); - item->fade.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f); - - const char_t *easingName = - yyjson_get_str(yyjson_obj_get(itemObj, "easing")); - easingtype_t easingValue = EASING_LINEAR; - if(easingName != NULL && - !cutsceneJsonLookupEasing(easingName, &easingValue)) { - errorThrow("Cutscene item (FADE): invalid 'easing'"); - } - item->fade.easing = easingValue; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenefade.h b/src/dusk/rpg/cutscene/item/ui/cutscenefade.h deleted file mode 100644 index 21e812b7..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenefade.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/color.h" -#include "animation/easing.h" -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - color_t from; - color_t to; - float_t duration; - easingtype_t easing; -} cutscenefade_t; - -/** - * Starts a fade item (begins the overlay transition). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneFadeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a fade item. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the overlay transition has completed. - */ -bool_t cutsceneFadeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a FADE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneFadeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c b/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c deleted file mode 100644 index 79dd0323..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.c +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "ui/dialog/keyboard/uikeyboard.h" - -void cutsceneKeyboardStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uiKeyboardOpen(&item->keyboard.open); -} - -bool_t cutsceneKeyboardUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - if(uiKeyboardIsOpen()) return false; - - cutsceneSystemSetTextCache(UI_KEYBOARD.text); - return true; -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h b/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h deleted file mode 100644 index ae97ebb2..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenekeyboard.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "ui/dialog/keyboard/uikeyboard.h" - -typedef struct { - // Passed straight through to uiKeyboardOpen - see uikeyboardopen_t. - uikeyboardopen_t open; -} cutscenekeyboard_t; - -/** - * Starts a keyboard item (opens the on-screen keyboard with the item's - * open parameters, unmodified - see uiKeyboardOpen). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneKeyboardStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a keyboard item. Blocks the cutscene while the keyboard is - * open; once it closes (confirmed or cancelled), caches its resulting - * text via cutsceneSystemSetTextCache (see cutsceneSystemGetTextCache) - * before completing, regardless of whether confirm or cancel was - * picked - check UI_KEYBOARD.confirmed from the item's own onInput - * callback if that distinction matters. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the keyboard has closed; false while still open. - */ -bool_t cutsceneKeyboardUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c deleted file mode 100644 index bad23afd..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.c +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "assert/assert.h" -#include "ui/widget/uimodal.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneModalStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - const cutscenemodal_t *modal = &item->modal; - assertTrue( - modal->optionCount <= CUTSCENE_MODAL_OPTIONS_MAX, - "Too many options for cutscene modal" - ); - - uiModalOpen( - modal->title, - modal->message, - modal->options, - modal->optionCount, - modal->callback, - NULL, - NULL - ); -} - -bool_t cutsceneModalUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - if(item->modal.optionCount == 0 && !item->modal.blocking) return true; - return !uiModalIsOpen(); -} - -void cutsceneModalCloseStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uiModalClose(NULL); -} - -bool_t cutsceneModalCloseUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneModalLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(yyjson_obj_get(itemObj, "options") != NULL) { - errorThrow( - "Cutscene item (MODAL): options are not supported here - use " - "MODAL_OPTIONS_MARKERS instead" - ); - } - cutsceneJsonCopyString( - itemObj, "title", "", item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS - ); - if(!cutsceneJsonCopyString( - itemObj, "message", NULL, item->modal.message, - CUTSCENE_MODAL_MESSAGE_MAX_CHARS - )) { - errorThrow("Cutscene item (MODAL): missing/too-long 'message'"); - } - item->modal.blocking = cutsceneJsonGetBool(itemObj, "blocking", false); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h deleted file mode 100644 index 78df7bc6..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodal.h +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -#define CUTSCENE_MODAL_TITLE_MAX_CHARS 64 -#define CUTSCENE_MODAL_MESSAGE_MAX_CHARS 256 -#define CUTSCENE_MODAL_OPTIONS_MAX 4 - -/** - * Callback invoked with the selected option once a cutscene modal - * item's dialog closes. Back/cancel input is disabled while it's open - * (see uiMenuSetDisableBack), so it must be dismissed by picking one. - * - * @param optionIndex Index into the options array that was selected. - * @param userData Unused - always NULL. Required by uiModalOpen's - * callback signature. - */ -typedef void (*cutscenemodaloptioncallback_t)( - const uint8_t optionIndex, void *userData -); - -typedef struct { - // Display text, or a locale message ID to translate - see - // uiModalLocalize. - char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS]; - char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS]; - - // NOT copied by this struct - these pointers are only read once, - // while uiModalOpen is opening the modal in cutsceneModalStart, so - // they only need to stay valid until then (e.g. string literals, as - // CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a - // locale message ID, same as title/message. - const char_t **options; - uint8_t optionCount; - - cutscenemodaloptioncallback_t callback; - - // Whether the cutscene should wait for this dialog to close before - // continuing. Always treated as true when optionCount > 0 (there's no - // other way to move on). Ignored/false by default for a message-only - // dialog, e.g. from CUTSCENE_MODAL - see cutsceneModalUpdate. - bool_t blocking; -} cutscenemodal_t; - -/** - * Starts a modal item (shows the modal dialog with the item's title, - * message, and options, wiring callback to fire with the selected - * option once it closes). optionCount may be 0 for a message-only - * dialog with no option buttons. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneModalStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a modal item. - * - * A message-only modal (optionCount == 0, e.g. from CUTSCENE_MODAL) - * completes immediately unless item->modal.blocking is set, in which - * case it waits for the dialog to close - either the player pressing - * accept/cancel (see uiModalMessageFocusSelected) or an explicit - * CUTSCENE_MODAL_CLOSE item elsewhere in the cutscene. - * - * A modal with options (optionCount > 0, e.g. from - * CUTSCENE_MODAL_OPTIONS) always blocks regardless of the blocking - * field - it's dismissed by the player picking one of them, and the - * option callback is responsible for moving the cutscene on from there - * (typically via cutsceneSystemGoTo or cutsceneRestart). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the modal has closed, or immediately for a - * non-blocking message-only modal. - */ -bool_t cutsceneModalUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a MODAL cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneModalLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); - -/** - * Starts a modal-close item (closes the currently open modal, if any). - * Whatever should happen next belongs in the cutscene's own item - * sequence (e.g. a CUTSCENE_CALLBACK or CUTSCENE_PRINT placed right - * after this item), not on this item itself. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneModalCloseStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a modal-close item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneModalCloseUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -// MODAL_CLOSE has no fields of its own to parse - see cutsceneItemLoadNoop -// (cutsceneitembase.h), used directly as its .load callback. diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c deleted file mode 100644 index f0390a92..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "ui/widget/uimodal.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneModalOptionsMarkersCallback( - const uint8_t optionIndex, - void *userData -) { - const cutscenemodaloptionsmarkers_t *options = - &cutsceneSystemGetCurrentItem()->modalOptionsMarkers; - - // uiModalOpen disables back/cancel for a dialog with options, so - // UI_MODAL_RESULT_NONE can't reach here via input - this fallback to - // the last option's marker only matters if something ever force-closes - // the modal directly (uiModalClose) before an option is picked. - const uint8_t index = - optionIndex < options->optionCount ? optionIndex : options->optionCount - 1; - cutsceneSystemGoTo(options->markers[index]); -} - -void cutsceneModalOptionsMarkersStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - const cutscenemodaloptionsmarkers_t *options = &item->modalOptionsMarkers; - - // uiModalOpen wants an array of string pointers; options->options is a - // 2D array of owned char buffers (see the struct's doc comment), so - // build the pointer array it expects here. - const char_t *optionPtrs[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX]; - for(uint8_t i = 0; i < options->optionCount; i++) { - optionPtrs[i] = options->options[i]; - } - - uiModalOpen( - options->title, - options->message, - optionPtrs, - options->optionCount, - cutsceneModalOptionsMarkersCallback, - NULL, - NULL - ); -} - -bool_t cutsceneModalOptionsMarkersUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return false; -} - -errorret_t cutsceneModalOptionsMarkersLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - cutsceneJsonCopyString( - itemObj, "title", "", item->modalOptionsMarkers.title, - CUTSCENE_MODAL_TITLE_MAX_CHARS - ); - if(!cutsceneJsonCopyString( - itemObj, "message", "", item->modalOptionsMarkers.message, - CUTSCENE_MODAL_MESSAGE_MAX_CHARS - )) { - errorThrow( - "Cutscene item (MODAL_OPTIONS_MARKERS): 'message' too long" - ); - } - - yyjson_val *options = yyjson_obj_get(itemObj, "options"); - size_t optionCount = yyjson_arr_size(options); - if(!yyjson_is_arr(options) || optionCount == 0 || - optionCount > CUTSCENE_MODAL_OPTIONS_MARKERS_MAX) { - errorThrow( - "Cutscene item (MODAL_OPTIONS_MARKERS): 'options' must have 1-%u " - "entries", (uint32_t)CUTSCENE_MODAL_OPTIONS_MARKERS_MAX - ); - } - item->modalOptionsMarkers.optionCount = (uint8_t)optionCount; - - yyjson_arr_iter iter = yyjson_arr_iter_with(options); - yyjson_val *optionVal; - size_t o = 0; - while((optionVal = yyjson_arr_iter_next(&iter)) != NULL) { - if( - !cutsceneJsonCopyString( - optionVal, "text", NULL, item->modalOptionsMarkers.options[o], - UI_MODAL_OPTION_TEXT_MAX - ) || - !cutsceneJsonCopyString( - optionVal, "marker", NULL, item->modalOptionsMarkers.markers[o], - CUTSCENE_MARKER_NAME_MAX - ) - ) { - errorThrow( - "Cutscene item (MODAL_OPTIONS_MARKERS): options[%u] needs " - "'text'/'marker'", (uint32_t)o - ); - } - o++; - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h b/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h deleted file mode 100644 index 9a46f178..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.h +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" -#include "rpg/cutscene/item/ui/cutscenemodal.h" -#include "rpg/cutscene/item/control/cutscenemarker.h" -#include "ui/widget/uimodal.h" - -// Two options is the most either JSON MODAL_OPTIONS_MARKERS item ever -// needs. -#define CUTSCENE_MODAL_OPTIONS_MARKERS_MAX 2 - -typedef struct { - // Display text, or a locale message ID to translate - see - // uiModalLocalize. - char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS]; - char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS]; - - // Each option's display text/locale message ID, and the marker to - // cutsceneSystemGoTo when that option is selected. Copied by value at parse - // time (see cutsceneModalOptionsMarkersLoad), so they stay valid - // regardless of the backing JSON doc's lifetime. - char_t options[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX][UI_MODAL_OPTION_TEXT_MAX]; - char_t markers[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX][CUTSCENE_MARKER_NAME_MAX]; - uint8_t optionCount; -} cutscenemodaloptionsmarkers_t; - -/** - * Starts a modal-options-markers item (shows the modal dialog with the - * item's title, message, and options). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneModalOptionsMarkersStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a modal-options-markers item. Never completes on its own - - * the cutscene blocks here indefinitely. Back/cancel input is disabled - * while it's open (see uiMenuSetDisableBack), so it must be dismissed - * by picking an option, which jumps straight to that option's marker - * via cutsceneSystemGoTo. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns false always. - */ -bool_t cutsceneModalOptionsMarkersUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a MODAL_OPTIONS_MARKERS cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneModalOptionsMarkersLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c deleted file mode 100644 index 674286f2..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/rpgcamera.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneShakeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - rpgCameraShake(item->shake.amount, item->shake.duration); -} - -bool_t cutsceneShakeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneShakeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - item->shake.amount = (uint8_t)cutsceneJsonGetInt(itemObj, "amount", 0); - item->shake.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h b/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h deleted file mode 100644 index 5ac06453..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutsceneshake.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t amount; - float_t duration; -} cutsceneshake_t; - -/** - * Starts a camera shake item (kicks off the shake on the RPG camera). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneShakeStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a camera shake item. The shake itself runs asynchronously on - * the RPG camera, so this always completes immediately. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneShakeUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a SHAKE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneShakeLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetext.c b/src/dusk/rpg/cutscene/item/ui/cutscenetext.c deleted file mode 100644 index 8dc5c70a..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetext.c +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "ui/rpg/textbox/uitextboxmain.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneTextStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uiTextboxMainSetText(item->text.text); -} - -bool_t cutsceneTextUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return !uiTextboxMainIsActive(); -} - -errorret_t cutsceneTextLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonCopyString( - itemObj, "text", NULL, item->text.text, CUTSCENE_TEXT_MAX_CHARS - )) { - errorThrow("Cutscene item (TEXT): missing/too-long 'text'"); - } - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetext.h b/src/dusk/rpg/cutscene/item/ui/cutscenetext.h deleted file mode 100644 index 6bf2bbe4..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetext.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -#define CUTSCENE_TEXT_MAX_CHARS 256 - -typedef struct { - char_t text[CUTSCENE_TEXT_MAX_CHARS]; -} cutscenetext_t; - -/** - * Starts a text item (shows the textbox with the item's text). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneTextStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a text item. - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true once the textbox has been dismissed. - */ -bool_t cutsceneTextUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a TEXT cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneTextLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); \ No newline at end of file diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c b/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c deleted file mode 100644 index 08bd27e9..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.c +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "ui/rpg/textbox/uitextboxminilist.h" -#include "rpg/cutscene/item/json/cutscenejsonfield.h" - -void cutsceneTextMiniStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uint8_t index = uiTextboxMiniListGetNext(); - uiTextboxMiniShow( - &UI_TEXTBOX_MINI_LIST[index], - item->textMini.text, - item->textMini.position, - item->textMini.duration, - NULL, - NULL - ); - CUTSCENE_SYSTEM.refs.textMiniLastCreated = index; -} - -bool_t cutsceneTextMiniUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneTextMiniLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - if(!cutsceneJsonCopyString( - itemObj, "text", NULL, item->textMini.text, CUTSCENE_TEXT_MINI_MAX_CHARS - )) { - errorThrow("Cutscene item (TEXT_MINI): missing/too-long 'text'"); - } - yyjson_val *position = yyjson_obj_get(itemObj, "position"); - if(!yyjson_is_arr(position) || yyjson_arr_size(position) != 3) { - errorThrow("Cutscene item (TEXT_MINI): 'position' must be [x, y, z]"); - } - item->textMini.position[0] = - (float_t)yyjson_get_num(yyjson_arr_get(position, 0)); - item->textMini.position[1] = - (float_t)yyjson_get_num(yyjson_arr_get(position, 1)); - item->textMini.position[2] = - (float_t)yyjson_get_num(yyjson_arr_get(position, 2)); - item->textMini.duration = cutsceneJsonGetFloat(itemObj, "duration", 0.0f); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h b/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h deleted file mode 100644 index ad8d0731..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetextmini.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -#define CUTSCENE_TEXT_MINI_MAX_CHARS 128 - -typedef struct { - char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS]; - vec3 position; - float_t duration; -} cutscenetextmini_t; - -/** - * Starts a mini text item (shows a mini textbox at the given world - * position for the given duration, then completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneTextMiniStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a mini text item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneTextMiniUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a TEXT_MINI cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneTextMiniLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c b/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c deleted file mode 100644 index b4df6066..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.c +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "ui/rpg/textbox/uitextboxminilist.h" - -void cutsceneTextMiniHideStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index); - uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]); -} - -bool_t cutsceneTextMiniHideUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneTextMiniHideLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - item->textMiniHide.index = - (uint8_t)yyjson_get_int(yyjson_obj_get(itemObj, "index")); - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h b/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h deleted file mode 100644 index 68f8f55d..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutscenetextminihide.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -typedef struct { - uint8_t index; -} cutscenetextminihide_t; - -/** - * Starts a mini text hide step (closes the mini textbox immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneTextMiniHideStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a mini text hide step (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneTextMiniHideUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a TEXT_MINI_HIDE cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneTextMiniHideLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c b/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c deleted file mode 100644 index 3ec69432..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.c +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/cutscene/item/cutsceneitem.h" -#include "ui/screen/mainmenu/uimainmenu.h" -#include "rpg/cutscene/item/json/cutscenejsonuiscreentype.h" - -void cutsceneUIShowStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - switch(item->uiShow.screen) { - case UI_SCREEN_TYPE_MAIN_MENU: - uiMainMenuOpen(); - break; - } -} - -bool_t cutsceneUIShowUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -) { - return true; -} - -errorret_t cutsceneUIShowLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -) { - uiscreentype_t screenValue; - if(!cutsceneJsonLookupUiScreenType( - yyjson_get_str(yyjson_obj_get(itemObj, "name")), &screenValue - )) { - errorThrow("Cutscene item (UI_SHOW): invalid 'name'"); - } - item->uiShow.screen = screenValue; - - errorOk(); -} diff --git a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h b/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h deleted file mode 100644 index 17f5ea8d..00000000 --- a/src/dusk/rpg/cutscene/item/ui/cutsceneuishow.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/cutscene/item/cutsceneitembase.h" - -// Full-screen UI panels a cutscene can declaratively open via UI_SHOW - not -// modals/dialogs (those already have their own dedicated item types, e.g. -// MODAL/MODAL_OPTIONS_MARKERS/KEYBOARD), just whole-screen views like the -// main menu. Add a case here (and to cutsceneUIShowStart) as more screens -// need to be openable this way. -typedef enum { - UI_SCREEN_TYPE_MAIN_MENU, -} uiscreentype_t; - -typedef struct { - uiscreentype_t screen; -} cutsceneuishow_t; - -/** - * Starts a UI_SHOW item, opening the requested full-screen UI panel. - * - * @param item The cutscene item. - * @param data Runtime data storage. - */ -void cutsceneUIShowStart( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Updates a UI_SHOW item (always completes immediately). - * - * @param item The cutscene item. - * @param data Runtime data storage. - * @returns true always. - */ -bool_t cutsceneUIShowUpdate( - const cutsceneitem_t *item, - cutsceneitemdata_t *data -); - -/** - * Parses a UI_SHOW cutscene item's JSON fields into item. - * - * @param itemObj The item's JSON object. - * @param item Destination item, already zeroed by the caller. - * @return Error code indicating success or failure of the parse. - */ -errorret_t cutsceneUIShowLoad( - yyjson_val *itemObj, - cutsceneitem_t *item -); diff --git a/src/dusk/rpg/entity/CMakeLists.txt b/src/dusk/rpg/entity/CMakeLists.txt deleted file mode 100644 index da55a228..00000000 --- a/src/dusk/rpg/entity/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - entity.c - entitydir.c - entitypathstep.c - player.c -) - -add_subdirectory(anim) -add_subdirectory(interact) -add_subdirectory(npc) -add_subdirectory(item) -add_subdirectory(global) \ No newline at end of file diff --git a/src/dusk/rpg/entity/anim/CMakeLists.txt b/src/dusk/rpg/entity/anim/CMakeLists.txt deleted file mode 100644 index 54c1015e..00000000 --- a/src/dusk/rpg/entity/anim/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - entityanim.c - entityanimidle.c - entityanimturn.c - entityanimwalk.c - entityanimrun.c -) diff --git a/src/dusk/rpg/entity/anim/entityanim.c b/src/dusk/rpg/entity/anim/entityanim.c deleted file mode 100644 index fa98628f..00000000 --- a/src/dusk/rpg/entity/anim/entityanim.c +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "rpg/overworld/map.h" -#include "rpg/overworld/tile.h" -#include "time/time.h" -#include "entityanimwalk.h" - -const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = { - [ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate }, - [ENTITY_ANIM_TURN] = { entityAnimTurnUpdate }, - [ENTITY_ANIM_WALK] = { entityAnimWalkUpdate }, - [ENTITY_ANIM_RUN] = { entityAnimRunUpdate }, -}; - -float_t entityAnimTileZOffset(const worldpos_t pos) { - return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f; -} - -void entityAnimUpdate(entity_t *entity) { - if(entity->animation != ENTITY_ANIM_IDLE) { - entity->animTime -= TIME.delta; - if(entity->animTime <= 0) { - if( - entity->animation == ENTITY_ANIM_WALK || - entity->animation == ENTITY_ANIM_RUN - ) { - entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN; - } - entity->animation = ENTITY_ANIM_IDLE; - entity->animTime = 0; - } - } - if(entity->walkEndCooldown > 0) { - entity->walkEndCooldown -= TIME.delta; - if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0; - } - ENTITY_ANIM_CALLBACKS[entity->animation].update(entity); -} diff --git a/src/dusk/rpg/entity/anim/entityanim.h b/src/dusk/rpg/entity/anim/entityanim.h deleted file mode 100644 index ff4cde20..00000000 --- a/src/dusk/rpg/entity/anim/entityanim.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "entityanimidle.h" -#include "entityanimturn.h" -#include "entityanimwalk.h" -#include "entityanimrun.h" - -typedef struct entity_s entity_t; - -typedef enum { - ENTITY_ANIM_IDLE, - ENTITY_ANIM_TURN, - ENTITY_ANIM_WALK, - ENTITY_ANIM_RUN, - ENTITY_ANIM_COUNT -} entityanim_t; - -typedef struct { - /** Updates the render position for this animation state. */ - void (*update)(entity_t *entity); -} entityanimcallback_t; - -extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT]; - -/** - * Updates the entity animation timer and render position. - * - * @param entity Pointer to the entity to update. - */ -void entityAnimUpdate(entity_t *entity); - -/** - * Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise. - * Used to lift entity render position to mid-ramp height. - * - * @param pos World position to sample. - * @returns float_t The Z offset to apply. - */ -float_t entityAnimTileZOffset(const worldpos_t pos); diff --git a/src/dusk/rpg/entity/anim/entityanimidle.c b/src/dusk/rpg/entity/anim/entityanimidle.c deleted file mode 100644 index fb9f5d4a..00000000 --- a/src/dusk/rpg/entity/anim/entityanimidle.c +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "entityanim.h" - -void entityAnimIdleUpdate(entity_t *entity) { - entity->renderPosition[0] = (float_t)entity->position.x; - entity->renderPosition[1] = (float_t)entity->position.y; - entity->renderPosition[2] = ( - (float_t)entity->position.z + entityAnimTileZOffset(entity->position) - ) * WORLD_LAYER_HEIGHT; -} diff --git a/src/dusk/rpg/entity/anim/entityanimidle.h b/src/dusk/rpg/entity/anim/entityanimidle.h deleted file mode 100644 index b7bd9a53..00000000 --- a/src/dusk/rpg/entity/anim/entityanimidle.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 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 entity_s entity_t; - -/** - * Updates render position for the idle animation state. - * - * @param entity Pointer to the entity to update. - */ -void entityAnimIdleUpdate(entity_t *entity); diff --git a/src/dusk/rpg/entity/anim/entityanimrun.c b/src/dusk/rpg/entity/anim/entityanimrun.c deleted file mode 100644 index 27a32c2c..00000000 --- a/src/dusk/rpg/entity/anim/entityanimrun.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "entityanim.h" - -void entityAnimRunUpdate(entity_t *entity) { - float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION); - float_t zFrom = (float_t)entity->lastPosition.z - + entityAnimTileZOffset(entity->lastPosition); - float_t zTo = (float_t)entity->position.z - + entityAnimTileZOffset(entity->position); - entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * ( - (float_t)entity->position.x - (float_t)entity->lastPosition.x - ); - entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * ( - (float_t)entity->position.y - (float_t)entity->lastPosition.y - ); - entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT; -} diff --git a/src/dusk/rpg/entity/anim/entityanimrun.h b/src/dusk/rpg/entity/anim/entityanimrun.h deleted file mode 100644 index ecace434..00000000 --- a/src/dusk/rpg/entity/anim/entityanimrun.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "time/time.h" - -typedef struct entity_s entity_t; - -#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6) - -/** - * Updates render position for the run animation state. - * - * @param entity Pointer to the entity to update. - */ -void entityAnimRunUpdate(entity_t *entity); diff --git a/src/dusk/rpg/entity/anim/entityanimturn.c b/src/dusk/rpg/entity/anim/entityanimturn.c deleted file mode 100644 index d31ad814..00000000 --- a/src/dusk/rpg/entity/anim/entityanimturn.c +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "entityanim.h" - -void entityAnimTurnUpdate(entity_t *entity) { - entity->renderPosition[0] = (float_t)entity->position.x; - entity->renderPosition[1] = (float_t)entity->position.y; - entity->renderPosition[2] = ( - (float_t)entity->position.z + entityAnimTileZOffset(entity->position) - ) * WORLD_LAYER_HEIGHT; -} diff --git a/src/dusk/rpg/entity/anim/entityanimturn.h b/src/dusk/rpg/entity/anim/entityanimturn.h deleted file mode 100644 index 33368d52..00000000 --- a/src/dusk/rpg/entity/anim/entityanimturn.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "time/time.h" - -typedef struct entity_s entity_t; - -#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4) - -/** - * Updates render position for the turn animation state. - * - * @param entity Pointer to the entity to update. - */ -void entityAnimTurnUpdate(entity_t *entity); diff --git a/src/dusk/rpg/entity/anim/entityanimwalk.c b/src/dusk/rpg/entity/anim/entityanimwalk.c deleted file mode 100644 index e8aa2833..00000000 --- a/src/dusk/rpg/entity/anim/entityanimwalk.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "entityanim.h" - -void entityAnimWalkUpdate(entity_t *entity) { - float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION); - float_t zFrom = (float_t)entity->lastPosition.z - + entityAnimTileZOffset(entity->lastPosition); - float_t zTo = (float_t)entity->position.z - + entityAnimTileZOffset(entity->position); - entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * ( - (float_t)entity->position.x - (float_t)entity->lastPosition.x - ); - entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * ( - (float_t)entity->position.y - (float_t)entity->lastPosition.y - ); - entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT; -} diff --git a/src/dusk/rpg/entity/anim/entityanimwalk.h b/src/dusk/rpg/entity/anim/entityanimwalk.h deleted file mode 100644 index 5ab12585..00000000 --- a/src/dusk/rpg/entity/anim/entityanimwalk.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "time/time.h" - -typedef struct entity_s entity_t; - -#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12) -#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4) - -/** - * Updates render position for the walk animation state. - * - * @param entity Pointer to the entity to update. - */ -void entityAnimWalkUpdate(entity_t *entity); diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c deleted file mode 100644 index dabe2f67..00000000 --- a/src/dusk/rpg/entity/entity.c +++ /dev/null @@ -1,333 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entity.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "time/time.h" -#include "util/math.h" -#include "console/console.h" -#include "rpg/overworld/map.h" -#include "rpg/overworld/maparea.h" -#include "rpg/overworld/chunk.h" -#include "rpg/overworld/tile.h" - -entity_t ENTITIES[ENTITY_COUNT]; - -void entityInit(entity_t *entity, const entitytype_t type) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type"); - assertTrue(type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); - assertTrue( - entity >= ENTITIES && entity < ENTITIES + ENTITY_COUNT, - "Entity pointer is out of bounds" - ); - - memoryZero(entity, sizeof(entity_t)); - entity->id = (uint8_t)(entity - ENTITIES); - entity->globalId = ENTITY_GLOBAL_ID_NULL; - entity->type = type; - entity->chunkIndex = -1; - - if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity); -} - -void entityUpdate(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type"); - assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type"); - - // What state is the entity in? - entityAnimUpdate(entity); - - // Movement code. - if(ENTITY_CALLBACKS[entity->type].movement != NULL) { - ENTITY_CALLBACKS[entity->type].movement(entity); - } -} - -bool_t entityCanTurn(entity_t *entity) { - return entity->animation == ENTITY_ANIM_IDLE && - entity->walkEndCooldown <= 0; -} - -bool_t entityCanWalk(entity_t *entity) { - return entity->animation == ENTITY_ANIM_IDLE; -} - -bool_t entityCanRun(entity_t *entity) { - return entity->animation == ENTITY_ANIM_IDLE; -} - -bool_t entityCanUnload(entity_t *entity) { - return entity->globalId < ENTITY_GLOBAL_ID_START; -} - -void entityTurn(entity_t *entity, const entitydir_t direction) { - if(!entityCanTurn(entity)) return; - entity->direction = direction; - entity->animation = ENTITY_ANIM_TURN; - entity->animTime = ENTITY_ANIM_TURN_DURATION; -} - -bool_t entityWalkCheckRampUp( - const tile_t tileCurrent, - const entitydir_t direction, - const worldpos_t newPos, - tile_t *tileNew -) { - if(!tileShapeIsRamp(tileCurrent.shape)) return false; - - bool_t facingRamp = - // Can only walk UP the direction the ramp faces. - (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape || - // If diagonal ramp, can go up one of two ways only. Inner ramps - // share the same allowed directions as their outer counterparts. - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST || - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) - ) || - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST || - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) - ) || - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST || - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) - ) || - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST || - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) - ); - if(!facingRamp) return false; - - tile_t tileNewSaved = *tileNew; - *tileNew = TILE_NULL; - worldpos_t abovePos = newPos; - abovePos.z += 1; - tile_t tileAbove = mapGetTile(abovePos); - - if( - tileAbove.shape != TILE_SHAPE_NULL && tileShapeIsWalkable(tileAbove.shape) - ) { - return true; - } - - *tileNew = tileNewSaved; - return false; -} - -bool_t entityWalkCheckFall( - const tile_t tileNew, const worldpos_t newPos, const entitydir_t direction -) { - if(tileNew.shape != TILE_SHAPE_NULL || newPos.z <= 0) return false; - - worldpos_t belowPos = newPos; - belowPos.z -= 1; - tile_t tileBelow = mapGetTile(belowPos); - if(tileBelow.shape == TILE_SHAPE_NULL || !tileShapeIsRamp(tileBelow.shape)) { - return false; - } - - return ( - // This handles regular cardinal ramps - (entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == - tileBelow.shape || - // This handles diagonal ramps. Inner ramps share the same - // allowed directions as their outer counterparts. - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST || - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) - ) || - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST || - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) - ) || - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST || - tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) - ) || - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST || - tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) - ) - ); -} - -bool_t entityWalkIsBlockedByEntity(entity_t *entity, const worldpos_t newPos) { - entity_t *other = ENTITIES; - do { - if(other == entity) continue; - if(other->type == ENTITY_TYPE_NULL) continue; - if(!worldPosIsEqual(other->position, newPos)) continue; - return true; - } while(++other, other < &ENTITIES[ENTITY_COUNT]); - - return false; -} - -void entityWalk(entity_t *entity, const entitydir_t direction) { - if(!entityCanWalk(entity)) return; - // TODO: Animation, delay, etc. - entity->direction = direction; - - // Where are we moving? - worldpos_t newPos = entity->position; - worldunits_t relX, relY; - { - entityDirGetRelative(direction, &relX, &relY); - newPos.x += relX; - newPos.y += relY; - } - - // Get tile under foot - tile_t tileCurrent = mapGetTile(entity->position); - tile_t tileNew = mapGetTile(newPos); - - bool_t raise = - entityWalkCheckRampUp(tileCurrent, direction, newPos, &tileNew); - bool_t fall = !raise && entityWalkCheckFall(tileNew, newPos, direction); - - // Can we walk here? - if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked - - // Raise/fall must be applied before checking for blocking entities, - // otherwise the check compares against the wrong z-level. - if(raise) { - newPos.z += 1; - } else if(fall) { - newPos.z -= 1; - } - - // Entity in way? - if(entityWalkIsBlockedByEntity(entity, newPos)) return;// Blocked - - entity->lastPosition = entity->position; - entity->position = newPos; - entity->animation = ENTITY_ANIM_WALK; - entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking - entityUpdateChunk(entity); - mapAreaCheckEntity(entity); -} - -void entityRun(entity_t *entity, const entitydir_t direction) { - if(!entityCanRun(entity)) return; - entityWalk(entity, direction); - if(entity->animation == ENTITY_ANIM_WALK) { - entity->animation = ENTITY_ANIM_RUN; - entity->animTime = ENTITY_ANIM_RUN_DURATION; - } -} - -entity_t * entityGetAt(const worldpos_t position) { - entity_t *ent = ENTITIES; - do { - if(ent->type == ENTITY_TYPE_NULL) continue; - if(!worldPosIsEqual(ent->position, position)) continue; - return ent; - } while(++ent, ent < &ENTITIES[ENTITY_COUNT]); - - return NULL; -} - -entity_t * entityGetByGlobalId(const entityglobalid_t globalId) { - entity_t *ent = ENTITIES; - do { - if(ent->type == ENTITY_TYPE_NULL) continue; - if(ent->globalId != globalId) continue; - return ent; - } while(++ent, ent < &ENTITIES[ENTITY_COUNT]); - - return NULL; -} - -uint8_t entityGetAvailable() { - entity_t *ent = ENTITIES; - do { - if(ent->type == ENTITY_TYPE_NULL) return ent - ENTITIES; - } while(++ent, ent < &ENTITIES[ENTITY_COUNT]); - - return 0xFF; -} - -void entityPositionSet(entity_t *entity, const worldpos_t pos) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - entity->lastPosition = pos; - entity->position = pos; - entity->animation = ENTITY_ANIM_IDLE; - entity->animTime = 0; - entity->walkEndCooldown = 0; - entityUpdateChunk(entity); -} - -void entitySetChunk(entity_t *entity, const chunkindex_t chunkIndex) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - - chunk_t *old = mapGetChunk(entity->chunkIndex); - if(old != NULL) { - for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { - if(old->entities[i] != entity->id) continue; - old->entities[i] = 0xFF; - } - } - - // Only claim the new chunk once actually inserted into one of its slots - - // otherwise entity->chunkIndex would point at a chunk that doesn't know - // about this entity, so it would never be torn down on unload. - entity->chunkIndex = CHUNK_INDEX_INVALID; - - chunk_t *next = mapGetChunk(chunkIndex); - if(next != NULL) { - for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { - if(next->entities[i] != 0xFF) continue; - next->entities[i] = entity->id; - entity->chunkIndex = chunkIndex; - break; - } - - if(entity->chunkIndex != chunkIndex) { - consolePrint( - "entitySetChunk: chunk %d has no free entity slots, entity %u " - "left untracked", - chunkIndex, entity->id - ); - } - } -} - -void entityUpdateChunk(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - - chunkpos_t cp; - worldPosToChunkPos(&entity->position, &cp); - chunkindex_t ci = mapGetChunkIndexAt(cp); - entitySetChunk(entity, ci); -} \ No newline at end of file diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h deleted file mode 100644 index 2ef8b64c..00000000 --- a/src/dusk/rpg/entity/entity.h +++ /dev/null @@ -1,215 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "entitydir.h" -#include "anim/entityanim.h" -#include "interact/entityinteract.h" -#include "entitytype.h" -#include "npc/npc.h" -#include "rpg/overworld/tile.h" - -typedef struct map_s map_t; - -typedef uint16_t entityglobalid_t; - -#define ENTITY_GLOBAL_ID_NULL 0 -#define ENTITY_GLOBAL_ID_START 1 -#define ENTITY_GLOBAL_ID_PLAYER 1 - -typedef struct entity_s { - uint8_t id; - entityglobalid_t globalId; - entitytype_t type; - entitytypedata_t data; - - // Movement - entitydir_t direction; - worldpos_t position; - worldpos_t lastPosition; - vec3 renderPosition; - - entityanim_t animation; - float_t animTime; - float_t walkEndCooldown; - - entityinteract_t interact; - - chunkindex_t chunkIndex; -} entity_t; - -extern entity_t ENTITIES[ENTITY_COUNT]; - -/** - * Initializes an entity structure. - * - * @param entity Pointer to the entity structure to initialize. - * @param type The type of the entity. - */ -void entityInit(entity_t *entity, const entitytype_t type); - -/** - * Updates an entity. - * - * @param entity Pointer to the entity structure to update. - */ -void entityUpdate(entity_t *entity); - -/** - * Returns true if the entity is in a state where it can turn. - * - * @param entity Pointer to the entity to check. - * @returns True if the entity can turn. - */ -bool_t entityCanTurn(entity_t *entity); - -/** - * Returns true if the entity is in a state where it can walk. - * - * @param entity Pointer to the entity to check. - * @returns True if the entity can walk. - */ -bool_t entityCanWalk(entity_t *entity); - -/** - * Returns true if the entity is in a state where it can run. - * - * @param entity Pointer to the entity to check. - * @returns True if the entity can run. - */ -bool_t entityCanRun(entity_t *entity); - -/** - * Returns true if the entity is allowed to be unloaded. By default this is - * true for entities whose global ID falls within the randomly assigned - * range below ENTITY_GLOBAL_ID_START. - * - * @param entity Pointer to the entity to check. - * @returns True if the entity can be unloaded. - */ -bool_t entityCanUnload(entity_t *entity); - -/** - * Turn an entity to face a new direction. - * - * @param entity Pointer to the entity to turn. - * @param direction The direction to face. - */ -void entityTurn(entity_t *entity, const entitydir_t direction); - -/** - * Checks whether an entity standing on a ramp tile can walk up it towards - * newPos. If so, tileNew is cleared to TILE_NULL so the caller treats the - * move as a raise rather than a normal walk onto whatever tile is there; - * if the tile above isn't walkable, tileNew is restored to its original - * value. - * - * @param tileCurrent The tile the entity is currently standing on. - * @param direction The direction the entity is moving in. - * @param newPos The world position the entity is moving to. - * @param tileNew In/out: the tile at newPos, possibly cleared to TILE_NULL. - * @returns True if the entity should be raised up onto the ramp. - */ -bool_t entityWalkCheckRampUp( - const tile_t tileCurrent, - const entitydir_t direction, - const worldpos_t newPos, - tile_t *tileNew -); - -/** - * Checks whether an entity moving onto an empty tile should fall down onto - * a ramp one z-level below that faces back towards where it came from. - * - * @param tileNew The tile at the position the entity is moving to. - * @param newPos The world position the entity is moving to. - * @param direction The direction the entity is moving in. - * @returns True if the entity should fall to the tile below newPos. - */ -bool_t entityWalkCheckFall( - const tile_t tileNew, const worldpos_t newPos, const entitydir_t direction -); - -/** - * Checks whether any other loaded entity already occupies a world position. - * - * @param entity The entity that is moving (excluded from the check). - * @param newPos The world position to check for occupancy. - * @returns True if another entity occupies newPos. - */ -bool_t entityWalkIsBlockedByEntity(entity_t *entity, const worldpos_t newPos); - -/** - * Make an entity walk in a direction. - * - * @param entity Pointer to the entity to make walk. - * @param direction The direction to walk in. - */ -void entityWalk(entity_t *entity, const entitydir_t direction); - -/** - * Make an entity run in a direction. - * - * @param entity Pointer to the entity to make run. - * @param direction The direction to run in. - */ -void entityRun(entity_t *entity, const entitydir_t direction); - -/** - * Gets the entity at a specific world position. - * - * @param map Pointer to the map to check. - * @param pos The world position to check. - * @return Pointer to the entity at the position, or NULL if none. - */ -entity_t *entityGetAt(const worldpos_t pos); - -/** - * Gets the entity with the given global ID, if one is currently loaded. - * - * @param globalId The global ID to search for. - * @return Pointer to the matching entity, or NULL if none is loaded. - */ -entity_t *entityGetByGlobalId(const entityglobalid_t globalId); - -/** - * Gets an available entity index. - * - * @return The index of an available entity, or 0xFF if none are available. - */ -uint8_t entityGetAvailable(); - -/** - * Assigns an entity to a chunk, removing it from its current chunk first. - * Pass CHUNK_INDEX_INVALID as chunkIndex to detach the entity from any - * chunk. If the target chunk has no free entity slots, the entity is - * left detached (chunkIndex CHUNK_INDEX_INVALID) rather than assigned to - * a chunk that isn't actually tracking it - entityUpdateChunk will keep - * retrying on subsequent moves. - * - * @param entity Pointer to the entity. - * @param chunkIndex Index of the chunk to assign to, or - * CHUNK_INDEX_INVALID for none. - */ -void entitySetChunk(entity_t *entity, const chunkindex_t chunkIndex); - -/** - * Resolves the chunk that an entity's current position falls into and - * assigns the entity to it via entitySetChunk. Leaves the entity's chunk - * unchanged if its position doesn't fall within any loaded chunk. - * - * @param entity Pointer to the entity to update. - */ -void entityUpdateChunk(entity_t *entity); - -/** - * Instantly moves an entity to a world position, resetting movement state. - * - * @param entity Pointer to the entity to move. - * @param pos The world position to place the entity at. - */ -void entityPositionSet(entity_t *entity, const worldpos_t pos); \ No newline at end of file diff --git a/src/dusk/rpg/entity/entitydir.c b/src/dusk/rpg/entity/entitydir.c deleted file mode 100644 index bf75e7c7..00000000 --- a/src/dusk/rpg/entity/entitydir.c +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entitydir.h" -#include "assert/assert.h" - -entitydir_t entityDirGetOpposite(const entitydir_t dir) { - switch(dir) { - case ENTITY_DIR_NORTH: return ENTITY_DIR_SOUTH; - case ENTITY_DIR_SOUTH: return ENTITY_DIR_NORTH; - case ENTITY_DIR_EAST: return ENTITY_DIR_WEST; - case ENTITY_DIR_WEST: return ENTITY_DIR_EAST; - default: return dir; - } -} - -void entityDirGetRelative( - const entitydir_t from, - worldunits_t *outX, - worldunits_t *outY -) { - assertValidEntityDir(from, "Invalid direction provided"); - assertNotNull(outX, "Output X pointer cannot be NULL"); - assertNotNull(outY, "Output Y pointer cannot be NULL"); - - switch(from) { - case ENTITY_DIR_NORTH: - *outX = 0; - *outY = 1; - break; - - case ENTITY_DIR_EAST: - *outX = 1; - *outY = 0; - break; - - case ENTITY_DIR_SOUTH: - *outX = 0; - *outY = -1; - break; - - case ENTITY_DIR_WEST: - *outX = -1; - *outY = 0; - break; - } -} \ No newline at end of file diff --git a/src/dusk/rpg/entity/entitydir.h b/src/dusk/rpg/entity/entitydir.h deleted file mode 100644 index 2eefab88..00000000 --- a/src/dusk/rpg/entity/entitydir.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/worldpos.h" - -typedef enum { - ENTITY_DIR_NORTH, - ENTITY_DIR_EAST, - ENTITY_DIR_SOUTH, - ENTITY_DIR_WEST, - ENTITY_DIR_UP = ENTITY_DIR_NORTH, - ENTITY_DIR_DOWN = ENTITY_DIR_SOUTH, - ENTITY_DIR_LEFT = ENTITY_DIR_WEST, - ENTITY_DIR_RIGHT = ENTITY_DIR_EAST, -} entitydir_t; - -/** - * Gets the opposite direction of a given direction. - * - * @param dir The direction to get the opposite of. - * @return entitydir_t The opposite direction. - */ -entitydir_t entityDirGetOpposite(const entitydir_t dir); - -/** - * Asserts a given direction is valid. - * - * @param dir The direction to validate. - * @param msg The message to display if the assertion fails. - */ -#define assertValidEntityDir(dir, msg) \ - assertTrue( \ - (dir) == ENTITY_DIR_NORTH || \ - (dir) == ENTITY_DIR_EAST || \ - (dir) == ENTITY_DIR_SOUTH || \ - (dir) == ENTITY_DIR_WEST, \ - msg \ - ) - -/** - * Gets the relative x and y offsets for a given direction. - * - * @param dir The direction to get offsets for. - * @param relX Pointer to store the relative x offset. - * @param relY Pointer to store the relative y offset. - */ -void entityDirGetRelative( - const entitydir_t dir, worldunits_t *relX, worldunits_t *relY -); \ No newline at end of file diff --git a/src/dusk/rpg/entity/entitypathstep.c b/src/dusk/rpg/entity/entitypathstep.c deleted file mode 100644 index bd474415..00000000 --- a/src/dusk/rpg/entity/entitypathstep.c +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entitypathstep.h" -#include "entitydir.h" -#include "assert/assert.h" - -bool_t entityPathStep( - entity_t *entity, - const worldpos_t target, - bool_t walkAround -) { - assertNotNull(entity, "Entity must not be NULL"); - assertTrue( - entity->type != ENTITY_TYPE_NULL, - "Cannot path step a NULL entity type" - ); - assertTrue( - entity >= ENTITIES && entity < ENTITIES + ENTITY_COUNT, - "Entity pointer is out of bounds" - ); - - if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) { - return true; - } - if(!entityCanWalk(entity)) return false; - - entitydir_t dir; - if(entity->position.x != target.x) { - dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST; - } else if(entity->position.y != target.y) { - dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH; - } else { - dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH; - } - - entityWalk(entity, dir); - - if(walkAround && entity->animation == ENTITY_ANIM_IDLE) { - // Primary direction blocked - try perpendicular axes. - entitydir_t alt, altOpp; - if(dir == ENTITY_DIR_EAST || dir == ENTITY_DIR_WEST) { - alt = entity->position.y <= target.y - ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH; - } else { - alt = entity->position.x <= target.x - ? ENTITY_DIR_EAST : ENTITY_DIR_WEST; - } - altOpp = entityDirGetOpposite(alt); - entityWalk(entity, alt); - if(entity->animation == ENTITY_ANIM_IDLE) entityWalk(entity, altOpp); - } - - return false; -} diff --git a/src/dusk/rpg/entity/entitypathstep.h b/src/dusk/rpg/entity/entitypathstep.h deleted file mode 100644 index 1608cae8..00000000 --- a/src/dusk/rpg/entity/entitypathstep.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "entity.h" - -/** - * Attempts to walk the entity one tile toward target. Prefers resolving X - * first, then Y, then Z. Does nothing if the entity cannot currently walk. - * When walkAround is true and the preferred direction is blocked by an entity, - * tries perpendicular directions to navigate around it. - * - * @param entity Pointer to the entity to move. - * @param target The world position to move toward. - * @param walkAround Whether to try perpendicular directions when blocked. - * @returns true if the entity is already at target, false otherwise. - */ -bool_t entityPathStep( - entity_t *entity, - const worldpos_t target, - bool_t walkAround -); diff --git a/src/dusk/rpg/entity/entitytype.h b/src/dusk/rpg/entity/entitytype.h deleted file mode 100644 index 6b7d2769..00000000 --- a/src/dusk/rpg/entity/entitytype.h +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/entity/player.h" -#include "npc/npc.h" -#include "item/entityitem.h" - -typedef enum { - ENTITY_TYPE_NULL, - - ENTITY_TYPE_PLAYER, - ENTITY_TYPE_NPC, - ENTITY_TYPE_ITEM, - - ENTITY_TYPE_COUNT -} entitytype_enum_t; - -typedef uint8_t entitytype_t; - -typedef union { - player_t player; - npc_t npc; - entityitem_t item; -} entitytypedata_t; - -typedef struct { - /** - * Initialization callback for the entity type. - * @param entity Pointer to the entity to initialize. - */ - void (*init)(entity_t *entity); - - /** - * Movement callback for the entity type. Gated by cutscene input. - * @param entity Pointer to the entity to move. - */ - void (*movement)(entity_t *entity); -} entitycallback_t; - -static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = { - [ENTITY_TYPE_NULL] = { NULL }, - - [ENTITY_TYPE_PLAYER] = { - .init = playerInit, - .movement = playerInput - }, - - [ENTITY_TYPE_NPC] = { - .init = npcInit, - .movement = npcMovement, - }, - - [ENTITY_TYPE_ITEM] = { - .init = entityItemInit, - .movement = entityItemMovement, - } -}; \ No newline at end of file diff --git a/src/dusk/rpg/entity/global/CMakeLists.txt b/src/dusk/rpg/entity/global/CMakeLists.txt deleted file mode 100644 index 9b35b01d..00000000 --- a/src/dusk/rpg/entity/global/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# 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 -) \ No newline at end of file diff --git a/src/dusk/rpg/entity/global/entityglobal.h b/src/dusk/rpg/entity/global/entityglobal.h deleted file mode 100644 index 10d51d60..00000000 --- a/src/dusk/rpg/entity/global/entityglobal.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "entityglobaldefs.h" -#include "entitygloballist.h" - -#define ENTITY_GLOBAL_LIST_COUNT ( \ - sizeof(ENTITY_GLOBAL_LIST) / \ - sizeof(ENTITY_GLOBAL_LIST[0]) \ -) - -//EOF diff --git a/src/dusk/rpg/entity/global/entityglobaldefs.h b/src/dusk/rpg/entity/global/entityglobaldefs.h deleted file mode 100644 index 73913e45..00000000 --- a/src/dusk/rpg/entity/global/entityglobaldefs.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "rpg/overworld/worldpos.h" -#include "rpg/entity/entity.h" - -typedef struct entity_s entity_t; - -typedef struct { - entity_t *entity; - worldpos_t position; -} entityglobalcreate_t; - -/** - * Callback invoked to initialize a global entity. - * - * @param create Pointer to the entity/position being initialized. - * @returns An error code. - */ -typedef void (*entityglobalinitcallback_t)( - entityglobalcreate_t *create -); - -typedef struct { - entitytype_t type; - entityglobalinitcallback_t callback; -} entityglobaldef_t; - -#define ENTITY_GLOBAL(id, entType, callbackFn) \ - [id] = { .type = entType, .callback = callbackFn } - -#define ENTITY_GLOBAL_CALLBACK(id) \ - static void ENTTIYT_GLOBAL_CALLBACK_##id(entityglobalcreate_t *create) - -#define ENTITY_GLOBAL_REF(id) \ - ENTTIYT_GLOBAL_CALLBACK_##id - -//EOF \ No newline at end of file diff --git a/src/dusk/rpg/entity/global/entitygloballist.h b/src/dusk/rpg/entity/global/entitygloballist.h deleted file mode 100644 index c2a83e79..00000000 --- a/src/dusk/rpg/entity/global/entitygloballist.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "entityglobaldefs.h" -#include "rpg/cutscene/item/cutsceneitem.h" - -ENTITY_GLOBAL_CALLBACK(3) { - create->entity->data.npc.moveType = NPC_MOVE_TYPE_PATH; - npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 }); - npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 }); - npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 4, 4, 0 }); - npcPathAddNode(&create->entity->data.npc, (worldpos_t){ 10, 10, 1 }); -} - -ENTITY_GLOBAL_CALLBACK(4) { - create->entity->interact.type = ENTITY_INTERACT_CUTSCENE_NAME; - create->entity->interact.data.cutsceneName = "cutscenes/battletest.jsonc"; -} - -static const entityglobaldef_t ENTITY_GLOBAL_LIST[] = { - ENTITY_GLOBAL(ENTITY_GLOBAL_ID_NULL, ENTITY_TYPE_NULL, NULL), - ENTITY_GLOBAL(ENTITY_GLOBAL_ID_PLAYER, ENTITY_TYPE_PLAYER, NULL), - - ENTITY_GLOBAL(3, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(3)), - ENTITY_GLOBAL(4, ENTITY_TYPE_NPC, ENTITY_GLOBAL_REF(4)), -}; - -//EOF diff --git a/src/dusk/rpg/entity/interact/CMakeLists.txt b/src/dusk/rpg/entity/interact/CMakeLists.txt deleted file mode 100644 index f7e07e35..00000000 --- a/src/dusk/rpg/entity/interact/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - entityinteract.c -) diff --git a/src/dusk/rpg/entity/interact/entityinteract.c b/src/dusk/rpg/entity/interact/entityinteract.c deleted file mode 100644 index 063466c0..00000000 --- a/src/dusk/rpg/entity/interact/entityinteract.c +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "assert/assert.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "ui/rpg/textbox/uitextboxmain.h" - -void entityInteractWith(entity_t *player, entity_t *target) { - assertNotNull(player, "Player entity pointer cannot be NULL"); - assertNotNull(target, "Target entity pointer cannot be NULL"); - - switch(target->interact.type) { - case ENTITY_INTERACT_CUTSCENE: - assertNotNull( - target->interact.data.cutscene, - "Interact cutscene pointer cannot be NULL" - ); - cutsceneSystemStartCutsceneWith( - target->interact.data.cutscene, - player, - target - ); - break; - - case ENTITY_INTERACT_CUTSCENE_NAME: { - cutscene_t *cutscene = - cutsceneCutsceneResolve(target->interact.data.cutsceneName); - if(cutscene != NULL) { - cutsceneSystemStartCutsceneWith(cutscene, player, target); - } - break; - } - - case ENTITY_INTERACT_PRINT: - uiTextboxMainSetText(target->interact.data.message); - - // If NPC turn to face player. - if(target->type == ENTITY_TYPE_NPC) { - target->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING; - target->animation = ENTITY_ANIM_IDLE; - entityTurn(target, entityDirGetOpposite(player->direction)); - } - - // entityTurn(player, player->direction); // Redundant (for now) - break; - - case ENTITY_INTERACT_CALLBACK: - assertNotNull( - target->interact.data.callback, - "Interact callback pointer cannot be NULL" - ); - target->interact.data.callback(player, target); - break; - - case ENTITY_INTERACT_NULL: - break; - - default: - assertUnreachable("Unknown entity interact type"); - break; - } -} \ No newline at end of file diff --git a/src/dusk/rpg/entity/interact/entityinteract.h b/src/dusk/rpg/entity/interact/entityinteract.h deleted file mode 100644 index 69f5a4ff..00000000 --- a/src/dusk/rpg/entity/interact/entityinteract.h +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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 entity_s entity_t; -typedef struct cutscene_s cutscene_t; - -/** - * Describes the type of interaction an entity supports. - */ -typedef enum { - ENTITY_INTERACT_NULL = 0, - - ENTITY_INTERACT_CUTSCENE, - ENTITY_INTERACT_CUTSCENE_NAME, - ENTITY_INTERACT_PRINT, - ENTITY_INTERACT_CALLBACK, - - ENTITY_INTERACT_COUNT -} entityinteracttype_t; - -/** - * Per-type data for an entity's interact component. - */ -typedef union { - // A compile-time cutscene_t* (a C-authored, test-only cutscene) - safe - // to hold onto indefinitely, since it's compiled directly into its own - // static arrays, never the shared CUTSCENE_SYSTEM.loadedItems buffer (see - // cutscenesystem.h). Non-const since cutsceneSystemStartCutsceneWith may - // splice CUTSCENE_ITEM_TYPE_INSERT items into it at runtime. - cutscene_t *cutscene; - // A bare cutscene name (as passed to cutsceneCutsceneResolve), resolved - // lazily at interact time rather than once up front - only one - // JSON-authored cutscene is ever resident at a time (see - // CUTSCENE_LOADED_ITEMS_MAX's doc comment), so resolving early and - // holding onto the result here would go stale the moment any other - // cutscene loads in the meantime. - const char_t *cutsceneName; - const char_t *message; - void (*callback)(entity_t *player, entity_t *target); -} entityinteractdata_t; - -/** - * Interact component attached to any entity that can be interacted with. - * Set type to ENTITY_INTERACT_NULL to mark the entity as non-interactable. - */ -typedef struct { - entityinteracttype_t type; - entityinteractdata_t data; -} entityinteract_t; - -/** - * Attempts to interact with the target entity on behalf of the player. - * Dispatches via the interact component; falls back to the entity type - * callback if no component is set. - * - * @param player Pointer to the player entity. - * @param target Pointer to the entity to interact with. - */ -void entityInteractWith(entity_t *player, entity_t *target); diff --git a/src/dusk/rpg/entity/item/CMakeLists.txt b/src/dusk/rpg/entity/item/CMakeLists.txt deleted file mode 100644 index 6834d19b..00000000 --- a/src/dusk/rpg/entity/item/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - entityitem.c -) diff --git a/src/dusk/rpg/entity/item/entityitem.c b/src/dusk/rpg/entity/item/entityitem.c deleted file mode 100644 index 5d39ce09..00000000 --- a/src/dusk/rpg/entity/item/entityitem.c +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entityitem.h" -#include "rpg/entity/entity.h" -#include "assert/assert.h" -#include "rpg/item/itemgive.h" -#include "ui/rpg/textbox/uitextboxmain.h" - -void entityItemInit(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - entity->interact.type = ENTITY_INTERACT_CALLBACK; - entity->interact.data.callback = entityItemInteract; -} - -void entityItemSet( - entity_t *entity, - const itemid_t item, - const uint8_t quantity -) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - entity->data.item.item = item; - entity->data.item.quantity = quantity; -} - -void entityItemInteract(entity_t *player, entity_t *target) { - assertNotNull(player, "Player entity pointer cannot be NULL"); - assertNotNull(target, "Target entity pointer cannot be NULL"); - - itemGive(target->data.item.item, target->data.item.quantity); - target->data.item.collected = true; -} - -void entityItemMovement(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - if(!entity->data.item.collected) return; - if(uiTextboxMainIsActive()) return; - - entitySetChunk(entity, CHUNK_INDEX_INVALID); - entity->type = ENTITY_TYPE_NULL; -} diff --git a/src/dusk/rpg/entity/item/entityitem.h b/src/dusk/rpg/entity/item/entityitem.h deleted file mode 100644 index 1485f1fa..00000000 --- a/src/dusk/rpg/entity/item/entityitem.h +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/item/item.h" - -typedef struct entity_s entity_t; - -typedef struct { - itemid_t item; - uint8_t quantity; - bool_t collected; -} entityitem_t; - -/** - * Initializes an item entity. Wires up the interact component so that - * interacting with the entity gives its item and removes it. - * - * @param entity Pointer to the entity structure to initialize. - */ -void entityItemInit(entity_t *entity); - -/** - * Sets the item and quantity represented by an item entity. - * - * @param entity Pointer to the entity structure. - * @param item The item ID to give when picked up. - * @param quantity The quantity to give when picked up. - */ -void entityItemSet( - entity_t *entity, - const itemid_t item, - const uint8_t quantity -); - -/** - * Interact callback for item entities. Gives the entity's item to the - * player and shows the pickup message. The entity itself is removed once - * the message has been dismissed, via entityItemMovement. - * - * @param player Pointer to the player entity. - * @param target Pointer to the item entity being picked up. - */ -void entityItemInteract(entity_t *player, entity_t *target); - -/** - * Update callback for item entities. Removes the entity once its pickup - * message has been dismissed. No-op if the item hasn't been collected. - * - * @param entity Pointer to the entity structure to update. - */ -void entityItemMovement(entity_t *entity); diff --git a/src/dusk/rpg/entity/npc/CMakeLists.txt b/src/dusk/rpg/entity/npc/CMakeLists.txt deleted file mode 100644 index 922a4e1d..00000000 --- a/src/dusk/rpg/entity/npc/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# 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 - npc.c - npcturn.c - npcwalk.c - npcpath.c -) diff --git a/src/dusk/rpg/entity/npc/npc.c b/src/dusk/rpg/entity/npc/npc.c deleted file mode 100644 index 4773e572..00000000 --- a/src/dusk/rpg/entity/npc/npc.c +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg/entity/entity.h" -#include "assert/assert.h" -#include "rpg/cutscene/cutscenesystem.h" - -const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT] = { - [NPC_MOVE_TYPE_NULL] = { 0 }, - - [NPC_MOVE_TYPE_RANDOM_TURN] = { - npcRandomTurnInit, - npcRandomTurnMovement - }, - - [NPC_MOVE_TYPE_RANDOM_WALK] = { - npcRandomWalkInit, - npcRandomWalkMovement - }, - - [NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK] = { - npcRandomTurnAndWalkInit, - npcRandomTurnAndWalkMovement - }, - - [NPC_MOVE_TYPE_PATH] = { - npcPathInit, - npcPathMovement - }, -}; - -void npcInit(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); -} - -void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - npc_t *npc = &entity->data.npc; - npc->moveType = moveType; - if(NPC_MOVE_CALLBACKS[moveType].init != NULL) { - NPC_MOVE_CALLBACKS[moveType].init(npc); - } -} - -void npcMovement(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_NPC) return; - - npc_t *npc = &entity->data.npc; - if(npc->interactState != NPC_INTERACT_STATE_NONE) return; - - const npcmovecallback_t *cb = &NPC_MOVE_CALLBACKS[npc->moveType]; - if(cb->movement != NULL) cb->movement(entity); -} \ No newline at end of file diff --git a/src/dusk/rpg/entity/npc/npc.h b/src/dusk/rpg/entity/npc/npc.h deleted file mode 100644 index be16ee9b..00000000 --- a/src/dusk/rpg/entity/npc/npc.h +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "npcturn.h" -#include "npcwalk.h" -#include "npcpath.h" - -typedef struct entity_s entity_t; - -typedef enum { - NPC_INTERACT_STATE_NONE, - NPC_INTERACT_STATE_CONVERSING, - NPC_INTERACT_STATE_COUNT -} npcinteractstate_t; - -typedef enum { - NPC_MOVE_TYPE_NULL, - NPC_MOVE_TYPE_RANDOM_TURN, - NPC_MOVE_TYPE_RANDOM_WALK, - NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK, - NPC_MOVE_TYPE_PATH, - NPC_MOVE_TYPE_COUNT -} npcmovetype_t; - -typedef union { - npcrandomturn_t randomTurn; - npcrandomwalk_t randomWalk; - npcrandomturnandwalk_t randomTurnAndWalk; - npcpath_t path; -} npcmovedata_t; - -typedef struct npc_s { - npcinteractstate_t interactState; - npcmovetype_t moveType; - npcmovedata_t moveData; -} npc_t; - -typedef struct { - /** Called once when the move type is set. */ - void (*init)(npc_t *npc); - /** Called each movement tick. */ - void (*movement)(entity_t *entity); -} npcmovecallback_t; - -extern const npcmovecallback_t NPC_MOVE_CALLBACKS[NPC_MOVE_TYPE_COUNT]; - -/** - * Initializes an NPC entity. - * - * @param entity Pointer to the entity structure to initialize. - */ -void npcInit(entity_t *entity); - -/** - * Sets the movement type for an NPC entity. - * - * @param entity Pointer to the entity structure. - * @param moveType The movement type to set. - */ -void npcSetMoveType(entity_t *entity, const npcmovetype_t moveType); - -/** - * Movement callback for an NPC entity. Gated by cutscene input. - * - * @param entity Pointer to the entity structure to update. - */ -void npcMovement(entity_t *entity); - -/** - * Free movement callback for an NPC entity. Runs always-run move types - * regardless of cutscene state. - * - * @param entity Pointer to the entity structure to update. - */ -void npcFreeMovement(entity_t *entity); \ No newline at end of file diff --git a/src/dusk/rpg/entity/npc/npcpath.c b/src/dusk/rpg/entity/npc/npcpath.c deleted file mode 100644 index 9d6ea47d..00000000 --- a/src/dusk/rpg/entity/npc/npcpath.c +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "npc.h" -#include "rpg/entity/entity.h" -#include "rpg/entity/entitypathstep.h" -#include "rpg/overworld/worldpos.h" -#include "assert/assert.h" - -void npcPathInit(npc_t *npc) { - npcpath_t *path = &npc->moveData.path; - path->count = 0; - path->index = 0; -} - -void npcPathAddNode(npc_t *npc, const worldpos_t pos) { - assertNotNull(npc, "NPC must not be NULL"); - assertTrue( - npc->moveData.path.count < NPC_PATH_COUNT_MAX, - "NPC path is full" - ); - npc->moveData.path.positions[npc->moveData.path.count++] = pos; -} - -void npcPathMovement(entity_t *entity) { - npcpath_t *path = &entity->data.npc.moveData.path; - if(path->count == 0) return; - if(!entityCanWalk(entity)) return; - - // Advance past any waypoints already reached (including the current one). - worldpos_t *target = &path->positions[path->index]; - if(entityPathStep(entity, *target, false)) { - path->index = (path->index + 1) % path->count; - target = &path->positions[path->index]; - // New target is the same tile - nothing to do this tick - if(worldPosIsEqual(entity->position, *target)) return; - entityPathStep(entity, *target, false); - } -} diff --git a/src/dusk/rpg/entity/npc/npcpath.h b/src/dusk/rpg/entity/npc/npcpath.h deleted file mode 100644 index d6469168..00000000 --- a/src/dusk/rpg/entity/npc/npcpath.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "rpg/overworld/worldpos.h" - -typedef struct npc_s npc_t; -typedef struct entity_s entity_t; - -/** Maximum number of waypoints in an NPC path. */ -#define NPC_PATH_COUNT_MAX 8 - -typedef struct { - worldpos_t positions[NPC_PATH_COUNT_MAX]; - uint8_t count; - uint8_t index; -} npcpath_t; - -/** - * Initializes the path movement data for an NPC. - * - * @param npc Pointer to the NPC to initialize. - */ -void npcPathInit(npc_t *npc); - -/** - * Appends a waypoint to the NPC's path. Has no effect if the path is full. - * - * @param npc Pointer to the NPC. - * @param pos The world position to append. - */ -void npcPathAddNode(npc_t *npc, const worldpos_t pos); - -/** - * Movement tick for an NPC following a path. - * - * @param entity Pointer to the entity to update. - */ -void npcPathMovement(entity_t *entity); diff --git a/src/dusk/rpg/entity/npc/npcturn.c b/src/dusk/rpg/entity/npc/npcturn.c deleted file mode 100644 index 63654ab3..00000000 --- a/src/dusk/rpg/entity/npc/npcturn.c +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "npc.h" -#include "rpg/entity/entity.h" -#include "util/random.h" -#include "time/time.h" -#include - -void npcRandomTurnInit(npc_t *npc) { - npcrandomturn_t *turn = &npc->moveData.randomTurn; - turn->frequencyMin = NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT; - turn->frequencyMax = NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT; - turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax); -} - -void npcRandomTurnMovement(entity_t *entity) { - npcrandomturn_t *turn = &entity->data.npc.moveData.randomTurn; - turn->timer -= TIME.delta; - if(turn->timer > 0.0f) return; - turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax); - if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4)); -} diff --git a/src/dusk/rpg/entity/npc/npcturn.h b/src/dusk/rpg/entity/npc/npcturn.h deleted file mode 100644 index 3568bbdb..00000000 --- a/src/dusk/rpg/entity/npc/npcturn.h +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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 npc_s npc_t; -typedef struct entity_s entity_t; - -/** Default min/max seconds between NPC random-turn ticks. */ -#define NPC_RANDOM_TURN_FREQUENCY_MIN_DEFAULT 2.0f -#define NPC_RANDOM_TURN_FREQUENCY_MAX_DEFAULT 4.0f - -typedef struct { - float_t frequencyMin; - float_t frequencyMax; - float_t timer; -} npcrandomturn_t; - -/** - * Initializes the random-turn movement data for an NPC. - * - * @param npc Pointer to the NPC to initialize. - */ -void npcRandomTurnInit(npc_t *npc); - -/** - * Movement tick for an NPC using random-turn movement. - * - * @param entity Pointer to the entity to update. - */ -void npcRandomTurnMovement(entity_t *entity); diff --git a/src/dusk/rpg/entity/npc/npcwalk.c b/src/dusk/rpg/entity/npc/npcwalk.c deleted file mode 100644 index dc8794ec..00000000 --- a/src/dusk/rpg/entity/npc/npcwalk.c +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "npc.h" -#include "rpg/entity/entity.h" -#include "util/random.h" -#include "time/time.h" -#include - -void npcRandomWalkInit(npc_t *npc) { - npcrandomwalk_t *walk = &npc->moveData.randomWalk; - walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT; - walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT; - walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax); -} - -void npcRandomWalkMovement(entity_t *entity) { - npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk; - walk->timer -= TIME.delta; - if(walk->timer > 0.0f) return; - walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax); - if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4)); -} - -void npcRandomTurnAndWalkInit(npc_t *npc) { - npcRandomTurnInit(npc); - npcrandomturnandwalk_t *tw = &npc->moveData.randomTurnAndWalk; - tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT; - tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT; - tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax); -} - -void npcRandomTurnAndWalkMovement(entity_t *entity) { - npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk; - - tw->turn.timer -= TIME.delta; - if(tw->turn.timer <= 0.0f) { - tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax); - if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4)); - } - - tw->walk.timer -= TIME.delta; - if(tw->walk.timer <= 0.0f) { - tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax); - if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4)); - } -} - diff --git a/src/dusk/rpg/entity/npc/npcwalk.h b/src/dusk/rpg/entity/npc/npcwalk.h deleted file mode 100644 index 4a65c4f8..00000000 --- a/src/dusk/rpg/entity/npc/npcwalk.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "npcturn.h" - -/** Default min/max seconds between NPC random-walk ticks. */ -#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f -#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f - -typedef struct { - float_t frequencyMin; - float_t frequencyMax; - float_t timer; -} npcrandomwalk_t; - -typedef struct { - npcrandomturn_t turn; - npcrandomwalk_t walk; -} npcrandomturnandwalk_t; - -/** - * Initializes the random-walk movement data for an NPC. - * - * @param npc Pointer to the NPC to initialize. - */ -void npcRandomWalkInit(npc_t *npc); - -/** - * Movement tick for an NPC using random-walk movement. - * - * @param entity Pointer to the entity to update. - */ -void npcRandomWalkMovement(entity_t *entity); - -/** - * Initializes the random-turn-and-walk movement data for an NPC. - * - * @param npc Pointer to the NPC to initialize. - */ -void npcRandomTurnAndWalkInit(npc_t *npc); - -/** - * Movement tick for an NPC using random-turn-and-walk movement. - * - * @param entity Pointer to the entity to update. - */ -void npcRandomTurnAndWalkMovement(entity_t *entity); - diff --git a/src/dusk/rpg/entity/player.c b/src/dusk/rpg/entity/player.c deleted file mode 100644 index c3ff2119..00000000 --- a/src/dusk/rpg/entity/player.c +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entity.h" -#include "assert/assert.h" -#include "rpg/rpgcamera.h" -#include "util/memory.h" -#include "time/time.h" -#include "ui/focus/uifocus.h" -#include "ui/screen/game/uigamemenu.h" -#include "rpg/cutscene/cutscenesystem.h" - -void playerInit(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - entity->globalId = ENTITY_GLOBAL_ID_PLAYER; -} - -bool_t playerCanInteract(entity_t *entity) { - return entity->animation == ENTITY_ANIM_IDLE; -} - -void playerInput(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return; - - // Toggle game menu on pause - if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) { - uiGameMenuClose(); - } else if( - inputPressed(INPUT_ACTION_PAUSE) && - entity->animation == ENTITY_ANIM_IDLE - ) { - uiGameMenuOpen(); - } - - // Can player act? - if(UI_FOCUS.count > 0) return; - - // Determine direction player may be trying to face/go. - const playerinputdirmap_t *dirMap; - - // First pass, we want to prefer a button relative to what the player is - // already facing, e.g. if they hold DOWN and start moving down, then tap - // right we want to prefer down over right. - - // Determine current dirmap based on facing direction. - dirMap = PLAYER_INPUT_DIR_MAP; - do { - if(dirMap->direction != entity->direction) continue; - break; - } while((++dirMap)->action != 0xFF); - - // Are we holding that button? - if(!inputIsDown(dirMap->action)) { - // No, so we need to check all directions. - dirMap = PLAYER_INPUT_DIR_MAP; - do { - if(!inputIsDown(dirMap->action)) continue; - break; - } while((++dirMap)->action != 0xFF); - } - - // Trying to direct? - if(dirMap->action != 0xFF) { - // Is the player attempting to turn? That is, is the player idle and has the - // turn lockout passed? - if(entityCanTurn(entity)) { - if(entity->direction != dirMap->direction) { - entityTurn(entity, dirMap->direction); - return; - } - } - - bool_t running = inputIsDown(INPUT_ACTION_CANCEL); - if(running && entityCanRun(entity)) { - entityRun(entity, dirMap->direction); - } else if(entityCanWalk(entity)) { - entityWalk(entity, dirMap->direction); - } - } - - // Interaction - if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) { - worldunit_t x, y, z; - { - worldunits_t relX, relY; - entityDirGetRelative(entity->direction, &relX, &relY); - x = entity->position.x + relX; - y = entity->position.y + relY; - z = entity->position.z; - } - - entity_t *target = entityGetAt((worldpos_t){ x, y, z }); - if(target == NULL) return; - entityInteractWith(entity, target); - } -} \ No newline at end of file diff --git a/src/dusk/rpg/entity/player.h b/src/dusk/rpg/entity/player.h deleted file mode 100644 index 13e369d1..00000000 --- a/src/dusk/rpg/entity/player.h +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "input/input.h" -#include "entitydir.h" - -typedef struct entity_s entity_t; - -typedef struct { - void *nothing; -} player_t; - -typedef struct { - inputaction_t action; - entitydir_t direction; -} playerinputdirmap_t; - -static const playerinputdirmap_t PLAYER_INPUT_DIR_MAP[] = { - { INPUT_ACTION_UP, ENTITY_DIR_NORTH }, - { INPUT_ACTION_DOWN, ENTITY_DIR_SOUTH }, - { INPUT_ACTION_LEFT, ENTITY_DIR_WEST }, - { INPUT_ACTION_RIGHT, ENTITY_DIR_EAST }, - - { 0xFF, 0xFF } -}; - -/** - * Initializes a player entity. - * - * @param entity Pointer to the entity structure to initialize. - */ -void playerInit(entity_t *entity); - -/** - * Returns true if the player entity is in a state where it can interact. - * - * @param entity Pointer to the player entity to check. - * @returns True if the entity can interact. - */ -bool_t playerCanInteract(entity_t *entity); - -/** - * Handles movement logic for the player entity. - * - * @param entity Pointer to the player entity structure. - */ -void playerInput(entity_t *entity); \ No newline at end of file diff --git a/src/dusk/rpg/item/CMakeLists.txt b/src/dusk/rpg/item/CMakeLists.txt deleted file mode 100644 index 29e79fb0..00000000 --- a/src/dusk/rpg/item/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - item.c - inventory.c - backpack.c - itemgive.c -) \ No newline at end of file diff --git a/src/dusk/rpg/item/backpack.c b/src/dusk/rpg/item/backpack.c deleted file mode 100644 index b668d0dd..00000000 --- a/src/dusk/rpg/item/backpack.c +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "backpack.h" -#include "assert/assert.h" - -backpack_t BACKPACK; - -void backpackInit() { - for(itemtype_t i = ITEM_TYPE_NULL + 1; i < ITEM.typeCount; i++) { - inventoryInit( - &BACKPACK.inventories[i], - BACKPACK.storage[i], - ITEM_DEF_COUNT_MAX - ); - } -} - -inventory_t *backpackGetInventory(const itemtype_t type) { - assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); - assertTrue(type < ITEM.typeCount, "Item type out of range"); - return &BACKPACK.inventories[type]; -} - -void backpackAdd(const itemid_t item, const uint8_t quantity) { - inventoryAdd(backpackGetInventory(itemGetTypeById(item)), item, quantity); -} - -void backpackRemove(const itemid_t item) { - inventoryRemove(backpackGetInventory(itemGetTypeById(item)), item); -} - -void backpackSet(const itemid_t item, const uint8_t quantity) { - inventorySet(backpackGetInventory(itemGetTypeById(item)), item, quantity); -} - -uint8_t backpackGetCount(const itemid_t item) { - return inventoryGetCount(backpackGetInventory(itemGetTypeById(item)), item); -} - -bool_t backpackItemExists(const itemid_t item) { - return inventoryItemExists(backpackGetInventory(itemGetTypeById(item)), item); -} - -bool_t backpackIsFull(const itemtype_t type) { - assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); - assertTrue(type < ITEM.typeCount, "Item type out of range"); - return inventoryIsFull(backpackGetInventory(type)); -} - -bool_t backpackItemFull(const itemid_t item) { - return inventoryItemFull(backpackGetInventory(itemGetTypeById(item)), item); -} - -void backpackSort( - const itemtype_t type, - const inventorysort_t sortBy, - const bool_t reverse -) { - assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null"); - assertTrue(type < ITEM.typeCount, "Item type out of range"); - inventorySort(backpackGetInventory(type), sortBy, reverse); -} diff --git a/src/dusk/rpg/item/backpack.h b/src/dusk/rpg/item/backpack.h deleted file mode 100644 index a8ab4419..00000000 --- a/src/dusk/rpg/item/backpack.h +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "inventory.h" - -typedef struct { - inventorystack_t storage[ITEM_TYPE_DEF_COUNT_MAX][ITEM_DEF_COUNT_MAX]; - inventory_t inventories[ITEM_TYPE_DEF_COUNT_MAX]; -} backpack_t; - -extern backpack_t BACKPACK; - -/** - * Initializes the backpack inventory for the player. - */ -void backpackInit(); - -/** - * Returns the inventory for a given item type. - * - * @param type The item type. - * @returns Pointer to the inventory for that type. - */ -inventory_t *backpackGetInventory(const itemtype_t type); - -/** - * Adds a quantity of an item to the backpack. - * - * @param item The item ID to add. - * @param quantity The quantity to add. - */ -void backpackAdd(const itemid_t item, const uint8_t quantity); - -/** - * Removes an item from the backpack. - * - * @param item The item ID to remove. - */ -void backpackRemove(const itemid_t item); - -/** - * Sets the quantity of an item in the backpack. - * - * @param item The item ID to set. - * @param quantity The quantity to set. - */ -void backpackSet(const itemid_t item, const uint8_t quantity); - -/** - * Gets the quantity of an item in the backpack. - * - * @param item The item ID to check. - * @returns The quantity held. - */ -uint8_t backpackGetCount(const itemid_t item); - -/** - * Checks if an item exists in the backpack (quantity > 0). - * - * @param item The item ID to check. - * @returns true if the item exists. - */ -bool_t backpackItemExists(const itemid_t item); - -/** - * Checks if the inventory for a given type is full. - * - * @param type The item type to check. - * @returns true if the type's inventory is full. - */ -bool_t backpackIsFull(const itemtype_t type); - -/** - * Checks if an item's stack is full in the backpack. - * - * @param item The item ID to check. - * @returns true if the item stack is full. - */ -bool_t backpackItemFull(const itemid_t item); - -/** - * Sorts the inventory for a given item type. - * - * @param type The item type whose inventory to sort. - * @param sortBy The sorting criteria. - * @param reverse Whether to sort in reverse order. - */ -void backpackSort( - const itemtype_t type, - const inventorysort_t sortBy, - const bool_t reverse -); \ No newline at end of file diff --git a/src/dusk/rpg/item/inventory.c b/src/dusk/rpg/item/inventory.c deleted file mode 100644 index 909b7296..00000000 --- a/src/dusk/rpg/item/inventory.c +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "inventory.h" -#include "util/memory.h" -#include "util/sort.h" -#include "assert/assert.h" - -void inventoryInit( - inventory_t* inventory, - inventorystack_t* storage, - uint8_t storageSize -) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(storage, "Storage pointer is NULL."); - assertTrue(storageSize > 0, "Storage size must be greater than zero."); - - inventory->storage = storage; - inventory->storageSize = storageSize; - - memoryZero(storage, sizeof(inventorystack_t) * storageSize); -} - -bool_t inventoryItemExists(const inventory_t *inventory, const itemid_t item) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(inventory->storage, "Storage pointer is NULL."); - assertTrue(inventory->storageSize > 0, "Storage too small."); - assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL."); - - inventorystack_t *stack = inventory->storage; - inventorystack_t *end = stack + inventory->storageSize; - do { - if(stack->item == ITEM_ID_NULL) break; - if(stack->item != item) continue; - assertTrue(stack->quantity > 0, "Item has quantity zero."); - return true; - } while(++stack < end); - - return false; -} - -void inventorySet( - inventory_t *inventory, - const itemid_t item, - const uint8_t quantity -) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(inventory->storage, "Storage pointer is NULL."); - assertTrue(inventory->storageSize > 0, "Storage too small."); - assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL."); - - // If quantity 0, remove. - if(quantity == 0) return inventoryRemove(inventory, item); - - // Search for existing stack. - inventorystack_t *stack = inventory->storage; - inventorystack_t *end = stack + inventory->storageSize; - do { - // Not in inventory yet, add as new stack. - if(stack->item == ITEM_ID_NULL) { - stack->item = item; - stack->quantity = quantity; - return; - } - - // Not the stack we're looking for. - if(stack->item != item) continue; - - // Update existing stack. - stack->quantity = quantity; - return; - } while(++stack < end); - - // No space in the inventory. - assertUnreachable("Inventory is full, cannot set more items."); -} - -void inventoryAdd( - inventory_t *inventory, - const itemid_t item, - const uint8_t quantity -) { - uint8_t current = inventoryGetCount(inventory, item); - uint16_t newQuantity = (uint16_t)current + (uint16_t)quantity; - - assertTrue( - newQuantity <= UINT8_MAX, - "Cannot add item, would overflow maximum quantity." - ); - - inventorySet(inventory, item, (uint8_t)newQuantity); -} - -void inventoryRemove(inventory_t *inventory, const itemid_t item) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(inventory->storage, "Storage pointer is NULL."); - assertTrue(inventory->storageSize > 0, "Storage too small."); - assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL."); - - inventorystack_t *stack = inventory->storage; - inventorystack_t *end = stack + inventory->storageSize; - - // Search for existing stack. - do { - // End of inventory, item not present. - if(stack->item == ITEM_ID_NULL) break; - - // Not matching stack. - if(stack->item != item) continue; - - // Match found, shift everything else down - memoryMove( - stack, - stack + 1, - (end - (stack + 1)) * sizeof(inventorystack_t) - ); - - // Clear last stack. - inventorystack_t *last = end - 1; - last->item = ITEM_ID_NULL; - - break; - } while(++stack < end); -} - -uint8_t inventoryGetCount(const inventory_t *inventory, const itemid_t item) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(inventory->storage, "Storage pointer is NULL."); - assertTrue(inventory->storageSize > 0, "Storage too small."); - assertTrue(item != ITEM_ID_NULL, "Item ID cannot be ITEM_ID_NULL."); - - inventorystack_t *stack = inventory->storage; - inventorystack_t *end = stack + inventory->storageSize; - do { - // End of inventory, item not present. - if(stack->item == ITEM_ID_NULL) break; - - // Not matching stack. - if(stack->item != item) continue; - - // Match found, return quantity. - return stack->quantity; - } while(++stack < end); - - return 0; -} - -bool_t inventoryIsFull(const inventory_t *inventory) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(inventory->storage, "Storage pointer is NULL."); - assertTrue(inventory->storageSize > 0, "Storage too small."); - - inventorystack_t *stack = inventory->storage; - inventorystack_t *end = stack + inventory->storageSize; - do { - // Found empty stack, not full. - if(stack->item == ITEM_ID_NULL) return false; - } while(++stack < end); - - return true; -} - -bool_t inventoryItemFull(const inventory_t *inventory, const itemid_t item) { - return inventoryGetCount(inventory, item) == ITEM_STACK_QUANTITY_MAX; -} - -// Sorters -int_t inventorySortById(const void *a, const void *b) { - const inventorystack_t *stackA = (const inventorystack_t*)a; - const inventorystack_t *stackB = (const inventorystack_t*)b; - if(stackA->item < stackB->item) return -1; - if(stackA->item > stackB->item) return 1; - return 0; -} - -int_t inventorySortByIdReverse(const void *a, const void *b) { - const inventorystack_t *stackA = (const inventorystack_t*)a; - const inventorystack_t *stackB = (const inventorystack_t*)b; - if(stackA->item < stackB->item) return 1; - if(stackA->item > stackB->item) return -1; - return 0; -} - -int_t inventorySortByType(const void *a, const void *b) { - const inventorystack_t *stackA = (const inventorystack_t*)a; - const inventorystack_t *stackB = (const inventorystack_t*)b; - const itemtype_t typeA = itemGetTypeById(stackA->item); - const itemtype_t typeB = itemGetTypeById(stackB->item); - if(typeA < typeB) return -1; - if(typeA > typeB) return 1; - return 0; -} - -int_t inventorySortByTypeReverse(const void *a, const void *b) { - const inventorystack_t *stackA = (const inventorystack_t*)a; - const inventorystack_t *stackB = (const inventorystack_t*)b; - const itemtype_t typeA = itemGetTypeById(stackA->item); - const itemtype_t typeB = itemGetTypeById(stackB->item); - if(typeA < typeB) return 1; - if(typeA > typeB) return -1; - return 0; -} - -void inventorySort( - inventory_t *inventory, - const inventorysort_t sortBy, - const bool_t reverse -) { - assertNotNull(inventory, "Inventory pointer is NULL."); - assertNotNull(inventory->storage, "Storage pointer is NULL."); - assertTrue(inventory->storageSize > 0, "Storage too small."); - assertTrue(sortBy < INVENTORY_SORT_COUNT, "Invalid sort type."); - - // Get count of used stacks - size_t count = 0; - inventorystack_t *stack = inventory->storage; - inventorystack_t *end = stack + inventory->storageSize; - do { - if(stack->item == ITEM_ID_NULL) break; - count++; - } while(++stack < end); - - if(count == 0) return; // Nothing to sort - - // Comparator - sortcompare_t comparator = NULL; - switch(sortBy) { - case INVENTORY_SORT_BY_ID: { - comparator = reverse ? inventorySortByIdReverse : inventorySortById; - break; - }; - - case INVENTORY_SORT_BY_TYPE: { - comparator = reverse ? inventorySortByTypeReverse : inventorySortByType; - break; - }; - - default: - assertUnreachable("Invalid sort type."); - break; - } - - assertNotNull(comparator, "Comparator function is NULL."); - - sort((void*)inventory->storage, count, sizeof(inventorystack_t), comparator); -} \ No newline at end of file diff --git a/src/dusk/rpg/item/inventory.h b/src/dusk/rpg/item/inventory.h deleted file mode 100644 index ba559dfe..00000000 --- a/src/dusk/rpg/item/inventory.h +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/item/item.h" - -#define ITEM_STACK_QUANTITY_MAX 99 - -typedef enum { - INVENTORY_SORT_BY_ID, - INVENTORY_SORT_BY_TYPE, - - INVENTORY_SORT_COUNT -} inventorysort_t; - -typedef struct { - itemid_t item; - uint8_t quantity; -} inventorystack_t; - -typedef struct { - inventorystack_t *storage; - uint8_t storageSize; -} inventory_t; - -/** - * Initializes an inventory. - * - * @param inventory The inventory to initialize. - * @param storage The storage array for the inventory. - * @param storageSize The size of the storage array. - */ -void inventoryInit( - inventory_t* inventory, - inventorystack_t* storage, - uint8_t storageSize -); - -/** - * Checks if a specific item exists in the inventory (and has quantity > 0). - * - * @param inventory The inventory to check. - * @param item The item ID to check. - * @return true if the item exists, false otherwise. - */ -bool_t inventoryItemExists(const inventory_t *inventory, const itemid_t item); - -/** - * Sets the quantity of a specific item in the inventory. - * - * @param inventory The inventory to modify. - * @param item The item ID to set. - * @param quantity The quantity to set. - */ -void inventorySet( - inventory_t *inventory, - const itemid_t item, - const uint8_t quantity -); - -/** - * Adds a specific quantity of an item to the inventory. - * - * @param inventory The inventory to modify. - * @param item The item ID to add. - * @param quantity The quantity to add. - */ -void inventoryAdd( - inventory_t *inventory, - const itemid_t item, - const uint8_t quantity -); - -/** - * Removes an item from the inventory. - * - * @param inventory The inventory to modify. - * @param item The item ID to remove. - */ -void inventoryRemove(inventory_t *inventory, const itemid_t item); - -/** - * Gets the count of a specific item in the inventory. - * - * @param inventory The inventory to check. - * @param item The item ID to check. - * @return The count of the item in the inventory. - */ -uint8_t inventoryGetCount(const inventory_t *inventory, const itemid_t item); - -/** - * Checks if the inventory is full. - * - * @param inventory The inventory to check. - * @return true if full, false otherwise. - */ -bool_t inventoryIsFull(const inventory_t *inventory); - -/** - * Checks if a specific item stack is full in the inventory. - * - * @param inventory The inventory to check. - * @param item The item ID to check. - * @return true if the item stack is full, false otherwise. - */ -bool_t inventoryItemFull(const inventory_t *inventory, const itemid_t item); - -/** - * Sorts the inventory based on the specified criteria. - * - * @param inventory The inventory to sort. - * @param sortBy The sorting criteria. - * @param reverse Whether to sort in reverse order. - */ -void inventorySort( - inventory_t *inventory, - const inventorysort_t sortBy, - const bool_t reverse -); \ No newline at end of file diff --git a/src/dusk/rpg/item/item.c b/src/dusk/rpg/item/item.c deleted file mode 100644 index 4be5f980..00000000 --- a/src/dusk/rpg/item/item.c +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "item.h" -#include "assert/assert.h" -#include "locale/localemanager.h" -#include "asset/asset.h" -#include "asset/loader/locale/assetlocaleloader.h" -#include "util/memory.h" -#include "util/string.h" -#include "yyjson.h" - -item_t ITEM; - -itemtype_t itemTypeGetOrCreate(const char_t *id) { - for(itemtype_t type = ITEM_TYPE_NULL + 1; type < ITEM.typeCount; type++) { - if(stringEquals(ITEM.types[type].id, id)) return type; - } - - assertTrue( - ITEM.typeCount < ITEM_TYPE_DEF_COUNT_MAX, "Too many item types defined." - ); - itemtype_t type = ITEM.typeCount++; - stringCopy(ITEM.types[type].id, id, ITEM_TYPE_NAME_MAX); - return type; -} - -errorret_t itemInit(void) { - memoryZero(&ITEM, sizeof(ITEM)); - ITEM.typeCount = ITEM_TYPE_NULL + 1; - ITEM.defCount = ITEM_ID_NULL + 1; - - assetentry_t *entry = - assetLock("items/items.jsonc", ASSET_LOADER_TYPE_JSON, NULL); - errorret_t ret = assetRequireLoaded(entry); - if(errorIsNotOk(ret)) { - assetUnlockEntry(entry); - errorChain(ret); - } - - yyjson_val *root = yyjson_doc_get_root(entry->data.json); - yyjson_val *items = yyjson_obj_get(root, "items"); - - yyjson_obj_iter iter = yyjson_obj_iter_with(items); - yyjson_val *key; - while((key = yyjson_obj_iter_next(&iter))) { - assertTrue(ITEM.defCount < ITEM_DEF_COUNT_MAX, "Too many item ids defined."); - - yyjson_val *val = yyjson_obj_iter_get_val(key); - const char_t *typeId = yyjson_get_str(yyjson_obj_get(val, "type")); - - itemid_t id = ITEM.defCount++; - stringCopy(ITEM.defs[id].id, yyjson_get_str(key), ITEM_ID_STRING_MAX); - ITEM.defs[id].type = itemTypeGetOrCreate(typeId); - } - - assetUnlockEntry(entry); - errorOk(); -} - -const itemdef_t *itemGetDefById(const itemid_t id) { - assertTrue(id > ITEM_ID_NULL, "Item ID must not be null"); - assertTrue(id < ITEM.defCount, "Item ID out of range"); - - return &ITEM.defs[id]; -} - -itemtype_t itemGetTypeById(const itemid_t id) { - return itemGetDefById(id)->type; -} - -itemid_t itemGetIdByName(const char_t *name) { - assertNotNull(name, "name must not be NULL"); - - for(itemid_t id = ITEM_ID_NULL + 1; id < ITEM.defCount; id++) { - if(stringEquals(ITEM.defs[id].id, name)) return id; - } - - return ITEM_ID_NULL; -} - -errorret_t itemGetName( - const itemid_t item, - char_t *buffer, - const size_t bufferSize -) { - const itemdef_t *def = itemGetDefById(item); - - char_t key[ITEM_ID_STRING_MAX + 16]; - stringFormat(key, sizeof(key), "item.%s.name", def->id); - errorChain(localeManagerGetString(key, buffer, bufferSize, 0)); - - errorOk(); -} diff --git a/src/dusk/rpg/item/item.h b/src/dusk/rpg/item/item.h deleted file mode 100644 index ccb42841..00000000 --- a/src/dusk/rpg/item/item.h +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define ITEM_TYPE_NAME_MAX 12 -#define ITEM_TYPE_DEF_COUNT_MAX 32 -#define ITEM_ID_STRING_MAX 12 -#define ITEM_DEF_COUNT_MAX 64 - -typedef uint8_t itemtype_t; -typedef uint16_t itemid_t; -#define ITEM_TYPE_NULL ((itemtype_t)0) -#define ITEM_ID_NULL ((itemid_t)0) - -typedef struct { - char_t id[ITEM_TYPE_NAME_MAX]; -} itemtypedef_t; - -typedef struct { - itemtype_t type; - char_t id[ITEM_ID_STRING_MAX]; -} itemdef_t; - -typedef struct { - itemtypedef_t types[ITEM_TYPE_DEF_COUNT_MAX]; - itemtype_t typeCount; - itemdef_t defs[ITEM_DEF_COUNT_MAX]; - itemid_t defCount; -} item_t; - -extern item_t ITEM; - -/** - * Initializes the item system. - * - * @return Any error that occurs. - */ -errorret_t itemInit(void); - -/** - * Finds the itemtype_t matching id among the types registered so far, - * registering a new one if this is the first time it's been seen. - * - * @param id The type's identifier string (see itemtypedef_t.id). - * @return The itemtype_t for id. - */ -itemtype_t itemTypeGetOrCreate(const char_t *id); - -/** - * Gets the itemdef_t for an item id. - * - * @param id The item ID to look up. Must not be ITEM_ID_NULL. - * @return The itemdef_t for id. - */ -const itemdef_t *itemGetDefById(const itemid_t id); - -/** - * Gets the itemtype_t for an item id. - * - * @param id The item ID to look up. Must not be ITEM_ID_NULL. - * @return The itemtype_t for id. - */ -itemtype_t itemGetTypeById(const itemid_t id); - -/** - * Finds the itemid_t whose id string matches name. - * - * @param name The item's identifier string (see itemdef_t.id). - * @return The matching itemid_t, or ITEM_ID_NULL if none matched. - */ -itemid_t itemGetIdByName(const char_t *name); - -/** - * Gets the localized display name for an item. - * - * @param item The item ID to look up. Must not be ITEM_ID_NULL. - * @param buffer Buffer to write the localized name into. - * @param bufferSize Size of the buffer. - * @return Any error that occurs. - */ -errorret_t itemGetName( - const itemid_t item, - char_t *buffer, - const size_t bufferSize -); diff --git a/src/dusk/rpg/item/itemgive.c b/src/dusk/rpg/item/itemgive.c deleted file mode 100644 index c7416dc2..00000000 --- a/src/dusk/rpg/item/itemgive.c +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "itemgive.h" -#include "rpg/item/backpack.h" -#include "rpg/item/item.h" -#include "ui/rpg/textbox/uitextboxmain.h" -#include "util/string.h" -#include "error/error.h" - -#define ITEM_GIVE_NAME_MAX_CHARS 32 - -void itemGive(const itemid_t item, const uint8_t quantity) { - backpackAdd(item, quantity); - - char_t name[ITEM_GIVE_NAME_MAX_CHARS]; - errorCatch(itemGetName(item, name, ITEM_GIVE_NAME_MAX_CHARS)); - - char_t msg[ITEM_GIVE_MESSAGE_MAX_CHARS]; - stringFormat( - msg, - ITEM_GIVE_MESSAGE_MAX_CHARS - 1, - "Received %s x%u", - name, - (uint32_t)quantity - ); - uiTextboxMainSetText(msg); -} diff --git a/src/dusk/rpg/item/itemgive.h b/src/dusk/rpg/item/itemgive.h deleted file mode 100644 index 5fa02b91..00000000 --- a/src/dusk/rpg/item/itemgive.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/item/item.h" - -#define ITEM_GIVE_MESSAGE_MAX_CHARS 256 - -/** - * Adds a quantity of an item to the player's backpack and shows a - * "Received x" message in the main textbox. - * - * @param item The item ID to give. - * @param quantity The quantity to give. - */ -void itemGive(const itemid_t item, const uint8_t quantity); diff --git a/src/dusk/rpg/overworld/CMakeLists.txt b/src/dusk/rpg/overworld/CMakeLists.txt deleted file mode 100644 index 24c8e3f5..00000000 --- a/src/dusk/rpg/overworld/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - chunk.c - map.c - maparea.c - worldpos.c - tile.c - tileshape.c -) - -add_subdirectory(global) - diff --git a/src/dusk/rpg/overworld/chunk.c b/src/dusk/rpg/overworld/chunk.c deleted file mode 100644 index 18a5316e..00000000 --- a/src/dusk/rpg/overworld/chunk.c +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "chunk.h" - -uint32_t chunkGetTileIndex(const chunkpos_t position) { - return (position.y * CHUNK_WIDTH) + position.x; -} - -bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) { - return (a.x == b.x) && (a.y == b.y) && (a.z == b.z); -} \ No newline at end of file diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h deleted file mode 100644 index 48180256..00000000 --- a/src/dusk/rpg/overworld/chunk.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/tile.h" -#include "worldpos.h" - -#define CHUNK_MESH_COUNT_MAX 10 -#define CHUNK_MESH_NAME_MAX 64 -#define CHUNK_ENTITY_COUNT_MAX 10 -#define CHUNK_ENTITY_SPAWN_COUNT_MAX 8 -#define CHUNK_AREA_COUNT_MAX 4 - -typedef struct assetentry_s assetentry_t; - -typedef struct chunk_s { - chunkpos_t position; - tile_t tiles[CHUNK_TILE_COUNT]; - - assetentry_t *dcfEntry; - - uint8_t meshCount; - char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; - vec3 meshOffsets[CHUNK_MESH_COUNT_MAX]; - mat4 meshModels[CHUNK_MESH_COUNT_MAX]; - assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX]; - - uint8_t entities[CHUNK_ENTITY_COUNT_MAX]; - - // Map area IDs (into MAP_AREAS) spawned from this chunk's file data. - // Removed via mapAreaRemove when this chunk unloads, and re-added if it - // streams back in - unlike entities (tracked by current position via - // entities[] above), areas have no position-based ownership mechanism of - // their own, so the owning chunk must track and tear them down directly. - uint8_t areas[CHUNK_AREA_COUNT_MAX]; -} chunk_t; - -/** - * Gets the tile index for a tile position within a chunk. - * - * @param position The position within the chunk. - * @return The tile index within the chunk. - */ -uint32_t chunkGetTileIndex(const chunkpos_t position); - -/** - * Checks if two chunk positions are equal. - * - * @param a The first chunk position. - * @param b The second chunk position. - * @return true if equal, false otherwise. - */ -bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b); diff --git a/src/dusk/rpg/overworld/global/CMakeLists.txt b/src/dusk/rpg/overworld/global/CMakeLists.txt deleted file mode 100644 index 27caaac2..00000000 --- a/src/dusk/rpg/overworld/global/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# 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 -) diff --git a/src/dusk/rpg/overworld/global/mapareaglobal.h b/src/dusk/rpg/overworld/global/mapareaglobal.h deleted file mode 100644 index 6f31dcd4..00000000 --- a/src/dusk/rpg/overworld/global/mapareaglobal.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "mapareaglobaldefs.h" -#include "mapareagloballist.h" - -#define MAP_AREA_CALLBACK_LIST_COUNT ( \ - sizeof(MAP_AREA_CALLBACK_LIST) / \ - sizeof(MAP_AREA_CALLBACK_LIST[0]) \ -) - -//EOF diff --git a/src/dusk/rpg/overworld/global/mapareaglobaldefs.h b/src/dusk/rpg/overworld/global/mapareaglobaldefs.h deleted file mode 100644 index 2f6d49b3..00000000 --- a/src/dusk/rpg/overworld/global/mapareaglobaldefs.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/maparea.h" - -#define MAP_AREA_CALLBACK(id) \ - static void MAP_AREA_CALLBACK_##id(entity_t *entity, const uint8_t trigger) - -#define MAP_AREA_CALLBACK_REF(id) \ - MAP_AREA_CALLBACK_##id - -//EOF diff --git a/src/dusk/rpg/overworld/global/mapareagloballist.h b/src/dusk/rpg/overworld/global/mapareagloballist.h deleted file mode 100644 index 9768582f..00000000 --- a/src/dusk/rpg/overworld/global/mapareagloballist.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "mapareaglobaldefs.h" -#include "console/console.h" - -MAP_AREA_CALLBACK(1) { - consolePrint("mapAreaGlobalCallback 1: trigger=%u", trigger); -} - -// Index 0 is reserved (not a valid callback ID) - see mapAreaAddGlobal. -static const mapareacallback_t MAP_AREA_CALLBACK_LIST[] = { - NULL, - MAP_AREA_CALLBACK_REF(1), -}; - -//EOF diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c deleted file mode 100644 index 78252261..00000000 --- a/src/dusk/rpg/overworld/map.c +++ /dev/null @@ -1,579 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "map.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "console/console.h" -#include "util/string.h" -#include "rpg/entity/global/entityglobal.h" -#include "rpg/entity/item/entityitem.h" -#include "rpg/overworld/maparea.h" -#include "save/save.h" -#include "time/time.h" - -map_t MAP; - -void mapChunkLoadingSlotClear(chunk_t *chunk) { - for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) { - if(MAP.loadingChunks[i] != chunk) continue; - MAP.loadingChunks[i] = NULL; - return; - } -} - -errorret_t mapInit() { - memoryZero(&MAP, sizeof(map_t)); - errorOk(); -} - -bool_t mapIsLoaded() { - return MAP.name[0] != '\0'; -} - -errorret_t mapSetMap(const char_t *map) { - assertNotNull(map, "mapSetMap: map name cannot be NULL"); - assertStrLenMax(map, MAP_NAME_MAX, "Map name too long"); - - if(mapIsLoaded()) { - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - mapChunkUnload(&MAP.chunks[i]); - } - } - - memoryZero(&MAP, sizeof(map_t)); - stringCopy(MAP.name, map, MAP_NAME_MAX); - - // Keep the current save slot's cached map name in sync so whichever map - // is active gets persisted next time the slot is actually saved. - stringCopy(SAVE.slot.cachedData.mapName, MAP.name, MAP_NAME_MAX); - - chunkindex_t i = 0; - for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) { - for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) { - for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) { - chunk_t *chunk = &MAP.chunks[i++]; - chunk->position = (chunkpos_t){ - (chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z - }; - errorChain(mapChunkLoad(chunk)); - } - } - } - - mapRebuildChunkOrder(); - errorOk(); -} - -errorret_t mapPositionSet(const chunkpos_t newPos) { - if(!mapIsLoaded()) errorThrow("No map loaded"); - if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk(); - - // Separate loaded chunks into "keep" and "free" buckets. - chunkindex_t chunksFreed[MAP_CHUNK_COUNT]; - uint32_t freedCount = 0; - - // Use a boolean grid so the inner load loop can check O(1). - bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH]; - memoryZero(posLoaded, sizeof(posLoaded)); - - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = &MAP.chunks[i]; - chunkunit_t rx = chunk->position.x - newPos.x; - chunkunit_t ry = chunk->position.y - newPos.y; - chunkunit_t rz = chunk->position.z - newPos.z; - if( - rx >= 0 && rx < MAP_CHUNK_WIDTH && - ry >= 0 && ry < MAP_CHUNK_HEIGHT && - rz >= 0 && rz < MAP_CHUNK_DEPTH - ) { - posLoaded[rx][ry][rz] = true; - } else { - mapChunkUnload(chunk); - chunksFreed[freedCount++] = i; - } - } - - for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) { - for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) { - for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) { - if(posLoaded[x][y][z]) continue; - assertTrue(freedCount > 0, "No free chunk slot available."); - chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]]; - chunk->position = (chunkpos_t){ - newPos.x + (chunkunit_t)x, - newPos.y + (chunkunit_t)y, - newPos.z + (chunkunit_t)z - }; - errorChain(mapChunkLoad(chunk)); - } - } - } - - MAP.chunkPosition = newPos; - mapRebuildChunkOrder(); - - // chunkOrder was just reshuffled, so entity->chunkIndex (an index into - // chunkOrder, not a direct chunk_t pointer) may now resolve to a - // completely different chunk than the one that actually still tracks - // this entity in its entities[] slot - a chunk that stays loaded keeps - // its entities[] contents untouched by the reorder, only its slot - // number within chunkOrder moves. Recompute the cached index directly - // from each entity's (unmoved) position rather than going through - // entitySetChunk, which would look up the stale old index, fail to - // find/clear the entity's real registration, and insert a duplicate - // entry alongside it. Entities already handled by mapChunkUnload above - // (detached to CHUNK_INDEX_INVALID, or despawned to ENTITY_TYPE_NULL) - // are skipped. - for(uint8_t i = 0; i < ENTITY_COUNT; i++) { - entity_t *entity = &ENTITIES[i]; - if(entity->type == ENTITY_TYPE_NULL) continue; - if(entity->chunkIndex == CHUNK_INDEX_INVALID) continue; - - chunkpos_t cp; - worldPosToChunkPos(&entity->position, &cp); - entity->chunkIndex = mapGetChunkIndexAt(cp); - } - - errorOk(); -} - -errorret_t mapUpdate() { - // TEST ONLY - see MAP_CHUNK_LOAD_DELAY_TEST. - if(MAP.chunkLoadCooldown > 0.0f) { - MAP.chunkLoadCooldown -= TIME.delta; - if(MAP.chunkLoadCooldown <= 0.0f) { - MAP.chunkLoadCooldown = 0.0f; - mapChunkLoadNext(); - } - } - - errorOk(); -} - -errorret_t mapDispose() { - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - mapChunkUnload(&MAP.chunks[i]); - } - MAP.name[0] = '\0'; - errorOk(); -} - -void mapChunkUnload(chunk_t *chunk) { - mapChunkLoadQueueRemove(chunk); - mapChunkLoadingSlotClear(chunk); - - for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { - if(chunk->entities[i] == 0xFF) continue; - entity_t *entity = &ENTITIES[chunk->entities[i]]; - if(!entityCanUnload(entity)) { - entitySetChunk(entity, CHUNK_INDEX_INVALID); - } else { - entity->type = ENTITY_TYPE_NULL; - } - } - - memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); - - for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) { - if(chunk->areas[i] == 0xFF) continue; - mapAreaRemove(chunk->areas[i]); - } - memorySet(chunk->areas, 0xFF, sizeof(chunk->areas)); - - if(chunk->dcfEntry != NULL) { - chunk->dcfEntry->onLoaded = NULL; - chunk->dcfEntry->onLoadedUser = NULL; - chunk->dcfEntry->onError = NULL; - chunk->dcfEntry->onErrorUser = NULL; - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; - } - - // modelEntries are borrowed pointers, not independently locked - the - // chunk asset entry (released above) is what actually holds the ref on - // each model, so nothing to unlock here, just drop our own copies. - for(uint8_t m = 0; m < chunk->meshCount; m++) { - chunk->modelEntries[m] = NULL; - } - chunk->meshCount = 0; -} - -errorret_t mapChunkLoad(chunk_t *chunk) { - if(!mapIsLoaded()) errorThrow("No map loaded"); - - mapChunkLoadQueueRemove(chunk); - mapChunkLoadingSlotClear(chunk); - - if(chunk->dcfEntry != NULL) { - chunk->dcfEntry->onLoaded = NULL; - chunk->dcfEntry->onLoadedUser = NULL; - chunk->dcfEntry->onError = NULL; - chunk->dcfEntry->onErrorUser = NULL; - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; - } - - memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); - - // Normally already empty (mapChunkUnload clears these before a chunk is - // handed back for reuse), but cleared defensively here too so a reload - // never leaks a MAP_AREAS slot referenced by a stale owned area ID. - for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) { - if(chunk->areas[i] == 0xFF) continue; - mapAreaRemove(chunk->areas[i]); - } - memorySet(chunk->areas, 0xFF, sizeof(chunk->areas)); - - chunk->meshCount = 0; - - char_t name[MAP_FILE_PATH_MAX]; - stringFormat( - name, sizeof(name), - "maps/%s/chunks/chunk_%d_%d_%d.json", - MAP.name, - (int32_t)chunk->position.x, - (int32_t)chunk->position.y, - (int32_t)chunk->position.z - ); - - if(!assetFileExists(name)) { - for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) { - // chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 }; - chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND }; - } - errorOk(); - } - - assertTrue( - MAP.loadQueueCount < MAP_CHUNK_COUNT, - "Chunk load queue overflow" - ); - MAP.loadQueue[MAP.loadQueueCount++] = chunk; - mapChunkLoadNext(); - errorOk(); -} - -void mapChunkLoadNext() { - // TEST ONLY - see MAP_CHUNK_LOAD_DELAY_TEST. Skip starting anything new - // until the cooldown set by the last load we started has expired; - // mapUpdate() ticks it down and retries this once it hits zero. - if(MAP.chunkLoadCooldown > 0.0f) return; - - for(uint32_t slot = 0; slot < MAP_CHUNK_LOAD_CONCURRENCY; slot++) { - if(MAP.loadingChunks[slot] != NULL) continue; - if(MAP.loadQueueCount == 0) return; - - chunk_t *chunk = MAP.loadQueue[0]; - for(uint32_t i = 1; i < MAP.loadQueueCount; i++) { - MAP.loadQueue[i - 1] = MAP.loadQueue[i]; - } - MAP.loadQueueCount--; - MAP.loadingChunks[slot] = chunk; - MAP.chunkLoadCooldown = MAP_CHUNK_LOAD_DELAY_TEST; - - char_t name[MAP_FILE_PATH_MAX]; - stringFormat( - name, sizeof(name), - "maps/%s/chunks/chunk_%d_%d_%d.json", - MAP.name, - (int32_t)chunk->position.x, - (int32_t)chunk->position.y, - (int32_t)chunk->position.z - ); - - assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL); - assertNotNull(entry, "Failed to get chunk asset entry"); - chunk->dcfEntry = entry; - - // The entry may already be resident from an earlier load that hasn't - // been reaped yet - in that case onLoaded/onError already fired once - // and never will again, so handle the terminal state directly instead - // of waiting on a subscription that would never trigger. Both of these - // recurse back into mapChunkLoadNext once they clear this slot, so the - // outer loop just continues on to try filling the next one. - if(entry->state == ASSET_ENTRY_STATE_LOADED) { - mapChunkLoaded(entry, chunk); - continue; - } - if(entry->state == ASSET_ENTRY_STATE_ERROR) { - mapChunkLoadError(entry, chunk); - continue; - } - - assertNull(entry->onLoaded, "Entry already has an onLoaded subscriber."); - assertNull(entry->onError, "Entry already has an onError subscriber."); - entry->onLoaded = mapChunkLoaded; - entry->onLoadedUser = chunk; - entry->onError = mapChunkLoadError; - entry->onErrorUser = chunk; - } -} - -void mapChunkLoadQueueRemove(chunk_t *chunk) { - for(uint32_t i = 0; i < MAP.loadQueueCount; i++) { - if(MAP.loadQueue[i] != chunk) continue; - for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) { - MAP.loadQueue[j - 1] = MAP.loadQueue[j]; - } - MAP.loadQueueCount--; - return; - } -} - - -chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) { - if(!mapIsLoaded()) return CHUNK_INDEX_INVALID; - - chunkpos_t relPos = { - position.x - MAP.chunkPosition.x, - position.y - MAP.chunkPosition.y, - position.z - MAP.chunkPosition.z - }; - - if( - relPos.x < 0 || relPos.y < 0 || relPos.z < 0 || - relPos.x >= MAP_CHUNK_WIDTH || - relPos.y >= MAP_CHUNK_HEIGHT || - relPos.z >= MAP_CHUNK_DEPTH - ) { - return CHUNK_INDEX_INVALID; - } - - return chunkPosToIndex(&relPos); -} - -chunk_t *mapGetChunk(const chunkindex_t index) { - if(index == CHUNK_INDEX_INVALID || index >= MAP_CHUNK_COUNT) return NULL; - if(!mapIsLoaded()) return NULL; - return MAP.chunkOrder[index]; -} - -tile_t mapGetTile(const worldpos_t position) { - if(!mapIsLoaded()) return TILE_NULL; - - chunkpos_t chunkPos; - worldPosToChunkPos(&position, &chunkPos); - chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos); - if(chunkIndex == CHUNK_INDEX_INVALID) return TILE_NULL; - - chunk_t *chunk = mapGetChunk(chunkIndex); - assertNotNull(chunk, "Chunk pointer cannot be NULL"); - chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position); - tile_t tile = chunk->tiles[tileIndex]; - if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL; - return tile; -} - -bool_t mapGetWalkableZNear( - const worldunit_t x, - const worldunit_t y, - const worldunit_t nearZ, - worldunit_t *outZ -) { - assertNotNull(outZ, "Output Z pointer cannot be NULL"); - - const worldunit_t candidates[] = { - nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1) - }; - for(uint8_t i = 0; i < 3; i++) { - const worldpos_t pos = { x, y, candidates[i] }; - if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue; - *outZ = candidates[i]; - return true; - } - - return false; -} - -entity_t * mapSpawnEntity( - const entityglobalid_t globalId, - const worldpos_t position -) { - assertTrue( - globalId > ENTITY_GLOBAL_ID_START, - "mapSpawnEntity requires a global ID greater than ENTITY_GLOBAL_ID_START" - ); - assertTrue( - globalId < ENTITY_GLOBAL_LIST_COUNT, - "Global ID is out of range for entity global init callbacks" - ); - - // Already spawned? Reuse the existing entity instead of making a - // duplicate - two entities must never share a global ID. - entity_t *existing = entityGetByGlobalId(globalId); - if(existing != NULL) return existing; - - // See if there is a callback for this entity first. - const entityglobaldef_t *def = &ENTITY_GLOBAL_LIST[globalId]; - assertNotNull(def, "No global entity definition for this ID"); - assertNotNull(def->callback, "No callback registered for this global ID"); - - // Get available entity. - uint8_t index = entityGetAvailable(); - assertTrue(index != 0xFF, "No available entity slots for mapSpawnEntity"); - - // Get the pointer and do the init. - entity_t *entity = &ENTITIES[index]; - entityInit(entity, def->type); - entity->globalId = globalId; - entityPositionSet(entity, position);// Also assigns the entity's chunk. - - // Invoke the callback to initialize the entity. - entityglobalcreate_t create = { - .entity = entity, - .position = position - }; - def->callback(&create); - return entity; -} - -void mapRebuildChunkOrder() { - memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder)); - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = &MAP.chunks[i]; - const chunkpos_t rel = { - chunk->position.x - MAP.chunkPosition.x, - chunk->position.y - MAP.chunkPosition.y, - chunk->position.z - MAP.chunkPosition.z - }; - if( - rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH || - rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT || - rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH - ) continue; - MAP.chunkOrder[chunkPosToIndex(&rel)] = chunk; - } -} - -void mapChunkLoadError(assetentry_t *entry, void *user) { - assertNotNull(entry, "mapChunkLoadError: entry cannot be NULL"); - assertNotNull(user, "mapChunkLoadError: user cannot be NULL"); - chunk_t *chunk = (chunk_t *)user; - if(chunk->dcfEntry != entry) return; - consolePrint( - "Chunk load error: %d %d %d", - (int32_t)chunk->position.x, - (int32_t)chunk->position.y, - (int32_t)chunk->position.z - ); - entry->onLoaded = NULL; - entry->onLoadedUser = NULL; - entry->onError = NULL; - entry->onErrorUser = NULL; - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; - memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); - - mapChunkLoadingSlotClear(chunk); - mapChunkLoadNext(); -} - -void mapChunkLoaded(assetentry_t *entry, void *user) { - assertNotNull(entry, "mapChunkLoaded: entry cannot be NULL"); - assertNotNull(user, "mapChunkLoaded: user cannot be NULL"); - chunk_t *chunk = (chunk_t *)user; - if(chunk->dcfEntry != entry) return; - // consolePrint( - // "Chunk loaded: %d %d %d", - // (int32_t)chunk->position.x, - // (int32_t)chunk->position.y, - // (int32_t)chunk->position.z - // ); - uint8_t meshCount = entry->data.chunk.meshCount; - memoryCopy( - chunk->tiles, - entry->data.chunk.tiles, - sizeof(chunk->tiles) - ); - worldpos_t wp; - chunkPosToWorldPos(&chunk->position, &wp); - vec3 wpf = { - (float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT - }; - for(uint8_t m = 0; m < meshCount; m++) { - stringCopy( - chunk->modelNames[m], - entry->data.chunk.modelNames[m], - CHUNK_MESH_NAME_MAX - ); - glm_vec3_copy( - entry->data.chunk.meshOffsets[m], - chunk->meshOffsets[m] - ); - vec3 scaledOffset = { - chunk->meshOffsets[m][0], - chunk->meshOffsets[m][1], - chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT - }; - vec3 pos; - glm_vec3_add(wpf, scaledOffset, pos); - glm_translate_make(chunk->meshModels[m], pos); - // Borrow the pointer rather than stealing it - the chunk asset entry - // keeps its own lock on each model (taken once while it loaded) and we - // keep the chunk asset entry itself locked (see below), so the models - // stay valid for as long as this chunk_t is using them. The entry may - // now be reused by a later mapChunkLoad for a different chunk_t once we - // eventually unlock it in mapChunkUnload, at which point its - // modelEntries must still be intact for that next reuse to copy from. - chunk->modelEntries[m] = entry->data.chunk.modelEntries[m]; - } - entry->onLoaded = NULL; - entry->onLoadedUser = NULL; - entry->onError = NULL; - entry->onErrorUser = NULL; - // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the - // chunk asset entry (and therefore its model locks) alive for as long as - // this chunk_t is displaying it. Released in mapChunkUnload instead. - chunk->meshCount = meshCount; - - // Spawn entities declared by this chunk's file. Global entities are - // deduped by mapSpawnEntity itself (a persistent NPC that streams back - // in won't be duplicated); item entities have no persistent identity, so - // each reload spawns a fresh one - picking an item up and then leaving - // and re-entering its chunk will currently respawn it, since nothing - // tracks "already collected" across a chunk unload/reload yet. - for(uint8_t s = 0; s < entry->data.chunk.entitySpawnCount; s++) { - chunkentityspawn_t *spawn = &entry->data.chunk.entitySpawns[s]; - if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL) { - mapSpawnEntity((entityglobalid_t)spawn->globalId, spawn->position); - continue; - } - - uint8_t index = entityGetAvailable(); - assertTrue(index != 0xFF, "No available entity slots for chunk spawn"); - entity_t *itemEntity = &ENTITIES[index]; - entityInit(itemEntity, ENTITY_TYPE_ITEM); - entityItemSet( - itemEntity, (itemid_t)spawn->itemId, spawn->itemQuantity - ); - entityPositionSet(itemEntity, spawn->position); - } - - // Spawn map areas declared by this chunk's file, tracked as owned by - // this chunk so mapChunkUnload can tear them down again. - for(uint8_t s = 0; s < entry->data.chunk.areaSpawnCount; s++) { - chunkareaspawn_t *area = &entry->data.chunk.areaSpawns[s]; - uint8_t areaId = mapAreaAddGlobal( - area->min, area->max, area->callbackId, area->notify, area->trigger - ); - - uint8_t slot = 0xFF; - for(uint8_t i = 0; i < CHUNK_AREA_COUNT_MAX; i++) { - if(chunk->areas[i] != 0xFF) continue; - slot = i; - break; - } - assertTrue(slot != 0xFF, "Chunk has no free owned-area slots"); - chunk->areas[slot] = areaId; - } - - mapChunkLoadingSlotClear(chunk); - mapChunkLoadNext(); -} diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h deleted file mode 100644 index 93b70057..00000000 --- a/src/dusk/rpg/overworld/map.h +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "rpg/overworld/chunk.h" -#include "rpg/entity/entity.h" - -#define MAP_FILE_PATH_MAX 128 - -// Buffer size for a map's name, including the null terminator - so map -// names are at most 9 characters. -#define MAP_NAME_MAX 10 - -// Number of chunks that may be mid-load (asset locked & awaiting onLoaded/ -// onError) at the same time - everything past this waits in loadQueue. -#define MAP_CHUNK_LOAD_CONCURRENCY 1 - -// TEST ONLY - deliberately exaggerated delay (seconds) enforced between -// starting one chunk load and starting the next, to observe its effect on -// framerate. See MAP.chunkLoadCooldown / mapChunkLoadNext. -#define MAP_CHUNK_LOAD_DELAY_TEST 0.05f - -typedef struct map_s { - // Name of the currently loaded map, or an empty string if none is - // loaded (see mapIsLoaded). - char_t name[MAP_NAME_MAX]; - - chunk_t chunks[MAP_CHUNK_COUNT]; - chunk_t *chunkOrder[MAP_CHUNK_COUNT]; - chunkpos_t chunkPosition; - - chunk_t *loadQueue[MAP_CHUNK_COUNT]; - uint32_t loadQueueCount; - chunk_t *loadingChunks[MAP_CHUNK_LOAD_CONCURRENCY]; - - // TEST ONLY - seconds remaining before mapChunkLoadNext is allowed to - // start another chunk load. See MAP_CHUNK_LOAD_DELAY_TEST. - float_t chunkLoadCooldown; -} map_t; - -extern map_t MAP; - -/** - * Initializes the map subsystem. No map is loaded until mapSetMap is - * called. - * - * @return An error code. - */ -errorret_t mapInit(); - -/** - * Checks if a map is loaded. - * - * @return true if a map is loaded, false otherwise. - */ -bool_t mapIsLoaded(); - -/** - * Sets the active map by name, unloading the previously loaded map (if - * any) first, then starts loading the new map's chunks around chunk - * position (0, 0, 0). - * - * @param map The map's name, at most MAP_NAME_MAX - 1 characters. - * @return An error code. - */ -errorret_t mapSetMap(const char_t *map); - -/** - * Updates the map. - * - * @return An error code. - */ -errorret_t mapUpdate(); - -/** - * Disposes of the map. - * - * @return An error code. - */ -errorret_t mapDispose(); - -/** - * Sets the map position and updates chunks accordingly. - * - * @param newPos The new chunk position. - * @return An error code. - */ -errorret_t mapPositionSet(const chunkpos_t newPos); - -/** - * Clears chunk's mid-load slot, if it currently holds one. - * - * @param chunk The chunk whose mid-load slot to clear. - */ -void mapChunkLoadingSlotClear(chunk_t *chunk); - -/** - * Unloads a chunk. - * - * @param chunk The chunk to unload. - */ -void mapChunkUnload(chunk_t* chunk); - -/** - * Loads a chunk. Starts async loading without blocking. - * - * @param chunk The chunk to load. - * @return An error code. - */ -errorret_t mapChunkLoad(chunk_t* chunk); - -/** - * Starts loading queued chunks until MAP_CHUNK_LOAD_CONCURRENCY chunks are - * mid-load. Called after mapChunkLoad enqueues a chunk, and again after a - * mid-load chunk finishes (or is unloaded) to advance the queue. - */ -void mapChunkLoadNext(); - -/** - * Removes a chunk from the load queue if present. Used when a chunk is - * re-queued or unloaded before its turn to load has come up. - * - * @param chunk The chunk to remove from the load queue. - */ -void mapChunkLoadQueueRemove(chunk_t *chunk); - -/** - * Callback invoked when a chunk DCF asset fails to load. Fills the - * chunk tiles with TILE_SHAPE_GROUND as a fallback. - * Always invoked on the main thread. - * - * @param entry The failed assetentry_t. - * @param user The chunk_t that owns the entry. - */ -void mapChunkLoadError(assetentry_t *entry, void *user); - -/** - * Callback invoked when a chunk DCF asset finishes loading. - * Always invoked on the main thread. - * - * @param entry The loaded assetentry_t. - * @param user The chunk_t that owns the entry. - */ -void mapChunkLoaded(assetentry_t *entry, void *user); - -/** - * Rebuilds chunkOrder from the loaded chunks that fall within the - * current render window. Called whenever chunkPosition changes. - */ -void mapRebuildChunkOrder(); - -/** - * Gets the index of a chunk, within the world, at the given position. - * - * @param position The chunk position. - * @return The index of the chunk, or CHUNK_INDEX_INVALID if out of - * bounds. - */ -chunkindex_t mapGetChunkIndexAt(const chunkpos_t position); - -/** - * Gets a chunk by its index. - * - * @param chunkIndex The index of the chunk. - * @return A pointer to the chunk, or NULL if chunkIndex is - * CHUNK_INDEX_INVALID, out of range, or no map is currently loaded. - */ -chunk_t * mapGetChunk(const chunkindex_t chunkIndex); - -/** - * Gets the tile at the given world position. - * - * @param position The world position. - * @return The tile at that position, or TILE_NULL if the chunk is unloaded. - */ -tile_t mapGetTile(const worldpos_t position); - -/** - * Finds the closest walkable Z layer to nearZ at the given X/Y. Checks - * nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever - * change height by one Z layer between adjacent tiles. - * - * @param x The world X coordinate to check. - * @param y The world Y coordinate to check. - * @param nearZ The reference Z layer to search outward from. - * @param outZ Output pointer, set to the resolved Z layer on success. - * @return true if a walkable tile was found, false otherwise. - */ -bool_t mapGetWalkableZNear( - const worldunit_t x, - const worldunit_t y, - const worldunit_t nearZ, - worldunit_t *outZ -); - -/** - * Spawns a global (persistent) entity into the world at the given position. - * Asserts globalId is greater than ENTITY_GLOBAL_ID_START - use entityInit - * directly for ephemeral, non-global entities. - * - * @param globalId The global entity ID to assign, must be greater than - * ENTITY_GLOBAL_ID_START. - * @param position The world position to spawn the entity at. - * @return Pointer to the spawned entity. - */ -entity_t * mapSpawnEntity( - const entityglobalid_t globalId, - const worldpos_t position -); \ No newline at end of file diff --git a/src/dusk/rpg/overworld/maparea.c b/src/dusk/rpg/overworld/maparea.c deleted file mode 100644 index 6c1a798b..00000000 --- a/src/dusk/rpg/overworld/maparea.c +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "maparea.h" -#include "assert/assert.h" -#include "util/math.h" -#include "util/memory.h" -#include "rpg/overworld/map.h" -#include "rpg/overworld/global/mapareaglobal.h" - -maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX]; - -void mapAreaInit( - maparea_t *area, - const worldpos_t min, - const worldpos_t max, - const mapareacallback_t callback, - const uint8_t notify, - const uint8_t trigger -) { - assertNotNull(area, "Map area pointer cannot be NULL"); - assertNotNull(callback, "Map area callback cannot be NULL"); - - area->min.x = mathMin(min.x, max.x); - area->min.y = mathMin(min.y, max.y); - area->min.z = mathMin(min.z, max.z); - - area->max.x = mathMax(min.x, max.x); - area->max.y = mathMax(min.y, max.y); - area->max.z = mathMax(min.z, max.z); - - area->callback = callback; - area->notify = notify; - area->trigger = trigger; - area->triggerCount = 0; - - memorySet(area->entities, 0, sizeof(area->entities)); -} - -bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) { - assertNotNull(area, "Map area pointer cannot be NULL"); - - return ( - position.x >= area->min.x && position.x <= area->max.x && - position.y >= area->min.y && position.y <= area->max.y && - position.z >= area->min.z && position.z <= area->max.z - ); -} - -bool_t mapAreaIsChunkOverlappingOrInside( - const maparea_t *area, - const chunk_t *chunk -) { - assertNotNull(area, "Map area pointer cannot be NULL"); - assertNotNull(chunk, "Chunk pointer cannot be NULL"); - - worldpos_t chunkMin, chunkMax; - chunkPosToWorldPos(&chunk->position, &chunkMin); - chunkMax.x = chunkMin.x + CHUNK_WIDTH - 1; - chunkMax.y = chunkMin.y + CHUNK_HEIGHT - 1; - chunkMax.z = chunkMin.z + CHUNK_DEPTH - 1; - - return ( - chunkMin.x <= area->max.x && area->min.x <= chunkMax.x && - chunkMin.y <= area->max.y && area->min.y <= chunkMax.y && - chunkMin.z <= area->max.z && area->min.z <= chunkMax.z - ); -} - -bool_t mapAreaCanUnload(const maparea_t *area) { - assertNotNull(area, "Map area pointer cannot be NULL"); - - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false; - } - - return true; -} - -bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity) { - assertNotNull(area, "Map area pointer cannot be NULL"); - assertNotNull(entity, "Entity pointer cannot be NULL"); - - switch(entity->type) { - case ENTITY_TYPE_PLAYER: - return (area->notify & MAP_AREA_NOTIFY_PLAYER) != 0; - - case ENTITY_TYPE_NPC: - return (area->notify & MAP_AREA_NOTIFY_NPC) != 0; - - default: - return false; - } -} - -uint8_t mapAreaAdd( - const worldpos_t min, - const worldpos_t max, - const mapareacallback_t callback, - const uint8_t notify, - const uint8_t trigger -) { - for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) { - if(MAP_AREAS[i].callback != NULL) continue; - mapAreaInit(&MAP_AREAS[i], min, max, callback, notify, trigger); - return i; - } - - assertUnreachable("No available map area slots"); - return 0xFF; -} - -void mapAreaRemove(const uint8_t id) { - assertTrue(id < MAP_AREA_COUNT_MAX, "Map area ID is out of range"); - MAP_AREAS[id].callback = NULL; -} - -void mapAreaCheckEntity(entity_t *entity) { - assertNotNull(entity, "Entity pointer cannot be NULL"); - - for(uint8_t i = 0; i < MAP_AREA_COUNT_MAX; i++) { - maparea_t *area = &MAP_AREAS[i]; - if(area->callback == NULL) continue; - if(!mapAreaShouldNotify(area, entity)) continue; - - bool_t wasInside = area->entities[entity->id] != 0; - bool_t isInside = mapAreaIsInside(area, entity->position); - uint8_t trigger = 0; - - if(isInside && !wasInside) { - area->entities[entity->id] = 1; - trigger = MAP_TRIGGER_ENTER; - } else if(isInside && wasInside) { - trigger = MAP_TRIGGER_STEP; - } else if(!isInside && wasInside) { - area->entities[entity->id] = 0; - trigger = MAP_TRIGGER_EXIT; - } - - if(trigger == 0 || !(area->trigger & trigger)) continue; - area->triggerCount++; - area->callback(entity, trigger); - } -} - -void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger) { -} - -uint8_t mapAreaAddGlobal( - const worldpos_t min, - const worldpos_t max, - const uint16_t callbackId, - const uint8_t notify, - const uint8_t trigger -) { - assertTrue(callbackId > 0, "Map area callback ID 0 is reserved"); - assertTrue( - callbackId < MAP_AREA_CALLBACK_LIST_COUNT, - "Map area callback ID is out of range" - ); - return mapAreaAdd( - min, max, MAP_AREA_CALLBACK_LIST[callbackId], notify, trigger - ); -} diff --git a/src/dusk/rpg/overworld/maparea.h b/src/dusk/rpg/overworld/maparea.h deleted file mode 100644 index 8ffb3de9..00000000 --- a/src/dusk/rpg/overworld/maparea.h +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "worldpos.h" -#include "rpg/entity/entity.h" - -typedef struct chunk_s chunk_t; -typedef struct maparea_s maparea_t; - -#define MAP_AREA_COUNT_MAX 32 - -#define MAP_AREA_NOTIFY_PLAYER (1 << 0) -#define MAP_AREA_NOTIFY_NPC (1 << 1) -#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC) - -#define MAP_TRIGGER_STEP (1 << 0) -#define MAP_TRIGGER_ENTER (1 << 1) -#define MAP_TRIGGER_EXIT (1 << 2) -#define MAP_TRIGGER_ALL \ - (MAP_TRIGGER_STEP | MAP_TRIGGER_ENTER | MAP_TRIGGER_EXIT) - -/** - * Callback invoked for a map area. - * - * @param entity Pointer to the entity associated with the callback. - * @param trigger Which MAP_TRIGGER_* condition invoked the callback. - */ -typedef void (*mapareacallback_t)(entity_t *entity, const uint8_t trigger); - -typedef struct maparea_s { - worldpos_t min; - worldpos_t max; - mapareacallback_t callback; - uint8_t notify; - uint8_t trigger; - uint8_t entities[ENTITY_COUNT]; - - // Incremented every time this area's callback is invoked. Lets other - // systems (e.g. cutscenes) detect "has this area fired since I last - // checked" without needing to be the callback themselves. - uint32_t triggerCount; -} maparea_t; - -extern maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX]; - -/** - * Initializes a map area with the given bounds, callback, notify flags - * and trigger flags. - * - * @param area Pointer to the map area to initialize. - * @param min The minimum world position of the area. - * @param max The maximum world position of the area. - * @param callback The callback to invoke for this area. Must not be NULL. - * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types - * should trigger the callback. - * @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should - * invoke the callback. - */ -void mapAreaInit( - maparea_t *area, - const worldpos_t min, - const worldpos_t max, - const mapareacallback_t callback, - const uint8_t notify, - const uint8_t trigger -); - -/** - * Checks whether a world position falls within a map area's bounds - * (inclusive on all axes). - * - * @param area Pointer to the map area to check. - * @param position The world position to check. - * @returns true if the position is inside the area. - */ -bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position); - -/** - * Checks whether a chunk's full footprint overlaps a map area's bounds, - * rather than just testing the chunk's origin corner. - * - * @param area Pointer to the map area to check. - * @param chunk Pointer to the chunk to check. - * @returns true if the chunk overlaps the area. - */ -bool_t mapAreaIsChunkOverlappingOrInside( - const maparea_t *area, - const chunk_t *chunk -); - -/** - * Checks whether a map area is safe to unload, i.e. no entity currently - * occupies a position within its bounds. - * - * @param area Pointer to the map area to check. - * @returns true if no entity is inside the area. - */ -bool_t mapAreaCanUnload(const maparea_t *area); - -/** - * Checks whether an entity's type is included in a map area's notify flags. - * - * @param area Pointer to the map area to check. - * @param entity Pointer to the entity to check. - * @returns true if the entity's type should trigger this area's callback. - */ -bool_t mapAreaShouldNotify(const maparea_t *area, const entity_t *entity); - -/** - * Adds a map area to the global MAP_AREAS list, in the first free slot. - * A slot is considered free if its callback is NULL. - * - * @param min The minimum world position of the area. - * @param max The maximum world position of the area. - * @param callback The callback to invoke for this area. Must not be NULL. - * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types - * should trigger the callback. - * @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should - * invoke the callback. - * @returns The ID of the newly added map area. - */ -uint8_t mapAreaAdd( - const worldpos_t min, - const worldpos_t max, - const mapareacallback_t callback, - const uint8_t notify, - const uint8_t trigger -); - -/** - * Removes a map area from the global MAP_AREAS list, freeing its slot by - * clearing its callback to NULL. - * - * @param id The ID of the map area to remove. - */ -void mapAreaRemove(const uint8_t id); - -/** - * Checks every active map area against an entity's current position, - * for areas whose notify flags include the entity's type. Invokes the - * area's callback with MAP_TRIGGER_ENTER the frame the entity first - * becomes inside, MAP_TRIGGER_STEP on every subsequent frame it remains - * inside, and MAP_TRIGGER_EXIT the frame it leaves. - * - * @param entity Pointer to the entity to check. - */ -void mapAreaCheckEntity(entity_t *entity); - -/** - * A map area callback that does nothing. Useful for areas whose only - * purpose is to be waited on (see CUTSCENE_MAP_AREA_WAIT), since the - * wait is driven by triggerCount rather than callback logic. - * - * @param entity Pointer to the entity associated with the callback. - * @param trigger Which MAP_TRIGGER_* condition invoked the callback. - */ -void mapAreaNoopCallback(entity_t *entity, const uint8_t trigger); - -/** - * Adds a map area using a compiled-in callback referenced by ID (see - * MAP_AREA_CALLBACK_LIST in rpg/overworld/global/mapareagloballist.h), - * rather than a direct function pointer. This is what lets chunk file - * data - which can only reference compiled code by a small integer ID, - * not a function pointer - declare map areas. - * - * @param min The minimum world position of the area. - * @param max The maximum world position of the area. - * @param callbackId Index into MAP_AREA_CALLBACK_LIST. Must be greater - * than 0 (0 is reserved) and within range. - * @param notify Bitwise MAP_AREA_NOTIFY_* flags for which entity types - * should trigger the callback. - * @param trigger Bitwise MAP_TRIGGER_* flags for which conditions should - * invoke the callback. - * @returns The ID of the newly added map area. - */ -uint8_t mapAreaAddGlobal( - const worldpos_t min, - const worldpos_t max, - const uint16_t callbackId, - const uint8_t notify, - const uint8_t trigger -); \ No newline at end of file diff --git a/src/dusk/rpg/overworld/tile.c b/src/dusk/rpg/overworld/tile.c deleted file mode 100644 index 34e25cda..00000000 --- a/src/dusk/rpg/overworld/tile.c +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "tile.h" diff --git a/src/dusk/rpg/overworld/tile.h b/src/dusk/rpg/overworld/tile.h deleted file mode 100644 index 82f30850..00000000 --- a/src/dusk/rpg/overworld/tile.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "tileshape.h" - -typedef struct { - tileshape_t shape; - - // Local Z layer (0..CHUNK_DEPTH-1) this tile occupies within its chunk's - // depth slab. Used for entity navigation since chunk_t only stores one - // tile per X/Y column rather than one per X/Y/Z. - uint8_t z; -} tile_t; - -#define TILE_NULL ((tile_t){ .shape = TILE_SHAPE_NULL, .z = 0 }) \ No newline at end of file diff --git a/src/dusk/rpg/overworld/tileshape.c b/src/dusk/rpg/overworld/tileshape.c deleted file mode 100644 index 13124c6f..00000000 --- a/src/dusk/rpg/overworld/tileshape.c +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "tileshape.h" - -bool_t tileShapeIsWalkable(const tileshape_t shape) { - switch(shape) { - case TILE_SHAPE_NULL: - return false; - - default: - return true; - } -} - -bool_t tileShapeIsRamp(const tileshape_t shape) { - switch(shape) { - case TILE_SHAPE_RAMP_NORTH: - case TILE_SHAPE_RAMP_SOUTH: - case TILE_SHAPE_RAMP_EAST: - case TILE_SHAPE_RAMP_WEST: - case TILE_SHAPE_RAMP_NORTHEAST: - case TILE_SHAPE_RAMP_NORTHWEST: - case TILE_SHAPE_RAMP_SOUTHEAST: - case TILE_SHAPE_RAMP_SOUTHWEST: - case TILE_SHAPE_RAMP_NORTHEAST_INNER: - case TILE_SHAPE_RAMP_NORTHWEST_INNER: - case TILE_SHAPE_RAMP_SOUTHEAST_INNER: - case TILE_SHAPE_RAMP_SOUTHWEST_INNER: - return true; - - default: - return false; - } -} \ No newline at end of file diff --git a/src/dusk/rpg/overworld/tileshape.h b/src/dusk/rpg/overworld/tileshape.h deleted file mode 100644 index 1187ac70..00000000 --- a/src/dusk/rpg/overworld/tileshape.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/entity/entitydir.h" - -typedef enum { - TILE_SHAPE_NULL = 0, - - TILE_SHAPE_GROUND = 1, - TILE_SHAPE_RAMP_NORTH = 2, - TILE_SHAPE_RAMP_EAST = 3, - TILE_SHAPE_RAMP_SOUTH = 4, - TILE_SHAPE_RAMP_WEST = 5, - TILE_SHAPE_RAMP_NORTHEAST = 6, - TILE_SHAPE_RAMP_NORTHWEST = 7, - TILE_SHAPE_RAMP_SOUTHEAST = 8, - TILE_SHAPE_RAMP_SOUTHWEST = 9, - TILE_SHAPE_RAMP_NORTHEAST_INNER = 10, - TILE_SHAPE_RAMP_NORTHWEST_INNER = 11, - TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12, - TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13, - - TILE_SHAPE_COUNT -} tileshape_t; - -/** - * Returns whether or not the given tile shape is a ramp. - * - * @param shape The tile shape to check. - * @return bool_t True if ramp, false if not. - */ -bool_t tileShapeIsRamp(const tileshape_t shape); - -/** - * Returns whether or not the given tile shape is walkable. - * - * @param shape The tile shape to check. - * @return bool_t True if walkable, false if not. - */ -bool_t tileShapeIsWalkable(const tileshape_t shape); \ No newline at end of file diff --git a/src/dusk/rpg/overworld/worldpos.c b/src/dusk/rpg/overworld/worldpos.c deleted file mode 100644 index 6112bf9a..00000000 --- a/src/dusk/rpg/overworld/worldpos.c +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "worldpos.h" -#include "assert/assert.h" -#include "util/endian.h" -#include "util/memory.h" - -bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b) { - return a.x == b.x && a.y == b.y && a.z == b.z; -} - -void chunkPosToWorldPos(const chunkpos_t* chunkPos, worldpos_t* out) { - assertNotNull(chunkPos, "Chunk position pointer cannot be NULL"); - assertNotNull(out, "Output world position pointer cannot be NULL"); - - out->x = (worldunit_t)(chunkPos->x * CHUNK_WIDTH); - out->y = (worldunit_t)(chunkPos->y * CHUNK_HEIGHT); - out->z = (worldunit_t)(chunkPos->z * CHUNK_DEPTH); -} - -void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out) { - assertNotNull(worldPos, "World position pointer cannot be NULL"); - assertNotNull(out, "Output chunk position pointer cannot be NULL"); - - if(worldPos->x < 0) { - out->x = (chunkunit_t)((worldPos->x - (CHUNK_WIDTH - 1)) / CHUNK_WIDTH); - } else { - out->x = (chunkunit_t)(worldPos->x / CHUNK_WIDTH); - } - - if(worldPos->y < 0) { - out->y = (chunkunit_t)((worldPos->y - (CHUNK_HEIGHT - 1)) / CHUNK_HEIGHT); - } else { - out->y = (chunkunit_t)(worldPos->y / CHUNK_HEIGHT); - } - - if(worldPos->z < 0) { - out->z = (chunkunit_t)((worldPos->z - (CHUNK_DEPTH - 1)) / CHUNK_DEPTH); - } else { - out->z = (chunkunit_t)(worldPos->z / CHUNK_DEPTH); - } -} - -chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos) { - assertNotNull(worldPos, "World position pointer cannot be NULL"); - - uint8_t localX, localY; - if(worldPos->x < 0) { - localX = (uint8_t)( - (CHUNK_WIDTH - 1) - ((-worldPos->x - 1) % CHUNK_WIDTH) - ); - } else { - localX = (uint8_t)(worldPos->x % CHUNK_WIDTH); - } - - if(worldPos->y < 0) { - localY = (uint8_t)( - (CHUNK_HEIGHT - 1) - ((-worldPos->y - 1) % CHUNK_HEIGHT) - ); - } else { - localY = (uint8_t)(worldPos->y % CHUNK_HEIGHT); - } - - chunktileindex_t chunkTileIndex = (chunktileindex_t)( - (localY * CHUNK_WIDTH) + localX - ); - assertTrue( - chunkTileIndex < CHUNK_TILE_COUNT, - "Calculated chunk tile index is out of bounds" - ); - return chunkTileIndex; -} - -uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos) { - assertNotNull(worldPos, "World position pointer cannot be NULL"); - - if(worldPos->z < 0) { - return (uint8_t)( - (CHUNK_DEPTH - 1) - ((-worldPos->z - 1) % CHUNK_DEPTH) - ); - } - return (uint8_t)(worldPos->z % CHUNK_DEPTH); -} - -chunkindex_t chunkPosToIndex(const chunkpos_t* pos) { - assertNotNull(pos, "Chunk position pointer cannot be NULL"); - - chunkindex_t chunkIndex = (chunkindex_t)( - (pos->z * MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT) + - (pos->y * MAP_CHUNK_WIDTH) + - pos->x - ); - - return chunkIndex; -} - -worldunit_t worldUnitReadLE(const uint8_t *data, size_t *offset) { - assertNotNull(data, "Data pointer cannot be NULL"); - assertNotNull(offset, "Offset pointer cannot be NULL"); - - int16_t value; - memoryCopy(&value, data + *offset, sizeof(int16_t)); - *offset += sizeof(int16_t); - return (worldunit_t)endianLittleToHost16((uint16_t)value); -} - -worldpos_t worldPosReadLE(const uint8_t *data, size_t *offset) { - assertNotNull(data, "Data pointer cannot be NULL"); - assertNotNull(offset, "Offset pointer cannot be NULL"); - - worldpos_t pos; - pos.x = worldUnitReadLE(data, offset); - pos.y = worldUnitReadLE(data, offset); - pos.z = worldUnitReadLE(data, offset); - return pos; -} \ No newline at end of file diff --git a/src/dusk/rpg/overworld/worldpos.h b/src/dusk/rpg/overworld/worldpos.h deleted file mode 100644 index 3169839c..00000000 --- a/src/dusk/rpg/overworld/worldpos.h +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/display.h" -#include "util/math.h" - -#define TILE_SIZE_PIXELS 24 - -// World-space Z distance of one Z-layer/story, in render/world-float -// space. A ramp rises this much over a horizontal run of 1 tile-width; -// chosen as 1/sqrt(2) so the ramp's slope, derived from a 45-45-90 -// triangle with hypotenuse 1, matches the length of a flat tile edge. -#define WORLD_LAYER_HEIGHT 0.70710678f - -#define CHUNK_WIDTH 16 -#define CHUNK_HEIGHT 16 -#define CHUNK_DEPTH 8 - -// Chunks store one tile per X/Y column, not one per X/Y/Z - each tile -// records its own local Z layer (see tile_t) instead. -#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT) - -#define MAP_CHUNK_WIDTH 3 -#define MAP_CHUNK_HEIGHT 3 -#define MAP_CHUNK_DEPTH 4 -#define MAP_CHUNK_COUNT (MAP_CHUNK_WIDTH * MAP_CHUNK_HEIGHT * MAP_CHUNK_DEPTH) - -#define ENTITY_COUNT 32 - -typedef int16_t worldunit_t; -typedef int16_t chunkunit_t; -typedef int16_t chunkindex_t; -typedef uint32_t chunktileindex_t; - -// Sentinel chunkindex_t value meaning "no chunk" - returned by -// mapGetChunkIndexAt when a position falls outside every loaded chunk, -// and used throughout as the "not assigned to any chunk" state. -#define CHUNK_INDEX_INVALID ((chunkindex_t)-1) - -typedef int32_t worldunits_t; -typedef int32_t chunkunits_t; - -typedef struct worldpos2d_s { - worldunit_t x, y; -} worldpos2d_t; - -typedef struct worldpos_s { - worldunit_t x, y, z; -} worldpos_t; - -typedef struct chunkpos_t { - chunkunit_t x, y, z; -} chunkpos_t; - -/** - * Compares two world positions for equality. - * - * @param a The first world position. - * @param b The second world position. - * @return true if equal, false otherwise. - */ -bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b); - -/** - * Converts a world position to a chunk position. - * - * @param worldPos The world position. - * @param out The output chunk position. - */ -void chunkPosToWorldPos(const chunkpos_t* chunkPos, worldpos_t* out); - -/** - * Converts a chunk position to a world position. - * - * @param worldPos The world position. - * @param out The output chunk position. - */ -void worldPosToChunkPos(const worldpos_t* worldPos, chunkpos_t* out); - -/** - * Converts a position in world-space to an index inside a chunk that the tile - * resides in. - * - * @param worldPos The world position. - * @return The tile index within the chunk. - */ -chunktileindex_t worldPosToChunkTileIndex(const worldpos_t* worldPos); - -/** - * Converts a world-space Z coordinate to the local Z layer (0..CHUNK_DEPTH-1) - * within the chunk that owns it, for comparison against tile_t.z. - * - * @param worldPos The world position. - * @return The local Z layer within the owning chunk. - */ -uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos); - -/** - * Converts a chunk position to a world position. - * - * @param worldPos The world position. - * @param out The output chunk position. - */ -chunkindex_t chunkPosToIndex(const chunkpos_t* pos); - -/** - * Reads a little-endian worldunit_t from a potentially-unaligned offset - * into a binary asset buffer, advancing *offset past it. Shared by the - * chunk and cutscene asset loaders' binary formats, both of which encode - * world positions the same way. - * - * @param data The buffer to read from. - * @param offset In/out cursor into data, advanced past the value read. - * @return The decoded worldunit_t. - */ -worldunit_t worldUnitReadLE(const uint8_t *data, size_t *offset); - -/** - * Reads a little-endian worldpos_t (x, then y, then z) from a - * potentially-unaligned offset into a binary asset buffer, advancing - * *offset past it. - * - * @param data The buffer to read from. - * @param offset In/out cursor into data, advanced past the value read. - * @return The decoded worldpos_t. - */ -worldpos_t worldPosReadLE(const uint8_t *data, size_t *offset); \ No newline at end of file diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c deleted file mode 100644 index 291f35cf..00000000 --- a/src/dusk/rpg/rpg.c +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpg.h" -#include "entity/entity.h" -#include "rpg/overworld/map.h" -#include "rpg/overworld/maparea.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/item/item.h" -#include "rpg/item/backpack.h" -#include "rpg/battle/party.h" -#include "rpg/battle/move/battlemove.h" -#include "save/save.h" -#include "ui/rpg/textbox/uitextboxminilist.h" -#include "time/time.h" -#include "rpgcamera.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "error/error.h" -#include "scene/scene.h" -#include "ui/rpg/uiemoji.h" -#include "rpg/story/story.h" - -errorret_t rpgInit(void) { - memoryZero(ENTITIES, sizeof(ENTITIES)); - memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); - - errorChain(storyInit()); - errorChain(itemInit()); - errorChain(battleMoveInit()); - backpackInit(); - cutsceneSystemInit(); - errorChain(mapInit()); - rpgCameraInit(); - - // The player is the one entity that isn't sourced from map/chunk data - - // every other entity (NPCs, items) and map area comes from the loaded - // chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded). - uint8_t entIndex = entityGetAvailable(); - assertTrue(entIndex != 0xFF, "No available entity slots!."); - entity_t *ent = &ENTITIES[entIndex]; - entityInit(ent, ENTITY_TYPE_PLAYER); - entityPositionSet(ent, (worldpos_t){ 10, 2, 0 });// Also assigns the chunk. - RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY; - RPG_CAMERA.followEntity.followEntityId = ent->id; - - // Starting inventory. - backpackAdd(itemGetIdByName("potion"), 5); - backpackAdd(itemGetIdByName("potato"), 3); - backpackAdd(itemGetIdByName("apple"), 8); - - - // All Good! - errorOk(); -} - -errorret_t rpgUpdate(void) { - #ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) { - errorOk(); - } - #endif - - // TODO: Do not update if the scene is not the map scene? - errorChain(mapUpdate()); - - // Update overworld ents - only while actually in the overworld. Entities - // (the player among them) keep existing across scene changes, but their - // input/movement/animation logic doesn't make sense to run mid-battle or - // before the initial scene has handed off to the overworld. - if(SCENE.current == SCENE_TYPE_OVERWORLD) { - entity_t *ent = &ENTITIES[0]; - do { - if(ent->type == ENTITY_TYPE_NULL) continue; - entityUpdate(ent); - } while(++ent < &ENTITIES[ENTITY_COUNT]); - } - - cutsceneSystemUpdate(); - errorChain(rpgCameraUpdate()); - errorOk(); -} - -errorret_t rpgDispose(void) { - cutsceneSystemDispose(); - errorChain(mapDispose()); - - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/rpg/rpg.h b/src/dusk/rpg/rpg.h deleted file mode 100644 index 9396c326..00000000 --- a/src/dusk/rpg/rpg.h +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - int32_t nothing; -} rpg_t; - -/** - * Initialize the RPG subsystem. - * - * @return An error code and state. - */ -errorret_t rpgInit(void); - -/** - * Update the RPG subsystem. - * - * @return An error code. - */ -errorret_t rpgUpdate(void); - -/** - * Dispose of the RPG subsystem. - * - * @return An error code. - */ -errorret_t rpgDispose(void); \ No newline at end of file diff --git a/src/dusk/rpg/rpgcamera.c b/src/dusk/rpg/rpgcamera.c deleted file mode 100644 index d3742d88..00000000 --- a/src/dusk/rpg/rpgcamera.c +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "rpgcamera.h" -#include "util/memory.h" -#include "util/random.h" -#include "rpg/entity/entity.h" -#include "rpg/overworld/map.h" -#include "assert/assert.h" -#include "time/time.h" - -#include "display/screen/screen.h" - -static const float_t RPG_CAMERA_SHAKE_AMOUNTS[] = { - 0.0f, 0.5f, 1.0f, 2.0f, 3.0f -}; - -rpgcamera_t RPG_CAMERA; - -void rpgCameraInit(void) { - memoryZero(&RPG_CAMERA, sizeof(rpgcamera_t)); - RPG_CAMERA.projectionDirty = true; -} - -void rpgCameraShake(uint8_t amount, float_t duration) { - assertTrue(amount <= 4, "Camera shake amount must be between 0 and 4"); - RPG_CAMERA.shakeAmount = RPG_CAMERA_SHAKE_AMOUNTS[amount]; - RPG_CAMERA.shakeDuration = duration; - RPG_CAMERA.shakeTime = 0.0f; -} - -void rpgCameraGetPosition(vec3 out) { - switch(RPG_CAMERA.mode) { - case RPG_CAMERA_MODE_FREE: - glm_vec3_copy(RPG_CAMERA.free, out); - return; - - case RPG_CAMERA_MODE_FOLLOW_ENTITY: { - entity_t *entity = &ENTITIES[RPG_CAMERA.followEntity.followEntityId]; - if(entity->type == ENTITY_TYPE_NULL) { - glm_vec3_zero(out); - return; - } - glm_vec3_copy(entity->renderPosition, out); - return; - } - - default: - assertUnreachable("Invalid RPG camera mode"); - } -} - -void rpgCameraUpdateProjection(void) { - #ifdef DUSK_DISPLAY_WIDTH - if(!RPG_CAMERA.projectionDirty) return; - RPG_CAMERA.projectionDirty = false; - #endif - - glm_perspective( - glm_rad(RPG_CAMERA_FOV), - SCREEN.aspect, - 0.1f, - 100.0f, - RPG_CAMERA.projection - ); -} - -void rpgCameraUpdateEye(void) { - float_t fov = glm_rad(RPG_CAMERA_FOV); - float_t pixelsPerUnit = TILE_SIZE_PIXELS; - float_t worldH = (float_t)(SCREEN.height / SCREEN.scale3d) / pixelsPerUnit; - float_t z = (worldH * 0.5f) / tanf(fov * 0.5f); - float_t offset = -24.0f * (worldH / TILE_SIZE_PIXELS); - - vec3 target; - rpgCameraGetPosition(target); - glm_vec3_add(target, (vec3){ 0.5f, 0.5f, 0.5f }, target); - - if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) { - float_t t = 1.0f - (RPG_CAMERA.shakeTime / RPG_CAMERA.shakeDuration); - float_t magnitude = RPG_CAMERA.shakeAmount * t; - target[0] += randomFloat(-magnitude, magnitude); - target[2] += randomFloat(-magnitude, magnitude); - } - - glm_lookat( - (vec3){ target[0], target[1] + offset, target[2] + z }, - target, - (vec3){ 0, 1, 0 }, // up - RPG_CAMERA.eye - ); -} - -void rpgCameraToScreen(vec3 worldPos, vec2 out) { - mat4 viewProj; - glm_mat4_mul(RPG_CAMERA.projection, RPG_CAMERA.eye, viewProj); - - vec4 viewport = { - 0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height - }; - vec3 window; - glm_project(worldPos, viewProj, viewport, window); - - out[0] = window[0]; - out[1] = (float_t)SCREEN.height - window[1]; -} - -errorret_t rpgCameraUpdate(void) { - if(RPG_CAMERA.shakeTime < RPG_CAMERA.shakeDuration) { - RPG_CAMERA.shakeTime += TIME.delta; - } - - if(!mapIsLoaded()) errorOk(); - - vec3 pos; - rpgCameraGetPosition(pos); - - chunkpos_t chunkPos = { - .x = (chunkunit_t)floorf(pos[0] / CHUNK_WIDTH), - .y = (chunkunit_t)floorf(pos[1] / CHUNK_HEIGHT), - .z = (chunkunit_t)floorf(pos[2] / WORLD_LAYER_HEIGHT / CHUNK_DEPTH) - }; - - errorChain(mapPositionSet((chunkpos_t){ - .x = chunkPos.x - (MAP_CHUNK_WIDTH / 2), - .y = chunkPos.y - (MAP_CHUNK_HEIGHT / 2), - .z = chunkPos.z - (MAP_CHUNK_DEPTH / 2) - })); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/rpg/rpgcamera.h b/src/dusk/rpg/rpgcamera.h deleted file mode 100644 index 12742ff7..00000000 --- a/src/dusk/rpg/rpgcamera.h +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2025 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/overworld/worldpos.h" -#include "error/error.h" - -#define RPG_CAMERA_FOV 35.0f - -typedef enum { - RPG_CAMERA_MODE_FREE, - RPG_CAMERA_MODE_FOLLOW_ENTITY, -} rpgcameramode_t; - -typedef struct { - rpgcameramode_t mode; - - union { - vec3 free; - - struct { - uint8_t followEntityId; - } followEntity; - }; - - mat4 eye; - mat4 projection; - bool_t projectionDirty; - - float_t shakeAmount; - float_t shakeDuration; - float_t shakeTime; -} rpgcamera_t; - -extern rpgcamera_t RPG_CAMERA; - -/** - * Initializes the RPG camera. - */ -void rpgCameraInit(void); - -/** - * Gets the RPG camera's position. - * - * @param out Output vec3 filled with the camera world position. - */ -void rpgCameraGetPosition(vec3 out); - -/** - * Updates the RPG camera. - * - * @return An error code. - */ -errorret_t rpgCameraUpdate(void); - -/** - * Recomputes the camera projection matrix and stores it in - * RPG_CAMERA.projection. On platforms with a fixed display size the - * matrix is computed once and cached; on dynamic-display platforms it - * is recomputed every call. - */ -void rpgCameraUpdateProjection(void); - -/** - * Recomputes the camera eye/view matrix from the camera's current mode - * and position, and stores it in RPG_CAMERA.eye. Unlike the projection - * matrix this is never cached, since the camera position can change - * every frame. - */ -void rpgCameraUpdateEye(void); - -/** - * Shakes the RPG camera, randomly offsetting its position by a - * decreasing amount over the given duration. - * - * @param amount Shake strength from 0 (no shake) to 4 (three tiles). - * 1 is half a tile, 2 is a full tile, 3 is two tiles, and 4 is three - * tiles. - * @param duration How long the shake lasts, in seconds. - */ -void rpgCameraShake(uint8_t amount, float_t duration); - -/** - * Converts a world-space position to screen-space pixel coordinates, - * using the camera's current eye and projection matrices. - * - * @param worldPos The world-space position to convert. - * @param out Output vec2 filled with the screen-space pixel position. - */ -void rpgCameraToScreen(vec3 worldPos, vec2 out); \ No newline at end of file diff --git a/src/dusk/rpg/story/CMakeLists.txt b/src/dusk/rpg/story/CMakeLists.txt deleted file mode 100644 index 8a945222..00000000 --- a/src/dusk/rpg/story/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - story.c -) diff --git a/src/dusk/rpg/story/story.c b/src/dusk/rpg/story/story.c deleted file mode 100644 index 007b1ff7..00000000 --- a/src/dusk/rpg/story/story.c +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "story.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "assert/assert.h" -#include "save/save.h" -#include "util/memory.h" -#include "util/string.h" -#include "yyjson.h" - -storyflagmap_t STORY_FLAG_MAP[STORY_FLAG_COUNT_MAX]; -storyflag_t STORY_FLAG_COUNT; - -errorret_t storyInit(void) { - memoryZero(STORY_FLAG_MAP, sizeof(STORY_FLAG_MAP)); - STORY_FLAG_COUNT = 0; - - assetentry_t *entry = assetLock( - "story/storyflag.jsonc", ASSET_LOADER_TYPE_JSON, NULL - ); - errorret_t ret = assetRequireLoaded(entry); - if(errorIsNotOk(ret)) { - assetUnlockEntry(entry); - errorChain(ret); - } - - yyjson_val *root = yyjson_doc_get_root(entry->data.json); - yyjson_val *flags = yyjson_obj_get(root, "flags"); - - yyjson_obj_iter iter = yyjson_obj_iter_with(flags); - yyjson_val *key; - while((key = yyjson_obj_iter_next(&iter))) { - assertTrue( - STORY_FLAG_COUNT < STORY_FLAG_COUNT_MAX, "Too many story flags defined." - ); - - yyjson_val *val = yyjson_obj_iter_get_val(key); - yyjson_val *initial = yyjson_obj_get(val, "initial"); - - storyflag_t flag = STORY_FLAG_COUNT++; - stringCopy(STORY_FLAG_MAP[flag].name, yyjson_get_str(key), STORY_FLAG_NAME_MAX); - STORY_FLAG_MAP[flag].initial = yyjson_is_bool(initial) ? - (storyflagval_t)yyjson_get_bool(initial) : - (storyflagval_t)yyjson_get_int(initial); - } - - assetUnlockEntry(entry); - errorOk(); -} - -storyflag_t storyGetFlagByName(const char_t *name) { - assertNotNull(name, "name must not be NULL"); - - for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) { - if(stringEquals(STORY_FLAG_MAP[flag].name, name)) return flag; - } - - return STORY_FLAG_COUNT_MAX; -} - -const char_t * storyGetNameByFlag(storyflag_t flag) { - if(flag >= STORY_FLAG_COUNT) return NULL; - return STORY_FLAG_MAP[flag].name; -} - -storyflagval_t storyGetFlagValue(storyflag_t flag) { - if(flag >= STORY_FLAG_COUNT) return 0; - return SAVE.slot.storyFlags[flag]; -} - -void storySetFlagValue(storyflag_t flag, storyflagval_t value) { - assertTrue(flag < STORY_FLAG_COUNT, "Bad story flag"); - SAVE.slot.storyFlags[flag] = value; - SAVE.slotDirty = true; -} - -void storySetFlagValueByName(const char_t *name, storyflagval_t value) { - storyflag_t flag = storyGetFlagByName(name); - assertTrue(flag < STORY_FLAG_COUNT, "Unknown story flag"); - storySetFlagValue(flag, value); -} diff --git a/src/dusk/rpg/story/story.h b/src/dusk/rpg/story/story.h deleted file mode 100644 index 108fe0fe..00000000 --- a/src/dusk/rpg/story/story.h +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define STORY_FLAG_COUNT_MAX 256 -#define STORY_FLAG_NAME_MAX 16 - -typedef uint16_t storyflag_t; -typedef uint8_t storyflagval_t; - -typedef struct { - char_t name[STORY_FLAG_NAME_MAX]; - storyflagval_t initial; -} storyflagmap_t; - -extern storyflagmap_t STORY_FLAG_MAP[STORY_FLAG_COUNT_MAX]; -extern storyflag_t STORY_FLAG_COUNT; - - -/** - * Initializes the story subsystem, loading assets/story/storyflag.jsonc. - * - * @return Any error that occurs. - */ -errorret_t storyInit(void); - -/** - * Gets the ID of a story flag by name. - * - * @param name The name of the story flag to get. - * @return The ID of the story flag, or STORY_FLAG_COUNT_MAX if not found. - */ -storyflag_t storyGetFlagByName(const char_t *name); - -/** - * Gets the name of a story flag by ID. - * - * @param flag The ID of the story flag to get. - * @return The name of the story flag, or NULL if not found. - */ -const char_t * storyGetNameByFlag(storyflag_t flag); - -/** - * Gets the value of a story flag. Reads directly from the active save - * slot (SAVE.slot.storyFlags) - flag values have no separate live copy. - * - * @param flag The ID of the story flag to get. - * @return The value of the story flag, or 0 if not found. - */ -storyflagval_t storyGetFlagValue(storyflag_t flag); - -/** - * Sets the value of a story flag. Writes directly to the active save - * slot (SAVE.slot.storyFlags) - flag values have no separate live copy. - * Marks the save slot dirty. - * - * @param flag The ID of the story flag to set. - * @param value The value to set the story flag to. - */ -void storySetFlagValue(storyflag_t flag, storyflagval_t value); - -/** - * Sets the value of a story flag by name. Writes directly to the active save - * slot (SAVE.slot.storyFlags) - flag values have no separate live copy. - * Marks the save slot dirty. - * - * @param name The name of the story flag to set. - * @param value The value to set the story flag to. - */ -void storySetFlagValueByName(const char_t *name, storyflagval_t value); diff --git a/src/dusk/save/CMakeLists.txt b/src/dusk/save/CMakeLists.txt deleted file mode 100644 index e75182ea..00000000 --- a/src/dusk/save/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# 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 - save.c - savedevice.c -) - -# Subdirs -add_subdirectory(slot) -add_subdirectory(settings) diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c deleted file mode 100644 index 9c8ece44..00000000 --- a/src/dusk/save/save.c +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "console/console.h" - -save_t SAVE; - -errorret_t saveInit() { - memoryZero(&SAVE, sizeof(save_t)); - - SAVE.deviceCurrent = 0xFF;// No current device. - SAVE.slotCurrent = 0xFF;// No current slot. - - // Initialize the slots and settings - saveSettingsInit(&SAVE.settings); - errorChain(saveSlotInit(&SAVE.slot)); - - // Update caches to match default data. - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - SAVE.caches[i] = SAVE.slot.cachedData; - } - - // Start by initializing each of the save devices. - savedevice_t *device = &SAVE.devices[0]; - do { - errorChain(saveDeviceInit(device)); - } while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]); - - errorOk(); -} - -errorret_t saveUpdate() { - assertIsMainThread("Invalid thread"); - - // Start by updating each device - savedevice_t *device = &SAVE.devices[0]; - do { - saveDeviceUpdate(device); - } while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]); - - // We may be waiting for a device to become available - if(SAVE.findingAvailableDevice) { - assertNotNull( - SAVE.findAvailableCallback, - "Callback cannot be null while looking for devices" - ); - - // Yes, we have a device available, fire the callback. - if(SAVE.deviceCurrent == 0xFF) { - // Are we still looking or did we fail to find anything? - if(SAVE.noAvailableDeviceFound) { - SAVE.findingAvailableDevice = false; - SAVE.findAvailableCallback( - NULL, - SAVE.findAvailableUser - ); - } - // Still searching - } else { - // Device was found, cache in the data. - errorChain(saveLoadSettings()); - errorChain(saveLoadAllSlots()); - - // Reset slot, implying no slot has been selected. - SAVE.slotCurrent = 0xFF; - - // TEST: write settings back out immediately once a device is - // selected, to confirm the write path actually works on hardware. - errorChain(saveDeviceSettingsWrite( - &SAVE.devices[SAVE.deviceCurrent], - &SAVE.settings - )); - - SAVE.findingAvailableDevice = false; - SAVE.findAvailableCallback( - &SAVE.devices[SAVE.deviceCurrent], - SAVE.findAvailableUser - ); - } - } - - errorOk(); -} - -void saveFindAvailableDevice( - savedevicestatecallback_t callback, - void *user -) { - assertIsMainThread("Invalid thread"); - assertNotNull(callback, "Callback cannot be null"); - assertFalse( - SAVE.findingAvailableDevice, - "Already finding an available device" - ); - - SAVE.findingAvailableDevice = true; - SAVE.findAvailableCallback = callback; - SAVE.findAvailableUser = user; - SAVE.noAvailableDeviceFound = false; - - // Is there an available device marked already? - if(SAVE.deviceCurrent != 0xFF) { - // Yes, fire the callback next tick. - return; - } - - // Do we have an available device not yet marked as current? - savedevice_t *device = &SAVE.devices[0]; - do { - if(device->state != SAVE_DEVICE_STATE_AVAILABLE) continue; - // Yes this device is available, set as current device. - SAVE.deviceCurrent = (uint8_t)(device - &SAVE.devices[0]); - return;// Next tick will fire the callback. - } while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]); - - // No currently available device, check them IN ORDER, starting at 0 - savedevice_t *deviceToCheck = &SAVE.devices[0]; - assertNotNull(deviceToCheck, "Device to check cannot be null"); - saveDeviceCheckAvailability( - deviceToCheck, - saveOnDeviceAvailabilityChecked, - NULL - ); -} - -void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user) { - assertNotNull(device, "Device cannot be null"); - assertTrue( - SAVE.findingAvailableDevice, - "Not currently finding an available device" - ); - - // Did we already find a device? - if(SAVE.deviceCurrent != 0xFF) return; - - // Is this device available? - if(device->state != SAVE_DEVICE_STATE_AVAILABLE) { - // Since it's unavailable we should tell the next device to check its - // availability. If there is no next device we give up. - savedevice_t *nextDevice = device + 1; - if(nextDevice >= &SAVE.devices[SAVE_DEVICE_COUNT]) { - // No more devices to check, we give up. - SAVE.noAvailableDeviceFound = true; - return; - } - - // Check the next device's availability. - saveDeviceCheckAvailability( - nextDevice, - saveOnDeviceAvailabilityChecked, - NULL - ); - return; - } - - // Yes this device is available, set as current device and next tick will - // invoke callback - SAVE.deviceCurrent = (uint8_t)(device - &SAVE.devices[0]); -} - -bool_t saveCanSave() { - return CUTSCENE_SYSTEM.scene == NULL; -} - -errorret_t saveSaveSettings() { - assertTrue(SAVE.deviceCurrent != 0xFF, "No current device"); - - if(!SAVE.settingsDirty) errorOk(); - - errorChain(saveDeviceSettingsWrite( - &SAVE.devices[SAVE.deviceCurrent], - &SAVE.settings - )); - - SAVE.settingsDirty = false; - errorOk(); -} - -errorret_t saveLoadSettings() { - assertTrue(SAVE.deviceCurrent != 0xFF, "No current device"); - - errorret_t ret = saveDeviceSettingsRead( - &SAVE.devices[SAVE.deviceCurrent], - &SAVE.settings - ); - if(errorIsNotOk(ret)) { - // Corrupt or incompatible (e.g. wrong/missing schema version) settings - // file - don't take the whole game down over it, just reset to - // defaults and carry on. - errorCatch(errorPrint(ret)); - consolePrint("Settings file could not be loaded, resetting to defaults.\n"); - saveSettingsInit(&SAVE.settings); - } - - SAVE.settingsDirty = false; - errorOk(); -} - -errorret_t saveSaveSlot() { - assertTrue(SAVE.deviceCurrent != 0xFF, "No current device"); - assertTrue(SAVE.slotCurrent < SAVE_SLOT_COUNT, "Invalid slot index"); - - errorChain(saveDeviceSlotWrite( - &SAVE.devices[SAVE.deviceCurrent], - &SAVE.slot, - SAVE.slotCurrent - )); - - // Update cache - SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData; - SAVE.slotDirty = false; - - errorOk(); -} - -errorret_t saveLoadSlot() { - assertTrue(SAVE.slotCurrent < SAVE_SLOT_COUNT, "Invalid slot index"); - - if(SAVE.deviceCurrent == 0xFF) { - // No save device available, so we assume an empty slot. - errorChain(saveSlotInit(&SAVE.slot)); - SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData; - errorOk(); - } - - errorret_t ret = saveDeviceSlotRead( - &SAVE.devices[SAVE.deviceCurrent], - &SAVE.slot, - SAVE.slotCurrent - ); - if(errorIsNotOk(ret)) { - // Corrupt or incompatible (e.g. wrong/missing schema version) slot - // file - don't take the whole game down over it, treat it the same as - // an empty/unused slot. - errorCatch(errorPrint(ret)); - consolePrint( - "Save slot %u could not be loaded, treating as empty.\n", - (uint32_t)SAVE.slotCurrent - ); - errorChain(saveSlotInit(&SAVE.slot)); - SAVE.slot.cachedData.corrupt = true; - } - - - // Update cache - SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData; - SAVE.slotDirty = false; - - errorOk(); -} - -errorret_t saveLoadAllSlots() { - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - SAVE.slotCurrent = i; - errorChain(saveSlotInit(&SAVE.slot)); - errorChain(saveLoadSlot());// Load slot updates the cache. - } - - errorOk(); -} - -errorret_t saveDispose() { - // Dispose each device. - savedevice_t *device = &SAVE.devices[0]; - do { - saveDeviceDispose(device); - } while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]); - - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h deleted file mode 100644 index 2d13e701..00000000 --- a/src/dusk/save/save.h +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "savedevice.h" -#include "save/slot/saveslot.h" -#include "save/settings/savesettings.h" - -typedef struct { - // File state - savesettings_t settings; - saveslotcache_t caches[SAVE_SLOT_COUNT]; - saveslot_t slot; - - uint8_t slotCurrent; - bool_t settingsDirty; - bool_t slotDirty; - - // Device state - savedevice_t devices[SAVE_DEVICE_COUNT]; - uint8_t deviceCurrent; - bool_t findingAvailableDevice; - bool_t noAvailableDeviceFound; - savedevicestatecallback_t findAvailableCallback; - void *findAvailableUser; -} save_t; - -extern save_t SAVE; - -/** - * Initializes the save system, this does not do anything related to mounting - * files, memory cards, etc, this is entirely prepping for that capability. - * - * @return Error state if any. - */ -errorret_t saveInit(); - -/** - * Updates the save system. - * - * @return Error state if any. - */ -errorret_t saveUpdate(); - -/** - * Requests the save system to try and find an available device. This will fire - * the callback whenever an avaialble device is found, or will fire with a NULL - * save device if no available save devices are found. - * - * @param callback The callback to fire when an available device is found. - * @param user The user data to pass to the callback. - */ -void saveFindAvailableDevice( - savedevicestatecallback_t callback, - void *user -); - -/** - * Internal method to fire the save callback, does it at the appropriate time. - * - * @param device The save device to fire the callback for. - * @param user Unused, present to match savedevicestatecallback_t. - */ -void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user); - -/** - * Whether the game is in a state where saving is currently allowed. - * Currently just checks that no cutscene is running. - * - * @return True if saving is currently allowed, false otherwise. - */ -bool_t saveCanSave(); - -/** - * Saves the current settings to the current device. - * - * @return Error state if any. - */ -errorret_t saveSaveSettings(); - -/** - * Loads the current settings from the current device. - * - * @return Error state if any. - */ -errorret_t saveLoadSettings(); - -/** - * Saves the current slot to the current device, at the current slot index. - * - * @return Error state if any. - */ -errorret_t saveSaveSlot(); - -/** - * Loads the current slot from the current device, at the current slot index. - * - * @return Error state if any. - */ -errorret_t saveLoadSlot(); - -/** - * Resets and loads every slot from the current device, updating the caches - * along the way. Leaves SAVE.slotCurrent pointing at the last slot index - * once done. - * - * @return Error state if any. - */ -errorret_t saveLoadAllSlots(); - -/** - * Disposes of the save system. - * - * @return Error state if any. - */ -errorret_t saveDispose(); \ No newline at end of file diff --git a/src/dusk/save/savedevice.c b/src/dusk/save/savedevice.c deleted file mode 100644 index 111fdb08..00000000 --- a/src/dusk/save/savedevice.c +++ /dev/null @@ -1,546 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/savedevice.h" -#include "assert/assert.h" -#include "util/memory.h" - -#if defined(SAVE_DEVICE_DATA_RAW) - #include "yyjson.h" - #include "save/savejson.h" - #include "save/slot/saveslot.h" - #include "save/settings/savesettings.h" - #include "util/crypt.h" - #include "util/endian.h" - #include -#endif - -errorret_t saveDeviceInit(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - - memoryZero(device, sizeof(savedevice_t)); - device->state = 0xFF;// We set this because the platform must define it. - errorChain(saveDevicePlatformInit(device)); - - // The platform must put the device into a state we can work with. - assertTrue( - device->state == SAVE_DEVICE_STATE_UNKNOWN || - device->state == SAVE_DEVICE_STATE_UNAVAILABLE || - device->state == SAVE_DEVICE_STATE_AVAILABLE || - device->state == SAVE_DEVICE_STATE_ERRORED, - "Save device must be in a state that allows checking availability" - ); - - errorOk(); -} - -errorret_t saveDeviceUpdate(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - - // Perform a device update. - errorChain(saveDevicePlatformUpdate(device)); - - // Fire the callback if desired. - if(device->fireCallback) { - device->fireCallback = false; - if(device->stateCallback) device->stateCallback(device, device->user); - } - - errorOk(); -} - -void saveDeviceFireCallback(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - assertFalse(device->fireCallback, "Device callback already fired?"); - device->fireCallback = true; -} - -void saveDeviceCheckAvailability( - savedevice_t *device, - savedevicestatecallback_t callback, - void *user -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(callback, "callback cannot be null"); - assertIsMainThread("Invalid thread"); - - // The device must be in a state that we allow checking the availability. - assertTrue( - device->state == SAVE_DEVICE_STATE_UNKNOWN || - device->state == SAVE_DEVICE_STATE_UNAVAILABLE || - device->state == SAVE_DEVICE_STATE_AVAILABLE || - device->state == SAVE_DEVICE_STATE_ERRORED, - "Save device must be in a state that allows checking availability" - ); - - // Set state data and callback. - device->state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; - device->stateCallback = callback; - device->user = user; - - // Handoff to the platform to do its checks, it can opt to do this either - // synchronously or asynchronously. - saveDeviceCheckAvailabilityPlatform(device); -} - -#if defined(SAVE_DEVICE_DATA_RAW) - -// Bumped to 2 for the addition of `generation` below. -#define SAVE_DEVICE_RAW_VERSION ((uint32_t)2) -// Sanity cap against a corrupted/hostile header driving a bogus allocation. -#define SAVE_DEVICE_RAW_MAX_SIZE ((uint32_t)(256 * 1024)) - -static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'}; - -// savedevicerawheader_t/savedevicerawspan_t/savedevicerawspans_t/ -// savedevicerawitem_t are declared in savedevice.h, since they appear in -// the signatures of the (non-static) functions below. - -void saveDeviceRawParseSpans( - const uint8_t *logical, - savedevicerawspans_t *spans -) { - size_t offset = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT); - - spans->settings.len = endianLittleToHost32(*(const uint32_t *)(logical + 0)); - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - spans->slots[i].len = endianLittleToHost32( - *(const uint32_t *)(logical + sizeof(uint32_t) * (1 + i)) - ); - } - - spans->settings.ptr = logical + offset; - offset += spans->settings.len; - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - spans->slots[i].ptr = logical + offset; - offset += spans->slots[i].len; - } -} - -// Cheaply checks whether `raw` is a well-formed, checksum-valid raw save -// blob, without decompressing it - just header + checksum validation. Used -// both by saveDeviceRawInflate below and, publicly (see savedevice.h), by -// platforms that keep more than one physical copy and need to pick the -// newest valid one (e.g. GameCube memory cards - see `generation` above). -bool_t saveDeviceRawIsValid( - const uint8_t *raw, - const size_t rawSize, - uint32_t *outGeneration -) { - if(raw == NULL || rawSize < sizeof(savedevicerawheader_t)) return false; - - const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw; - if(memoryCompare(header->magic, SAVE_DEVICE_RAW_MAGIC, 4) != 0) { - return false; - } - if(endianLittleToHost32(header->version) != SAVE_DEVICE_RAW_VERSION) { - return false; - } - - uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize); - uint32_t compressedSize = endianLittleToHost32(header->compressedSize); - if( - uncompressedSize > SAVE_DEVICE_RAW_MAX_SIZE || - compressedSize > SAVE_DEVICE_RAW_MAX_SIZE || - // Trailing bytes beyond the declared payload are tolerated as padding - - // e.g. a GameCube memory card file rounded up to a whole sector. - rawSize < sizeof(savedevicerawheader_t) + compressedSize - ) { - return false; - } - - uint32_t checksum = - cryptCRC32(raw + sizeof(savedevicerawheader_t), compressedSize); - if(checksum != endianLittleToHost32(header->checksum)) return false; - - if(outGeneration != NULL) { - *outGeneration = endianLittleToHost32(header->generation); - } - return true; -} - -// Validates the header and inflates the compressed payload that follows it. -// On success, *outLogical is a memoryAllocate'd buffer the caller must free. -errorret_t saveDeviceRawInflate( - const uint8_t *raw, - const size_t rawSize, - uint8_t **outLogical, - size_t *outLogicalSize, - uint32_t *outGeneration -) { - if(!saveDeviceRawIsValid(raw, rawSize, outGeneration)) { - errorThrow("Save data failed validation (magic/version/size/checksum)"); - } - - const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw; - uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize); - uint32_t compressedSize = endianLittleToHost32(header->compressedSize); - const uint8_t *compressed = raw + sizeof(savedevicerawheader_t); - - uint8_t *logical = memoryAllocate(uncompressedSize); - uLongf destLen = (uLongf)uncompressedSize; - int result = uncompress( - logical, &destLen, compressed, (uLong)compressedSize - ); - if(result != Z_OK || destLen != uncompressedSize) { - memoryFree(logical); - errorThrow("Failed to decompress save data (zlib error %d)", result); - } - - *outLogical = logical; - *outLogicalSize = uncompressedSize; - errorOk(); -} - -// Serializes `settings` to a freshly malloc'd JSON string via yyjson - the -// result must be freed with plain free(), not memoryFree(). -errorret_t saveDeviceRawSerializeSettings( - savesettings_t *settings, - char_t **outJson, - size_t *outLen -) { - writeInit(); - - errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object); - if(errorIsNotOk(writeResult)) { - yyjson_mut_doc_free(doc); - errorChain(writeResult); - } - - *outJson = yyjson_mut_write(doc, 0, outLen); - yyjson_mut_doc_free(doc); - if(*outJson == NULL) errorThrow("Failed to write settings JSON"); - - errorOk(); -} - -// Serializes `slot` to a freshly malloc'd JSON string via yyjson - the -// result must be freed with plain free(), not memoryFree(). -errorret_t saveDeviceRawSerializeSlot( - saveslot_t *slot, - char_t **outJson, - size_t *outLen -) { - writeInit(); - - errorret_t writeResult = saveSlotWriteJSON(slot, doc, object); - if(errorIsNotOk(writeResult)) { - yyjson_mut_doc_free(doc); - errorChain(writeResult); - } - - *outJson = yyjson_mut_write(doc, 0, outLen); - yyjson_mut_doc_free(doc); - if(*outJson == NULL) errorThrow("Failed to write slot JSON"); - - errorOk(); -} - -// Frees whatever's been collected so far - safe to call at any point since -// unset items are zero-initialized (owned == NULL is a no-op skip). -void saveDeviceRawCleanupStore( - uint8_t *oldRaw, - uint8_t *oldLogical, - savedevicerawitem_t *settingsItem, - savedevicerawitem_t *slotItems -) { - if(oldRaw != NULL) memoryFree(oldRaw); - if(oldLogical != NULL) memoryFree(oldLogical); - if(settingsItem->owned != NULL) free(settingsItem->owned); - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - if(slotItems[i].owned != NULL) free(slotItems[i].owned); - } -} - -// Reads the existing combined blob (if any), replaces exactly one item - -// `settings`, or the slot at `slotIndex` when `slot` is given, exactly one -// of the two must be non-NULL - and rewrites the whole blob. Every other -// item's JSON is carried through byte-for-byte from what was already -// stored (or freshly defaulted if nothing was stored yet), so a save never -// reconstructs data it wasn't given. -errorret_t saveDeviceRawStoreItem( - savedevice_t *device, - savesettings_t *settings, - saveslot_t *slot, - const uint8_t slotIndex -) { - uint8_t *oldRaw = NULL; - size_t oldRawSize = 0; - errorChain(saveDeviceDataReadPlatform(device, &oldRaw, &oldRawSize)); - - uint8_t *oldLogical = NULL; - size_t oldLogicalSize = 0; - uint32_t oldGeneration = 0; - savedevicerawspans_t oldSpans; - bool_t haveOld = oldRaw != NULL; - if(haveOld) { - errorret_t inflateResult = saveDeviceRawInflate( - oldRaw, oldRawSize, &oldLogical, &oldLogicalSize, &oldGeneration - ); - if(errorIsNotOk(inflateResult)) { - memoryFree(oldRaw); - errorChain(inflateResult); - } - saveDeviceRawParseSpans(oldLogical, &oldSpans); - } - - savedevicerawitem_t settingsItem = {0}; - savedevicerawitem_t slotItems[SAVE_SLOT_COUNT] = {0}; - - if(settings != NULL) { - char_t *json = NULL; - size_t len = 0; - errorret_t result = saveDeviceRawSerializeSettings(settings, &json, &len); - if(errorIsNotOk(result)) { - saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); - errorChain(result); - } - settingsItem = (savedevicerawitem_t){ - .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json - }; - } else if(haveOld) { - settingsItem.ptr = oldSpans.settings.ptr; - settingsItem.len = oldSpans.settings.len; - } else { - savesettings_t defaultSettings; - saveSettingsInit(&defaultSettings); - char_t *json = NULL; - size_t len = 0; - errorret_t result = saveDeviceRawSerializeSettings( - &defaultSettings, &json, &len - ); - if(errorIsNotOk(result)) { - saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); - errorChain(result); - } - settingsItem = (savedevicerawitem_t){ - .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json - }; - } - - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - if(slot != NULL && i == slotIndex) { - char_t *json = NULL; - size_t len = 0; - errorret_t result = saveDeviceRawSerializeSlot(slot, &json, &len); - if(errorIsNotOk(result)) { - saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); - errorChain(result); - } - slotItems[i] = (savedevicerawitem_t){ - .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json - }; - } else if(haveOld) { - slotItems[i].ptr = oldSpans.slots[i].ptr; - slotItems[i].len = oldSpans.slots[i].len; - } else { - // A raw reset, not saveSlotInit() - this fills in a slot index the - // current write isn't touching, and saveSlotInit() only ever - // operates on SAVE.slot now. A blank ("" name) slot never actually - // needs a properly-initialized party (see saveSlotReadJSON's early - // return for an unused slot), so this is equivalent for this - // filler-only purpose. - saveslot_t defaultSlot; - memorySet(&defaultSlot, 0, sizeof(defaultSlot)); - defaultSlot.version = 1; - char_t *json = NULL; - size_t len = 0; - errorret_t result = saveDeviceRawSerializeSlot(&defaultSlot, &json, &len); - if(errorIsNotOk(result)) { - saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); - errorChain(result); - } - slotItems[i] = (savedevicerawitem_t){ - .ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json - }; - } - } - - // Assemble the new logical blob: length table, then each item's bytes. - size_t tableSize = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT); - size_t logicalSize = tableSize + settingsItem.len; - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) logicalSize += slotItems[i].len; - - uint8_t *newLogical = memoryAllocate(logicalSize); - *(uint32_t *)(newLogical + 0) = endianLittleToHost32(settingsItem.len); - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - *(uint32_t *)(newLogical + sizeof(uint32_t) * (1 + i)) = - endianLittleToHost32(slotItems[i].len); - } - - size_t offset = tableSize; - memoryCopy(newLogical + offset, settingsItem.ptr, settingsItem.len); - offset += settingsItem.len; - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - memoryCopy(newLogical + offset, slotItems[i].ptr, slotItems[i].len); - offset += slotItems[i].len; - } - - saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems); - - // Compress and frame with the header. - uLongf compressedCap = compressBound((uLong)logicalSize); - uint8_t *compressedBuf = memoryAllocate(compressedCap); - uLongf compressedLen = compressedCap; - int compressResult = compress2( - compressedBuf, &compressedLen, newLogical, (uLong)logicalSize, - Z_DEFAULT_COMPRESSION - ); - memoryFree(newLogical); - if(compressResult != Z_OK) { - memoryFree(compressedBuf); - errorThrow("Failed to compress save data (zlib error %d)", compressResult); - } - - savedevicerawheader_t header; - memoryCopy(header.magic, SAVE_DEVICE_RAW_MAGIC, 4); - header.version = endianLittleToHost32(SAVE_DEVICE_RAW_VERSION); - header.uncompressedSize = endianLittleToHost32((uint32_t)logicalSize); - header.compressedSize = endianLittleToHost32((uint32_t)compressedLen); - header.checksum = - endianLittleToHost32(cryptCRC32(compressedBuf, compressedLen)); - header.generation = endianLittleToHost32(haveOld ? oldGeneration + 1 : 1); - - size_t finalSize = sizeof(header) + compressedLen; - uint8_t *finalBuf = memoryAllocate(finalSize); - memoryCopy(finalBuf, &header, sizeof(header)); - memoryCopy(finalBuf + sizeof(header), compressedBuf, compressedLen); - memoryFree(compressedBuf); - - errorret_t writeResult = saveDeviceDataWritePlatform( - device, finalBuf, finalSize - ); - memoryFree(finalBuf); - errorChain(writeResult); - - errorOk(); -} - -// Reads the combined blob and populates exactly one item - `settings`, or -// the slot at `slotIndex` when `slot` is given, exactly one of the two must -// be non-NULL. Leaves the destination untouched if nothing's been saved yet. -errorret_t saveDeviceRawFetchItem( - savedevice_t *device, - savesettings_t *settings, - saveslot_t *slot, - const uint8_t slotIndex -) { - uint8_t *raw = NULL; - size_t rawSize = 0; - errorChain(saveDeviceDataReadPlatform(device, &raw, &rawSize)); - if(raw == NULL) errorOk(); - - uint8_t *logical = NULL; - size_t logicalSize = 0; - errorret_t inflateResult = saveDeviceRawInflate( - raw, rawSize, &logical, &logicalSize, NULL - ); - memoryFree(raw); - errorChain(inflateResult); - - savedevicerawspans_t spans; - saveDeviceRawParseSpans(logical, &spans); - const savedevicerawspan_t *span = settings != NULL ? - &spans.settings : &spans.slots[slotIndex]; - - yyjson_doc *jsonDoc = yyjson_read((const char_t *)span->ptr, span->len, 0); - if(jsonDoc == NULL) { - memoryFree(logical); - errorThrow("Failed to parse save data item JSON"); - } - - yyjson_val *object = yyjson_doc_get_root(jsonDoc); - if(object == NULL) { - yyjson_doc_free(jsonDoc); - memoryFree(logical); - errorThrow("Save data item JSON missing root object"); - } - - errorret_t readResult = settings != NULL ? - saveSettingsReadJSON(settings, object) : saveSlotReadJSON(slot, object); - yyjson_doc_free(jsonDoc); - memoryFree(logical); - errorChain(readResult); - - errorOk(); -} - -#endif// defined(SAVE_DEVICE_DATA_RAW) - -errorret_t saveDeviceSlotWrite( - savedevice_t *device, - saveslot_t *slot, - const uint8_t slotIndex -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(slot, "slot cannot be null"); - - #if defined(SAVE_DEVICE_DATA_RAW) - errorChain(saveDeviceRawStoreItem(device, NULL, slot, slotIndex)); - #elif defined(saveDeviceSlotWritePlatform) - errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex)); - #endif - - errorOk(); -} - -errorret_t saveDeviceSlotRead( - savedevice_t *device, - saveslot_t *slot, - const uint8_t slotIndex -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(slot, "slot cannot be null"); - - #if defined(SAVE_DEVICE_DATA_RAW) - errorChain(saveDeviceRawFetchItem(device, NULL, slot, slotIndex)); - #elif defined(saveDeviceSlotReadPlatform) - errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex)); - #endif - - errorOk(); -} - -errorret_t saveDeviceSettingsWrite( - savedevice_t *device, - savesettings_t *settings -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(settings, "settings cannot be null"); - - #if defined(SAVE_DEVICE_DATA_RAW) - errorChain(saveDeviceRawStoreItem(device, settings, NULL, 0)); - #elif defined(saveDeviceSettingsWritePlatform) - errorChain(saveDeviceSettingsWritePlatform(device, settings)); - #endif - - errorOk(); -} - -errorret_t saveDeviceSettingsRead( - savedevice_t *device, - savesettings_t *settings -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(settings, "settings cannot be null"); - - #if defined(SAVE_DEVICE_DATA_RAW) - errorChain(saveDeviceRawFetchItem(device, settings, NULL, 0)); - #elif defined(saveDeviceSettingsReadPlatform) - errorChain(saveDeviceSettingsReadPlatform(device, settings)); - #endif - - errorOk(); -} - -errorret_t saveDeviceDispose(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - errorChain(saveDevicePlatformDispose(device)); - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/save/savedevice.h b/src/dusk/save/savedevice.h deleted file mode 100644 index 543f5054..00000000 --- a/src/dusk/save/savedevice.h +++ /dev/null @@ -1,345 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "save/savedeviceplatform.h" - -// Save device count e.g. Memory Cards count. -#ifndef SAVE_DEVICE_COUNT - #error "SAVE_DEVICE_COUNT must be defined" -#endif - -// Platform opt-in: define this in a platform's savedeviceplatform.h (instead -// of the four saveDevice{Slot,Settings}{Write,Read}Platform macros) for -// devices without free-form filesystem access - e.g. a memory card/stick -// that suits one combined save blob better than N+1 separate files. When -// defined, the platform must instead provide these two: -// -// errorret_t saveDeviceDataWritePlatform( -// savedevice_t *device, const uint8_t *buffer, size_t size -// ); -// errorret_t saveDeviceDataReadPlatform( -// savedevice_t *device, uint8_t **outBuffer, size_t *outSize -// ); -// -// savedevice.c compresses settings + all save slots' JSON into one -// magic/version/checksum-framed blob and read/writes it as a single raw -// buffer through these two hooks instead. saveDeviceDataReadPlatform must -// allocate its output with memoryAllocate (ownership passes to the caller, -// who frees it with memoryFree), and set *outBuffer = NULL/*outSize = 0 -// (not an error) when nothing has been saved yet - same convention as the -// "file not found" case in the non-raw four-hook mode. -// #define SAVE_DEVICE_DATA_RAW -// -// saveDeviceDataReadPlatform must always resolve to "the one current blob" -// even if the platform keeps more than one physical copy under the hood - -// e.g. a platform with no rename primitive (GameCube memory cards) that -// ping-pongs between two fixed files instead. saveDeviceRawIsValid() below -// is what such a platform uses to tell which of its copies is newest. - -typedef struct savedevice_s savedevice_t; -typedef struct savesettings_s savesettings_t; -typedef struct saveslot_s saveslot_t; - -typedef void (*savedevicestatecallback_t)(savedevice_t *device, void *user); - -typedef enum { - SAVE_DEVICE_STATE_UNKNOWN, - SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, - SAVE_DEVICE_STATE_UNAVAILABLE, - SAVE_DEVICE_STATE_AVAILABLE, - SAVE_DEVICE_STATE_ERRORED -} savedevicestate_t; - -typedef struct savedevice_s { - savedevicestate_t state; - savedeviceplatform_t platform; - const char_t *reasonKey; - bool_t fireCallback; - void *user; - savedevicestatecallback_t stateCallback; -} savedevice_t; - -/** - * Initializes the save device. - * - * @param device The save device to initialize. - * @return Error state if any. - */ -errorret_t saveDeviceInit(savedevice_t *device); - -/** - * Updates the save device. - * - * @param device The save device to update. - * @return Error state if any. - */ -errorret_t saveDeviceUpdate(savedevice_t *device); - -/** - * Internal method to fire the save callback, does it at the appropriate time. - * - * @param device The save device to fire the callback for. - */ -void saveDeviceFireCallback(savedevice_t *device); - -/** - * Requests the device to check its availability, this will call the callback - * whence completed. - * - * @param device The save device to check availability. - * @param callback The callback to call when the availability check is complete. - * @param user User data to pass to the callback. - */ -void saveDeviceCheckAvailability( - savedevice_t *device, - savedevicestatecallback_t callback, - void *user -); - -/** - * Writes the given save slot to the device at the given slot index. - * - * @param device The save device to write to. - * @param slot The save slot to write. - * @param slotIndex The slot index to write to. - * @return Error state if any. - */ -errorret_t saveDeviceSlotWrite( - savedevice_t *device, - saveslot_t *slot, - const uint8_t slotIndex -); - -/** - * Reads a save slot from the device at the given slot index. - * - * @param device The save device to read from. - * @param slot The save slot to read into. - * @param slotIndex The slot index to read from. - * @return Error state if any. - */ -errorret_t saveDeviceSlotRead( - savedevice_t *device, - saveslot_t *slot, - const uint8_t slotIndex -); - -/** - * Writes the given save settings to the device. - * - * @param device The save device to write to. - * @param settings The save settings to write. - * @return Error state if any. - */ -errorret_t saveDeviceSettingsWrite( - savedevice_t *device, - savesettings_t *settings -); - -/** - * Reads the save settings from the device. - * - * @param device The save device to read from. - * @param settings The save settings to read into. - * @return Error state if any. - */ -errorret_t saveDeviceSettingsRead( - savedevice_t *device, - savesettings_t *settings -); - -/** - * Disposes of the save device. - * - * @param device The save device to dispose. - * @return Error state if any. - */ -errorret_t saveDeviceDispose(savedevice_t *device); - -#if defined(SAVE_DEVICE_DATA_RAW) - -// Mirrors the same guarded default in save/slot/saveslot.h - can't -// #include that header here to get it directly, since it itself includes -// this header (savedevice.h), and relying on include order to resolve the -// cycle correctly would be fragile. -#ifndef SAVE_SLOT_COUNT - #define SAVE_SLOT_COUNT 3 -#endif - -#pragma pack(push, 1) -typedef struct { - char_t magic[4]; - uint32_t version; - uint32_t uncompressedSize;// size of the logical blob, pre-compression - uint32_t compressedSize;// size of the payload following this header - uint32_t checksum;// cryptCRC32() over the compressed payload - // Monotonically increasing with every store. Unused by single-file - // platforms (rename already guarantees the current file is the latest), - // but load-bearing for platforms with no rename primitive and multiple - // physical copies - e.g. GameCube memory cards ping-ponging between two - // fixed files, where this is how saveDeviceRawIsValid() picks the newest - // valid copy. - uint32_t generation; -} savedevicerawheader_t; -#pragma pack(pop) - -typedef struct { - const uint8_t *ptr; - uint32_t len; -} savedevicerawspan_t; - -typedef struct { - savedevicerawspan_t settings; - savedevicerawspan_t slots[SAVE_SLOT_COUNT]; -} savedevicerawspans_t; - -// A single settings/slot item's JSON bytes, either borrowed from an existing -// decompressed blob (owned == NULL) or freshly serialized just now via -// yyjson_mut_write (owned != NULL, must be freed with plain free()). -typedef struct { - const uint8_t *ptr; - uint32_t len; - char_t *owned; -} savedevicerawitem_t; - -/** - * Parses the length-prefixed span table at the front of a decompressed - * logical blob. Spans point directly into `logical`, nothing is copied. - * - * @param logical The decompressed logical blob to parse. - * @param spans Output spans, filled in to point into `logical`. - */ -void saveDeviceRawParseSpans( - const uint8_t *logical, - savedevicerawspans_t *spans -); - -/** - * Validates the header and inflates the compressed payload that follows it. - * - * @param raw The raw framed blob (header + compressed payload). - * @param rawSize Number of bytes in raw. - * @param outLogical Receives a memoryAllocate'd buffer the caller must free. - * @param outLogicalSize Receives the size of *outLogical. - * @param outGeneration Receives the blob's generation counter, may be NULL. - * @return Error state if any. - */ -errorret_t saveDeviceRawInflate( - const uint8_t *raw, - const size_t rawSize, - uint8_t **outLogical, - size_t *outLogicalSize, - uint32_t *outGeneration -); - -/** - * Serializes `settings` to a freshly malloc'd JSON string via yyjson - the - * result must be freed with plain free(), not memoryFree(). - * - * @param settings The settings to serialize. - * @param outJson Receives the malloc'd JSON string. - * @param outLen Receives the length of *outJson. - * @return Error state if any. - */ -errorret_t saveDeviceRawSerializeSettings( - savesettings_t *settings, - char_t **outJson, - size_t *outLen -); - -/** - * Serializes `slot` to a freshly malloc'd JSON string via yyjson - the - * result must be freed with plain free(), not memoryFree(). - * - * @param slot The slot to serialize. - * @param outJson Receives the malloc'd JSON string. - * @param outLen Receives the length of *outJson. - * @return Error state if any. - */ -errorret_t saveDeviceRawSerializeSlot( - saveslot_t *slot, - char_t **outJson, - size_t *outLen -); - -/** - * Frees whatever's been collected so far - safe to call at any point since - * unset items are zero-initialized (owned == NULL is a no-op skip). - * - * @param oldRaw Previously read raw blob, or NULL. - * @param oldLogical Previously inflated logical blob, or NULL. - * @param settingsItem Settings item collected so far. - * @param slotItems Slot items collected so far (SAVE_SLOT_COUNT entries). - */ -void saveDeviceRawCleanupStore( - uint8_t *oldRaw, - uint8_t *oldLogical, - savedevicerawitem_t *settingsItem, - savedevicerawitem_t *slotItems -); - -/** - * Reads the existing combined blob (if any), replaces exactly one item - - * `settings`, or the slot at `slotIndex` when `slot` is given, exactly one - * of the two must be non-NULL - and rewrites the whole blob. Every other - * item's JSON is carried through byte-for-byte from what was already - * stored (or freshly defaulted if nothing was stored yet), so a save never - * reconstructs data it wasn't given. - * - * @param device The save device to write to. - * @param settings The settings to store, or NULL to leave settings as-is. - * @param slot The slot to store, or NULL to leave every slot as-is. - * @param slotIndex Which slot `slot` belongs to, ignored if slot is NULL. - * @return Error state if any. - */ -errorret_t saveDeviceRawStoreItem( - savedevice_t *device, - savesettings_t *settings, - saveslot_t *slot, - const uint8_t slotIndex -); - -/** - * Reads the combined blob and populates exactly one item - `settings`, or - * the slot at `slotIndex` when `slot` is given, exactly one of the two must - * be non-NULL. Leaves the destination untouched if nothing's been saved yet. - * - * @param device The save device to read from. - * @param settings The settings to populate, or NULL to skip. - * @param slot The slot to populate, or NULL to skip. - * @param slotIndex Which slot to populate `slot` from, ignored if NULL. - * @return Error state if any. - */ -errorret_t saveDeviceRawFetchItem( - savedevice_t *device, - savesettings_t *settings, - saveslot_t *slot, - const uint8_t slotIndex -); - -#endif// defined(SAVE_DEVICE_DATA_RAW) - -/** - * Only relevant to SAVE_DEVICE_DATA_RAW platforms that keep more than one - * physical copy of the save blob (see the note above) - cheaply checks - * whether `raw` is a well-formed, checksum-valid raw save blob without - * decompressing it, and if so reports its generation counter so the caller - * can tell which of several copies is newest. - * - * @param raw The raw bytes to validate. - * @param rawSize The number of bytes in raw. - * @param outGeneration Receives the blob's generation counter if valid, may - * be NULL if the caller doesn't need it. - * @return true if raw is a valid, checksum-passing save blob. - */ -bool_t saveDeviceRawIsValid( - const uint8_t *raw, - const size_t rawSize, - uint32_t *outGeneration -); \ No newline at end of file diff --git a/src/dusk/save/savejson.h b/src/dusk/save/savejson.h deleted file mode 100644 index 9537257e..00000000 --- a/src/dusk/save/savejson.h +++ /dev/null @@ -1,600 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "yyjson.h" -#include "error/error.h" -#include "assert/assert.h" -#include "util/string.h" -#include "time/timeepoch.h" - -#define SAVE_JSON_STRING_BUFFER_SIZE 256 - -/** - * Creates a new mutable JSON document with an empty root object, ready for - * the writeX macros below to populate. Declares `doc` and `object` in the - * calling scope. - */ -#define writeInit() \ - yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); \ - assertNotNull(doc, "Failed to create JSON document"); \ - yyjson_mut_val *object = yyjson_mut_obj(doc); \ - yyjson_mut_doc_set_root(doc, object) - -/** - * Parses the given JSON text and exposes its root object for the readX - * macros below to consume. Declares `jsonDoc` (keep it around to dispose - * with yyjson_doc_free once reading is done) and `object` in the calling - * scope. Errors if the text fails to parse or has no object root. - * - * @param jsonText The JSON text to parse. - * @param jsonLength The length of the JSON text, in bytes. - */ -#define readInit(jsonText, jsonLength) \ - yyjson_doc *jsonDoc = yyjson_read((jsonText), (jsonLength), 0); \ - if(jsonDoc == NULL) errorThrow("Failed to parse save JSON"); \ - yyjson_val *object = yyjson_doc_get_root(jsonDoc); \ - if(object == NULL) errorThrow("Save JSON missing root object") - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasInt32(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireInt32(key) \ - if(!hasInt32(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes an int32_t to the current JSON object. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeInt32(key, value) \ - yyjson_mut_obj_add_int(doc, object, key, (int64_t)(value)) - -/** - * Reads an int32_t from the current JSON object, falling back to the given - * default if the key is missing. - * - * @param key The key to read from. - * @param dest The destination variable to assign to. - * @param def The default value to use if the key is missing. - */ -#define readInt32(key, dest, def) \ - (dest) = hasInt32(key) ? \ - (int32_t)yyjson_get_int(yyjson_obj_get(object, key)) : (int32_t)(def) - -/** - * Writes a schema version into the current JSON object's "version" key. - * Pair with requireVersion() on the read side. - * - * @param version The version constant to write (e.g. SAVE_SLOT_CURRENT). - */ -#define writeVersion(version) writeInt32("version", (version)) - -/** - * Requires the current JSON object's "version" key to exist and exactly - * match the given version constant, throwing otherwise. Used to reject a - * save file outright rather than trying to migrate an old schema. - * - * @param version The expected version constant (e.g. SAVE_SLOT_CURRENT). - */ -#define requireVersion(version) { \ - requireInt32("version"); \ - int32_t saveJsonVersion; \ - readInt32("version", saveJsonVersion, 0); \ - if(saveJsonVersion != (version)) { \ - errorThrow( \ - "Save JSON version mismatch (expected %d, got %d)", \ - (int)(version), (int)saveJsonVersion \ - ); \ - } \ -} - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasUInt32(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireUInt32(key) \ - if(!hasUInt32(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a uint32_t to the current JSON object. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeUInt32(key, value) \ - yyjson_mut_obj_add_uint(doc, object, key, (uint64_t)(value)) - -/** - * Reads a uint32_t from the current JSON object, falling back to the given - * default if the key is missing. - * - * @param key The key to read from. - * @param dest The destination variable to assign to. - * @param def The default value to use if the key is missing. - */ -#define readUInt32(key, dest, def) \ - (dest) = hasUInt32(key) ? \ - (uint32_t)yyjson_get_uint(yyjson_obj_get(object, key)) : (uint32_t)(def) - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasUInt8(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireUInt8(key) \ - if(!hasUInt8(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a uint8_t to the current JSON object. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeUInt8(key, value) \ - yyjson_mut_obj_add_uint(doc, object, key, (uint64_t)(value)) - -/** - * Reads a uint8_t from the current JSON object, falling back to the given - * default if the key is missing. - * - * @param key The key to read from. - * @param dest The destination variable to assign to. - * @param def The default value to use if the key is missing. - */ -#define readUInt8(key, dest, def) \ - (dest) = hasUInt8(key) ? \ - (uint8_t)yyjson_get_uint(yyjson_obj_get(object, key)) : (uint8_t)(def) - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasInt64(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireInt64(key) \ - if(!hasInt64(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes an int64_t to the current JSON object. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeInt64(key, value) \ - yyjson_mut_obj_add_sint(doc, object, key, (int64_t)(value)) - -/** - * Reads an int64_t from the current JSON object, falling back to the given - * default if the key is missing. - * - * @param key The key to read from. - * @param dest The destination variable to assign to. - * @param def The default value to use if the key is missing. - */ -#define readInt64(key, dest, def) \ - (dest) = hasInt64(key) ? \ - yyjson_get_sint(yyjson_obj_get(object, key)) : (int64_t)(def) - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasUInt64(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireUInt64(key) \ - if(!hasUInt64(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a uint64_t to the current JSON object. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeUInt64(key, value) \ - yyjson_mut_obj_add_uint(doc, object, key, (uint64_t)(value)) - -/** - * Reads a uint64_t from the current JSON object, falling back to the given - * default if the key is missing. - * - * @param key The key to read from. - * @param dest The destination variable to assign to. - * @param def The default value to use if the key is missing. - */ -#define readUInt64(key, dest, def) \ - (dest) = hasUInt64(key) ? \ - yyjson_get_uint(yyjson_obj_get(object, key)) : (uint64_t)(def) - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasFloat(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireFloat(key) \ - if(!hasFloat(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a float_t to the current JSON object. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeFloat(key, value) \ - yyjson_mut_obj_add_real(doc, object, key, (double)(value)) - -/** - * Reads a float_t from the current JSON object, falling back to the given - * default if the key is missing. - * - * @param key The key to read from. - * @param dest The destination variable to assign to. - * @param def The default value to use if the key is missing. - */ -#define readFloat(key, dest, def) \ - (dest) = hasFloat(key) ? \ - (float_t)yyjson_get_real(yyjson_obj_get(object, key)) : (float_t)(def) - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasFloatArray(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireFloatArray(key) \ - if(!hasFloatArray(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a float_t array to the current JSON object. - * - * @param key The key to write to. - * @param value The array to write. - * @param count Number of elements in the array. - */ -#define writeFloatArray(key, value, count) { \ - yyjson_mut_val *saveJsonArr = yyjson_mut_arr(doc); \ - size_t saveJsonArrIdx = 0; \ - while(saveJsonArrIdx < (size_t)(count)) { \ - yyjson_mut_arr_add_real( \ - doc, saveJsonArr, (double)(value)[saveJsonArrIdx] \ - ); \ - saveJsonArrIdx++; \ - } \ - yyjson_mut_obj_add_val(doc, object, key, saveJsonArr); \ -} - -/** - * Reads a float_t array from the current JSON object into dest, falling - * back to filling every element with def if the key is missing. Errors if - * the JSON array's length doesn't exactly match count. - * - * @param key The key to read from. - * @param dest The destination array to assign to. - * @param count Number of elements dest holds. - * @param def The default value to fill every element with if the key is - * missing. - */ -#define readFloatArray(key, dest, count, def) { \ - yyjson_val *saveJsonArr = yyjson_obj_get(object, key); \ - if(saveJsonArr != NULL) { \ - if(yyjson_arr_size(saveJsonArr) != (size_t)(count)) { \ - errorThrow( \ - "Save JSON array '%s' has wrong length, expected %d", \ - key, (int)(count) \ - ); \ - } \ - size_t saveJsonArrIdx, saveJsonArrLen; \ - yyjson_val *saveJsonArrElem; \ - yyjson_arr_foreach( \ - saveJsonArr, saveJsonArrIdx, saveJsonArrLen, saveJsonArrElem \ - ) { \ - (dest)[saveJsonArrIdx] = (float_t)yyjson_get_real(saveJsonArrElem); \ - } \ - } else { \ - size_t saveJsonArrIdx = 0; \ - while(saveJsonArrIdx < (size_t)(count)) { \ - (dest)[saveJsonArrIdx] = (float_t)(def); \ - saveJsonArrIdx++; \ - } \ - } \ -} - -/** - * Attaches an already-built mutable value (e.g. a yyjson_mut_arr() of - * per-element objects the caller populated by hand) to the current JSON - * object under the given key. - * - * @param key The key to write to. - * @param value The yyjson_mut_val* to attach. - */ -#define writeValue(key, value) yyjson_mut_obj_add_val(doc, object, key, value) - -/** - * Appends a new JSON object to the given mutable array as one element and - * declares `object` in the calling scope pointing at it, shadowing any - * outer `object` - so the writeX macros above, once called inside the loop - * that builds `arr`, populate this element instead of the outer object. - * - * @param arr The yyjson_mut_val* array (from yyjson_mut_arr()) to append to. - */ -#define writeArrayObject(arr) \ - yyjson_mut_val *object = yyjson_mut_arr_add_obj(doc, arr) - -/** - * Requires a key on the current JSON object to be a JSON array of exactly - * the given size, throwing otherwise. Declares `dest` (a yyjson_val*) in - * the calling scope pointing at the array, ready for the caller to iterate - * (e.g. with yyjson_arr_foreach) - unlike the readXArray macros, this - * doesn't copy the elements out itself, since each element here is a - * nested object the caller parses by hand. - * - * @param key The key to read from. - * @param dest The yyjson_val* variable to declare, pointing at the array. - * @param count The exact number of elements the array must have. - */ -#define requireArrayOfSize(key, dest, count) \ - yyjson_val *dest = yyjson_obj_get(object, key); \ - if((dest) == NULL) errorThrow("Save JSON missing '%s' key", key); \ - if(yyjson_arr_size(dest) != (size_t)(count)) { \ - errorThrow( \ - "Save JSON array '%s' has wrong length, expected %d", \ - key, (int)(count) \ - ); \ - } - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasUInt8Array(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireUInt8Array(key) \ - if(!hasUInt8Array(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a uint8_t array to the current JSON object. - * - * @param key The key to write to. - * @param value The array to write. - * @param count Number of elements in the array. - */ -#define writeUInt8Array(key, value, count) { \ - yyjson_mut_val *saveJsonArr = yyjson_mut_arr(doc); \ - size_t saveJsonArrIdx = 0; \ - while(saveJsonArrIdx < (size_t)(count)) { \ - yyjson_mut_arr_add_uint( \ - doc, saveJsonArr, (uint64_t)(value)[saveJsonArrIdx] \ - ); \ - saveJsonArrIdx++; \ - } \ - yyjson_mut_obj_add_val(doc, object, key, saveJsonArr); \ -} - -/** - * Reads a uint8_t array from the current JSON object into dest, falling - * back to filling every element with def if the key is missing. Errors if - * the JSON array's length doesn't exactly match count. - * - * @param key The key to read from. - * @param dest The destination array to assign to. - * @param count Number of elements dest holds. - * @param def The default value to fill every element with if the key is - * missing. - */ -#define readUInt8Array(key, dest, count, def) { \ - yyjson_val *saveJsonArr = yyjson_obj_get(object, key); \ - if(saveJsonArr != NULL) { \ - if(yyjson_arr_size(saveJsonArr) != (size_t)(count)) { \ - errorThrow( \ - "Save JSON array '%s' has wrong length, expected %d", \ - key, (int)(count) \ - ); \ - } \ - size_t saveJsonArrIdx, saveJsonArrLen; \ - yyjson_val *saveJsonArrElem; \ - yyjson_arr_foreach( \ - saveJsonArr, saveJsonArrIdx, saveJsonArrLen, saveJsonArrElem \ - ) { \ - (dest)[saveJsonArrIdx] = (uint8_t)yyjson_get_uint(saveJsonArrElem); \ - } \ - } else { \ - size_t saveJsonArrIdx = 0; \ - while(saveJsonArrIdx < (size_t)(count)) { \ - (dest)[saveJsonArrIdx] = (uint8_t)(def); \ - saveJsonArrIdx++; \ - } \ - } \ -} - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasString(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireString(key) \ - if(!hasString(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Errors if the given key is missing, not a string, or a string shorter - * than the given minimum length - e.g. requireStrMin(key, 1) to reject a - * missing/null/empty string outright. - * - * @param key The key to check. - * @param len The minimum string length required. - */ -#define requireStrMin(key, len) { \ - yyjson_val *saveJsonStrMinVal = yyjson_obj_get(object, key); \ - if( \ - saveJsonStrMinVal == NULL || !yyjson_is_str(saveJsonStrMinVal) || \ - yyjson_get_len(saveJsonStrMinVal) < (size_t)(len) \ - ) { \ - errorThrow( \ - "Save JSON '%s' must be a string of at least length %d", key, (int)(len) \ - ); \ - } \ -} - -/** - * Writes a string to the current JSON object. The string is copied, so it - * does not need to outlive the JSON document. - * - * @param key The key to write to. - * @param value The value to write. - */ -#define writeString(key, value) \ - yyjson_mut_obj_add_strcpy(doc, object, key, value) - -/** - * Reads a string from the current JSON object into dest, falling back to - * the given default if the key is missing. Errors if the JSON string is - * longer than maxLength. Uses the caller's `saveJsonStringBuffer` local as - * scratch space. - * - * @param key The key to read from. - * @param dest The destination buffer to copy into. - * @param def The default value to use if the key is missing. - * @param maxLength The maximum length of dest, excluding the null terminator. - */ -#define readString(key, dest, def, maxLength) { \ - yyjson_val *saveJsonStrVal = yyjson_obj_get(object, key); \ - if(saveJsonStrVal != NULL) { \ - if(yyjson_get_len(saveJsonStrVal) > (size_t)(maxLength)) { \ - errorThrow( \ - "Save JSON string '%s' exceeds max length of %d", \ - key, (int)(maxLength) \ - ); \ - } \ - stringCopy( \ - saveJsonStringBuffer, yyjson_get_str(saveJsonStrVal), (maxLength) + 1 \ - ); \ - } else { \ - stringCopy(saveJsonStringBuffer, (def), (maxLength) + 1); \ - } \ - stringCopy((dest), saveJsonStringBuffer, (maxLength) + 1); \ -} - -/** - * Checks if the given key exists on the current JSON object. - * - * @param key The key to check for. - */ -#define hasTime(key) (yyjson_obj_get(object, key) != NULL) - -/** - * Errors if the given key does not exist on the current JSON object. - * - * @param key The key that must exist. - */ -#define requireTime(key) \ - if(!hasTime(key)) errorThrow("Save JSON missing '%s' key", key) - -/** - * Writes a dusktimeepoch_t to the current JSON object as a nested object of - * its time/timeZone/offsetTime fields. - * - * @param key The key to write to. - * @param value The dusktimeepoch_t value to write. - */ -#define writeTime(key, value) { \ - yyjson_mut_val *saveJsonTimeObj = yyjson_mut_obj_add_obj(doc, object, key); \ - yyjson_mut_obj_add_real(doc, saveJsonTimeObj, "time", (value).time); \ - yyjson_mut_obj_add_real(doc, saveJsonTimeObj, "timeZone", (value).timeZone); \ - yyjson_mut_obj_add_real( \ - doc, saveJsonTimeObj, "offsetTime", (value).offsetTime \ - ); \ -} - -/** - * Reads a dusktimeepoch_t from the current JSON object's nested time/ - * timeZone/offsetTime fields. Errors if the key or any sub-field is missing. - * - * @param key The key to read from. - * @param dest The destination dusktimeepoch_t to assign to. - */ -#define readTime(key, dest) { \ - yyjson_val *saveJsonTimeObj = yyjson_obj_get(object, key); \ - if(saveJsonTimeObj == NULL) errorThrow("Save JSON missing '%s' key", key); \ - yyjson_val *saveJsonTimeTime = yyjson_obj_get(saveJsonTimeObj, "time"); \ - yyjson_val *saveJsonTimeZone = yyjson_obj_get(saveJsonTimeObj, "timeZone"); \ - yyjson_val *saveJsonTimeOffset = \ - yyjson_obj_get(saveJsonTimeObj, "offsetTime"); \ - if(saveJsonTimeTime == NULL) { \ - errorThrow("Save JSON missing '%s.time' key", key); \ - } \ - if(saveJsonTimeZone == NULL) { \ - errorThrow("Save JSON missing '%s.timeZone' key", key); \ - } \ - if(saveJsonTimeOffset == NULL) { \ - errorThrow("Save JSON missing '%s.offsetTime' key", key); \ - } \ - (dest).time = yyjson_get_real(saveJsonTimeTime); \ - (dest).timeZone = yyjson_get_real(saveJsonTimeZone); \ - (dest).offsetTime = yyjson_get_real(saveJsonTimeOffset); \ -} diff --git a/src/dusk/save/saverror.h b/src/dusk/save/saverror.h deleted file mode 100644 index bab2de2e..00000000 --- a/src/dusk/save/saverror.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -// Custom error codes for the save system, these start at 2 since ERROR_OK -// and ERROR_NOT_OK occupy 0 and 1 respectively. -#define SAVE_ERROR_NO_CURRENT_DEVICE ((errorcode_t)2) -#define SAVE_ERROR_NO_CURRENT_SLOT ((errorcode_t)3) -#define SAVE_ERROR_DEVICE_NOT_AVAILABLE ((errorcode_t)4) -#define SAVE_ERROR_SLOT_NOT_IN_USE ((errorcode_t)5) diff --git a/src/dusk/save/settings/CMakeLists.txt b/src/dusk/save/settings/CMakeLists.txt deleted file mode 100644 index d1beefa8..00000000 --- a/src/dusk/save/settings/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - savesettings.c - savesettingscurrent.c - savesettingsver1.c -) diff --git a/src/dusk/save/settings/savesettings.c b/src/dusk/save/settings/savesettings.c deleted file mode 100644 index cac48463..00000000 --- a/src/dusk/save/settings/savesettings.c +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "savesettings.h" -#include "save/settings/savesettingscurrent.h" -#include "assert/assert.h" -#include "util/memory.h" - -void saveSettingsInit(savesettings_t *settings) { - assertNotNull(settings, "Settings cannot be null"); - - memorySet(settings, 0, sizeof(savesettings_t)); - - settings->audioMasterVolume = 1.0f; - audiomixerchannel_t channel = 0; - while(channel < AUDIO_MIXER_CHANNEL_COUNT) { - settings->audioChannelVolume[channel] = 1.0f; - channel++; - } -} - -errorret_t saveSettingsWriteJSON( - savesettings_t *settings, - yyjson_mut_doc *doc, - yyjson_mut_val *object -) { - errorChain(saveSettingsCurrentWriteJSON(settings, doc, object)); - errorOk(); -} - -errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object) { - errorChain(saveSettingsCurrentReadJSON(settings, object)); - errorOk(); -} diff --git a/src/dusk/save/settings/savesettings.h b/src/dusk/save/settings/savesettings.h deleted file mode 100644 index 91c8ec70..00000000 --- a/src/dusk/save/settings/savesettings.h +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/savedevice.h" -#include "audio/mixer/audiomixerchannel.h" -#include "yyjson.h" - -// The stable, version-independent in-memory shape of save settings - what -// the rest of the engine actually reads/writes, using live engine constants -// (AUDIO_MIXER_CHANNEL_COUNT) freely. Every save schema version -// (savesettingsver1_t, ...) redefines its own frozen copy of these fields -// for its JSON wire format; saveSettingsCurrentWriteJSON()/ReadJSON() -// (savesettingscurrent.c) manually copy field-by-field between this and -// whichever version is current, so a version's on-disk shape can never -// drift just because a live engine constant changes. -typedef struct savesettings_s { - // Persisted mixer volume preferences - see audiomixerchannelstate_t's own - // comment on why these live here rather than on the mixer itself. - float_t audioMasterVolume; - float_t audioChannelVolume[AUDIO_MIXER_CHANNEL_COUNT]; -} savesettings_t; - -/** - * Inits the save settings with the default state, this is functionally "new - * game" but will not set the player name, as that is what we use to determine - * if a save slot is "in use" or not. - * - * @param settings The save settings to init. - */ -void saveSettingsInit(savesettings_t *settings); - -/** - * Writes the given save settings out to the given JSON object. - * - * @param settings The save settings to write. - * @param doc The mutable JSON document that owns the object. - * @param object The mutable JSON object to write into. - * @return Error state if any. - */ -errorret_t saveSettingsWriteJSON( - savesettings_t *settings, - yyjson_mut_doc *doc, - yyjson_mut_val *object -); - -/** - * Reads the given save settings in from the given JSON object. - * - * @param settings The save settings to read into. - * @param object The JSON object to read from. - * @return Error state if any. - */ -errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object); diff --git a/src/dusk/save/settings/savesettingscurrent.c b/src/dusk/save/settings/savesettingscurrent.c deleted file mode 100644 index e16c0df7..00000000 --- a/src/dusk/save/settings/savesettingscurrent.c +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "savesettingscurrent.h" -#include "assert/assert.h" - -errorret_t saveSettingsCurrentWriteJSON( - savesettings_t *settings, - yyjson_mut_doc *doc, - yyjson_mut_val *object -) { - assertNotNull(settings, "Settings cannot be null"); - assertNotNull(doc, "Doc cannot be null"); - assertNotNull(object, "Object cannot be null"); - - savesettingsver1_t ver1; - ver1.audioMasterVolume = settings->audioMasterVolume; - - // Bounded by the smaller of the two counts, in case a future live channel - // count ever diverges from this frozen v1 schema's count. - uint8_t channelCount = AUDIO_MIXER_CHANNEL_COUNT; - if(SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT < channelCount) { - channelCount = SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT; - } - for(uint8_t i = 0; i < channelCount; i++) { - ver1.audioChannelVolume[i] = settings->audioChannelVolume[i]; - } - for( - uint8_t i = channelCount; i < SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT; i++ - ) { - ver1.audioChannelVolume[i] = 1.0f; - } - - errorChain(saveSettingsVer1WriteJSON(&ver1, doc, object)); - errorOk(); -} - -errorret_t saveSettingsCurrentReadJSON( - savesettings_t *settings, - yyjson_val *object -) { - assertNotNull(settings, "Settings cannot be null"); - assertNotNull(object, "Object cannot be null"); - - savesettingsver1_t ver1; - errorChain(saveSettingsVer1ReadJSON(&ver1, object)); - - settings->audioMasterVolume = ver1.audioMasterVolume; - - uint8_t channelCount = AUDIO_MIXER_CHANNEL_COUNT; - if(SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT < channelCount) { - channelCount = SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT; - } - for(uint8_t i = 0; i < channelCount; i++) { - settings->audioChannelVolume[i] = ver1.audioChannelVolume[i]; - } - for(uint8_t i = channelCount; i < AUDIO_MIXER_CHANNEL_COUNT; i++) { - settings->audioChannelVolume[i] = 1.0f; - } - - errorOk(); -} diff --git a/src/dusk/save/settings/savesettingscurrent.h b/src/dusk/save/settings/savesettingscurrent.h deleted file mode 100644 index 38e8d75a..00000000 --- a/src/dusk/save/settings/savesettingscurrent.h +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/settings/savesettings.h" -#include "save/settings/savesettingsver1.h" - -/** - * Converts the given save settings to whichever schema SAVE_SETTINGS_CURRENT - * (savesettingsver.h) selects and writes it out to the given JSON object. - * Update this (only) when bumping to a new SAVE_SETTINGS_VER_N. - * - * @param settings The save settings to write. - * @param doc The mutable JSON document that owns the object. - * @param object The mutable JSON object to write into. - * @return Error state if any. - */ -errorret_t saveSettingsCurrentWriteJSON( - savesettings_t *settings, - yyjson_mut_doc *doc, - yyjson_mut_val *object -); - -/** - * Reads whichever schema SAVE_SETTINGS_CURRENT (savesettingsver.h) selects - * from the given JSON object and converts it into the given save settings. - * Update this (only) when bumping to a new SAVE_SETTINGS_VER_N. - * - * @param settings The save settings to read into. - * @param object The JSON object to read from. - * @return Error state if any. - */ -errorret_t saveSettingsCurrentReadJSON( - savesettings_t *settings, - yyjson_val *object -); diff --git a/src/dusk/save/settings/savesettingsver.h b/src/dusk/save/settings/savesettingsver.h deleted file mode 100644 index c4035ecb..00000000 --- a/src/dusk/save/settings/savesettingsver.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "time/timeepoch.h" -#include "save/savedevice.h" -#include "yyjson.h" - -#define SAVE_SETTINGS_VER_1 1 - -#define SAVE_SETTINGS_CURRENT SAVE_SETTINGS_VER_1 diff --git a/src/dusk/save/settings/savesettingsver1.c b/src/dusk/save/settings/savesettingsver1.c deleted file mode 100644 index 69b2a837..00000000 --- a/src/dusk/save/settings/savesettingsver1.c +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "savesettingsver1.h" -#include "assert/assert.h" -#include "save/savejson.h" - -errorret_t saveSettingsVer1WriteJSON( - savesettingsver1_t *settings, - yyjson_mut_doc *doc, - yyjson_mut_val *object -) { - assertNotNull(settings, "Settings cannot be null"); - assertNotNull(doc, "Doc cannot be null"); - assertNotNull(object, "Object cannot be null"); - - writeVersion(SAVE_SETTINGS_CURRENT); - writeFloat("audioMasterVolume", settings->audioMasterVolume); - writeFloatArray( - "audioChannelVolume", settings->audioChannelVolume, - SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT - ); - - errorOk(); -} - -errorret_t saveSettingsVer1ReadJSON( - savesettingsver1_t *settings, - yyjson_val *object -) { - assertNotNull(settings, "Settings cannot be null"); - assertNotNull(object, "Object cannot be null"); - - requireVersion(SAVE_SETTINGS_CURRENT); - - readFloat("audioMasterVolume", settings->audioMasterVolume, 1.0f); - readFloatArray( - "audioChannelVolume", settings->audioChannelVolume, - SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT, 1.0f - ); - - errorOk(); -} diff --git a/src/dusk/save/settings/savesettingsver1.h b/src/dusk/save/settings/savesettingsver1.h deleted file mode 100644 index 8f9c24a9..00000000 --- a/src/dusk/save/settings/savesettingsver1.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "save/settings/savesettingsver.h" -#include "yyjson.h" - -// This schema's own frozen channel count - never reference -// AUDIO_MIXER_CHANNEL_COUNT (or any other live constant) here. Once -// shipped, this file's on-disk shape must never change underneath old save -// files, even if the live engine constant does. -#define SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT 7 - -// Independent from savesettings_t (save/settings/savesettings.h) by design -// - this is what version 1 actually put on disk, frozen forever. -// saveSettingsCurrentWriteJSON()/ReadJSON() (savesettingscurrent.c) manually -// copy field-by-field between the two, since they're separate types that -// merely happen to currently look alike. -typedef struct savesettingsver1_s { - float_t audioMasterVolume; - float_t audioChannelVolume[SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT]; -} savesettingsver1_t; - -/** - * Writes the given v1 save settings data out to the given JSON object, - * stamping SAVE_SETTINGS_CURRENT as the "version" field. - * - * @param settings The v1 save settings data to write. - * @param doc The mutable JSON document that owns the object. - * @param object The mutable JSON object to write into. - * @return Error state if any. - */ -errorret_t saveSettingsVer1WriteJSON( - savesettingsver1_t *settings, - yyjson_mut_doc *doc, - yyjson_mut_val *object -); - -/** - * Reads v1 save settings data in from the given JSON object. Throws if the - * JSON's "version" is missing or doesn't match SAVE_SETTINGS_CURRENT. - * - * @param settings The v1 save settings data to read into. - * @param object The JSON object to read from. - * @return Error state if any. - */ -errorret_t saveSettingsVer1ReadJSON( - savesettingsver1_t *settings, - yyjson_val *object -); diff --git a/src/dusk/save/slot/CMakeLists.txt b/src/dusk/save/slot/CMakeLists.txt deleted file mode 100644 index de4c3dcd..00000000 --- a/src/dusk/save/slot/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - saveslot.c - saveslotcurrent.c - saveslotver1.c -) diff --git a/src/dusk/save/slot/saveslot.c b/src/dusk/save/slot/saveslot.c deleted file mode 100644 index cb966442..00000000 --- a/src/dusk/save/slot/saveslot.c +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "saveslot.h" -#include "save/slot/saveslotcurrent.h" -#include "save/save.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" - -errorret_t saveSlotInit(saveslot_t *slot) { - assertNotNull(slot, "Slot cannot be null"); - - // Zero the save slot. - memoryZero(slot, sizeof(saveslot_t)); - slot->version = 1; - - // Set the default story flags. - for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) { - slot->storyFlags[flag] = STORY_FLAG_MAP[flag].initial; - } - - // Set the default party. - errorChain(partyInit(&slot->party)); - - // Set the default current cutscene. - stringCopy( - slot->currentCutscene, SAVE_SLOT_CUTSCENE_DEFAULT, - sizeof(slot->currentCutscene) - 1 - ); - - errorOk(); -} - -errorret_t saveSlotNewGame(void) { - errorChain(saveSlotInit(&SAVE.slot)); - - stringCopy( - SAVE.slot.cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT, - sizeof(SAVE.slot.cachedData.mapName) - ); - - errorOk(); -} - -void saveSlotSetCurrentCutscene(const char_t *file) { - assertNotNull(file, "File cannot be null"); - - stringCopy( - SAVE.slot.currentCutscene, file, sizeof(SAVE.slot.currentCutscene) - 1 - ); - SAVE.slotDirty = true; -} - -bool_t saveSlotInUse(saveslotcache_t *slot) { - assertNotNull(slot, "Slot cannot be null"); - return slot->name[0] != '\0'; -} - -bool_t saveSlotHasSaved(saveslotcache_t *slot) { - assertNotNull(slot, "Slot cannot be null"); - return slot->time.time != 0; -} - -errorret_t saveSlotWriteJSON( - saveslot_t *slot, - yyjson_mut_doc *doc, - yyjson_mut_val *object -) { - errorChain(saveSlotCurrentWriteJSON(slot, doc, object)); - errorOk(); -} - -errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) { - assertNotNull(slot, "Slot cannot be null"); - assertNotNull(object, "Object cannot be null"); - - // saveSlotInit() now takes slot directly, so this can reset through it - // rather than duplicating the memSet/version stub - saveSlotCurrent - // ReadJSON() fully overwrites every field it sets anyway (including - // party.members/order), so seeding a fresh party.jsonc roster here only - // to replace it is wasted work, but keeps this in one reset path rather - // than two subtly different ones. - errorChain(saveSlotInit(slot)); - - errorChain(saveSlotCurrentReadJSON(slot, object)); - errorOk(); -} diff --git a/src/dusk/save/slot/saveslot.h b/src/dusk/save/slot/saveslot.h deleted file mode 100644 index 6a934f08..00000000 --- a/src/dusk/save/slot/saveslot.h +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/savedevice.h" -#include "rpg/overworld/map.h" -#include "rpg/battle/party.h" -#include "rpg/story/story.h" -#include "time/timeepoch.h" -#include "asset/assetfile.h" -#include "yyjson.h" - -#define SAVE_SLOT_NAME_LENGTH 8 -#ifndef SAVE_SLOT_COUNT - #define SAVE_SLOT_COUNT 3 -#endif - -#define SAVE_SLOT_MAP_NAME_DEFAULT "overworld" -#define SAVE_SLOT_CUTSCENE_NAME_MAX ASSET_FILE_NAME_MAX -#define SAVE_SLOT_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc" - -typedef struct { - char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator - dusktimeepoch_t time; - int32_t playerLevel; - char_t mapName[MAP_NAME_MAX]; - bool_t corrupt; -} saveslotcache_t; - -typedef struct saveslot_s { - uint8_t version; - uint8_t dataType; - saveslotcache_t cachedData; - party_t party; - storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX]; - // Asset path of the cutscene currently in progress for this slot (e.g. - // resumed on load) - defaults to SAVE_SLOT_CUTSCENE_DEFAULT on a fresh - // slot (see saveSlotInit). - char_t currentCutscene[SAVE_SLOT_CUTSCENE_NAME_MAX]; -} saveslot_t; - -/** - * Inits slot with the default state, this is functionally "new game" but - * will not set the player name, as that is what we use to determine if a - * save slot is "in use" or not. Seeds the party roster from party.jsonc - * (see partyInit). - * - * @param slot The save slot to initialize. - * @return Error if the starting party roster (party.jsonc) fails to - * load/parse. - */ -errorret_t saveSlotInit(saveslot_t *slot); - -/** - * Sets up the current save slot (SAVE.slot) for a brand new game: calls - * saveSlotInit (which seeds the starting party roster) and sets the - * default starting map. Use this (not saveSlotInit directly) whenever a - * new save is actually being created for a player. Callers creating a - * new save at a specific index should set SAVE.slotCurrent first. - * - * @return Error if saveSlotInit fails. - */ -errorret_t saveSlotNewGame(void); - -/** - * Sets the current save slot's (SAVE.slot) in-progress cutscene and - * dirties the save (SAVE.slotDirty), same as storySetFlagValue - so the - * next save write persists it. Use this (not writing SAVE.slot. - * currentCutscene directly) anywhere gameplay needs to change it. - * - * @param file Asset path of the cutscene now in progress, or "" for none. - */ -void saveSlotSetCurrentCutscene(const char_t *file); - -/** - * Checks if the save slot is in use, this is determined by checking if the - * player name is set or not. - * - * @param slot The save slot to check. - * @return True if the save slot is in use, false otherwise. - */ -bool_t saveSlotInUse(saveslotcache_t *slot); - -/** - * Returns whether or not the given save slot has ever saved (has an epoc non 0) - * - * @param slot The save slot to check. - * @return True if the save slot has ever saved, false otherwise. - */ -bool_t saveSlotHasSaved(saveslotcache_t *slot); - -/** - * Writes the given save slot out to the given JSON object. - * - * @param slot The save slot to write. - * @param doc The mutable JSON document that owns the object. - * @param object The mutable JSON object to write into. - * @return Error state if any. - */ -errorret_t saveSlotWriteJSON( - saveslot_t *slot, - yyjson_mut_doc *doc, - yyjson_mut_val *object -); - -/** - * Reads the given save slot in from the given JSON object. - * - * @param slot The save slot to read into. - * @param object The JSON object to read from. - * @return Error state if any. - */ -errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object); diff --git a/src/dusk/save/slot/saveslotcurrent.c b/src/dusk/save/slot/saveslotcurrent.c deleted file mode 100644 index 76c199e4..00000000 --- a/src/dusk/save/slot/saveslotcurrent.c +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "saveslotcurrent.h" -#include "assert/assert.h" -#include "util/string.h" -#include "util/memory.h" -#include "time/time.h" - -errorret_t saveSlotCurrentWriteJSON( - saveslot_t *slot, - yyjson_mut_doc *doc, - yyjson_mut_val *object -) { - assertNotNull(slot, "Slot cannot be null"); - assertNotNull(doc, "Doc cannot be null"); - assertNotNull(object, "Object cannot be null"); - - // Stamped on the stable struct (not the throwaway ver1 copy below) so the - // fresh timestamp survives the write - saveSlotHasSaved() depends on it. - slot->cachedData.time = timeGetEpoch(); - - saveslotver1_t ver1; - stringCopy(ver1.name, slot->cachedData.name, sizeof(ver1.name)); - ver1.time = slot->cachedData.time; - ver1.playerLevel = slot->cachedData.playerLevel; - stringCopy(ver1.mapName, slot->cachedData.mapName, sizeof(ver1.mapName)); - - memoryCopy( - ver1.partyMembers, slot->party.members, sizeof(ver1.partyMembers) - ); - memoryCopy(ver1.partyOrder, slot->party.order, sizeof(ver1.partyOrder)); - - memoryCopy(ver1.storyFlags, slot->storyFlags, sizeof(ver1.storyFlags)); - - stringCopy( - ver1.currentCutscene, slot->currentCutscene, sizeof(ver1.currentCutscene) - ); - - errorChain(saveSlotVer1WriteJSON(&ver1, doc, object)); - errorOk(); -} - -errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) { - assertNotNull(slot, "Slot cannot be null"); - assertNotNull(object, "Object cannot be null"); - - // Zeroed up front: saveSlotVer1ReadJSON returns early (leaving everything - // past "name" untouched) for a blank/never-used slot, so this must not be - // left as uninitialized stack garbage. - saveslotver1_t ver1; - memoryZero(&ver1, sizeof(ver1)); - errorChain(saveSlotVer1ReadJSON(&ver1, object)); - - stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name)); - slot->cachedData.time = ver1.time; - slot->cachedData.playerLevel = ver1.playerLevel; - stringCopy( - slot->cachedData.mapName, ver1.mapName, sizeof(slot->cachedData.mapName) - ); - - memoryCopy( - slot->party.members, ver1.partyMembers, sizeof(slot->party.members) - ); - memoryCopy(slot->party.order, ver1.partyOrder, sizeof(slot->party.order)); - - memoryCopy(slot->storyFlags, ver1.storyFlags, sizeof(slot->storyFlags)); - - stringCopy( - slot->currentCutscene, ver1.currentCutscene, - sizeof(slot->currentCutscene) - ); - - errorOk(); -} diff --git a/src/dusk/save/slot/saveslotcurrent.h b/src/dusk/save/slot/saveslotcurrent.h deleted file mode 100644 index a4666fb8..00000000 --- a/src/dusk/save/slot/saveslotcurrent.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/slot/saveslot.h" -#include "save/slot/saveslotver1.h" - -/** - * Converts the given save slot to whichever schema SAVE_SLOT_CURRENT - * (saveslotver.h) selects and writes it out to the given JSON object. - * Update this (only) when bumping to a new SAVE_SLOT_VER_N. - * - * @param slot The save slot to write. - * @param doc The mutable JSON document that owns the object. - * @param object The mutable JSON object to write into. - * @return Error state if any. - */ -errorret_t saveSlotCurrentWriteJSON( - saveslot_t *slot, - yyjson_mut_doc *doc, - yyjson_mut_val *object -); - -/** - * Reads whichever schema SAVE_SLOT_CURRENT (saveslotver.h) selects from the - * given JSON object and converts it into the given save slot. Update this - * (only) when bumping to a new SAVE_SLOT_VER_N. - * - * @param slot The save slot to read into. - * @param object The JSON object to read from. - * @return Error state if any. - */ -errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object); diff --git a/src/dusk/save/slot/saveslotver.h b/src/dusk/save/slot/saveslotver.h deleted file mode 100644 index 2774e9ee..00000000 --- a/src/dusk/save/slot/saveslotver.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -// One constant per save slot JSON schema this codebase has ever shipped. -// Never remove or renumber an existing entry - old save files on disk may -// still declare it. -#define SAVE_SLOT_VER_1 1 - -// The schema saveSlotCurrentWriteJSON()/saveSlotCurrentReadJSON() use today. -// Bump this to a new SAVE_SLOT_VER_N when the slot JSON shape changes. -#define SAVE_SLOT_CURRENT SAVE_SLOT_VER_1 diff --git a/src/dusk/save/slot/saveslotver1.c b/src/dusk/save/slot/saveslotver1.c deleted file mode 100644 index 247166b6..00000000 --- a/src/dusk/save/slot/saveslotver1.c +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "saveslotver1.h" -#include "assert/assert.h" -#include "save/savejson.h" - -errorret_t saveSlotVer1WriteJSON( - saveslotver1_t *slot, - yyjson_mut_doc *doc, - yyjson_mut_val *object -) { - assertNotNull(slot, "Slot cannot be null"); - assertNotNull(doc, "Doc cannot be null"); - assertNotNull(object, "Object cannot be null"); - - writeVersion(SAVE_SLOT_CURRENT); - writeString("name", slot->name); - writeTime("time", slot->time); - writeInt32("playerLevel", slot->playerLevel); - writeString("mapName", slot->mapName); - - yyjson_mut_val *partyMembersArr = yyjson_mut_arr(doc); - for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) { - yyjson_mut_val *memberObj = - battleFighterSerialize(&slot->partyMembers[i], doc); - yyjson_mut_arr_add_val(partyMembersArr, memberObj); - } - writeValue("partyMembers", partyMembersArr); - - writeUInt8Array("partyOrder", slot->partyOrder, PARTY_ACTIVE_SIZE_MAX); - - yyjson_mut_val *storyFlagsObj = yyjson_mut_obj_add_obj(doc, object, "storyFlags"); - for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) { - yyjson_mut_obj_add_uint( - doc, storyFlagsObj, storyGetNameByFlag(flag), slot->storyFlags[flag] - ); - } - - writeString("currentCutscene", slot->currentCutscene); - - errorOk(); -} - -errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) { - assertNotNull(slot, "Slot cannot be null"); - assertNotNull(object, "Object cannot be null"); - - char_t saveJsonStringBuffer[SAVE_JSON_STRING_BUFFER_SIZE]; - - requireVersion(SAVE_SLOT_CURRENT); - - readString("name", slot->name, "", SAVE_SLOT_VER1_NAME_LENGTH); - if(slot->name[0] == '\0') errorOk(); - - readTime("time", slot->time); - readInt32("playerLevel", slot->playerLevel, 1); - requireStrMin("mapName", 1); - readString( - "mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1 - ); - - requireArrayOfSize( - "partyMembers", partyMembersArr, PARTY_MEMBER_COUNT_MAX - ); - - size_t memberIdx, memberLen; - yyjson_val *memberObj; - yyjson_arr_foreach( - partyMembersArr, memberIdx, memberLen, memberObj - ) { - battlefighter_t *member = &slot->partyMembers[memberIdx]; - member->id = (uint8_t)memberIdx; - errorChain(battleFighterParse(member, memberObj)); - } - - readUInt8Array( - "partyOrder", slot->partyOrder, PARTY_ACTIVE_SIZE_MAX, - PARTY_ORDER_EMPTY - ); - - yyjson_val *storyFlagsObj = yyjson_obj_get(object, "storyFlags"); - if(storyFlagsObj != NULL) { - yyjson_obj_iter iter = yyjson_obj_iter_with(storyFlagsObj); - yyjson_val *flagKey; - while((flagKey = yyjson_obj_iter_next(&iter))) { - storyflag_t flag = storyGetFlagByName(yyjson_get_str(flagKey)); - if(flag >= STORY_FLAG_COUNT) continue;// Flag no longer exists, ignore - - slot->storyFlags[flag] = - (storyflagval_t)yyjson_get_uint(yyjson_obj_iter_get_val(flagKey)); - } - } - - readString( - "currentCutscene", slot->currentCutscene, SAVE_SLOT_VER1_CUTSCENE_DEFAULT, - SAVE_SLOT_VER1_CUTSCENE_NAME_MAX - 1 - ); - - errorOk(); -} diff --git a/src/dusk/save/slot/saveslotver1.h b/src/dusk/save/slot/saveslotver1.h deleted file mode 100644 index 77040c74..00000000 --- a/src/dusk/save/slot/saveslotver1.h +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "time/timeepoch.h" -#include "save/slot/saveslotver.h" -#include "rpg/battle/party.h" -#include "rpg/story/story.h" -#include "yyjson.h" - -// This schema's own frozen copies of save/slot/saveslot.h's field sizes - -// never reference SAVE_SLOT_NAME_LENGTH/MAP_NAME_MAX (or any other live -// constant) here. Once shipped, this file's on-disk shape must never change -// underneath old save files, even if the live engine constants do. The -// party arrays below are the exception - saveSlotVer1ReadJSON() checks their -// JSON array lengths explicitly, so PARTY_MEMBER_COUNT_MAX/ -// PARTY_ACTIVE_SIZE_MAX (rpg/battle/party.h) can be referenced directly. -// storyFlags is a further exception, for a different reason - it's written -// keyed by flag name (not position), so its on-disk shape is decoupled from -// STORY_FLAG_COUNT_MAX entirely; a save simply loses whatever flags no -// longer resolve by name (see storyGetFlagByName) if that cap ever shrinks. -// -// partyMembers is a THIRD exception, deliberately broken pre-release: its -// per-member shape now comes from battleFighterSerialize/battleFighterParse -// (rpg/battle/fighter/battlefighter.h) - the shared fighter JSON format -// used everywhere else (party.jsonc, BATTLE_START enemies) - instead of its -// own hand-rolled field set. This is a real on-disk break (different key -// names, no more "statAttack" etc.) with no migration - an old v1 save -// simply reads back as corrupt. Acceptable only because nothing has -// shipped yet; don't repeat this once real saves exist without a version -// bump instead. -#define SAVE_SLOT_VER1_NAME_LENGTH 8 -#define SAVE_SLOT_VER1_MAP_NAME_MAX 10 -#define SAVE_SLOT_VER1_CUTSCENE_NAME_MAX 128 -// Own frozen copy of save/slot/saveslot.h's SAVE_SLOT_CUTSCENE_DEFAULT, for -// the same reason as the sizes above - never reference the live constant. -#define SAVE_SLOT_VER1_CUTSCENE_DEFAULT "cutscenes/new_game.jsonc" - -// Independent from saveslotcache_t (save/slot/saveslot.h) by design - this -// is what version 1 actually put on disk, frozen forever. saveSlotCurrent -// WriteJSON()/ReadJSON() (saveslotcurrent.c) manually copy field-by-field -// between the two, since they're separate types that merely happen to -// currently look alike. -typedef struct saveslotver1_s { - char_t name[SAVE_SLOT_VER1_NAME_LENGTH + 1]; - dusktimeepoch_t time; - int32_t playerLevel; - char_t mapName[SAVE_SLOT_VER1_MAP_NAME_MAX]; - - battlefighter_t partyMembers[PARTY_MEMBER_COUNT_MAX]; - uint8_t partyOrder[PARTY_ACTIVE_SIZE_MAX]; - - storyflagval_t storyFlags[STORY_FLAG_COUNT_MAX]; - - char_t currentCutscene[SAVE_SLOT_VER1_CUTSCENE_NAME_MAX]; -} saveslotver1_t; - -/** - * Writes the given v1 save slot data out to the given JSON object, stamping - * SAVE_SLOT_CURRENT as the "version" field. - * - * @param slot The v1 save slot data to write. - * @param doc The mutable JSON document that owns the object. - * @param object The mutable JSON object to write into. - * @return Error state if any. - */ -errorret_t saveSlotVer1WriteJSON( - saveslotver1_t *slot, - yyjson_mut_doc *doc, - yyjson_mut_val *object -); - -/** - * Reads v1 save slot data in from the given JSON object. Throws if the - * JSON's "version" is missing or doesn't match SAVE_SLOT_CURRENT. - * - * @param slot The v1 save slot data to read into. - * @param object The JSON object to read from. - * @return Error state if any. - */ -errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object); diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt deleted file mode 100644 index 8273e118..00000000 --- a/src/dusk/scene/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - scene.c - scenetype.c -) - -# Subdirs -add_subdirectory(initial) -add_subdirectory(overworld) -add_subdirectory(battle) \ No newline at end of file diff --git a/src/dusk/scene/battle/CMakeLists.txt b/src/dusk/scene/battle/CMakeLists.txt deleted file mode 100644 index a743129d..00000000 --- a/src/dusk/scene/battle/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - scenebattle.c -) diff --git a/src/dusk/scene/battle/scenebattle.c b/src/dusk/scene/battle/scenebattle.c deleted file mode 100644 index 190db10a..00000000 --- a/src/dusk/scene/battle/scenebattle.c +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "scenebattle.h" -#include "assert/assert.h" -#include "display/display.h" -#include "display/displaystate.h" -#include "display/shader/shader.h" -#include "display/shader/shaderunlit.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" -#include "display/color.h" -#include "scene/scene.h" - -errorret_t sceneBattleInit(scenedata_t *sceneData) { - errorOk(); -} - -errorret_t sceneBattleUpdate(scenedata_t *sceneData) { - battleUpdate(); - errorOk(); -} - -errorret_t sceneBattleRender(scenedata_t *sceneData) { - scenebattle_t *battle = &sceneData->battle; - - sceneBattleCameraUpdateProjection(battle); - sceneBattleCameraUpdateEye(battle); - - errorChain(shaderBind(&SHADER_UNLIT)); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity - )); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, battle->projection - )); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_VIEW, battle->eye - )); - - errorChain(displaySetState((displaystate_t){ - .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL - })); - - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - errorChain(sceneBattleDrawFighter(i)); - } - - errorOk(); -} - -errorret_t sceneBattleDispose(scenedata_t *sceneData) { - errorOk(); -} - -void sceneBattleCameraUpdateProjection(scenebattle_t *battle) { - glm_perspective( - glm_rad(SCENE_BATTLE_CAMERA_FOV), - SCREEN.aspect, - SCENE_BATTLE_CAMERA_NEAR, - SCENE_BATTLE_CAMERA_FAR, - battle->projection - ); -} - -void sceneBattleCameraUpdateEye(scenebattle_t *battle) { - glm_lookat( - (vec3){ 0.0f, 4.0f, 7.0f }, - (vec3){ 0.0f, 0.5f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f }, - battle->eye - ); -} - -void sceneBattleWorldToScreen( - scenebattle_t *battle, - vec3 renderPos, - vec2 out -) { - assertNotNull(battle, "battle cannot be NULL"); - assertNotNull(renderPos, "renderPos cannot be NULL"); - assertNotNull(out, "out cannot be NULL"); - - mat4 viewProj; - glm_mat4_mul(battle->projection, battle->eye, viewProj); - - vec4 viewport = { - 0.0f, 0.0f, (float_t)SCREEN.width, (float_t)SCREEN.height - }; - vec3 window; - glm_project(renderPos, viewProj, viewport, window); - - out[0] = window[0]; - out[1] = (float_t)SCREEN.height - window[1]; -} - -void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); - const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - assertNotNull(fighter, "Fighter cannot be NULL"); - assertNotNull(out, "out cannot be NULL"); - - // Ordinal (and count) of this fighter among its living-slot teammates, - // in slot order, so each team lays out as an evenly spaced row. - uint8_t ordinal = 0; - uint8_t teamCount = 0; - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - const battlefighter_t *candidate = BATTLE.fighters[i]; - if(candidate == NULL) continue; - if(candidate->status == BATTLE_FIGHTER_STATUS_NULL) continue; - if(candidate->team != fighter->team) continue; - if(i < fighterIndex) ordinal++; - teamCount++; - } - - const float_t x = - ((float_t)ordinal - ((float_t)(teamCount - 1) * 0.5f)) * - SCENE_BATTLE_FIGHTER_SPACING; - const float_t z = fighter->team == BATTLE_FIGHTER_TEAM_ALLY - ? SCENE_BATTLE_ALLY_Z : SCENE_BATTLE_ENEMY_Z; - - out[0] = x; - out[1] = 0.0f; - out[2] = z; -} - -errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex) { - assertTrue(fighterIndex < BATTLE_FIGHTER_POS_COUNT, "fighterIndex out of bounds"); - const battlefighter_t *fighter = BATTLE.fighters[fighterIndex]; - if(fighter == NULL) errorOk(); - if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) errorOk(); - - vec3 position; - sceneBattleGetFighterPosition(fighterIndex, position); - - spritebatchsprite_t sprite; - glm_vec3_copy(position, sprite.min); - glm_vec3_copy(position, sprite.max); - glm_vec3_add(sprite.max, (vec3){ 1.0f, 1.0f, 0.0f }, sprite.max); - glm_vec2_copy((vec2){ 0.0f, 0.0f }, sprite.uvMin); - glm_vec2_copy((vec2){ 1.0f, 1.0f }, sprite.uvMax); - - color_t color; - if(fighter->status == BATTLE_FIGHTER_STATUS_DEAD) { - color = color4b(96, 96, 96, 255); - } else if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) { - color = COLOR_BLUE; - } else { - color = COLOR_RED; - } - - shadermaterial_t material = { - .unlit = { .color = color, .texture = NULL } - }; - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - errorChain(spriteBatchFlush()); - - errorOk(); -} diff --git a/src/dusk/scene/battle/scenebattle.h b/src/dusk/scene/battle/scenebattle.h deleted file mode 100644 index bfe0e98e..00000000 --- a/src/dusk/scene/battle/scenebattle.h +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "scene/scenebase.h" -#include "rpg/battle/battle.h" - -// Fixed mock camera, "to begin": looking down at the arena from above and -// in front. Not yet driven by anything (no shake, no per-fighter framing). -#define SCENE_BATTLE_CAMERA_FOV 45.0f -#define SCENE_BATTLE_CAMERA_NEAR 0.1f -#define SCENE_BATTLE_CAMERA_FAR 100.0f - -// Fighter row layout, in render-space units. Allies stand closer to the -// camera (+Z), enemies further away (-Z); each team is spread evenly -// along X, centered on the origin. -#define SCENE_BATTLE_FIGHTER_SPACING 2.0f -#define SCENE_BATTLE_ALLY_Z 2.5f -#define SCENE_BATTLE_ENEMY_Z (-2.5f) - -typedef struct { - mat4 eye; - mat4 projection; -} scenebattle_t; - -/** - * Initializes the battle scene. The battle itself (BATTLE global) is - * expected to already be started, e.g. by a StartBattle cutscene item. - * - * @param sceneData The scene data used for this scene. - * @return An error if the init failed, or errorOk() if it succeeded. - */ -errorret_t sceneBattleInit(scenedata_t *sceneData); - -/** - * Updates the battle scene, ticking the battle simulation. - * - * @param sceneData The scene data used for this scene. - * @return An error if the update failed, or errorOk() if it succeeded. - */ -errorret_t sceneBattleUpdate(scenedata_t *sceneData); - -/** - * Renders the battle scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the render failed, or errorOk() if it succeeded. - */ -errorret_t sceneBattleRender(scenedata_t *sceneData); - -/** - * Disposes the battle scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the dispose failed, or errorOk() if it succeeded. - */ -errorret_t sceneBattleDispose(scenedata_t *sceneData); - -/** - * Recomputes the battle camera's projection matrix from the current - * screen aspect ratio, storing it in battle->projection. - * - * @param battle The battle scene data to update. - */ -void sceneBattleCameraUpdateProjection(scenebattle_t *battle); - -/** - * Recomputes the battle camera's eye/view matrix, storing it in - * battle->eye. Fixed for now -- doesn't yet track anything. - * - * @param battle The battle scene data to update. - */ -void sceneBattleCameraUpdateEye(scenebattle_t *battle); - -/** - * Converts a render-space position to screen-space pixel coordinates, - * using the battle camera's current eye and projection matrices. - * - * @param battle The battle scene data holding the camera matrices. - * @param renderPos The render-space position to convert. - * @param out Output vec2 filled with the screen-space pixel position. - */ -void sceneBattleWorldToScreen( - scenebattle_t *battle, - vec3 renderPos, - vec2 out -); - -/** - * Computes the render-space position of a fighter's feet, laying out - * each team as an evenly spaced row facing the camera. - * - * @param fighterIndex Index into BATTLE.fighters. - * @param out Output vec3 filled with the fighter's render-space position. - */ -void sceneBattleGetFighterPosition(const uint8_t fighterIndex, vec3 out); - -/** - * Draws a single fighter as a colored 1x1 quad, skipping empty slots. - * Color differentiates team (ally/enemy) and status (dead fighters are - * drawn dimmed). - * - * @param fighterIndex Index into BATTLE.fighters. - * @return An error if drawing failed, or errorOk() if it succeeded. - */ -errorret_t sceneBattleDrawFighter(const uint8_t fighterIndex); diff --git a/src/dusk/scene/initial/CMakeLists.txt b/src/dusk/scene/initial/CMakeLists.txt deleted file mode 100644 index 97ded407..00000000 --- a/src/dusk/scene/initial/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - sceneinitial.c -) diff --git a/src/dusk/scene/initial/sceneinitial.c b/src/dusk/scene/initial/sceneinitial.c deleted file mode 100644 index 29fada88..00000000 --- a/src/dusk/scene/initial/sceneinitial.c +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "scene/scene.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "error/error.h" -#include "display/screen/screen.h" -#include "console/console.h" -#include "rpg/cutscene/cutscenesystem.h" - -errorret_t sceneInitialInit(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - memoryZero(&sceneData->initial, sizeof(sceneinitial_t)); - - SCREEN.background = COLOR_BLACK; - cutsceneSystemLoad("cutscenes/initial.jsonc"); - - errorOk(); -} - -errorret_t sceneInitialUpdate(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - errorOk(); -} - -errorret_t sceneInitialRender(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - errorOk(); -} - -errorret_t sceneInitialDispose(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - errorOk(); -} diff --git a/src/dusk/scene/initial/sceneinitial.h b/src/dusk/scene/initial/sceneinitial.h deleted file mode 100644 index 321f2da2..00000000 --- a/src/dusk/scene/initial/sceneinitial.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "scene/scenebase.h" - -typedef struct { - // uint32_t callbackState; - void *nothing; -} sceneinitial_t; - -/** - * Initialises the initial (boot) scene - kicks off the save - * availability/existence check that decides which prompt, if any, to - * show before proceeding to the main menu. - * - * @param sceneData The scene data used for this scene. - * @return An error if the init failed, or errorOk() if it succeeded. - */ -errorret_t sceneInitialInit(scenedata_t *sceneData); - -/** - * Updates the initial scene. Currently a no-op - all the work happens in - * save callbacks and the two modal UI elements' own button handling. - * - * @param sceneData The scene data used for this scene. - * @return An error if the update failed, or errorOk() if it succeeded. - */ -errorret_t sceneInitialUpdate(scenedata_t *sceneData); - -/** - * Renders the initial scene. Currently a no-op - the modals draw - * themselves via the global UI element pipeline. - * - * @param sceneData The scene data used for this scene. - * @return An error if the render failed, or errorOk() if it succeeded. - */ -errorret_t sceneInitialRender(scenedata_t *sceneData); - -/** - * Disposes the initial scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the dispose failed, or errorOk() if it succeeded. - */ -errorret_t sceneInitialDispose(scenedata_t *sceneData); diff --git a/src/dusk/scene/overworld/CMakeLists.txt b/src/dusk/scene/overworld/CMakeLists.txt deleted file mode 100644 index 6038dc07..00000000 --- a/src/dusk/scene/overworld/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - sceneoverworld.c -) \ No newline at end of file diff --git a/src/dusk/scene/overworld/sceneoverworld.c b/src/dusk/scene/overworld/sceneoverworld.c deleted file mode 100644 index 6a7214b5..00000000 --- a/src/dusk/scene/overworld/sceneoverworld.c +++ /dev/null @@ -1,264 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "scene/scene.h" -#include "console/console.h" -#include "assert/assert.h" - -#include "display/display.h" -#include "display/displaystate.h" -#include "display/shader/shader.h" -#include "display/screen/screen.h" -#include "display/shader/shaderunlit.h" -#include "display/spritebatch/spritebatch.h" -#include "display/texture/texture.h" - -#include "rpg/overworld/map.h" -#include "rpg/entity/entity.h" -#include "rpg/rpgcamera.h" - -#include "asset/loader/assetloader.h" -#include "asset/loader/assetentry.h" - -#include "util/memory.h" - -errorret_t sceneOverworldInit(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - errorOk(); -} - -errorret_t sceneOverworldUpdate(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - errorOk(); -} - -errorret_t sceneOverworldRender(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - - sceneoverworld_t *overworld = &sceneData->overworld; - sceneOverworldCullUpdate(overworld); - - - // Overworld camera - errorChain(shaderBind(&SHADER_UNLIT)); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity - )); - - rpgCameraUpdateProjection(); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, RPG_CAMERA.projection - )); - - rpgCameraUpdateEye(); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_VIEW, RPG_CAMERA.eye - )); - - // Chunk tiles - errorChain(displaySetState((displaystate_t){ - .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL - })); - errorChain(sceneOverworldDrawChunksBase(overworld)); - - // Entities - errorChain(displaySetState((displaystate_t){ - .flags = DISPLAY_STATE_FLAG_CULL - })); - - for(uint8_t i = 0; i < ENTITY_COUNT; i++) { - entity_t *ent = &ENTITIES[i]; - if(ent->type == ENTITY_TYPE_NULL) continue; - errorChain(sceneOverworldDrawEntity(overworld, ent)); - } - - // Chunk props - errorChain(displaySetState((displaystate_t){ - .flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL - })); - errorChain(sceneOverworldDrawChunksProps(overworld)); - - // Weather effects - // errorChain(shaderSetMatrix( - // &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj - // )); - // errorChain(shaderSetMatrix( - // &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView - // )); - // errorChain(displaySetState((displaystate_t){ - // .flags = DISPLAY_STATE_FLAG_BLEND - // })); - - // spritebatchsprite_t skyboxSprite; - // memoryZero(&skyboxSprite, sizeof(spritebatchsprite_t)); - // skyboxSprite.max[0] = (float_t)(SCREEN.width / SCREEN.scaleUi); - // skyboxSprite.max[1] = (float_t)(SCREEN.height / SCREEN.scaleUi); - // shadermaterial_t skyboxMaterial = { - // .unlit = { .color = color(0xFF, 0x00, 0x00, 0xFF/2), .texture = NULL } - // }; - // errorChain( - // spriteBatchBuffer(&skyboxSprite, 1, &SHADER_UNLIT, skyboxMaterial) - // ); - // errorChain(spriteBatchFlush()); - - errorOk(); -} - -void sceneOverworldCullUpdate(sceneoverworld_t *overworld) { - rpgCameraGetPosition(overworld->cullTarget); - - const float_t worldH = - (float_t)(SCREEN.height / SCREEN.scale3d) / TILE_SIZE_PIXELS; - const float_t worldW = worldH * SCREEN.aspect; - - overworld->cullHalfRangeX = - (worldW * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_X; - overworld->cullHalfRangeY = - (worldH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y; -} - -bool_t sceneOverworldChunkShouldRender( - const sceneoverworld_t *overworld, - const chunk_t *chunk -) { - worldpos_t worldPos; - chunkPosToWorldPos(&chunk->position, &worldPos); - - const float_t chunkCenterX = (float_t)worldPos.x + (CHUNK_WIDTH * 0.5f); - const float_t chunkCenterY = (float_t)worldPos.y + (CHUNK_HEIGHT * 0.5f); - - if(fabsf(overworld->cullTarget[0] - chunkCenterX) > - overworld->cullHalfRangeX) return false; - if(fabsf(overworld->cullTarget[1] - chunkCenterY) > - overworld->cullHalfRangeY) return false; - - return true; -} - -bool_t sceneOverworldEntityShouldRender( - const sceneoverworld_t *overworld, - const entity_t *ent -) { - if(fabsf(overworld->cullTarget[0] - ent->renderPosition[0]) > - overworld->cullHalfRangeX) return false; - if(fabsf(overworld->cullTarget[1] - ent->renderPosition[1]) > - overworld->cullHalfRangeY) return false; - - return true; -} - -errorret_t sceneOverworldDrawEntity( - const sceneoverworld_t *overworld, - entity_t *ent -) { - if(!sceneOverworldEntityShouldRender(overworld, ent)) errorOk(); - - spritebatchsprite_t sprite; - glm_vec3_copy(ent->renderPosition, sprite.min); - glm_vec3_add(sprite.min, (vec3){ 0, -0.05f, 0.05f }, sprite.min);// Stop Fight - glm_vec3_copy(sprite.min, sprite.max); - glm_vec3_add(sprite.max, (vec3){ 1, 1, 0 }, sprite.max); - glm_vec2_copy((vec2){ 0, 0 }, sprite.uvMin); - glm_vec2_copy((vec2){ 1, 1 }, sprite.uvMax); - - color_t color; - switch(ent->direction) { - case ENTITY_DIR_NORTH: color = COLOR_YELLOW; break; - case ENTITY_DIR_EAST: color = COLOR_RED; break; - case ENTITY_DIR_SOUTH: color = COLOR_GREEN; break; - case ENTITY_DIR_WEST: color = COLOR_BLUE; break; - default: color = COLOR_CYAN; break; - } - - shadermaterial_t material = { - .unlit = { .color = color, .texture = NULL } - }; - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - errorChain(spriteBatchFlush()); - - errorOk(); -} - -errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) { - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = MAP.chunkOrder[i]; - if(chunk == NULL) continue; - if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue; - if(chunk->meshCount == 0) continue; - if(chunk->modelEntries[0] == NULL) continue; - if(chunk->modelEntries[0]->state != ASSET_ENTRY_STATE_LOADED) continue; - - assetmodeloutput_t *model = &chunk->modelEntries[0]->data.model; - if(model->meshEntry == NULL) continue; - - texture_t *tex = model->texEntry != NULL - ? &model->texEntry->data.texture - : NULL; - - shadermaterial_t mat = { - .unlit = { .color = model->color, .texture = tex } - }; - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[0] - )); - errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat)); - errorChain(meshDraw( - &model->meshEntry->data.mesh.mesh, 0, -1 - )); - } - - // Restore identity model so subsequent renders (e.g. entities) are - // not affected by the last chunk transform. - mat4 identity; - glm_mat4_identity(identity); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, identity)); - - errorOk(); -} - -errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) { - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - chunk_t *chunk = MAP.chunkOrder[i]; - if(chunk == NULL) continue; - if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue; - - for(uint8_t m = 1; m < chunk->meshCount; m++) { - if(chunk->modelEntries[m] == NULL) continue; - if(chunk->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) continue; - - assetmodeloutput_t *model = &chunk->modelEntries[m]->data.model; - if(model->meshEntry == NULL) continue; - - texture_t *tex = model->texEntry != NULL - ? &model->texEntry->data.texture - : NULL; - - shadermaterial_t mat = { - .unlit = { .color = model->color, .texture = tex } - }; - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_MODEL, chunk->meshModels[m] - )); - errorChain(shaderSetMaterial(&SHADER_UNLIT, &mat)); - errorChain(meshDraw( - &model->meshEntry->data.mesh.mesh, 0, -1 - )); - } - } - - // Restore identity model in case anything renders after props. - mat4 identity; - glm_mat4_identity(identity); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, identity)); - - errorOk(); -} - -errorret_t sceneOverworldDispose(scenedata_t *sceneData) { - assertNotNull(sceneData, "Scene data cannot be null"); - errorOk(); -} diff --git a/src/dusk/scene/overworld/sceneoverworld.h b/src/dusk/scene/overworld/sceneoverworld.h deleted file mode 100644 index 048e20d0..00000000 --- a/src/dusk/scene/overworld/sceneoverworld.h +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "scene/scenebase.h" -#include "rpg/overworld/chunk.h" -#include "rpg/entity/entity.h" - -typedef struct { - // Cached per-frame chunk culling bounds, refreshed by - // sceneOverworldCullUpdate() so sceneOverworldChunkShouldRender() - // does not need to recompute them for every chunk. - vec3 cullTarget; - float_t cullHalfRangeX; - float_t cullHalfRangeY; -} sceneoverworld_t; - -// Extra world-space margin, in tiles, added on top of the rough -// screen-size skin below - cheap insurance against the approximation -// culling a chunk that is actually still just in view. Y gets its own -// value since the camera's tilt makes the vertical check less exact. -#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_X 8.0f -#define SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y 4.0f - -// The camera is tilted between the ground-depth (Y) and height (Z) -// axes, so a chunk's on-screen vertical extent also depends on its Z -// layers. Rather than projecting every corner, fold that into a -// generous skin margin on the depth check. -#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_X \ - ((CHUNK_WIDTH * 0.5f) + SCENE_OVERWORLD_CHUNK_CULL_PADDING_X) -#define SCENE_OVERWORLD_CHUNK_CULL_SKIN_Y \ - ((CHUNK_HEIGHT * 0.5f) + (CHUNK_DEPTH * WORLD_LAYER_HEIGHT) + \ - SCENE_OVERWORLD_CHUNK_CULL_PADDING_Y) - -/** - * Initialises the overworld scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the init failed, or errorOk() if it succeeded. - */ -errorret_t sceneOverworldInit(scenedata_t *sceneData); - -/** - * Updates the overworld scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the update failed, or errorOk() if it succeeded. - */ -errorret_t sceneOverworldUpdate(scenedata_t *sceneData); - -/** - * Refreshes the chunk culling bounds cached on the overworld scene data, - * from the current camera position and screen dimensions. Must be - * called once per frame before sceneOverworldChunkShouldRender(). - * - * @param overworld The overworld scene data to update. - */ -void sceneOverworldCullUpdate(sceneoverworld_t *overworld); - -/** - * Roughly checks whether a chunk falls within the visible screen area, - * based on chunk size and screen dimensions. Cheap approximation, not - * an exact frustum check - includes a generous skin margin to account - * for the camera's angle tilting height/depth onto the screen's - * vertical axis, so it may pass some chunks that are actually just out - * of view but will never wrongly cull one that is visible. - * - * @param overworld The overworld scene data holding the cached culling - * bounds, as refreshed by sceneOverworldCullUpdate(). - * @param chunk The chunk to check. - * @return true if the chunk should be rendered, false otherwise. - */ -bool_t sceneOverworldChunkShouldRender( - const sceneoverworld_t *overworld, - const chunk_t *chunk -); - -/** - * Draws the base (tile) mesh of every loaded chunk, with normal depth - * testing. Must be called before entities are rendered. - * - * @param overworld The overworld scene data holding the cached culling - * bounds, as refreshed by sceneOverworldCullUpdate(). - * @return An error if drawing failed, or errorOk() on success. - */ -errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld); - -/** - * Draws every loaded chunk's additional meshes (trees, buildings, etc), - * with normal depth testing so they correctly occlude entities standing - * beneath them. Must be called after entities are rendered. - * - * @param overworld The overworld scene data holding the cached culling - * bounds, as refreshed by sceneOverworldCullUpdate(). - * @return An error if drawing failed, or errorOk() on success. - */ -errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld); - -/** - * Roughly checks whether an entity falls within the visible screen - * area. Reuses the same cached chunk culling bounds, since an entity - * is just a point within that same space - deliberately cheap, a - * couple of subtractions and comparisons, since it runs per entity - * every frame and it is not worth spending more math than that to - * avoid drawing one that is slightly off-screen. - * - * @param overworld The overworld scene data holding the cached culling - * bounds, as refreshed by sceneOverworldCullUpdate(). - * @param ent The entity to check. - * @return true if the entity should be rendered, false otherwise. - */ -bool_t sceneOverworldEntityShouldRender( - const sceneoverworld_t *overworld, - const entity_t *ent -); - -/** - * Draws a single entity as a sprite, colored by its facing direction. - * Skips drawing (without error) if the entity should not render. - * - * @param overworld The overworld scene data holding the cached culling - * bounds, as refreshed by sceneOverworldCullUpdate(). - * @param ent The entity to draw. - * @return An error if drawing failed, or errorOk() on success. - */ -errorret_t sceneOverworldDrawEntity( - const sceneoverworld_t *overworld, - entity_t *ent -); - -/** - * Renders the overworld scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the render failed, or errorOk() if it succeeded. - */ -errorret_t sceneOverworldRender(scenedata_t *sceneData); - -/** - * Disposes the overworld scene. - * - * @param sceneData The scene data used for this scene. - * @return An error if the dispose failed, or errorOk() if it succeeded. - */ -errorret_t sceneOverworldDispose(scenedata_t *sceneData); \ No newline at end of file diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c deleted file mode 100644 index 72c4e9d9..00000000 --- a/src/dusk/scene/scene.c +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2026 Dominic Masters -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -#include "scene.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "time/time.h" -#include "display/screen/screen.h" -#include "display/shader/shaderunlit.h" -#include "display/display.h" -#include "ui/ui.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "console/console.h" - -scene_t SCENE; - -errorret_t sceneInit(void) { - memoryZero(&SCENE, sizeof(scene_t)); - errorOk(); -} - -errorret_t sceneUpdate(void) { - // Handle scene change. - if(SCENE.next != SCENE_TYPE_NULL) { - if( - SCENE.current != SCENE_TYPE_NULL && - SCENE_TYPES[SCENE.current].dispose != NULL - ) { - errorChain(SCENE_TYPES[SCENE.current].dispose(&SCENE.data)); - } - - SCENE.current = SCENE.next; - SCENE.next = SCENE_TYPE_NULL; - - if( - SCENE.current != SCENE_TYPE_NULL && - SCENE_TYPES[SCENE.current].init != NULL - ) { - errorChain(SCENE_TYPES[SCENE.current].init(&SCENE.data)); - } - } - - #if DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) errorOk(); - #endif - - if( - SCENE.current != SCENE_TYPE_NULL && - SCENE_TYPES[SCENE.current].update != NULL - ) { - errorChain(SCENE_TYPES[SCENE.current].update(&SCENE.data)); - } - - errorOk(); -} - -errorret_t sceneRender(void) { - // Setup screen matrices for 3D rendering. - glm_mat4_identity(SCENE.screenIdentity); - - glm_ortho( - 0.0f, (float_t)(SCREEN.width / SCREEN.scaleUi), - (float_t)(SCREEN.height / SCREEN.scaleUi), 0.0f, - 0.1f, 100.0f, - SCENE.screenProj - ); - - glm_lookat( - (vec3){ 0.0f, 0.0f, 1.0f }, - (vec3){ 0.0f, 0.0f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f }, - SCENE.screenView - ); - - // Scene rendering - if( - SCENE.current != SCENE_TYPE_NULL && - SCENE_TYPES[SCENE.current].render != NULL - ) { - errorChain(SCENE_TYPES[SCENE.current].render(&SCENE.data)); - } - - // UI Rendering - errorChain(shaderBind(&SHADER_UNLIT)); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_MODEL, SCENE.screenIdentity - )); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_PROJECTION, SCENE.screenProj - )); - errorChain(shaderSetMatrix( - &SHADER_UNLIT, SHADER_UNLIT_VIEW, SCENE.screenView - )); - - errorChain(displaySetState((displaystate_t){ - .flags = DISPLAY_STATE_FLAG_BLEND - })); - errorChain(uiRender()); - - errorOk(); -} - -void sceneSet(const scenetype_t type) { - assertTrue(type < SCENE_TYPE_COUNT, "Invalid scene type."); - SCENE.next = type; -} - -errorret_t sceneDispose(void) { - if( - SCENE.current != SCENE_TYPE_NULL && - SCENE_TYPES[SCENE.current].dispose != NULL - ) { - errorChain(SCENE_TYPES[SCENE.current].dispose(&SCENE.data)); - } - - errorOk(); -} diff --git a/src/dusk/scene/scene.h b/src/dusk/scene/scene.h deleted file mode 100644 index db138699..00000000 --- a/src/dusk/scene/scene.h +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "scenetype.h" - -typedef struct { - scenetype_t current; - scenetype_t next; - scenedata_t data; - mat4 screenProj; - mat4 screenView; - mat4 screenIdentity; -} scene_t; - -extern scene_t SCENE; - -/** - * Initialises the scene manager. - * - * @return An error if the init failed, or errorOk() if it succeeded. - */ -errorret_t sceneInit(void); - -/** - * Ticks the scene manager. Processes any pending scene transition, then - * calls scriptSceneUpdate on the active scene. - * - * @return An error if the update failed, or errorOk() if it succeeded. - */ -errorret_t sceneUpdate(void); - -/** - * Renders the current scene (entities, render pipeline, UI). - * - * @return An error if the render failed, or errorOk() if it succeeded. - */ -errorret_t sceneRender(void); - -/** - * Requests the next frame to change to this scene. - * - * @param type The scene type to change to. - */ -void sceneSet(const scenetype_t type); - -/** - * Disposes the active scene immediately. - * - * @return An error if the dispose failed, or errorOk() if it succeeded. - */ -errorret_t sceneDispose(void); diff --git a/src/dusk/scene/scenebase.h b/src/dusk/scene/scenebase.h deleted file mode 100644 index 7846a9cc..00000000 --- a/src/dusk/scene/scenebase.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef union scenedata_u scenedata_t; \ No newline at end of file diff --git a/src/dusk/scene/scenetype.c b/src/dusk/scene/scenetype.c deleted file mode 100644 index 3dad6627..00000000 --- a/src/dusk/scene/scenetype.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "scenetype.h" - -scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = { - [SCENE_TYPE_NULL] = { 0 }, - - [SCENE_TYPE_INITIAL] = { - .init = sceneInitialInit, - .update = sceneInitialUpdate, - .render = sceneInitialRender, - .dispose = sceneInitialDispose - }, - - [SCENE_TYPE_OVERWORLD] = { - .init = sceneOverworldInit, - .update = sceneOverworldUpdate, - .render = sceneOverworldRender, - .dispose = sceneOverworldDispose - }, - - [SCENE_TYPE_BATTLE] = { - .init = sceneBattleInit, - .update = sceneBattleUpdate, - .render = sceneBattleRender, - .dispose = sceneBattleDispose - }, -}; - diff --git a/src/dusk/scene/scenetype.h b/src/dusk/scene/scenetype.h deleted file mode 100644 index 677959ee..00000000 --- a/src/dusk/scene/scenetype.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "scene/scenebase.h" -#include "scene/initial/sceneinitial.h" -#include "scene/overworld/sceneoverworld.h" -#include "scene/battle/scenebattle.h" - -typedef union scenedata_u { - sceneinitial_t initial; - sceneoverworld_t overworld; - scenebattle_t battle; -} scenedata_t; - -typedef errorret_t (*scenecallback_t)(scenedata_t *); - -typedef struct { - scenecallback_t init; - scenecallback_t update; - scenecallback_t render; - scenecallback_t dispose; -} scenecallbacks_t; - -typedef enum { - SCENE_TYPE_NULL, - - SCENE_TYPE_INITIAL, - SCENE_TYPE_OVERWORLD, - SCENE_TYPE_BATTLE, - - SCENE_TYPE_COUNT -} scenetype_t; - -extern scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT]; \ No newline at end of file diff --git a/src/dusk/system/CMakeLists.txt b/src/dusk/system/CMakeLists.txt deleted file mode 100644 index 4dbc46f6..00000000 --- a/src/dusk/system/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - system.c -) diff --git a/src/dusk/system/system.c b/src/dusk/system/system.c deleted file mode 100644 index fe9c6584..00000000 --- a/src/dusk/system/system.c +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "system.h" -#include "system/systemplatform.h" - -#ifndef systemGetActiveDialogTypePlatform - #error "systemGetActiveDialogTypePlatform is not defined" -#endif - -#ifndef systemInitPlatform - #error "systemInitPlatform is not defined" -#endif - -#ifndef systemGetLocalePlatform - #error "systemGetLocalePlatform is not defined" -#endif - -errorret_t systemInit() { - return systemInitPlatform(); -} - -systemdialogtype_t systemGetActiveDialogType() { - return systemGetActiveDialogTypePlatform(); -} - -const localeinfo_t * systemGetLocale(void) { - return systemGetLocalePlatform(); -} - -systemplatform_t systemGetPlatform(void) { - #if defined(DUSK_KNULLI) - return SYSTEM_PLATFORM_KNULLI; - #elif defined(DUSK_PSP) - return SYSTEM_PLATFORM_PSP; - #elif defined(DUSK_GAMECUBE) - return SYSTEM_PLATFORM_GAMECUBE; - #elif defined(DUSK_WII) - return SYSTEM_PLATFORM_WII; - #else - return SYSTEM_PLATFORM_LINUX; - #endif -} \ No newline at end of file diff --git a/src/dusk/system/system.h b/src/dusk/system/system.h deleted file mode 100644 index 2985a194..00000000 --- a/src/dusk/system/system.h +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "system/systemplatformlist.h" -#include "locale/localeinfo.h" - -#define SYSTEM_PLATFORM(name, value) SYSTEM_PLATFORM_##name = value, -typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t; -#undef SYSTEM_PLATFORM - -typedef enum { - SYSTEM_DIALOG_TYPE_NONE, - SYSTEM_DIALOG_TYPE_RENDER_BLOCKING, - SYSTEM_DIALOG_TYPE_TICK_BLOCKING -} systemdialogtype_t; - -/** - * Initializes the system module. This is called really early in the init - * process of the engine. - * - * @return Error code indicating success or failure. - */ -errorret_t systemInit(void); - -/** - * Basically this is only used on a few system types, it is to ask the plaform, - * e.g. PSP "What dialog is currently open?" and then the engine will change the - * behavior of the main loop to accomodate. - * - * For example, PSP can show dialogs for things like, save files, wifi, system - * information, etc. When these are open, the engine really can't do much and - * the system needs to finish processing. - * - * For most systems this will go unused. - * - * @return Dialog type currently open. - */ -systemdialogtype_t systemGetActiveDialogType(); - -/** - * Returns the platform the engine is currently running on. - * - * @return The current platform. - */ -systemplatform_t systemGetPlatform(void); - -/** - * Returns the current locale of the system. - * - * @return The current locale. - */ -const localeinfo_t * systemGetLocale(void); \ No newline at end of file diff --git a/src/dusk/system/systemplatformlist.h b/src/dusk/system/systemplatformlist.h deleted file mode 100644 index af5f2f0c..00000000 --- a/src/dusk/system/systemplatformlist.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -/** - * X-macro list of all supported platforms. - * Usage: SYSTEM_PLATFORM(name, value) - * - * Example: - * #define SYSTEM_PLATFORM(name, value) \ - * case SYSTEM_PLATFORM_##name: return #name; - * SYSTEM_PLATFORM_LIST - * #undef SYSTEM_PLATFORM - */ -#define SYSTEM_PLATFORM_LIST \ - SYSTEM_PLATFORM(LINUX, 0) \ - SYSTEM_PLATFORM(KNULLI, 1) \ - SYSTEM_PLATFORM(PSP, 2) \ - SYSTEM_PLATFORM(GAMECUBE, 3) \ - SYSTEM_PLATFORM(WII, 4) diff --git a/src/dusk/thread/CMakeLists.txt b/src/dusk/thread/CMakeLists.txt deleted file mode 100644 index 17267ba9..00000000 --- a/src/dusk/thread/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - thread.c - threadmutex.c -) \ No newline at end of file diff --git a/src/dusk/thread/thread.c b/src/dusk/thread/thread.c deleted file mode 100644 index f0d09195..00000000 --- a/src/dusk/thread/thread.c +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "thread.h" -#include "util/memory.h" -#include "assert/assert.h" - -void threadInit(thread_t *thread, const threadcallback_t callback) { - assertNotNull(thread, "Thread cannot be NULL."); - assertNotNull(callback, "Thread callback cannot be NULL."); - - memoryZero(thread, sizeof(thread_t)); - threadMutexInit(&thread->stateMutex); - - thread->state = THREAD_STATE_STOPPED; - thread->callback = callback; - - #ifdef DUSK_THREAD_PTHREAD - thread->threadId = 0; - #endif -} - -void threadStartRequest(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - - threadMutexLock(&thread->stateMutex); - thread->state = THREAD_STATE_STARTING; - threadMutexUnlock(&thread->stateMutex); - - #ifdef DUSK_THREAD_PTHREAD - assertTrue(thread->threadId == 0, "Thread id not 0."); - - pthread_create( - &thread->threadId, - NULL, - (void * (*)(void *))threadHandler, - thread - ); - pthread_detach(thread->threadId); - #endif -} - -void threadStopRequest(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - - - threadMutexLock(&thread->stateMutex); - if(thread->state != THREAD_STATE_STOPPED) { - thread->state = THREAD_STATE_STOP_REQUESTED; - } - threadMutexUnlock(&thread->stateMutex); -} - -void threadStart(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - - threadStartRequest(thread); - threadMutexLock(&thread->stateMutex); - while(thread->state == THREAD_STATE_STARTING) { - threadMutexWaitLock(&thread->stateMutex); - } - threadMutexUnlock(&thread->stateMutex); -} - -void threadStop(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - threadStopRequest(thread); - threadMutexLock(&thread->stateMutex); - while(thread->state != THREAD_STATE_STOPPED) { - threadMutexWaitLock(&thread->stateMutex); - } - threadMutexUnlock(&thread->stateMutex); -} - -bool_t threadShouldStop(thread_t *thread) { - bool_t state; - assertNotNull(thread, "Thread cannot be NULL."); - - threadMutexLock(&thread->stateMutex); - switch(thread->state) { - case THREAD_STATE_STOPPED: - case THREAD_STATE_STOP_REQUESTED: - state = true; - break; - - default: - state = false; - break; - } - threadMutexUnlock(&thread->stateMutex); - return state; -} - -#ifdef DUSK_THREAD_PTHREAD - void * threadHandler(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - - threadMutexLock(&thread->stateMutex); - thread->state = THREAD_STATE_RUNNING; - threadMutexSignal(&thread->stateMutex); - threadMutexUnlock(&thread->stateMutex); - - // Send to callback. - thread->callback(thread); - - threadMutexLock(&thread->stateMutex); - thread->state = THREAD_STATE_STOPPED; - thread->threadId = 0; - threadMutexSignal(&thread->stateMutex); - threadMutexUnlock(&thread->stateMutex); - return NULL; - } -#endif \ No newline at end of file diff --git a/src/dusk/thread/thread.h b/src/dusk/thread/thread.h deleted file mode 100644 index 046747f5..00000000 --- a/src/dusk/thread/thread.h +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "thread/threadlocal.h" -#include "thread/threadmutex.h" - -typedef struct thread_s thread_t; - -typedef void (*threadcallback_t)(thread_t *thread); - -typedef enum { - // Thread is definitely not running. - THREAD_STATE_STOPPED, - - // Start has been requested, it may even be started by the time you - // see this value. - THREAD_STATE_STARTING, - - // Thread has definitely started and is running. - THREAD_STATE_RUNNING, - - // Something has requested the thread to stop, but it may not have - // stopped yet. - THREAD_STATE_STOP_REQUESTED, -} threadstate_t; - -typedef struct thread_s { - threadstate_t state; - threadmutex_t stateMutex; - threadcallback_t callback; - void *data; - - #ifdef DUSK_THREAD_PTHREAD - pthread_t threadId; - #endif -} thread_t; - -/** - * Initializes a thread structure. - * - * @param thread Pointer to the thread structure to initialize. - * @param callback The callback function to be called when the thread runs. - */ -void threadInit(thread_t *thread, const threadcallback_t callback); - -/** - * Starts the thread, does not wait for it to finish starting. - * - * @param thread Pointer to the thread structure to start. - */ -void threadStartRequest(thread_t *thread); - -/** - * Requests the thread to stop, does not block the calling thread. - * - * @param thread Pointer to the thread structure to stop. - */ -void threadStopRequest(thread_t *thread); - -/** - * Starts the thread, blocking until it has started. Does this as efficiently as - * possible depending on the threading implementation. Note that it is possible - * for the thread to fully COMPLETE before this function returns. - * - * @param thread Pointer to the thread structure to start. - */ -void threadStart(thread_t *thread); - -/** - * Stops the thread, blocking until it has stopped. Does this as efficiently as - * possible depending on the threading implementation. Note that it is possible - * for the thread to fully COMPLETE well before this function returns. - * - * @param thread Pointer to the thread structure to stop. - */ -void threadStop(thread_t *thread); - -/** - * Checks if the thread should stop, based on its state. - * - * @param thread Pointer to the thread structure to check. - * @return true if the thread should stop, false otherwise. - */ -bool_t threadShouldStop(thread_t *thread); - -#ifdef DUSK_THREAD_PTHREAD - /** - * Handles the thread's lifecycle for pthreads. - * @param thread Pointer to the thread structure to handle. - */ - void * threadHandler(thread_t *thread); -#endif \ No newline at end of file diff --git a/src/dusk/thread/threadlocal.h b/src/dusk/thread/threadlocal.h deleted file mode 100644 index 0e2cad82..00000000 --- a/src/dusk/thread/threadlocal.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#ifdef DUSK_THREAD_PTHREAD - #define THREAD_LOCAL __thread -#endif - -#ifndef THREAD_LOCAL - #error "No threading implementation found, cannot define THREAD_LOCAL." -#endif diff --git a/src/dusk/thread/threadmutex.c b/src/dusk/thread/threadmutex.c deleted file mode 100644 index 366b4bf5..00000000 --- a/src/dusk/thread/threadmutex.c +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "threadmutex.h" - -void threadMutexInit(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - pthread_mutex_init(&lock->mutex, NULL); - pthread_cond_init(&lock->cond, NULL); - #endif -} - -void threadMutexLock(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - pthread_mutex_lock(&lock->mutex); - #endif -} - -bool_t threadMutexTryLock(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - return pthread_mutex_trylock(&lock->mutex) == 0; - #endif -} - -void threadMutexUnlock(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - pthread_mutex_unlock(&lock->mutex); - #endif -} - -void threadMutexWaitLock(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - pthread_cond_wait(&lock->cond, &lock->mutex); - #endif -} - -void threadMutexSignal(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - pthread_cond_signal(&lock->cond); - #endif -} - -void threadMutexDispose(threadmutex_t *lock) { - #ifdef DUSK_THREAD_PTHREAD - pthread_mutex_destroy(&lock->mutex); - pthread_cond_destroy(&lock->cond); - #endif -} \ No newline at end of file diff --git a/src/dusk/thread/threadmutex.h b/src/dusk/thread/threadmutex.h deleted file mode 100644 index 29ef7ba7..00000000 --- a/src/dusk/thread/threadmutex.h +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#ifdef DUSK_THREAD_PTHREAD - #include -#else - #error "At least one threading implementation must be defined." -#endif - -typedef struct threadlock_t { - #ifdef DUSK_THREAD_PTHREAD - pthread_mutex_t mutex; - pthread_cond_t cond; - #endif -} threadmutex_t; - -/** - * Initializes a thread mutex. - * - * @param lock Pointer to the thread mutex structure to initialize. - */ -void threadMutexInit(threadmutex_t *lock); - -/** - * Locks the thread mutex. - * - * @param lock Pointer to the thread mutex structure to lock. - */ -void threadMutexLock(threadmutex_t *lock); - -/** - * Attempts to lock the thread mutex without blocking. - * - * @param lock Pointer to the thread mutex structure to try locking. - * @return true if the lock was acquired, false otherwise. - */ -bool_t threadMutexTryLock(threadmutex_t *lock); - -/** - * Unlocks the thread mutex. - * - * @param lock Pointer to the thread mutex structure to unlock. - */ -void threadMutexUnlock(threadmutex_t *lock); - -/** - * Releases the thread mutex and waits for it to be locked again. - * - * @param lock Pointer to the thread mutex structure to wait on. - */ -void threadMutexWaitLock(threadmutex_t *lock); - -/** - * Signals the thread mutex, allowing a waiting thread to proceed as soon as - * this mutex is unlocked. - * - * @param lock Pointer to the thread mutex structure to signal. - */ -void threadMutexSignal(threadmutex_t *lock); - -/** - * Disposes of the thread mutex. - * - * @param lock Pointer to the thread mutex structure to dispose. - */ -void threadMutexDispose(threadmutex_t *lock); \ No newline at end of file diff --git a/src/dusk/time/CMakeLists.txt b/src/dusk/time/CMakeLists.txt deleted file mode 100644 index a202dc37..00000000 --- a/src/dusk/time/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - time.c - timeepoch.c -) \ No newline at end of file diff --git a/src/dusk/time/time.c b/src/dusk/time/time.c deleted file mode 100644 index ab4f20a5..00000000 --- a/src/dusk/time/time.c +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "time.h" -#include "util/memory.h" -#include "assert/assert.h" - -#include "console/console.h" - -dusktime_t TIME; - -void timeInit(void) { - memoryZero(&TIME, sizeof(TIME)); - - // Set these to something non-zero. - TIME.time = DUSK_TIME_STEP; - TIME.delta = DUSK_TIME_STEP; - - #ifdef DUSK_TIME_DYNAMIC - TIME.dynamicTime = DUSK_TIME_STEP; - TIME.dynamicDelta = DUSK_TIME_STEP; - TIME.dynamicUpdate = false; - TIME.lastNonDynamic = TIME.dynamicTime; - #endif -} - -void timeUpdate(void) { - #ifdef DUSK_TIME_DYNAMIC - timeTickPlatform(); - TIME.dynamicDelta = timeGetDeltaPlatform(); - TIME.dynamicTime += TIME.dynamicDelta; - TIME.dynamicUpdate = true; - - assertTrue(TIME.dynamicDelta >= 0.0f, "Time delta is negative"); - - // Is within 1ms of a full step? - if(TIME.dynamicTime - TIME.lastNonDynamic >= DUSK_TIME_STEP * 0.999f) { - TIME.dynamicUpdate = false; - TIME.lastNonDynamic = TIME.dynamicTime; - TIME.delta = DUSK_TIME_STEP; - TIME.time += DUSK_TIME_STEP; - } - #else - TIME.delta = DUSK_TIME_STEP; - TIME.time += DUSK_TIME_STEP; - #endif - - // Print time in UTC style string - dusktimeepoch_t epoch = timeGetEpoch(); - char_t buffer[256]; - timeEpochFormat(epoch, "%Y-%m-%d %H:%M:%S", buffer, sizeof(buffer)); - // consolePrint("Real Time: %s", buffer); -} - -dusktimeepoch_t timeGetEpoch(void) { - return timeEpochInit(timeGetRealPlatform(), timeGetRealTimeZonePlatform()); -} \ No newline at end of file diff --git a/src/dusk/time/time.h b/src/dusk/time/time.h deleted file mode 100644 index 68f53537..00000000 --- a/src/dusk/time/time.h +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "timeepoch.h" -#include "time/timeplatform.h" - -#ifndef DUSK_TIME_STEP - #define DUSK_TIME_STEP (16.0f / 1000.0f) -#endif - -/** - * Converts a fixed-step tick count to a duration in seconds. - * - * @param ticks The number of ticks to convert. - * @return The duration in seconds. - */ -#define TIME_TICKS_TO_TIME(ticks) ((float_t)(ticks) * DUSK_TIME_STEP) - -/** - * Converts a duration in seconds to a fixed-step tick count. - * - * @param seconds The duration in seconds to convert. - * @return The number of ticks. - */ -#define TIME_TIME_TO_TICKS(seconds) ((int32_t)((seconds) / DUSK_TIME_STEP)) - -#ifdef DUSK_TIME_DYNAMIC - #ifndef timeTickPlatform - #error "DUSK_TIME_DYNAMIC needs tick method defined" - #endif - #ifndef timeGetDeltaPlatform - #error "DUSK_TIME_DYNAMIC needs delta method defined" - #endif -#endif - -typedef struct { - float_t delta; - float_t time; - - #ifdef DUSK_TIME_DYNAMIC - float_t lastNonDynamic; - bool_t dynamicUpdate; - float_t dynamicDelta; - float_t dynamicTime; - #endif -} dusktime_t; - -extern dusktime_t TIME; - - -/** - * Initializes the time system. - */ -void timeInit(void); - -/** - * Updates the time system - */ -void timeUpdate(void); - -/** - * Returns the current epoch time, in seconds since January 1, 1970, with the - * current time zone offset applied. - * - * @return The current epoch time. - */ -dusktimeepoch_t timeGetEpoch(void); \ No newline at end of file diff --git a/src/dusk/time/timeepoch.c b/src/dusk/time/timeepoch.c deleted file mode 100644 index 9acbd22a..00000000 --- a/src/dusk/time/timeepoch.c +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "time.h" -#include "assert/assert.h" - -dusktimeepoch_t timeEpochInit(const double_t time, const double_t timeZone) { - dusktimeepoch_t epoch; - epoch.time = time; - epoch.timeZone = timeZone; - epoch.offsetTime = time + timeZone; - return epoch; -} - -dusktimeepoch_t timeEpochSwitchTimeZone( - const dusktimeepoch_t epoch, - const double_t offset -) { - return timeEpochInit( - epoch.time + offset - epoch.timeZone, - offset - ); -} - -int32_t timeEpochCompare(const dusktimeepoch_t a, const dusktimeepoch_t b) { - if(a.time < b.time) return -1; - if(a.time > b.time) return 1; - return 0; -} - -int32_t timeEpochGetHours(const dusktimeepoch_t epoch) { - return ((int32_t)(epoch.offsetTime / 3600.0)) % 24; -} - -int32_t timeEpochGetMinutes(const dusktimeepoch_t epoch) { - return ((int32_t)(epoch.offsetTime / 60.0)) % 60; -} - -int32_t timeEpochGetSeconds(const dusktimeepoch_t epoch) { - return ((int32_t)(epoch.offsetTime)) % 60; -} - -int32_t timeEpochGetDayOfMonth(const dusktimeepoch_t epoch) { - int64_t days = (int64_t)(epoch.offsetTime / 86400.0); - - int32_t year = TIME_EPOCH_START_YEAR; - - while(1) { - int32_t daysInYear = TIME_EPOCH_DAYS_IN_YEAR(year); - if(days < daysInYear) break; - - days -= daysInYear; - year++; - } - - int32_t month = timeEpochGetMonth(epoch); - - for(int32_t i = 0; i < month - 1; i++) { - int32_t daysInMonth = TIME_EPOCH_DAYS_IN_MONTH[i]; - - if(i == 1 && timeEpochIsLeapYear(year)) { - daysInMonth = 29; - } - - days -= daysInMonth; - } - - return (int32_t)days + 1; -} - -int32_t timeEpochGetMonth(const dusktimeepoch_t epoch) { - int64_t days = (int64_t)(epoch.offsetTime / 86400.0); - - int32_t year = TIME_EPOCH_START_YEAR; - - while(1) { - int32_t daysInYear = TIME_EPOCH_DAYS_IN_YEAR(year); - if(days < daysInYear) break; - - days -= daysInYear; - year++; - } - - for(int32_t month = 0; month < 12; month++) { - int32_t daysInMonth = TIME_EPOCH_DAYS_IN_MONTH[month]; - - if(month == 1 && timeEpochIsLeapYear(year)) daysInMonth = 29; - if(days < daysInMonth) return month + 1; - days -= daysInMonth; - } - - return -1; -} - -int32_t timeEpochGetYear(const dusktimeepoch_t epoch) { - int64_t days = (int64_t)(epoch.offsetTime / 86400.0); - int32_t year = TIME_EPOCH_START_YEAR; - - while(1) { - int32_t daysInYear = TIME_EPOCH_DAYS_IN_YEAR(year); - if(days < daysInYear) return year; - - days -= daysInYear; - year++; - } -} - -bool_t timeEpochIsLeapYear(const int32_t year) { - return ( - ((year % 4 == 0) && (year % 100 != 0)) || - (year % 400 == 0) - ); -} - -size_t timeEpochFormat( - const dusktimeepoch_t epoch, - const char_t* format, - char_t* buffer, - const size_t bufferSize -) { - assertNotNull(format, "Format string cannot be null"); - assertNotNull(buffer, "Buffer cannot be null"); - assertTrue(bufferSize > 0, "Buffer size must be greater than 0"); - - size_t bufferIndex = 0; - - for(size_t i = 0; format[i] != '\0'; i++) { - if(format[i] == '%') { - i++; - char_t formatSpecifier = format[i]; - - char_t tempBuffer[16]; - size_t tempBufferIndex = 0; - - switch(formatSpecifier) { - case 'Y': - tempBufferIndex = snprintf( - tempBuffer, sizeof(tempBuffer), "%04d", - timeEpochGetYear(epoch) - ); - break; - case 'm': - tempBufferIndex = snprintf( - tempBuffer, sizeof(tempBuffer), "%02d", - timeEpochGetMonth(epoch) - ); - break; - case 'd': - tempBufferIndex = snprintf( - tempBuffer, sizeof(tempBuffer), "%02d", - timeEpochGetDayOfMonth(epoch) - ); - break; - case 'H': - tempBufferIndex = snprintf( - tempBuffer, sizeof(tempBuffer), "%02d", - timeEpochGetHours(epoch) - ); - break; - case 'M': - tempBufferIndex = snprintf( - tempBuffer, sizeof(tempBuffer), "%02d", - timeEpochGetMinutes(epoch) - ); - break; - case 'S': - tempBufferIndex = snprintf( - tempBuffer, sizeof(tempBuffer), "%02d", - timeEpochGetSeconds(epoch) - ); - break; - default: - continue; - } - - for(size_t j = 0; j < tempBufferIndex; j++) { - if(bufferIndex < bufferSize - 1) { - buffer[bufferIndex++] = tempBuffer[j]; - } else { - break; - } - } - } else { - if(bufferIndex < bufferSize - 1) { - buffer[bufferIndex++] = format[i]; - } else { - break; - } - } - } - - buffer[bufferIndex] = '\0'; - return bufferIndex; -} \ No newline at end of file diff --git a/src/dusk/time/timeepoch.h b/src/dusk/time/timeepoch.h deleted file mode 100644 index d7da2b9e..00000000 --- a/src/dusk/time/timeepoch.h +++ /dev/null @@ -1,145 +0,0 @@ -/** - * 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 { - double_t time; - double_t timeZone; - double_t offsetTime; -} dusktimeepoch_t; - -static const int32_t TIME_EPOCH_DAYS_IN_MONTH[] = { - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 -}; - -#define TIME_EPOCH_START_YEAR 1970 -#define TIME_EPOCH_DAYS_IN_YEAR_NON_LEAP 365 -#define TIME_EPOCH_DAYS_IN_YEAR_LEAP 366 -#define TIME_EPOCH_DAYS_IN_YEAR(year) (\ - timeEpochIsLeapYear(year) ? \ - TIME_EPOCH_DAYS_IN_YEAR_LEAP : \ - TIME_EPOCH_DAYS_IN_YEAR_NON_LEAP\ -) - -/** - * Initializes an epoch time with the given time and time zone. - * - * @param time The time in seconds since January 1, 1970. - * @param timeZone The time zone offset in seconds from UTC. - * @return The initialized epoch time. - */ -dusktimeepoch_t timeEpochInit(const double_t time, const double_t timeZone); - -/** - * Changes the time from one timezone to another. - * - * @param epoch The epoch time to change the timezone of. - * @param offset The offset in seconds to change the timezone by. - * @return The epoch time with the timezone changed. - */ -dusktimeepoch_t timeEpochSwitchTimeZone( - const dusktimeepoch_t epoch, - const double_t offset -); - -/** - * Compares two epoch times. - * - * @param a The first epoch time. - * @param b The second epoch time. - * @return A negative value if a < b, zero if a == b, a positive value if a > b. - */ -int32_t timeEpochCompare(const dusktimeepoch_t a, const dusktimeepoch_t b); - -/** - * Returns the hours component of the epoch time, in the range [0, 23]. This is - * in the timezone of the epoch. - * - * @param epoch The epoch time to get the hours component of. - * @return The hours component of the epoch time. - */ -int32_t timeEpochGetHours(const dusktimeepoch_t epoch); - -/** - * Returns the minutes component of the epoch time, in the range [0, 59]. This - * is in the timezone of the epoch. - * - * @param epoch The epoch time to get the minutes component of. - * @return The minutes component of the epoch time. - */ -int32_t timeEpochGetMinutes(const dusktimeepoch_t epoch); - -/** - * Returns the seconds component of the epoch time, in the range [0, 59]. This - * is in the timezone of the epoch. - * - * @param epoch The epoch time to get the seconds component of. - * @return The seconds component of the epoch time. - */ -int32_t timeEpochGetSeconds(const dusktimeepoch_t epoch); - -/** - * Returns the day of the month component of the epoch time, in the range of [ - * 0, 30]. This is in the timezone of the epoch. e.g. Jan 1st is 0, Jan 31st is - * 30. - * - * @param epoch The epoch time to get the day of the month component of. - * @return The day of the month component of the epoch time. - */ -int32_t timeEpochGetDayOfMonth(const dusktimeepoch_t epoch); - -/** - * Returns the month component of the epoch time, in the range of [0, 11]. This - * is in the timezone of the epoch. e.g. Jan is 0, Dec is 11. - * - * @param epoch The epoch time to get the month component of. - * @return The month component of the epoch time. - */ -int32_t timeEpochGetMonth(const dusktimeepoch_t epoch); - -/** - * Returns the year component of the epoch time. This is in the timezone of the - * epoch. - * - * @param epoch The epoch time to get the year component of. - * @return The year component of the epoch time. - */ -int32_t timeEpochGetYear(const dusktimeepoch_t epoch); - -/** - * Returns whether the given year is a leap year. - * - * @param year The year to check if it is a leap year. - * @return Whether the given year is a leap year. - */ -bool_t timeEpochIsLeapYear(const int32_t year); - -/** - * Buffers the epoch time into a string according to the given format. - * - * Format parameters: - * %Y - Year with century as a decimal number. - * %m - Month as a decimal number (01-12). - * %d - Day of the month as a decimal number (01-31). - * %H - Hour (24-hour clock) as a decimal number (00-23). - * %M - Minute as a decimal number (00-59). - * %S - Second as a decimal number (00-59). - * - * @param epoch The epoch time to format. - * @param format The format string to format the epoch time with. - * @param buffer The buffer to write the formatted string to. - * @param bufferSize The size of the buffer to write the formatted string to. - * @return The number of characters written to the buffer, excluding the null - */ -size_t timeEpochFormat( - const dusktimeepoch_t epoch, - const char_t* format, - char_t* buffer, - const size_t bufferSize -); \ No newline at end of file diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt deleted file mode 100644 index ac7639b2..00000000 --- a/src/dusk/ui/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -add_subdirectory(debug) -add_subdirectory(dialog) -add_subdirectory(screen) -add_subdirectory(focus) -add_subdirectory(overlay) -add_subdirectory(rpg) -add_subdirectory(transition) -add_subdirectory(widget) - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ui.c - uielement.c - uielementlist.c - # uitextbox.c -) \ No newline at end of file diff --git a/src/dusk/ui/debug/CMakeLists.txt b/src/dusk/ui/debug/CMakeLists.txt deleted file mode 100644 index 52d8095a..00000000 --- a/src/dusk/ui/debug/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uiconsole.c - uicutscene.c - uifps.c - uiplayerpos.c -) diff --git a/src/dusk/ui/debug/uiconsole.c b/src/dusk/ui/debug/uiconsole.c deleted file mode 100644 index 631bd1fd..00000000 --- a/src/dusk/ui/debug/uiconsole.c +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiconsole.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/spritebatch/spritebatch.h" - -uiconsole_t UICONSOLE; - -errorret_t uiConsoleInit(void) { - float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight; - - for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - uiLabelInit( - &UICONSOLE.labels[i], - CONSOLE.line[i], - UICONSOLE.sprites[i], CONSOLE_LINE_MAX - ); - uiLabelSetX(&UICONSOLE.labels[i], (float_t)SCREEN.scanX); - uiLabelSetY( - &UICONSOLE.labels[i], (float_t)SCREEN.scanY + lineH * (float_t)i - ); - } - - errorOk(); -} - -errorret_t uiConsoleDraw(void) { - if(!CONSOLE.visible) errorOk(); - - for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - UICONSOLE.labels[i].dirty = true; - errorChain(uiLabelRender(&UICONSOLE.labels[i], COLOR_RED)); - } - return spriteBatchFlush(); -} diff --git a/src/dusk/ui/debug/uiconsole.h b/src/dusk/ui/debug/uiconsole.h deleted file mode 100644 index be745ab3..00000000 --- a/src/dusk/ui/debug/uiconsole.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "console/console.h" -#include "ui/widget/uilabel.h" - -typedef struct { - uilabel_t labels[CONSOLE_HISTORY_MAX]; - spritebatchsprite_t sprites[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; -} uiconsole_t; - -extern uiconsole_t UICONSOLE; - -/** - * Initializes the console's history labels. - * - * @return Any error that occurs. - */ -errorret_t uiConsoleInit(void); - -/** - * Renders the console history into the scan-safe area. - * No-ops when the console is not visible. - * - * @return Any error that occurs. - */ -errorret_t uiConsoleDraw(void); diff --git a/src/dusk/ui/debug/uicutscene.c b/src/dusk/ui/debug/uicutscene.c deleted file mode 100644 index 94bdf321..00000000 --- a/src/dusk/ui/debug/uicutscene.c +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uicutscene.h" -#include "util/string.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/spritebatch/spritebatch.h" -#include "display/color.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/item/cutsceneitem.h" - -uicutscene_t UICUTSCENE; - -errorret_t uiCutsceneInit() { - uiLabelInit( - &UICUTSCENE.countLabel, - UICUTSCENE.countText, - UICUTSCENE.countSprites, UI_CUTSCENE_SPRITES_MAX - ); - uiLabelSetX(&UICUTSCENE.countLabel, (float_t)SCREEN.scanX); - uiLabelSetY(&UICUTSCENE.countLabel, (float_t)SCREEN.scanY + - (float_t)FONT_DEFAULT.tileset->tileHeight * 2 - ); - - uiLabelInit( - &UICUTSCENE.itemLabel, - UICUTSCENE.itemText, - UICUTSCENE.itemSprites, UI_CUTSCENE_SPRITES_MAX - ); - uiLabelSetX(&UICUTSCENE.itemLabel, (float_t)SCREEN.scanX); - uiLabelSetY(&UICUTSCENE.itemLabel, (float_t)SCREEN.scanY + - (float_t)FONT_DEFAULT.tileset->tileHeight * 3 - ); - - errorOk(); -} - -errorret_t uiCutsceneDraw() { - const cutsceneitem_t *item = cutsceneSystemGetCurrentItem(); - if(!item || !CUTSCENE_SYSTEM.scene) { - stringCopy( - UICUTSCENE.countText, "Cutscene Inactive", UI_CUTSCENE_TEXT_MAX - 1 - ); - UICUTSCENE.itemText[0] = '\0'; - } else { - const char_t *name = CUTSCENE_ITEM_CALLBACKS[item->type].jsonName; - if(!name) name = "???"; - - stringFormat( - UICUTSCENE.countText, - UI_CUTSCENE_TEXT_MAX - 1, - "Cutscene Item Count: %d", - (int_t)CUTSCENE_SYSTEM.scene->itemCount - ); - stringFormat( - UICUTSCENE.itemText, - UI_CUTSCENE_TEXT_MAX - 1, - "Current Item: %s", - name - ); - } - UICUTSCENE.countLabel.dirty = true; - UICUTSCENE.itemLabel.dirty = true; - - errorChain(uiLabelRender(&UICUTSCENE.countLabel, COLOR_GREEN)); - errorChain(uiLabelRender(&UICUTSCENE.itemLabel, COLOR_GREEN)); - return spriteBatchFlush(); -} diff --git a/src/dusk/ui/debug/uicutscene.h b/src/dusk/ui/debug/uicutscene.h deleted file mode 100644 index 07fa9441..00000000 --- a/src/dusk/ui/debug/uicutscene.h +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" - -#define UI_CUTSCENE_TEXT_MAX 64 -#define UI_CUTSCENE_SPRITES_MAX UI_CUTSCENE_TEXT_MAX - -typedef struct { - uilabel_t countLabel; - char_t countText[UI_CUTSCENE_TEXT_MAX]; - spritebatchsprite_t countSprites[UI_CUTSCENE_SPRITES_MAX]; - - uilabel_t itemLabel; - char_t itemText[UI_CUTSCENE_TEXT_MAX]; - spritebatchsprite_t itemSprites[UI_CUTSCENE_SPRITES_MAX]; -} uicutscene_t; - -extern uicutscene_t UICUTSCENE; - -/** - * Initializes the cutscene debug labels. - * - * @return Any error that occurs. - */ -errorret_t uiCutsceneInit(); - -/** - * Draws the currently running cutscene's item count and current item - * type as two lines ("Item Count: %d" / "Current Item: %s"), or a - * single "Cutscene Inactive" line if no cutscene is running. Renders - * below UIPLAYERPOS (see ui/debug/uiplayerpos.h). - * - * @return Any error that occurs. - */ -errorret_t uiCutsceneDraw(); diff --git a/src/dusk/ui/debug/uifps.c b/src/dusk/ui/debug/uifps.c deleted file mode 100644 index d5df8ac5..00000000 --- a/src/dusk/ui/debug/uifps.c +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uifps.h" -#include "time/time.h" -#include "util/string.h" -#include "display/spritebatch/spritebatch.h" -#include "display/color.h" -#include "display/screen/screen.h" -#include "engine/engine.h" - -uifps_t UIFPS; - -errorret_t uiFPSInit() { - uiLabelInit( - &UIFPS.fpsLabel, - UIFPS.fpsText, - UIFPS.fpsSprites, UI_FPS_SPRITES_MAX - ); - uiLabelSetX(&UIFPS.fpsLabel, (float_t)SCREEN.scanX); - uiLabelSetY(&UIFPS.fpsLabel, (float_t)SCREEN.scanY); - - uiLabelInit( - &UIFPS.versionLabel, - UIFPS.versionText, - UIFPS.versionSprites, UI_FPS_VERSION_SPRITES_MAX - ); - stringCopy(UIFPS.versionText, ENGINE.version, UI_FPS_VERSION_TEXT_MAX - 1); - UIFPS.versionLabel.dirty = true; - uiLabelRebuffer(&UIFPS.versionLabel); - uiLabelSetX(&UIFPS.versionLabel, (float_t)( - SCREEN.scanX + SCREEN.scanWidth - UIFPS.versionLabel.width - )); - uiLabelSetY(&UIFPS.versionLabel, (float_t)( - SCREEN.scanY + SCREEN.scanHeight - UIFPS.versionLabel.height - )); - - errorOk(); -} - -errorret_t uiFPSDraw() { - // Get now. - dusktimeepoch_t now = timeGetEpoch(); - double_t delta = now.time - UIFPS.lastTick.time; - UIFPS.lastTick = now; - - // Raw current FPS - float_t fps = delta > 0 ? 1.0 / delta : 0.0; - - // Average FPS using exponential moving average - const float_t alpha = 0.1f; // Smoothing factor - if(UIFPS.fpsAverage == 0.0f) { - UIFPS.fpsAverage = fps; // Initialize average on first run - } else { - UIFPS.fpsAverage = alpha * fps + (1.0f - alpha) * UIFPS.fpsAverage; - } - - stringFormat( - UIFPS.fpsText, - UI_FPS_TEXT_MAX - 1, - "%.1f/%.1fms", - UIFPS.fpsAverage, - delta * 1000.0f - ); - UIFPS.fpsLabel.dirty = true; - - color_t textColor; - if(fps >= 55.0f) { - textColor = COLOR_GREEN; - } else if(fps >= 45.0f) { - textColor = COLOR_YELLOW; - } else { - textColor = COLOR_RED; - } - - errorChain(uiLabelRender(&UIFPS.fpsLabel, textColor)); - errorChain(spriteBatchFlush()); - - errorChain(uiLabelRender(&UIFPS.versionLabel, color(255, 255, 255, 128))); - - return spriteBatchFlush(); -} diff --git a/src/dusk/ui/debug/uifps.h b/src/dusk/ui/debug/uifps.h deleted file mode 100644 index fda1345b..00000000 --- a/src/dusk/ui/debug/uifps.h +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "time/timeepoch.h" -#include "ui/widget/uilabel.h" - -#define UI_FPS_TEXT_MAX 32 -#define UI_FPS_SPRITES_MAX UI_FPS_TEXT_MAX -#define UI_FPS_VERSION_TEXT_MAX 32 -#define UI_FPS_VERSION_SPRITES_MAX UI_FPS_VERSION_TEXT_MAX - -typedef struct { - dusktimeepoch_t lastTick; - float_t fpsAverage; - - uilabel_t fpsLabel; - char_t fpsText[UI_FPS_TEXT_MAX]; - spritebatchsprite_t fpsSprites[UI_FPS_SPRITES_MAX]; - - uilabel_t versionLabel; - char_t versionText[UI_FPS_VERSION_TEXT_MAX]; - spritebatchsprite_t versionSprites[UI_FPS_VERSION_SPRITES_MAX]; -} uifps_t; - -extern uifps_t UIFPS; - -/** - * Initializes the FPS counter's labels. - * - * @return Any error that occurs. - */ -errorret_t uiFPSInit(); - -/** - * Draws the FPS counter on the screen, and also does the update (for now). - * - * @return Any error that occurs. - */ -errorret_t uiFPSDraw(); \ No newline at end of file diff --git a/src/dusk/ui/debug/uiplayerpos.c b/src/dusk/ui/debug/uiplayerpos.c deleted file mode 100644 index ce7972c2..00000000 --- a/src/dusk/ui/debug/uiplayerpos.c +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiplayerpos.h" -#include "util/string.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/spritebatch/spritebatch.h" -#include "rpg/entity/entity.h" -#include "rpg/entity/entitytype.h" -#include "rpg/overworld/worldpos.h" -#include "rpg/overworld/map.h" - -uiplayerpos_t UIPLAYERPOS; - -errorret_t uiPlayerPosInit() { - uiLabelInit( - &UIPLAYERPOS.label, - UIPLAYERPOS.text, - UIPLAYERPOS.sprites, UI_PLAYERPOS_SPRITES_MAX - ); - uiLabelSetX(&UIPLAYERPOS.label, (float_t)SCREEN.scanX); - uiLabelSetY(&UIPLAYERPOS.label, (float_t)SCREEN.scanY + - (float_t)FONT_DEFAULT.tileset->tileHeight - ); - - errorOk(); -} - -errorret_t uiPlayerPosDraw() { - entity_t *player = NULL; - for(uint8_t i = 0; i < ENTITY_COUNT; i++) { - if(ENTITIES[i].type == ENTITY_TYPE_PLAYER) { - player = &ENTITIES[i]; - break; - } - } - if(!player) errorOk(); - - chunkpos_t chunkPos; - worldPosToChunkPos(&player->position, &chunkPos); - - stringFormat( - UIPLAYERPOS.text, - UI_PLAYERPOS_TEXT_MAX - 1, - "%s %d,%d,%d[%d,%d,%d]#%d", - MAP.name, - (int_t)player->position.x, - (int_t)player->position.y, - (int_t)player->position.z, - (int_t)chunkPos.x, - (int_t)chunkPos.y, - (int_t)chunkPos.z, - (int_t)player->chunkIndex - ); - UIPLAYERPOS.label.dirty = true; - - errorChain(uiLabelRender(&UIPLAYERPOS.label, COLOR_GREEN)); - return spriteBatchFlush(); -} diff --git a/src/dusk/ui/debug/uiplayerpos.h b/src/dusk/ui/debug/uiplayerpos.h deleted file mode 100644 index 1fb03ff7..00000000 --- a/src/dusk/ui/debug/uiplayerpos.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" - -#define UI_PLAYERPOS_TEXT_MAX 64 -#define UI_PLAYERPOS_SPRITES_MAX UI_PLAYERPOS_TEXT_MAX - -typedef struct { - uilabel_t label; - char_t text[UI_PLAYERPOS_TEXT_MAX]; - spritebatchsprite_t sprites[UI_PLAYERPOS_SPRITES_MAX]; -} uiplayerpos_t; - -extern uiplayerpos_t UIPLAYERPOS; - -/** - * Initializes the player position label. - * - * @return Any error that occurs. - */ -errorret_t uiPlayerPosInit(); - -/** - * Draws the current map name, player world position, chunk position, and - * chunk index on screen. Searches for the first ENTITY_TYPE_PLAYER and - * renders: MAPNAME WORLDX,WORLDY,WORLDZ[chunkx,chunky,chunkz]#chunkIndex - * - * @return Any error that occurs. - */ -errorret_t uiPlayerPosDraw(); diff --git a/src/dusk/ui/dialog/CMakeLists.txt b/src/dusk/ui/dialog/CMakeLists.txt deleted file mode 100644 index bfaab3ef..00000000 --- a/src/dusk/ui/dialog/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uiconfirm.c -) - -add_subdirectory(save) -add_subdirectory(keyboard) \ No newline at end of file diff --git a/src/dusk/ui/dialog/keyboard/CMakeLists.txt b/src/dusk/ui/dialog/keyboard/CMakeLists.txt deleted file mode 100644 index 9e5f2567..00000000 --- a/src/dusk/ui/dialog/keyboard/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uikeyboard.c -) diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.c b/src/dusk/ui/dialog/keyboard/uikeyboard.c deleted file mode 100644 index 8d56f69b..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.c +++ /dev/null @@ -1,610 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uikeyboard.h" -#include "ui/widget/uiframe.h" -#include "ui/widget/uibutton.h" -#include "ui/dialog/uiconfirm.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "util/math.h" -#include "display/screen/screen.h" -#include "display/color.h" -#include "display/text/font.h" -#include "display/spritebatch/spritebatch.h" -#include "time/time.h" - -// Key cell size, expressed in font tiles so it scales if the font ever -// does. Row height is derived from this ratio in uiKeyboardDraw rather -// than being its own fixed constant, so a key cell keeps its proportions -// even when contentWidth is stretched wider by a long title/text label. -#define UI_KEYBOARD_KEY_WIDTH ((float_t)(FONT_DEFAULT_TILE_WIDTH * 4)) -#define UI_KEYBOARD_KEY_HEIGHT ((float_t)(FONT_DEFAULT_TILE_HEIGHT * 2)) - -uikeyboard_t UI_KEYBOARD; - -// String literals have static storage duration, so these stay valid for -// as long as the process runs - safe for uiButtonInit's non-copying -// label contract. "" cells are blank filler/reserved slots, NULL is -// shorthand for the same thing where there's no key at all - see -// uikeyboard.h and uiKeyboardNormalizeKey. -// -// Unshifted, caps off - also the canonical table (see -// uiKeyboardGetCanonicalKey). -static const char_t *const UI_KEYBOARD_KEYS_NONE[UI_KEYBOARD_KEY_COUNT] = { - // 1 2 3 4 5 6 7 8 9 0 DEL - "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", - UI_KEYBOARD_KEY_BACKSPACE, - - // q w e r t y u i o p - "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", NULL, - - // CAPS a s d f g h j k l (NEWLINE) - UI_KEYBOARD_KEY_CAPS, - "a", "s", "d", "f", "g", "h", "j", "k", "l", NULL, - - // SHIFT z x c v b n m SHIFT - UI_KEYBOARD_KEY_SHIFT, - NULL, - "z", "x", "c", "v", "b", "n", "m", - NULL, - UI_KEYBOARD_KEY_SHIFT, - - // (CANCEL) SPACE (CONFIRM) - NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL, - NULL -}; - -// Caps on - letters uppercase; caps doesn't affect digits/symbols on a -// physical keyboard, so this table is otherwise identical to _NONE. -static const char_t *const UI_KEYBOARD_KEYS_CAPS[UI_KEYBOARD_KEY_COUNT] = { - "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", - UI_KEYBOARD_KEY_BACKSPACE, - - "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL, - - UI_KEYBOARD_KEY_CAPS, - "A", "S", "D", "F", "G", "H", "J", "K", "L", NULL, - - UI_KEYBOARD_KEY_SHIFT, - NULL, - "Z", "X", "C", "V", "B", "N", "M", - NULL, - UI_KEYBOARD_KEY_SHIFT, - - NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL, - NULL -}; - -// Shift held - letters uppercase, digits become their US-layout shift -// symbols, '-'/'=' become '_'/'+'. Takes priority over caps (see -// uiKeyboardGetChar). -static const char_t *const UI_KEYBOARD_KEYS_SHIFT[UI_KEYBOARD_KEY_COUNT] = { - "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", - UI_KEYBOARD_KEY_BACKSPACE, - - "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL, - - UI_KEYBOARD_KEY_CAPS, - "A", "S", "D", "F", "G", "H", "J", "K", "L", NULL, - - UI_KEYBOARD_KEY_SHIFT, - NULL, - "Z", "X", "C", "V", "B", "N", "M", - NULL, - UI_KEYBOARD_KEY_SHIFT, - - NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL, - NULL -}; - -// Collapses a table entry's NULL ("no key here") down to "" (blank -// filler), the only form of "nothing" callers need to handle. -const char_t * uiKeyboardNormalizeKey(const char_t *key) { - return key != NULL ? key : ""; -} - -// Shift takes priority over caps - there's no fourth table for "shift -// while caps is on", unlike a physical keyboard. -const char_t *const * uiKeyboardSelectTable( - const bool_t shift, - const bool_t caps -) { - if(shift) return UI_KEYBOARD_KEYS_SHIFT; - if(caps) return UI_KEYBOARD_KEYS_CAPS; - return UI_KEYBOARD_KEYS_NONE; -} - -uint8_t uiKeyboardBuildItems( - uimenuitem_t *items, - const bool_t shift, - const bool_t caps -) { - assertNotNull(items, "Items cannot be NULL"); - const char_t *const *table = uiKeyboardSelectTable(shift, caps); - - for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) { - items[i].type = UI_MENU_WIDGET_TYPE_BUTTON; - uiButtonInit(&items[i].button, uiKeyboardNormalizeKey(table[i])); - } - - return UI_KEYBOARD_KEY_COUNT; -} - -void uiKeyboardUpdateDisplay( - uimenuitem_t *items, - const bool_t shift, - const bool_t caps -) { - assertNotNull(items, "Items cannot be NULL"); - const char_t *const *table = uiKeyboardSelectTable(shift, caps); - - for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) { - // These reserved slots are blank in every shift/caps table - their - // real labels (NEWLINE/CANCEL/CONFIRM) are patched in once at open - // time and aren't affected by shift/caps, so leave them alone here. - if( - i == UI_KEYBOARD_NEWLINE_INDEX || - i == UI_KEYBOARD_CANCEL_INDEX || - i == UI_KEYBOARD_CONFIRM_INDEX - ) continue; - - items[i].button.label.text = uiKeyboardNormalizeKey(table[i]); - items[i].button.label.dirty = true; - - const char_t *canonical = uiKeyboardGetCanonicalKey(i); - if(stringCompare(canonical, UI_KEYBOARD_KEY_CAPS) == 0) { - uiButtonSetActive(&items[i].button, caps); - } else if(stringCompare(canonical, UI_KEYBOARD_KEY_SHIFT) == 0) { - uiButtonSetActive(&items[i].button, shift); - } - } -} - -const char_t * uiKeyboardGetCanonicalKey(const uint8_t index) { - assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range"); - return uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]); -} - -bool_t uiKeyboardIsShiftSensitive(const uint8_t index) { - assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range"); - return stringCompare( - uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]), - uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_SHIFT[index]) - ) != 0; -} - -char_t uiKeyboardGetChar( - const uint8_t index, - const bool_t shift, - const bool_t caps -) { - assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range"); - const char_t *const *table = uiKeyboardSelectTable(shift, caps); - return uiKeyboardNormalizeKey(table[index])[0]; -} - -void uiKeyboardFocusConfirm(void) { - uiMenuSetPosition( - &UI_KEYBOARD.menu, - UI_KEYBOARD_CONFIRM_INDEX % UI_KEYBOARD_COLUMNS, - UI_KEYBOARD_CONFIRM_INDEX / UI_KEYBOARD_COLUMNS - ); -} - -bool_t uiKeyboardTextIsBlank(void) { - for(const char_t *c = UI_KEYBOARD.text; *c != '\0'; c++) { - if(!stringIsWhitespace(*c)) return false; - } - return true; -} - -void uiKeyboardAppendChar(const char_t c) { - size_t length = strlen(UI_KEYBOARD.text); - - // Full - drop the last character instead of ignoring the keypress, so - // the new one overrides it. Falls through to the normal append below, - // which now has room again. - if(length >= UI_KEYBOARD.maxLength) { - length--; - UI_KEYBOARD.text[length] = '\0'; - } - - if(c == '\n') { - uint8_t lines = 1; - for(size_t i = 0; i < length; i++) { - if(UI_KEYBOARD.text[i] == '\n') lines++; - } - if(lines >= UI_KEYBOARD.lineCount) return; - } - - UI_KEYBOARD.text[length] = c; - UI_KEYBOARD.text[length + 1] = '\0'; - UI_KEYBOARD.textLabel.dirty = true; - uiLabelRebuffer(&UI_KEYBOARD.textLabel); - - // Nothing more can be typed - jump focus to confirm so accepting is a - // single button press away. - if(length + 1 >= UI_KEYBOARD.maxLength) uiKeyboardFocusConfirm(); -} - -void uiKeyboardBackspace(void) { - size_t length = strlen(UI_KEYBOARD.text); - if(length == 0) return; - - UI_KEYBOARD.text[length - 1] = '\0'; - UI_KEYBOARD.textLabel.dirty = true; - uiLabelRebuffer(&UI_KEYBOARD.textLabel); -} - -void uiKeyboardMenuClosed(const uimenu_t *menu) { - if(UI_KEYBOARD.onInput != NULL) { - UI_KEYBOARD.onInput( - UI_KEYBOARD.text, UI_KEYBOARD.confirmed, UI_KEYBOARD.user - ); - } -} - -bool_t uiKeyboardMenuCancel(const uimenu_t *menu) { - if(strlen(UI_KEYBOARD.text) > 0) { - uiKeyboardBackspace(); - return true; - } - - // Nothing left to delete - close, but only if this dialog is even - // allowed to be cancelled. Closed explicitly (rather than returning - // false to fall through to uiFocusUpdate's default pop) since that - // path only pops the focus stack - it never clears UI_KEYBOARD.open, - // which would leave uiKeyboardDraw drawing a focus-less dialog forever - // and the next uiKeyboardOpen() tripping its already-open assert. - if(!UI_KEYBOARD.cancel) return true; - - uiKeyboardDoCancel(); - return true; -} - -void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user) { - if(!result) return; - - UI_KEYBOARD.confirmed = true; - uiKeyboardClose(); -} - -void uiKeyboardDoCancel(void) { - if(UI_KEYBOARD.cancelConfirm) { - uiConfirmOpen( - UI_KEYBOARD.cancelConfirmQuestion, - uiKeyboardCancelConfirmSecondaryResult, NULL - ); - return; - } - - UI_KEYBOARD.confirmed = false; - uiKeyboardClose(); -} - -void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user) { - if(!result) return; - - UI_KEYBOARD.confirmed = false; - uiKeyboardClose(); -} - -void uiKeyboardMenuSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - if(index == UI_KEYBOARD_CONFIRM_INDEX) { - if(UI_KEYBOARD.trimmed) { - stringTrim(UI_KEYBOARD.text); - UI_KEYBOARD.textLabel.dirty = true; - uiLabelRebuffer(&UI_KEYBOARD.textLabel); - } - if(!UI_KEYBOARD.allowBlank && uiKeyboardTextIsBlank()) return; - - if(UI_KEYBOARD.confirm) { - uiConfirmOpen( - UI_KEYBOARD.confirmQuestion, uiKeyboardConfirmSecondaryResult, NULL - ); - return; - } - - UI_KEYBOARD.confirmed = true; - uiKeyboardClose(); - return; - } - - if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_CANCEL_INDEX) { - uiKeyboardDoCancel(); - return; - } - - if(index == UI_KEYBOARD_NEWLINE_INDEX && UI_KEYBOARD.lineCount > 1) { - if( - UI_KEYBOARD.onKeyPress != NULL && - !UI_KEYBOARD.onKeyPress('\n', UI_KEYBOARD.user) - ) return; - uiKeyboardAppendChar('\n'); - return; - } - - // The canonical (unshifted) key, independent of whatever character - // it's currently drawn as - see uiKeyboardGetCanonicalKey. Used to - // identify the key, not item->button.label.text, which changes with - // shift/caps. - const char_t *key = uiKeyboardGetCanonicalKey(index); - - // Blank filler/reserved cell (includes an unused cancel/newline slot). - if(key[0] == '\0') return; - - if(stringCompare(key, UI_KEYBOARD_KEY_BACKSPACE) == 0) { - uiKeyboardBackspace(); - return; - } - - if(stringCompare(key, UI_KEYBOARD_KEY_SHIFT) == 0) { - UI_KEYBOARD.shift = !UI_KEYBOARD.shift; - uiKeyboardUpdateDisplay( - UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps - ); - return; - } - - if(stringCompare(key, UI_KEYBOARD_KEY_CAPS) == 0) { - UI_KEYBOARD.caps = !UI_KEYBOARD.caps; - uiKeyboardUpdateDisplay( - UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps - ); - return; - } - - char_t c; - if(stringCompare(key, UI_KEYBOARD_KEY_SPACE) == 0) { - c = ' '; - } else { - c = uiKeyboardGetChar(index, UI_KEYBOARD.shift, UI_KEYBOARD.caps); - - // Shift is a one-shot, consumed by any letter/digit/-/= it actually - // affected (and the display refreshed to drop back to whatever caps - // alone says); caps is a persistent toggle. - if(UI_KEYBOARD.shift && uiKeyboardIsShiftSensitive(index)) { - UI_KEYBOARD.shift = false; - uiKeyboardUpdateDisplay( - UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps - ); - } - } - - if( - UI_KEYBOARD.onKeyPress != NULL && - !UI_KEYBOARD.onKeyPress(c, UI_KEYBOARD.user) - ) { - return; - } - uiKeyboardAppendChar(c); -} - -errorret_t uiKeyboardInit(void) { - memoryZero(&UI_KEYBOARD, sizeof(uikeyboard_t)); - - uiLabelInit( - &UI_KEYBOARD.titleLabel, UI_KEYBOARD.titleText, - UI_KEYBOARD.titleSprites, UI_KEYBOARD_TITLE_SPRITES_MAX - ); - uiLabelInit( - &UI_KEYBOARD.textLabel, UI_KEYBOARD.text, - UI_KEYBOARD.textSprites, UI_KEYBOARD_TEXT_SPRITES_MAX - ); - uiLabelInit( - &UI_KEYBOARD.cursorLabel, UI_KEYBOARD_CURSOR_TEXT, - UI_KEYBOARD.cursorSprites, UI_KEYBOARD_CURSOR_SPRITES_MAX - ); - UI_KEYBOARD.cursorLabel.dirty = true; - - errorOk(); -} - -bool_t uiKeyboardIsOpen(void) { - return UI_KEYBOARD.open; -} - -void uiKeyboardOpen(const uikeyboardopen_t *open) { - assertNotNull(open, "Open parameters cannot be NULL"); - assertFalse(UI_KEYBOARD.open, "Keyboard is already open"); - assertTrue( - open->maxLength <= UI_KEYBOARD_TEXT_MAX - 1, - "maxLength exceeds UI_KEYBOARD_TEXT_MAX" - ); - assertTrue( - open->lineCount <= UI_KEYBOARD_LINE_COUNT_MAX, - "lineCount exceeds UI_KEYBOARD_LINE_COUNT_MAX" - ); - - stringCopy( - UI_KEYBOARD.titleText, - open->title != NULL ? open->title : UI_KEYBOARD_TITLE_DEFAULT, - UI_KEYBOARD_TITLE_TEXT_MAX - 1 - ); - UI_KEYBOARD.titleLabel.dirty = true; - uiLabelRebuffer(&UI_KEYBOARD.titleLabel); - - UI_KEYBOARD.text[0] = '\0'; - UI_KEYBOARD.textLabel.dirty = true; - uiLabelRebuffer(&UI_KEYBOARD.textLabel); - - UI_KEYBOARD.onInput = open->onInput; - UI_KEYBOARD.onKeyPress = open->onKeyPress; - UI_KEYBOARD.cancel = open->cancel; - UI_KEYBOARD.maxLength = open->maxLength == 0 ? - UI_KEYBOARD_TEXT_MAX - 1 : open->maxLength; - UI_KEYBOARD.lineCount = open->lineCount == 0 ? 1 : open->lineCount; - UI_KEYBOARD.confirm = open->confirm; - stringCopy( - UI_KEYBOARD.confirmQuestion, - open->confirmLabel != NULL ? - open->confirmLabel : UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT, - UI_KEYBOARD_CONFIRM_QUESTION_MAX - 1 - ); - UI_KEYBOARD.trimmed = open->trimmed; - UI_KEYBOARD.allowBlank = open->allowBlank; - UI_KEYBOARD.user = open->user; - UI_KEYBOARD.confirmed = false; - UI_KEYBOARD.caps = false; - UI_KEYBOARD.shift = false; - UI_KEYBOARD.open = true; - - // Builds the full fixed grid, including blank filler at the - // newline/confirm/cancel reserved slots - patched with real buttons - // below where applicable. Letters start lowercase and digits - // unshifted, matching caps/shift both being reset to false above. - uiKeyboardBuildItems(UI_KEYBOARD.items, false, false); - - if(UI_KEYBOARD.lineCount > 1) { - uiButtonInit( - &UI_KEYBOARD.items[UI_KEYBOARD_NEWLINE_INDEX].button, - UI_KEYBOARD_KEY_NEWLINE - ); - } - - uiButtonInit( - &UI_KEYBOARD.items[UI_KEYBOARD_CONFIRM_INDEX].button, - UI_KEYBOARD_CONFIRM_LABEL - ); - - if(UI_KEYBOARD.cancel) { - uiButtonInit( - &UI_KEYBOARD.items[UI_KEYBOARD_CANCEL_INDEX].button, - UI_KEYBOARD_CANCEL_LABEL - ); - } - - uiMenuInit( - &UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL - ); - uiMenuSetItems( - &UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_KEY_COUNT, - UI_KEYBOARD_COLUMNS - ); - - // Back deletes a character rather than closing the dialog outright - - // see uiKeyboardMenuCancel. - uiMenuSetCancelCallback(&UI_KEYBOARD.menu, uiKeyboardMenuCancel); - uiMenuOpen(&UI_KEYBOARD.menu); -} - -void uiKeyboardClose(void) { - if(!UI_KEYBOARD.open) return; - UI_KEYBOARD.open = false; - - if(uiMenuIsActive(&UI_KEYBOARD.menu)) { - uiMenuClose(&UI_KEYBOARD.menu); - } else { - uiKeyboardMenuClosed(NULL); - } -} - -errorret_t uiKeyboardDraw(void) { - if(!UI_KEYBOARD.open) errorOk(); - - float_t x = (float_t)SCREEN.scanX; - float_t y = (float_t)SCREEN.scanY; - float_t width = (float_t)SCREEN.scanWidth; - float_t height = (float_t)SCREEN.scanHeight; - - errorChain(uiFrameDraw(x, y, width, height)); - - float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - // When multi-line, reserve the full lineCount rows up front so the - // dialog doesn't resize as newlines are typed - otherwise fall back to - // the label's own measured height (floored at one row for an empty - // string, which measures to zero). - float_t textHeight = UI_KEYBOARD.lineCount > 1 ? - (float_t)UI_KEYBOARD.lineCount * rowHeight : - mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height); - uint8_t columns = UI_KEYBOARD.menu.columns; - uint8_t rows = (UI_KEYBOARD.menu.itemCount + columns - 1) / columns; - - float_t contentWidth = mathMax( - mathMax( - (float_t)UI_KEYBOARD.titleLabel.width, - (float_t)UI_KEYBOARD.textLabel.width - ), - (float_t)columns * UI_KEYBOARD_KEY_WIDTH - ); - float_t colStep = contentWidth / (float_t)columns; - float_t keyRowHeight = - colStep * (UI_KEYBOARD_KEY_HEIGHT / UI_KEYBOARD_KEY_WIDTH); - float_t contentHeight = rowHeight + UI_FRAME_PADDING_Y - + textHeight + UI_FRAME_PADDING_Y - + ((float_t)rows * keyRowHeight); - - // The frame now fills the whole screen, but the title/text/keys still - // lay out as one content block - center that block within the screen. - float_t contentX = x + (width - contentWidth) * 0.5f; - float_t contentY = y + (height - contentHeight) * 0.5f; - - uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX); - uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY); - errorChain(uiLabelRender(&UI_KEYBOARD.titleLabel, COLOR_WHITE)); - - float_t textY = contentY + rowHeight + UI_FRAME_PADDING_Y; - uiLabelSetX(&UI_KEYBOARD.textLabel, contentX); - uiLabelSetY(&UI_KEYBOARD.textLabel, textY); - errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE)); - - // Insertion point is always the end of text - there's no mid-string - // cursor movement - so find it by counting characters since the last - // newline (column) and newlines overall (line). Once maxLength is - // reached, a new key overrides the last character (see - // uiKeyboardAppendChar) rather than appending one, so stop one - // character short here too - the cursor should sit on that last - // character, not past it, to reflect what typing will actually do. - size_t textLength = strlen(UI_KEYBOARD.text); - size_t cursorLimit = textLength >= UI_KEYBOARD.maxLength ? - textLength - 1 : textLength; - - uint8_t cursorLine = 0; - uint8_t cursorCol = 0; - for(size_t i = 0; i < cursorLimit; i++) { - if(UI_KEYBOARD.text[i] == '\n') { cursorLine++; cursorCol = 0; } - else cursorCol++; - } - - if(mathModFloat(TIME.time, UI_KEYBOARD_CURSOR_BLINK_RATE * 2.0f) < - UI_KEYBOARD_CURSOR_BLINK_RATE - ) { - uiLabelSetX( - &UI_KEYBOARD.cursorLabel, - contentX + (float_t)cursorCol * (float_t)FONT_DEFAULT_TILE_WIDTH - ); - uiLabelSetY( - &UI_KEYBOARD.cursorLabel, textY + (float_t)cursorLine * rowHeight - ); - errorChain(uiLabelRender(&UI_KEYBOARD.cursorLabel, COLOR_WHITE)); - } - - // Drawn directly rather than via uiMenuDraw - that helper hardcodes row - // spacing to the font's native tile height, which would keep the grid - // vertically compact no matter how much contentWidth grows. Focus - // highlighting is unaffected: it's applied to item->button.highlighted - // by the focus system independently of whoever calls uiButtonDraw. - float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y; - for(uint8_t i = 0; i < UI_KEYBOARD.menu.itemCount; i++) { - float_t ix = contentX + (float_t)(i % columns) * colStep; - float_t iy = menuY + (float_t)(i / columns) * keyRowHeight; - errorChain(uiButtonDraw(&UI_KEYBOARD.items[i].button, ix, iy)); - } - - errorChain(spriteBatchFlush()); - errorOk(); -} - -errorret_t uiKeyboardDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.h b/src/dusk/ui/dialog/keyboard/uikeyboard.h deleted file mode 100644 index 565d7cba..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.h +++ /dev/null @@ -1,490 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" -#include "ui/widget/uimenu.h" -#include "display/text/font.h" - -// A fixed 11x5 QWERTY grid - the only layout that exists (numbers/ -// symbols layouts were removed as separate concepts; digits already -// live on this one): -// 1 2 3 4 5 6 7 8 9 0 DEL -// q w e r t y u i o p -// CAPS a s d f g h j k l (NEWLINE) -// SHIFT z x c v b n m SHIFT -// (CANCEL) SPACE (CONFIRM) -// Rows shorter than 11 columns are padded with blank filler buttons -// (empty label) so every key lands in its intended position - the menu/ -// focus grid only supports uniform columns, it has no concept of a key -// spanning multiple cells or a row being narrower than the grid, so this -// is the only way to get real positioning out of it. A filler cell is -// still a focusable/navigable-to grid slot, it just does nothing when -// selected - see uiKeyboardMenuSelected's blank-label check. A -// UI_KEYBOARD_KEYS_* table may use NULL for a slot with no key at all - -// see uiKeyboardGetCanonicalKey, which normalizes it to the same blank -// filler behavior. -// -// NEWLINE/CANCEL/CONFIRM are reserved slots the tables always leave -// blank - uiKeyboardOpen patches them with real buttons depending on -// lineCount/cancel (CONFIRM is unconditional, just needed as a fixed -// slot addressable by constant). -#define UI_KEYBOARD_COLUMNS 11 -#define UI_KEYBOARD_ROWS 5 -#define UI_KEYBOARD_KEY_COUNT (UI_KEYBOARD_COLUMNS * UI_KEYBOARD_ROWS) - -// Reserved slot indices uiKeyboardOpen patches with real buttons. -#define UI_KEYBOARD_NEWLINE_INDEX 32 -#define UI_KEYBOARD_CANCEL_INDEX 44 -#define UI_KEYBOARD_CONFIRM_INDEX 54 - -// Labels uiKeyboardMenuSelected checks for by pointer/string compare to -// tell special keys apart from an ordinary single-character key. A blank -// ("") label is a filler cell - see above. Identical across every -// UI_KEYBOARD_KEYS_* table, since these don't change with shift/caps. -#define UI_KEYBOARD_KEY_SPACE FONT_ICON_SPACE -#define UI_KEYBOARD_KEY_BACKSPACE FONT_ICON_BACKSPACE -#define UI_KEYBOARD_KEY_SHIFT FONT_ICON_SHIFT -#define UI_KEYBOARD_KEY_CAPS FONT_ICON_CAPSLOCK -// Only patched into UI_KEYBOARD_NEWLINE_INDEX when -// uikeyboardopen_t.lineCount > 1 - otherwise that slot stays blank. -#define UI_KEYBOARD_KEY_NEWLINE FONT_ICON_NEWLINE - -#define UI_KEYBOARD_TITLE_TEXT_MAX 64 -#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX -#define UI_KEYBOARD_TEXT_MAX 64 -#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX -// The grid is a fixed size - only one layout exists today, so this is -// just its key count. -#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_KEY_COUNT - -// Hardcoded for now - a uiModalOpen-style title/button text override is -// planned (see uikeyboardopen_t) but not built yet. Patched into the -// grid's reserved slots (UI_KEYBOARD_CONFIRM_INDEX etc.) rather than -// appended, since the grid is a fixed shape. -#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM" -#define UI_KEYBOARD_CANCEL_LABEL "CANCEL" -#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT" - -#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64 -// Used when uikeyboardopen_t.confirmLabel is NULL. -#define UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT "Is this correct?" - -#define UI_KEYBOARD_CANCEL_QUESTION_MAX 64 -// Used when uikeyboardopen_t.cancelConfirmLabel is NULL. -#define UI_KEYBOARD_CANCEL_QUESTION_DEFAULT "Discard your changes?" - -// Maximum number of lines uikeyboardopen_t.lineCount may request. -#define UI_KEYBOARD_LINE_COUNT_MAX 4 - -// Marks where the next typed character will be inserted (always the end -// of the current text - there's no mid-string cursor movement). -#define UI_KEYBOARD_CURSOR_TEXT "_" -#define UI_KEYBOARD_CURSOR_SPRITES_MAX 1 -// Seconds per on/off half-cycle of the cursor blink. -#define UI_KEYBOARD_CURSOR_BLINK_RATE 0.5f - -/** - * Callback invoked once the keyboard dialog is dismissed. - * - * @param text The entered text. Only meaningful when confirmed is true; - * points at UI_KEYBOARD's own buffer, so copy it out if it needs to - * outlive the next uiKeyboardOpen call. - * @param confirmed True if the confirm button was picked, false if - * cancelled. - * @param user Arbitrary pointer passed via uikeyboardopen_t.user. - */ -typedef void (*uikeyboardinputcallback_t)( - const char_t *text, - const bool_t confirmed, - void *user -); - -/** - * Callback invoked before a key press is applied, giving the caller a - * chance to reject it - e.g. an email field rejecting "%". - * - * @param c The character about to be entered (SPACE included, DEL/ - * confirm/cancel excluded - those aren't text being entered). - * @param user Arbitrary pointer passed via uikeyboardopen_t.user. - * @returns True to allow the character, false to reject it (ignored, - * nothing is entered). - */ -typedef bool_t (*uikeyboardkeypresscallback_t)(const char_t c, void *user); - -/** - * Parameters for uiKeyboardOpen. Expect this to grow (e.g. button text - * overrides) as more dialogs start using the keyboard. - */ -typedef struct { - // Title text shown above the entered text; copied internally, safe to - // be transient. Displayed as-is - resolve any locale string yourself - // before passing it in. NULL uses UI_KEYBOARD_TITLE_DEFAULT ("ENTER - // YOUR TEXT"). - const char_t *title; - - // Fired once the dialog is dismissed, confirmed or cancelled. May be - // NULL. - uikeyboardinputcallback_t onInput; - - // Fired for each key press before it's applied. May be NULL to allow - // everything. - uikeyboardkeypresscallback_t onKeyPress; - - // Whether the cancel button is shown, and whether pressing back with - // no text entered closes the dialog - see uiKeyboardMenuCancel. - bool_t cancel; - - // Maximum length of the entered text, in characters excluding the - // null terminator. 0 defaults to the buffer's full capacity - // (UI_KEYBOARD_TEXT_MAX - 1). Must not exceed that. - size_t maxLength; - - // When true, picking confirm opens a second uiConfirm "are you sure" - // dialog (see confirmLabel) on top of the keyboard before actually - // closing it - picking Cancel there returns to the keyboard with the - // text untouched. - bool_t confirm; - - // Question text/locale message ID for the second confirm dialog when - // confirm is true; copied internally, safe to be transient. NULL uses - // UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT. Ignored when confirm is false. - const char_t *confirmLabel; - - // When true, actually cancelling (the CANCEL button, or back with - // nothing left to delete - see uiKeyboardMenuCancel) opens a second - // uiConfirm "are you sure" dialog (see cancelConfirmLabel) before the - // keyboard closes - picking Cancel there returns to the keyboard with - // the text untouched. Ignored when cancel is false, since there's - // nothing to confirm. - bool_t cancelConfirm; - - // Question text/locale message ID for the cancel confirm dialog when - // cancelConfirm is true; copied internally, safe to be transient. NULL - // uses UI_KEYBOARD_CANCEL_QUESTION_DEFAULT. Ignored when cancelConfirm - // is false. - const char_t *cancelConfirmLabel; - - // If true, leading/trailing whitespace is trimmed from the entered - // text when confirm is pressed, before it's checked against - // allowBlank or passed to onInput. - bool_t trimmed; - - // If true, a blank result is accepted when confirm is pressed - blank - // means an empty string, or (when trimmed is false) a string of only - // spaces. When false, pressing confirm does nothing while the text is - // blank - see uiKeyboardTextIsBlank. - bool_t allowBlank; - - // Maximum number of lines the entered text can span, from 1 to - // UI_KEYBOARD_LINE_COUNT_MAX. 0 defaults to 1. When > 1, a NEWLINE key - // is added to the grid, capped at inserting lineCount - 1 newlines. - uint8_t lineCount; - - // Arbitrary pointer passed to onInput/onKeyPress. - void *user; -} uikeyboardopen_t; - -typedef struct { - uilabel_t titleLabel; - char_t titleText[UI_KEYBOARD_TITLE_TEXT_MAX]; - spritebatchsprite_t titleSprites[UI_KEYBOARD_TITLE_SPRITES_MAX]; - - uilabel_t textLabel; - char_t text[UI_KEYBOARD_TEXT_MAX]; - spritebatchsprite_t textSprites[UI_KEYBOARD_TEXT_SPRITES_MAX]; - - // Blinking insertion-point marker, positioned each draw at the end of - // text - a separate label rather than appending to text/textSprites, - // since neither buffer has spare room for it (see - // UI_KEYBOARD_TEXT_MAX/UI_KEYBOARD_TEXT_SPRITES_MAX). - uilabel_t cursorLabel; - spritebatchsprite_t cursorSprites[UI_KEYBOARD_CURSOR_SPRITES_MAX]; - - uimenu_t menu; - uimenuitem_t items[UI_KEYBOARD_ITEMS_MAX]; - - char_t confirmQuestion[UI_KEYBOARD_CONFIRM_QUESTION_MAX]; - char_t cancelConfirmQuestion[UI_KEYBOARD_CANCEL_QUESTION_MAX]; - - uikeyboardinputcallback_t onInput; - uikeyboardkeypresscallback_t onKeyPress; - void *user; - size_t maxLength; - uint8_t lineCount; - bool_t cancel; - bool_t confirm; - bool_t cancelConfirm; - bool_t trimmed; - bool_t allowBlank; - bool_t open; - bool_t confirmed; - - // Persistent caps-lock toggle, and a one-shot shift consumed by the - // next letter/digit/-/= typed - see uiKeyboardMenuSelected. Key labels - // are redrawn to match (see uiKeyboardUpdateDisplay) rather than - // staying static. - bool_t caps; - bool_t shift; -} uikeyboard_t; - -extern uikeyboard_t UI_KEYBOARD; - -/** - * Initializes the on-screen keyboard. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardInit(void); - -/** - * Draws the on-screen keyboard: a semi-transparent black backdrop - * covering the whole screen, then its own centered frame with the title, - * currently entered text, and the key grid. No-op when not open. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardDraw(void); - -/** - * Returns true when the keyboard dialog is currently open. - * - * @returns True if open. - */ -bool_t uiKeyboardIsOpen(void); - -/** - * Opens the on-screen keyboard with an empty text buffer. - * - * @param open Parameters for this dialog; copied internally, safe to be - * transient. Cannot be NULL. - */ -void uiKeyboardOpen(const uikeyboardopen_t *open); - -/** - * Closes the keyboard dialog, invoking the result callback set by - * uiKeyboardOpen as cancelled (confirmed=false) unless the confirm - * button was what triggered this close. No-op when already closed. - */ -void uiKeyboardClose(void); - -/** - * Disposes of the on-screen keyboard. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardDispose(void); - -/** - * Appends a character to the currently entered text, silently ignoring - * it once UI_KEYBOARD.maxLength is reached, or (for '\n') once the text - * already has UI_KEYBOARD.lineCount - 1 newlines. Does not consult - * onKeyPress - that happens in uiKeyboardMenuSelected before this is - * called. Moves focus to the confirm button once this append fills the - * text to maxLength - see uiKeyboardFocusConfirm. - * - * @param c The character to append. - */ -void uiKeyboardAppendChar(const char_t c); - -/** - * Moves the menu's focus to the confirm button. - */ -void uiKeyboardFocusConfirm(void); - -/** - * Returns whether the currently entered text counts as blank for - * UI_KEYBOARD.allowBlank purposes - an empty string, or (when - * UI_KEYBOARD.trimmed is false, so whitespace-only text was never - * reduced to empty) a string of only whitespace. - * - * @returns True if blank. - */ -bool_t uiKeyboardTextIsBlank(void); - -/** - * Removes the last character of the currently entered text. No-op when - * already empty. - */ -void uiKeyboardBackspace(void); - -/** - * Internal menu callback - routes a picked key/confirm/cancel item to - * uiKeyboardAppendChar/uiKeyboardBackspace/uiKeyboardClose, consulting - * onKeyPress before a character key is applied. When UI_KEYBOARD.confirm - * is set, picking confirm opens a second uiConfirm dialog (see - * uiKeyboardConfirmSecondaryResult) instead of closing immediately. - * SHIFT/CAPS toggle UI_KEYBOARD.shift/caps instead of entering anything. - * A blank ("") label is a filler/reserved grid cell and is a no-op. - * - * @param menu The keyboard's menu. - * @param index Index of the picked item. - * @param item The picked item. - */ -void uiKeyboardMenuSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -); - -/** - * Result callback for the second "are you sure" uiConfirm dialog opened - * when UI_KEYBOARD.confirm is set - see uiKeyboardMenuSelected. - * - * @param result True if Confirm was picked there, false if Cancel. - * @param user Unused. - */ -void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user); - -/** - * Actually cancels the keyboard - sets confirmed to false and closes the - * dialog. Shared by uiKeyboardMenuCancel (back with nothing to delete) - * and uiKeyboardMenuSelected (the CANCEL button); called directly when - * UI_KEYBOARD.cancelConfirm is false, or as the result callback (see - * uiKeyboardCancelConfirmSecondaryResult) once the "are you sure" dialog - * confirms it when cancelConfirm is true. - */ -void uiKeyboardDoCancel(void); - -/** - * Result callback for the "are you sure" uiConfirm dialog opened when - * UI_KEYBOARD.cancelConfirm is set - see uiKeyboardDoCancel. - * - * @param result True if Confirm was picked there, false if Cancel. - * @param user Unused. - */ -void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user); - -/** - * Internal menu callback - fires the result callback set by - * uiKeyboardOpen once the menu finishes closing. - * - * @param menu Unused; matches uimenuclosedcallback_t. - */ -void uiKeyboardMenuClosed(const uimenu_t *menu); - -/** - * Internal menu cancel callback (see uiMenuSetCancelCallback): back - * deletes the last entered character; with nothing left to delete, it - * closes the dialog as cancelled via uiKeyboardClose, but only when - * UI_KEYBOARD.cancel allows it - otherwise back is swallowed and does - * nothing. - * - * @param menu The keyboard's menu. - * @returns Always true - this always fully handles back itself - * (backspace, explicit close, or swallow), never falling through to the - * default pop. - */ -bool_t uiKeyboardMenuCancel(const uimenu_t *menu); - -/** - * Collapses a UI_KEYBOARD_KEYS_* table entry's NULL ("no key here") down - * to "" (blank filler), the only form of "nothing" callers need to - * handle. - * - * @param key A table entry, possibly NULL. - * @returns key, or "" if key was NULL. - */ -const char_t * uiKeyboardNormalizeKey(const char_t *key); - -/** - * Picks which of UI_KEYBOARD_KEYS_NONE/_CAPS/_SHIFT is active for the - * given state - shift takes priority over caps when both are set. - * - * @param shift Current shift state. - * @param caps Current caps state. - * @returns The selected table. - */ -const char_t *const * uiKeyboardSelectTable( - const bool_t shift, - const bool_t caps -); - -/** - * Builds the key grid's buttons into items, starting at index 0, one - * UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_KEY_COUNT of - * them, in row-major order across UI_KEYBOARD_COLUMNS columns). Each - * button's label is a single character, one of the UI_KEYBOARD_KEY_* - * literals, or "" for a blank filler/reserved slot, read straight from - * whichever of the three UI_KEYBOARD_KEYS_* tables shift/caps selects - * (see uiKeyboardGetChar) - can be refreshed later with - * uiKeyboardUpdateDisplay. See uiKeyboardGetCanonicalKey for identifying - * a picked key independent of its displayed character. - * - * @param items Destination array; must have room for at least - * UI_KEYBOARD_KEY_COUNT entries. - * @param shift Initial shift state - see uiKeyboardGetChar. - * @param caps Initial caps state - see uiKeyboardGetChar. - * @returns The number of items written (UI_KEYBOARD_KEY_COUNT). - */ -uint8_t uiKeyboardBuildItems( - uimenuitem_t *items, - const bool_t shift, - const bool_t caps -); - -/** - * Repoints every key already built into items by uiKeyboardBuildItems at - * whichever UI_KEYBOARD_KEYS_* table the new shift/caps state selects, - * marking all of them dirty so they redraw. Unconditional (not just the - * letter/digit/-/= keys that actually differ between tables) - simpler - * than tracking which changed, and cheap since this only runs on an - * infrequent SHIFT/CAPS press. - * - * @param items The same array passed to uiKeyboardBuildItems. - * @param shift New shift state - see uiKeyboardGetChar. - * @param caps New caps state - see uiKeyboardGetChar. - */ -void uiKeyboardUpdateDisplay( - uimenuitem_t *items, - const bool_t shift, - const bool_t caps -); - -/** - * Returns a grid slot's canonical (unshifted, no-caps) key label, - * independent of whatever character it's currently drawn as - use this - * to identify which key was picked instead of reading a selected item's - * own (possibly shifted) label.text. - * - * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1. - * @returns The slot's canonical label. - */ -const char_t * uiKeyboardGetCanonicalKey(const uint8_t index); - -/** - * True if a grid slot's character actually differs between the - * unshifted and shifted tables - i.e. a letter, digit, '-', or '='. False - * for a slot whose UI_KEYBOARD_KEYS_* entry is the same in every table - * (SPACE, DEL, SHIFT, CAPS, or blank filler). - * - * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1. - * @returns True if shift affects this slot's character. - */ -bool_t uiKeyboardIsShiftSensitive(const uint8_t index); - -/** - * Looks up the character a grid slot represents under the given shift/ - * caps state, straight from whichever UI_KEYBOARD_KEYS_* table they - * select - shift takes priority over caps when both are set (so, unlike - * a physical keyboard, shift while caps is on still gives uppercase, not - * lowercase - there is no fourth table for that combination). For a slot - * whose label isn't a single character (a blank filler cell, or a - * multi-character special key like "SPACE"), returns that label's first - * byte ('\0' for blank). - * - * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1. - * @param shift Current shift state (one-shot, see UI_KEYBOARD.shift). - * @param caps Current caps state (persistent toggle, see UI_KEYBOARD.caps). - * @returns The character to display/type. - */ -char_t uiKeyboardGetChar( - const uint8_t index, - const bool_t shift, - const bool_t caps -); diff --git a/src/dusk/ui/dialog/save/CMakeLists.txt b/src/dusk/ui/dialog/save/CMakeLists.txt deleted file mode 100644 index aae3928e..00000000 --- a/src/dusk/ui/dialog/save/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uisaveslot.c - uiselectsave.c -) diff --git a/src/dusk/ui/dialog/save/uisaveslot.c b/src/dusk/ui/dialog/save/uisaveslot.c deleted file mode 100644 index b4e90486..00000000 --- a/src/dusk/ui/dialog/save/uisaveslot.c +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uisaveslot.h" -#include "ui/widget/uiframe.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/color.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" - -void uiSaveSlotInit( - uisaveslot_t *saveSlot, - const uint8_t slotIndex, - const char_t *fileName -) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - assertNotNull(fileName, "File name cannot be NULL"); - - memoryZero(saveSlot, sizeof(uisaveslot_t)); - saveSlot->slotIndex = slotIndex; - - uiLabelInit( - &saveSlot->numberLabel, saveSlot->numberText, - saveSlot->numberSprites, UI_SAVE_SLOT_NUMBER_SPRITES_MAX - ); - errorret_t result = localeManagerGetText( - "ui.save_slot.number_format", - saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX, 0, - slotIndex + 1 - ); - if(errorIsNotOk(result)) errorCatch(result); - saveSlot->numberLabel.dirty = true; - - uiLabelInit( - &saveSlot->nameLabel, saveSlot->nameText, - saveSlot->nameSprites, UI_SAVE_SLOT_NAME_SPRITES_MAX - ); - stringCopy(saveSlot->nameText, fileName, UI_SAVE_SLOT_NAME_TEXT_MAX); - saveSlot->nameLabel.dirty = true; -} - -bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - return saveSlot->highlighted; -} - -void uiSaveSlotSetHighlighted( - uisaveslot_t *saveSlot, const bool_t highlighted -) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - saveSlot->highlighted = highlighted; -} - -bool_t uiSaveSlotIsCorrupt(const uisaveslot_t *saveSlot) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - return saveSlot->corrupt; -} - -void uiSaveSlotSetCorrupt(uisaveslot_t *saveSlot, const bool_t corrupt) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - saveSlot->corrupt = corrupt; -} - -uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - return saveSlot->displayType; -} - -void uiSaveSlotSetDisplayType( - uisaveslot_t *saveSlot, - const uisaveslotdisplaytype_t displayType -) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - saveSlot->displayType = displayType; -} - -errorret_t uiSaveSlotDraw( - uisaveslot_t *saveSlot, - const float_t x, - const float_t y, - const float_t width, - const float_t height -) { - assertNotNull(saveSlot, "Save slot cannot be NULL"); - - errorChain(uiFrameDraw(x, y, width, height)); - - const float_t contentX = x + UI_FRAME_START_X; - const float_t contentY = y + UI_FRAME_START_Y; - - color_t color; - if(saveSlot->corrupt) { - color = COLOR_RED; - } else if(saveSlot->highlighted) { - color = COLOR_RED; - } else if(saveSlot->displayType == UI_SAVE_SLOT_DISPLAY_TYPE_DELETE) { - color = COLOR_ORANGE; - } else { - color = COLOR_WHITE; - } - - uiLabelSetX(&saveSlot->numberLabel, contentX); - uiLabelSetY(&saveSlot->numberLabel, contentY); - errorChain(uiLabelRender(&saveSlot->numberLabel, color)); - - const float_t nameY = contentY + (float_t)saveSlot->numberLabel.height + - UI_FRAME_PADDING_Y; - uiLabelSetX(&saveSlot->nameLabel, contentX); - uiLabelSetY(&saveSlot->nameLabel, nameY); - errorChain(uiLabelRender(&saveSlot->nameLabel, color)); - - errorOk(); -} diff --git a/src/dusk/ui/dialog/save/uisaveslot.h b/src/dusk/ui/dialog/save/uisaveslot.h deleted file mode 100644 index 31b7ea43..00000000 --- a/src/dusk/ui/dialog/save/uisaveslot.h +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" - -#define UI_SAVE_SLOT_NUMBER_TEXT_MAX 16 -#define UI_SAVE_SLOT_NUMBER_SPRITES_MAX UI_SAVE_SLOT_NUMBER_TEXT_MAX -#define UI_SAVE_SLOT_NAME_TEXT_MAX 64 -#define UI_SAVE_SLOT_NAME_SPRITES_MAX UI_SAVE_SLOT_NAME_TEXT_MAX - -/** - * How a save slot entry should be styled: NORMAL for everyday - * save/load browsing, DELETE to visually flag the row as part of a - * destructive delete flow. - */ -typedef enum { - UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL, - UI_SAVE_SLOT_DISPLAY_TYPE_DELETE -} uisaveslotdisplaytype_t; - -/** - * A single framed save slot entry: a slot number and a file name. Later - * this will grow to show more per-slot save data (level, playtime, - * etc). - */ -typedef struct { - uint8_t slotIndex; - uisaveslotdisplaytype_t displayType; - - uilabel_t numberLabel; - char_t numberText[UI_SAVE_SLOT_NUMBER_TEXT_MAX]; - spritebatchsprite_t numberSprites[UI_SAVE_SLOT_NUMBER_SPRITES_MAX]; - - uilabel_t nameLabel; - char_t nameText[UI_SAVE_SLOT_NAME_TEXT_MAX]; - spritebatchsprite_t nameSprites[UI_SAVE_SLOT_NAME_SPRITES_MAX]; - - bool_t highlighted; - bool_t corrupt; -} uisaveslot_t; - -/** - * Initializes a save slot widget with the given slot index and display - * file name. - * - * @param saveSlot The save slot widget to initialize. - * @param slotIndex The save slot index this widget represents. - * @param fileName Display file name; copied internally, safe to be - * transient. - */ -void uiSaveSlotInit( - uisaveslot_t *saveSlot, - const uint8_t slotIndex, - const char_t *fileName -); - -/** - * Returns whether the save slot widget is highlighted. - * - * @param saveSlot The save slot widget to query. - * @returns True if highlighted. - */ -bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot); - -/** - * Sets the highlighted state of the save slot widget. - * - * @param saveSlot The save slot widget to update. - * @param highlighted The new highlighted state. - */ -void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted); - -/** - * Returns whether the save slot widget is flagged as corrupt. - * - * @param saveSlot The save slot widget to query. - * @returns True if flagged as corrupt. - */ -bool_t uiSaveSlotIsCorrupt(const uisaveslot_t *saveSlot); - -/** - * Sets whether the save slot widget is flagged as corrupt - drawn in red - * regardless of highlighted/display-type state, taking priority over both. - * - * @param saveSlot The save slot widget to update. - * @param corrupt The new corrupt state. - */ -void uiSaveSlotSetCorrupt(uisaveslot_t *saveSlot, const bool_t corrupt); - -/** - * Returns the save slot widget's display type. - * - * @param saveSlot The save slot widget to query. - * @returns The display type. - */ -uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot); - -/** - * Sets the save slot widget's display type. - * - * @param saveSlot The save slot widget to update. - * @param displayType The new display type. - */ -void uiSaveSlotSetDisplayType( - uisaveslot_t *saveSlot, - const uisaveslotdisplaytype_t displayType -); - -/** - * Draws the save slot widget as a framed box at the given position and - * size, showing its slot number and file name. - * - * @param saveSlot The save slot widget to draw. - * @param x Screen x position. - * @param y Screen y position. - * @param width Frame width. - * @param height Frame height. - * @return Any error that occurs. - */ -errorret_t uiSaveSlotDraw( - uisaveslot_t *saveSlot, - const float_t x, - const float_t y, - const float_t width, - const float_t height -); diff --git a/src/dusk/ui/dialog/save/uiselectsave.c b/src/dusk/ui/dialog/save/uiselectsave.c deleted file mode 100644 index dcdf4c2c..00000000 --- a/src/dusk/ui/dialog/save/uiselectsave.c +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiselectsave.h" -#include "ui/widget/uiframe.h" -#include "ui/widget/uimodal.h" -#include "ui/dialog/uiconfirm.h" -#include "ui/dialog/keyboard/uikeyboard.h" -#include "ui/overlay/uifatalerror.h" -#include "save/save.h" -#include "time/timeepoch.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" - -uiselectsave_t UI_SELECT_SAVE; - -void uiSelectSaveFormatSlot( - const uint8_t index, - char_t *buffer, - const size_t bufferSize -) { - saveslotcache_t *cache = &SAVE.caches[index]; - - if(cache->corrupt) { - errorret_t result = localeManagerGetString( - "ui.select_save.corrupt", buffer, bufferSize, 0 - ); - if(errorIsNotOk(result)) errorCatch(result); - return; - } - - if(!saveSlotInUse(cache)) { - errorret_t result = localeManagerGetString( - "ui.select_save.empty", buffer, bufferSize, 0 - ); - if(errorIsNotOk(result)) errorCatch(result); - return; - } - - char_t dateText[32]; - timeEpochFormat(cache->time, "%Y-%m-%d %H:%M", dateText, sizeof(dateText)); - - errorret_t result = localeManagerGetText( - "ui.select_save.slot_format", buffer, bufferSize, 0, - cache->name, cache->playerLevel, dateText - ); - if(errorIsNotOk(result)) errorCatch(result); -} - -void uiSelectSaveRefreshSlot(const uint8_t index) { - char_t fileName[UI_SAVE_SLOT_NAME_TEXT_MAX]; - uiSelectSaveFormatSlot(index, fileName, sizeof(fileName)); - uiSaveSlotInit(&UI_SELECT_SAVE.slots[index], index, fileName); - - uisaveslotdisplaytype_t displayType = - UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE ? - UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL; - uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[index], displayType); - uiSaveSlotSetCorrupt( - &UI_SELECT_SAVE.slots[index], SAVE.caches[index].corrupt - ); -} - -void uiSelectSaveSetActionLabel(const uiselectsavetype_t type) { - const char_t *labelKey = type == UI_SELECT_SAVE_TYPE_LOAD ? - "ui.select_save.delete_mode" : "ui.confirm.cancel"; - - errorret_t result = localeManagerGetString( - labelKey, UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX, 0 - ); - if(errorIsNotOk(result)) errorCatch(result); - - UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button.label.dirty = - true; -} - -void uiSelectSaveSwitchType(const uiselectsavetype_t type) { - UI_SELECT_SAVE.type = type; - - uisaveslotdisplaytype_t displayType = type == UI_SELECT_SAVE_TYPE_DELETE ? - UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL; - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[i], displayType); - } - - uiSelectSaveSetActionLabel(type); -} - -void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) { - if(!result) return; - - assertTrue(SAVE.deviceCurrent != 0xFF, "No current device"); - const uint8_t index = UI_SELECT_SAVE.pendingDeleteIndex; - - // uiSelectSaveOpen is only ever reached before any gameplay starts (see - // uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever - // index is being written here - nothing else is relying on it. - SAVE.slotCurrent = index; - errorret_t initResult = saveSlotInit(&SAVE.slot); - if(errorIsNotOk(initResult)) { - errorCatch(errorPrint(initResult)); - uiFatalErrorOpen(initResult.state->message); - return; - } - - errorret_t writeResult = saveDeviceSlotWrite( - &SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index - ); - if(errorIsNotOk(writeResult)) { - errorCatch(errorPrint(writeResult)); - return; - } - - SAVE.caches[index] = SAVE.slot.cachedData; - - uiSelectSaveSwitchType(UI_SELECT_SAVE_TYPE_LOAD); - uiSelectSaveRefreshSlot(index); - uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[index], true); -} - -void uiSelectSaveConfirmDelete(const uint8_t index) { - UI_SELECT_SAVE.pendingDeleteIndex = index; - uiConfirmOpen( - "ui.select_save.delete_confirm", uiSelectSaveDeleteConfirmed, NULL - ); -} - -void uiSelectSaveNameEntered( - const char_t *text, - const bool_t confirmed, - void *user -) { - if(!confirmed) return; - - const uint8_t index = UI_SELECT_SAVE.pendingNameIndex; - - // uiSelectSaveOpen is only ever reached before any gameplay starts (see - // uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever - // index is being written here - nothing else is relying on it. - SAVE.slotCurrent = index; - errorret_t newGameResult = saveSlotNewGame(); - if(errorIsNotOk(newGameResult)) { - errorCatch(errorPrint(newGameResult)); - uiFatalErrorOpen(newGameResult.state->message); - return; - } - stringCopy(SAVE.slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH); - - // No save device available - proceed with an in-memory-only slot rather - // than writing to a device that doesn't exist (see saveLoadSlot()'s own - // comment on the same "continue without a save device" flow). - if(SAVE.deviceCurrent != 0xFF) { - errorret_t writeResult = saveDeviceSlotWrite( - &SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index - ); - if(errorIsNotOk(writeResult)) { - errorCatch(errorPrint(writeResult)); - uiFatalErrorOpen(writeResult.state->message); - return; - } - } - - SAVE.caches[index] = SAVE.slot.cachedData; - uiSelectSaveRefreshSlot(index); - - UI_SELECT_SAVE.result = index; - uiSelectSaveClose(); -} - -void uiSelectSaveNameEmptySlot(const uint8_t index) { - UI_SELECT_SAVE.pendingNameIndex = index; - - char_t title[UI_KEYBOARD_TITLE_TEXT_MAX]; - errorret_t result = localeManagerGetString( - "ui.select_save.name_title", title, sizeof(title), 0 - ); - if(errorIsNotOk(result)) errorCatch(result); - - uikeyboardopen_t open = { - .title = title, - .onInput = uiSelectSaveNameEntered, - .cancel = true, - .maxLength = SAVE_SLOT_NAME_LENGTH, - .trimmed = true, - .allowBlank = false - }; - uiKeyboardOpen(&open); -} - -void uiSelectSaveWarnCorrupt(void) { - const char_t *options[] = { "ui.confirm.confirm" }; - // Acknowledging just closes the warning - the player stays on the - // select-save list rather than being pushed into naming/overwriting - // the slot. - uiModalOpen( - "ui.select_save.corrupt", "ui.select_save.corrupt_warning", - options, 1, - NULL, NULL, NULL - ); -} - -bool_t uiSelectSaveMenuCancelled(const uimenu_t *menu) { - if(UI_SELECT_SAVE.type != UI_SELECT_SAVE_TYPE_DELETE) return false; - - uiSelectSaveSwitchType(UI_SELECT_SAVE_TYPE_LOAD); - return true; -} - -void uiSelectSaveSlotSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - if(UI_SELECT_SAVE.hasAction && index == UI_SELECT_SAVE_ACTION_INDEX) { - uiSelectSaveSwitchType( - UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_LOAD ? - UI_SELECT_SAVE_TYPE_DELETE : UI_SELECT_SAVE_TYPE_LOAD - ); - return; - } - - if(UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE) { - uiSelectSaveConfirmDelete(index); - return; - } - - if(SAVE.caches[index].corrupt) { - uiSelectSaveWarnCorrupt(); - return; - } - - if(!saveSlotInUse(&SAVE.caches[index])) { - uiSelectSaveNameEmptySlot(index); - return; - } - - UI_SELECT_SAVE.result = index; - uiSelectSaveClose(); -} - -void uiSelectSaveSlotChanged( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], i == index); - } - - // The action row sits fixed below the scrollable list, so it never - // needs to be scrolled into view. - if(index >= SAVE_SLOT_COUNT) return; - - uiScrollingEnsureVisible( - &UI_SELECT_SAVE.scroll, index, SAVE_SLOT_COUNT, UI_SELECT_SAVE_VISIBLE_ROWS - ); -} - -void uiSelectSaveSlotClosed(const uimenu_t *menu) { - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], false); - } - - if(UI_SELECT_SAVE.callback != NULL) { - UI_SELECT_SAVE.callback(UI_SELECT_SAVE.result, UI_SELECT_SAVE.user); - } -} - -errorret_t uiSelectSaveInit(void) { - memoryZero(&UI_SELECT_SAVE, sizeof(uiselectsave_t)); - - uiLabelInit( - &UI_SELECT_SAVE.titleLabel, UI_SELECT_SAVE.titleText, - UI_SELECT_SAVE.titleSprites, UI_SELECT_SAVE_TITLE_SPRITES_MAX - ); - errorChain(localeManagerGetString( - "ui.select_save.title", - UI_SELECT_SAVE.titleText, - UI_SELECT_SAVE_TITLE_TEXT_MAX, - 0 - )); - UI_SELECT_SAVE.titleLabel.dirty = true; - uiLabelRebuffer(&UI_SELECT_SAVE.titleLabel); - - uiMenuInit( - &UI_SELECT_SAVE.slotMenu, uiSelectSaveSlotSelected, uiSelectSaveSlotClosed, - uiSelectSaveSlotChanged - ); - uiMenuSetCancelCallback(&UI_SELECT_SAVE.slotMenu, uiSelectSaveMenuCancelled); - uiMenuSetItems( - &UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, SAVE_SLOT_COUNT, 1 - ); - UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].type = - UI_MENU_WIDGET_TYPE_BUTTON; - uiButtonInit( - &UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button, - UI_SELECT_SAVE.actionText - ); - - uiScrollingInit(&UI_SELECT_SAVE.scroll); - - errorOk(); -} - -errorret_t uiSelectSaveDraw(void) { - if(!uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) errorOk(); - - const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - const float_t rowFrameHeight = - (UI_FRAME_START_Y * 2) + (rowHeight * 2) + UI_FRAME_PADDING_Y; - const float_t listHeight = - (rowFrameHeight * (float_t)UI_SELECT_SAVE_VISIBLE_ROWS) + - (UI_SELECT_SAVE_ROW_GAP * (float_t)(UI_SELECT_SAVE_VISIBLE_ROWS - 1)); - - const float_t width = UI_SELECT_SAVE_WIDTH; - const float_t height = (UI_FRAME_START_Y * 2) - + (float_t)UI_SELECT_SAVE.titleLabel.height + UI_FRAME_PADDING_Y - + listHeight - + (UI_SELECT_SAVE.hasAction ? UI_FRAME_PADDING_Y + rowHeight : 0.0f); - - const float_t x = (float_t)SCREEN.scanX + - ((float_t)SCREEN.scanWidth - width) * 0.5f; - const float_t y = (float_t)SCREEN.scanY + - ((float_t)SCREEN.scanHeight - height) * 0.5f; - - errorChain(uiFrameDraw(x, y, width, height)); - - const float_t contentX = x + UI_FRAME_START_X; - const float_t contentY = y + UI_FRAME_START_Y; - const float_t contentWidth = width - (UI_FRAME_START_X * 2); - - uiLabelSetX(&UI_SELECT_SAVE.titleLabel, contentX); - uiLabelSetY(&UI_SELECT_SAVE.titleLabel, contentY); - errorChain(uiLabelRender(&UI_SELECT_SAVE.titleLabel, COLOR_WHITE)); - - const float_t listY = contentY + (float_t)UI_SELECT_SAVE.titleLabel.height + - UI_FRAME_PADDING_Y; - - const uint8_t offset = UI_SELECT_SAVE.scroll.offset; - uint8_t visibleEnd = (uint8_t)(offset + UI_SELECT_SAVE_VISIBLE_ROWS); - if(visibleEnd > SAVE_SLOT_COUNT) visibleEnd = SAVE_SLOT_COUNT; - - for(uint8_t i = offset; i < visibleEnd; i++) { - const float_t iy = - listY + (float_t)(i - offset) * (rowFrameHeight + UI_SELECT_SAVE_ROW_GAP); - errorChain(uiSaveSlotDraw( - &UI_SELECT_SAVE.slots[i], contentX, iy, contentWidth, rowFrameHeight - )); - } - - if(UI_SELECT_SAVE.hasAction) { - const float_t actionY = listY + listHeight + UI_FRAME_PADDING_Y; - errorChain(uiButtonDraw( - &UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button, - contentX, actionY - )); - } - - errorChain(spriteBatchFlush()); - errorOk(); -} - -bool_t uiSelectSaveIsOpen(void) { - return uiMenuIsActive(&UI_SELECT_SAVE.slotMenu); -} - -void uiSelectSaveOpen( - const uiselectsavetype_t type, - uiselectsaveresultcallback_t callback, - void *user -) { - assertNotNull(callback, "Callback cannot be NULL"); - if(uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) return; - - UI_SELECT_SAVE.type = type; - UI_SELECT_SAVE.callback = callback; - UI_SELECT_SAVE.user = user; - UI_SELECT_SAVE.result = UI_SELECT_SAVE_RESULT_NONE; - - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - uiSelectSaveRefreshSlot(i); - } - - UI_SELECT_SAVE.hasAction = - type == UI_SELECT_SAVE_TYPE_LOAD || type == UI_SELECT_SAVE_TYPE_DELETE; - uint8_t itemCount = SAVE_SLOT_COUNT; - if(UI_SELECT_SAVE.hasAction) { - uiSelectSaveSetActionLabel(type); - itemCount = UI_SELECT_SAVE_ITEM_CAPACITY; - } - uiMenuSetItems( - &UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, itemCount, 1 - ); - - uiScrollingInit(&UI_SELECT_SAVE.scroll); - uiMenuOpen(&UI_SELECT_SAVE.slotMenu); -} - -void uiSelectSaveClose(void) { - uiMenuClose(&UI_SELECT_SAVE.slotMenu); -} - -errorret_t uiSelectSaveDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/dialog/save/uiselectsave.h b/src/dusk/ui/dialog/save/uiselectsave.h deleted file mode 100644 index ccc5bf31..00000000 --- a/src/dusk/ui/dialog/save/uiselectsave.h +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" -#include "ui/widget/uimenu.h" -#include "ui/widget/uiscrolling.h" -#include "ui/dialog/save/uisaveslot.h" -#include "save/slot/saveslot.h" - -#define UI_SELECT_SAVE_TITLE_TEXT_MAX 64 -#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX -#define UI_SELECT_SAVE_ACTION_LABEL_MAX 32 -#define UI_SELECT_SAVE_VISIBLE_ROWS (SAVE_SLOT_COUNT < 3 ? SAVE_SLOT_COUNT : 3) -#define UI_SELECT_SAVE_WIDTH 260.0f -#define UI_SELECT_SAVE_ROW_GAP 4.0f -#define UI_SELECT_SAVE_RESULT_NONE 0xFF - -// Slot rows plus one trailing action row (switch-to-delete/cancel), only -// present for the LOAD and DELETE types - see uiSelectSaveOpen. -#define UI_SELECT_SAVE_ITEM_CAPACITY (SAVE_SLOT_COUNT + 1) -#define UI_SELECT_SAVE_ACTION_INDEX SAVE_SLOT_COUNT - -/** - * The action the select-save panel is being used for. The caller - * chooses this when calling uiSelectSaveOpen. - */ -typedef enum { - UI_SELECT_SAVE_TYPE_SAVE, - UI_SELECT_SAVE_TYPE_LOAD, - UI_SELECT_SAVE_TYPE_DELETE -} uiselectsavetype_t; - -/** - * Callback invoked once the select-save panel closes: slotIndex is the - * chosen slot, or UI_SELECT_SAVE_RESULT_NONE if backed out of without - * choosing. - * - * @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE. - * @param user Arbitrary pointer passed to uiSelectSaveOpen. - */ -typedef void (*uiselectsaveresultcallback_t)( - const uint8_t slotIndex, void *user -); - -typedef struct { - uilabel_t titleLabel; - char_t titleText[UI_SELECT_SAVE_TITLE_TEXT_MAX]; - spritebatchsprite_t titleSprites[UI_SELECT_SAVE_TITLE_SPRITES_MAX]; - - uiselectsavetype_t type; - bool_t hasAction; - char_t actionText[UI_SELECT_SAVE_ACTION_LABEL_MAX]; - - uimenu_t slotMenu; - uimenuitem_t slotItems[UI_SELECT_SAVE_ITEM_CAPACITY]; - uisaveslot_t slots[SAVE_SLOT_COUNT]; - uiscrolling_t scroll; - uint8_t pendingDeleteIndex; - uint8_t pendingNameIndex; - - uiselectsaveresultcallback_t callback; - void *user; - uint8_t result; -} uiselectsave_t; - -extern uiselectsave_t UI_SELECT_SAVE; - -/** - * Initializes the select-save panel. - * - * @return Any error that occurs. - */ -errorret_t uiSelectSaveInit(void); - -/** - * Draws the select-save panel: a centered frame with a title, a - * scrolling list of save/save.h's slots (each shown as a framed - * uisaveslot_t, showing at most UI_SELECT_SAVE_VISIBLE_ROWS at a time), - * and - when UI_SELECT_SAVE.hasAction is set - the switch-to-delete/ - * cancel action row fixed below the scrollable list, outside it. No-op - * when not open. - * - * @return Any error that occurs. - */ -errorret_t uiSelectSaveDraw(void); - -/** - * Returns true when the select-save panel is currently open. - * - * @returns True if open. - */ -bool_t uiSelectSaveIsOpen(void); - -/** - * Opens the select-save panel for the given action, refreshing each row - * from the current SAVE.caches state. callback is invoked exactly once - * with the result, whether a slot was chosen or the panel was backed - * out of without choosing. - * - * UI_SELECT_SAVE_TYPE_LOAD adds a trailing row to switch to - * UI_SELECT_SAVE_TYPE_DELETE in place, and UI_SELECT_SAVE_TYPE_DELETE - * adds a trailing "Cancel" row to switch back to - * UI_SELECT_SAVE_TYPE_LOAD - neither closes the panel or invokes - * callback, they just toggle UI_SELECT_SAVE.type and the slot rows' - * styling. UI_SELECT_SAVE_TYPE_SAVE has no trailing row. - * - * @param type The action the caller is opening the panel for. - * @param callback Called with the result once the panel closes. Cannot - * be NULL. - * @param user Arbitrary pointer passed through to callback. - */ -void uiSelectSaveOpen( - const uiselectsavetype_t type, - uiselectsaveresultcallback_t callback, - void *user -); - -/** - * Closes the select-save panel. No-op when already closed. - */ -void uiSelectSaveClose(void); - -/** - * Disposes of the select-save panel. - * - * @return Any error that occurs. - */ -errorret_t uiSelectSaveDispose(void); diff --git a/src/dusk/ui/dialog/uiconfirm.c b/src/dusk/ui/dialog/uiconfirm.c deleted file mode 100644 index e7da213d..00000000 --- a/src/dusk/ui/dialog/uiconfirm.c +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiconfirm.h" -#include "ui/widget/uimodal.h" -#include "assert/assert.h" -#include "util/memory.h" - -#define UI_CONFIRM_INDEX_CONFIRM 0 -#define UI_CONFIRM_INDEX_CANCEL 1 - -uiconfirm_t UI_CONFIRM; - -void uiConfirmModalCallback(const uint8_t optionIndex, void *user) { - UI_CONFIRM.result = optionIndex == UI_CONFIRM_INDEX_CONFIRM; - if(UI_CONFIRM.callback != NULL) { - UI_CONFIRM.callback(UI_CONFIRM.result, user); - } -} - -errorret_t uiConfirmInit(void) { - memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t)); - errorOk(); -} - -bool_t uiConfirmIsOpen(void) { - return uiModalIsOpen(); -} - -bool_t uiConfirmGetResult(void) { - return UI_CONFIRM.result; -} - -void uiConfirmOpen( - const char_t *question, - uiconfirmcallback_t callback, - void *user -) { - assertNotNull(question, "Question cannot be NULL"); - - UI_CONFIRM.callback = callback; - UI_CONFIRM.result = false; - - const char_t *options[] = { - "ui.confirm.confirm", - "ui.confirm.cancel" - }; - uiModalOpen( - NULL, question, options, sizeof(options) / sizeof(options[0]), - uiConfirmModalCallback, NULL, user - ); -} - -void uiConfirmClose(void) { - uiModalClose(NULL); -} - -errorret_t uiConfirmDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/dialog/uiconfirm.h b/src/dusk/ui/dialog/uiconfirm.h deleted file mode 100644 index 3b76ac59..00000000 --- a/src/dusk/ui/dialog/uiconfirm.h +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -/** - * Callback invoked once a confirm dialog is dismissed. Back/cancel - * input is disabled while it's open, so the dialog can only be - * dismissed by picking Confirm or Cancel. - * - * @param result True if Confirm was selected, false if Cancel was - * selected. - * @param user Arbitrary pointer passed to uiConfirmOpen. - */ -typedef void (*uiconfirmcallback_t)(const bool_t result, void *user); - -typedef struct { - uiconfirmcallback_t callback; - bool_t result; -} uiconfirm_t; - -extern uiconfirm_t UI_CONFIRM; - -/** - * Initializes the confirm dialog. - * - * @return Any error that occurs. - */ -errorret_t uiConfirmInit(void); - -/** - * Returns true when the confirm dialog is currently open. - * - * @returns True if open. - */ -bool_t uiConfirmIsOpen(void); - -/** - * Returns the result of the most recently dismissed confirm dialog. - * - * @returns True if Confirm was selected, false otherwise. - */ -bool_t uiConfirmGetResult(void); - -/** - * Opens a confirm dialog, backed by the shared uimodal_t, with the given - * question text and Confirm/Cancel buttons. callback is invoked exactly - * once with the result once a button is selected. - * - * @param question Display text, or a locale message ID; copied - * internally by uimodal, safe to be transient. - * @param callback Called with the result once the dialog closes. May be - * NULL. - * @param user Arbitrary pointer passed through to callback. - */ -void uiConfirmOpen( - const char_t *question, - uiconfirmcallback_t callback, - void *user -); - -/** - * Closes the confirm dialog. No-op when already closed. - */ -void uiConfirmClose(void); - -/** - * Disposes of the confirm dialog. - * - * @return Any error that occurs. - */ -errorret_t uiConfirmDispose(void); diff --git a/src/dusk/ui/focus/CMakeLists.txt b/src/dusk/ui/focus/CMakeLists.txt deleted file mode 100644 index 819217b7..00000000 --- a/src/dusk/ui/focus/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uifocus.c -) diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c deleted file mode 100644 index 443ed035..00000000 --- a/src/dusk/ui/focus/uifocus.c +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uifocus.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "input/input.h" -#include "time/time.h" - -const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = { - { INPUT_ACTION_UP, UI_FOCUS_DIRECTION_UP, 0, -1 }, - { INPUT_ACTION_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 }, - { INPUT_ACTION_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 }, - { INPUT_ACTION_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 }, - { INPUT_ACTION_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 }, -}; - -uifocus_t UI_FOCUS; - -void uiFocusInit(void) { - memoryZero(&UI_FOCUS, sizeof(uifocus_t)); -} - -uifocusitem_t * uiFocusPush( - const uint8_t cols, - const uint8_t rows, - uifocusitemcallback_t selected, - uifocusitemcallback_t changed, - uifocusitemcallback_t closed, - uifocusitemdirectioncallback_t direction, - void *user -) { - assertTrue( - UI_FOCUS.count < UI_FOCUS_STACK_MAX, - "UI focus stack overflow" - ); - assertTrue(cols > 0, "Focus item cols must be > 0"); - assertTrue(rows > 0, "Focus item rows must be > 0"); - - uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count]; - memoryZero(item, sizeof(uifocusitem_t)); - item->cols = cols; - item->rows = rows; - item->selected = selected; - item->changed = changed; - item->closed = closed; - item->direction = direction; - item->user = user; - UI_FOCUS.count++; - UI_FOCUS.pushedThisTick = true; - if(item->changed != NULL) item->changed(item); - return item; -} - -void uiFocusPop(void) { - assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow"); - - // Count is decremented before the closed callback fires so that a - // push triggered from within it (e.g. reopening a menu once another - // one reports it closed with no result) lands in the now-free slot - // instead of one past it, which would silently strand the new item - // above the tracked stack top. - UI_FOCUS.count--; - uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count]; - if(item->closed != NULL) item->closed(item); -} - -void uiFocusPopItem(uifocusitem_t *item) { - assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow"); - assertTrue(item >= UI_FOCUS.items, "Item is not on the focus stack"); - assertTrue( - item < UI_FOCUS.items + UI_FOCUS.count, - "Item is not on the focus stack" - ); - - while(&UI_FOCUS.items[UI_FOCUS.count - 1] != item) { - uiFocusPop(); - } - uiFocusPop(); -} - -void uiFocusSetPosition(uifocusitem_t *item, const uint8_t x, const uint8_t y) { - assertTrue(UI_FOCUS.count > 0, "No active focus item"); - assertTrue(item->cols > 0, "Focus item cols must be > 0"); - assertTrue(item->rows > 0, "Focus item rows must be > 0"); - - uint8_t newX = x % item->cols; - uint8_t newY = y % item->rows; - - if(item->changed != NULL) { - uint8_t oldX = item->x; - uint8_t oldY = item->y; - item->x = newX; - item->y = newY; - if(!item->changed(item)) { - item->x = oldX; - item->y = oldY; - } - return; - } - - item->x = newX; - item->y = newY; -} - -void uiFocusMoveDirection( - uifocusitem_t *item, - const uifocusdirection_t dir -) { - for( - const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; - m->action != INPUT_ACTION_NULL; - m++ - ) { - if(m->direction != dir) continue; - - // Widened to int16_t so a move off the left/top edge (e.g. x=0, - // dx=-1) stays negative here instead of wrapping straight to 255 via - // a uint8_t truncation - that would then land on `255 % cols` - // (column 3 of a 14-wide grid, not the intended last column) rather - // than wrapping to the row/column's actual last cell. - int16_t x = (int16_t)item->x + m->dx; - int16_t y = (int16_t)item->y + m->dy; - if(x < 0) x += item->cols; - if(y < 0) y += item->rows; - - uiFocusSetPosition(item, (uint8_t)x, (uint8_t)y); - break; - } -} - -void uiFocusHandleDirection( - uifocusitem_t *item, - const uifocusdirection_t dir -) { - if(item->direction != NULL && item->direction(item, dir)) return; - uiFocusMoveDirection(item, dir); -} - -void uiFocusUpdate(void) { - if(UI_FOCUS.count == 0) return; - - #ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) return; - #endif - - if(UI_FOCUS.pushedThisTick) { - UI_FOCUS.pushedThisTick = false; - return; - } - - uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1]; - - if(inputPressed(INPUT_ACTION_ACCEPT)) { - if(item->selected != NULL) item->selected(item); - return; - } - - if(inputPressed(INPUT_ACTION_CANCEL)) { - if(item->cancel != NULL && item->cancel(item)) return; - if(!item->disableBack) uiFocusPop(); - return; - } - - for( - const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; - m->action != INPUT_ACTION_NULL; - m++ - ) { - if(!inputPressed(m->action)) continue; - UI_FOCUS.direction = m->direction; - UI_FOCUS.timeHeld = 0.0f; - uiFocusHandleDirection(item, m->direction); - return; - } - - bool_t held = false; - for( - const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; - m->action != INPUT_ACTION_NULL; - m++ - ) { - if(m->direction != UI_FOCUS.direction) continue; - held = inputIsDown(m->action); - break; - } - - if(!held) { - UI_FOCUS.direction = UI_FOCUS_DIRECTION_NONE; - UI_FOCUS.timeHeld = 0.0f; - return; - } - - UI_FOCUS.timeHeld += TIME.delta; - - if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return; - if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return; - UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY; - uiFocusHandleDirection(item, UI_FOCUS.direction); -} diff --git a/src/dusk/ui/focus/uifocus.h b/src/dusk/ui/focus/uifocus.h deleted file mode 100644 index d68de640..00000000 --- a/src/dusk/ui/focus/uifocus.h +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "uifocusitem.h" -#include "input/inputaction.h" - -/** Maximum depth of the focus stack. */ -#define UI_FOCUS_STACK_MAX 8 - -/** - * How long a direction must be held before repeating begins, in seconds. - */ -#define UI_FOCUS_HOLD_DELAY 0.5f - -/** - * Interval between repeated moves while a direction is held, in seconds. - */ -#define UI_FOCUS_HOLD_REPEAT 0.1f - -typedef struct { - inputaction_t action; - uifocusdirection_t direction; - int8_t dx; - int8_t dy; -} uifocusdirmap_t; - -/** - * Mapping of input actions to focus directions, terminated by an - * entry with action == INPUT_ACTION_NULL. - */ -extern const uifocusdirmap_t UI_FOCUS_DIR_MAP[]; - -/** - * A stack of focused UI items. Push an item when a widget captures - * focus; pop it when focus is released. The topmost item is always - * the active focus context. - */ -typedef struct { - uifocusitem_t items[UI_FOCUS_STACK_MAX]; - uint8_t count; - uifocusdirection_t direction; - float_t timeHeld; - bool_t pushedThisTick; -} uifocus_t; - -extern uifocus_t UI_FOCUS; - -/** - * Initializes the focus system, zeroing all state. - */ -void uiFocusInit(void); - -/** - * Pushes a new focus item onto the stack with the given grid dimensions - * and callbacks. x and y are initialized to 0. - * - * @param cols Number of columns in the focus grid. - * @param rows Number of rows in the focus grid. - * @param selected Called when the user selects the focused cell. - * @param changed Called when the focused cell position changes. - * @param closed Called when this focus item is popped. - * @param direction Called on a direction press/hold before the default - * cell-to-cell movement is applied; may be NULL. - * @param user Arbitrary pointer stored on the item before changed fires. - * @returns Pointer to the newly pushed focus item. - */ -uifocusitem_t * uiFocusPush( - const uint8_t cols, - const uint8_t rows, - uifocusitemcallback_t selected, - uifocusitemcallback_t changed, - uifocusitemcallback_t closed, - uifocusitemdirectioncallback_t direction, - void *user -); - -/** - * Pops the topmost focus item from the stack, invoking its closed - * callback if one is set. - */ -void uiFocusPop(void); - -/** - * Pops an item and anything that was pushed after it from the focus stack. - * - * @param item The focus item to pop to. Must be on the stack. - */ -void uiFocusPopItem(uifocusitem_t *item); - -/** - * Manually sets the cursor position of the topmost focus item and - * fires its changed callback. - * - * @param x Column to move to. - * @param y Row to move to. - */ -void uiFocusSetPosition(uifocusitem_t *item, const uint8_t x, const uint8_t y); - -/** - * Moves the topmost focus item one step in the given direction, - * wrapping at the grid edges, and fires its changed callback. - * - * @param item The focus item to move. - * @param dir Direction to move. - */ -void uiFocusMoveDirection( - uifocusitem_t *item, - const uifocusdirection_t dir -); - -/** - * Handles a direction press/hold for the given item: gives the item's - * direction callback (if any) first refusal, falling back to the - * default cell-to-cell movement if it's unset or returns false. - * - * @param item The focus item to move. - * @param dir Direction that was pressed or held. - */ -void uiFocusHandleDirection( - uifocusitem_t *item, - const uifocusdirection_t dir -); - -/** - * Updates the focus system. Handles first-press movement and - * held-direction repeating. Called once per game tick. - */ -void uiFocusUpdate(void); diff --git a/src/dusk/ui/focus/uifocusitem.h b/src/dusk/ui/focus/uifocusitem.h deleted file mode 100644 index 32654321..00000000 --- a/src/dusk/ui/focus/uifocusitem.h +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 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 uifocusitem_s uifocusitem_t; - -typedef enum { - UI_FOCUS_DIRECTION_NONE, - UI_FOCUS_DIRECTION_UP, - UI_FOCUS_DIRECTION_DOWN, - UI_FOCUS_DIRECTION_LEFT, - UI_FOCUS_DIRECTION_RIGHT -} uifocusdirection_t; - -/** - * Callback invoked when a focus item's selected cell changes. - * - * @param item The focus item that changed. - */ -typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item); - -/** - * Callback invoked when a direction is pressed or held-repeated while - * this item is focused, before the default cell-to-cell movement is - * applied. - * - * @param item The focus item that is currently focused. - * @param direction The direction that was pressed. - * @returns True if the direction was fully handled and the default - * grid movement should be skipped; false to fall through to it. - */ -typedef bool_t (*uifocusitemdirectioncallback_t)( - const uifocusitem_t *item, - const uifocusdirection_t direction -); - -/** - * A single entry on the UI focus stack. Tracks the focused cell - * within a grid of cols x rows, and the current x/y position. - */ -struct uifocusitem_s { - uint8_t cols; - uint8_t rows; - uint8_t x; - uint8_t y; - uifocusitemcallback_t selected; - uifocusitemcallback_t changed; - uifocusitemcallback_t closed; - uifocusitemdirectioncallback_t direction; - - // Called when INPUT_ACTION_CANCEL is pressed, before disableBack/the - // default pop is applied; may be NULL. Returns true if fully handled - // (e.g. it did something else instead, or popped itself), which skips - // the default pop; false to fall through to the default disableBack/ - // pop behavior below. - uifocusitemcallback_t cancel; - - void *user; - - // When true, INPUT_ACTION_CANCEL is ignored while this item is the - // topmost focus item - it does not pop. Does not affect a - // programmatic uiFocusPop()/uiFocusPopItem() call. Only consulted - // when cancel is unset or returns false. - bool_t disableBack; -}; diff --git a/src/dusk/ui/overlay/CMakeLists.txt b/src/dusk/ui/overlay/CMakeLists.txt deleted file mode 100644 index 2c49abf3..00000000 --- a/src/dusk/ui/overlay/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uicrop.c - uifullbox.c - uiloading.c - uifatalerror.c -) diff --git a/src/dusk/ui/overlay/uicrop.c b/src/dusk/ui/overlay/uicrop.c deleted file mode 100644 index 3e8d388b..00000000 --- a/src/dusk/ui/overlay/uicrop.c +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uicrop.h" -#include "display/screen/screen.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" -#include "display/texture/texture.h" - -uicrop_t UI_CROP; - -errorret_t uiCropInit(void) { - UI_CROP.color = COLOR_BLACK; - errorOk(); -} - -errorret_t uiCropDraw(void) { - #ifndef DUSK_DISPLAY_SIZE_DYNAMIC - errorOk(); - #endif - - if( - SCREEN.scanX == 0 && - SCREEN.scanY == 0 && - SCREEN.scanWidth == SCREEN.width && - SCREEN.scanHeight == SCREEN.height - ) errorOk(); - - float_t x0 = (float_t)SCREEN.scanX; - float_t y0 = (float_t)SCREEN.scanY; - float_t x1 = (float_t)(SCREEN.scanX + SCREEN.scanWidth); - float_t y1 = (float_t)(SCREEN.scanY + SCREEN.scanHeight); - float_t w = (float_t)SCREEN.width; - float_t h = (float_t)SCREEN.height; - - spritebatchsprite_t sprites[4]; - int32_t count = 0; - - if(SCREEN.scanX > 0) { - sprites[count++] = (spritebatchsprite_t){ - .min = { 0.0f, 0.0f, 0.0f }, - .max = { x0, h, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - } - - if(SCREEN.scanX + SCREEN.scanWidth < SCREEN.width) { - sprites[count++] = (spritebatchsprite_t){ - .min = { x1, 0.0f, 0.0f }, - .max = { w, h, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - } - - if(SCREEN.scanY > 0) { - sprites[count++] = (spritebatchsprite_t){ - .min = { x0, 0.0f, 0.0f }, - .max = { x1, y0, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - } - - if(SCREEN.scanY + SCREEN.scanHeight < SCREEN.height) { - sprites[count++] = (spritebatchsprite_t){ - .min = { x0, y1, 0.0f }, - .max = { x1, h, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - } - - if(count == 0) errorOk(); - - shadermaterial_t material = { - .unlit = { - .color = UI_CROP.color, - .texture = &TEXTURE_WHITE - } - }; - errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material)); - return spriteBatchFlush(); -} diff --git a/src/dusk/ui/overlay/uicrop.h b/src/dusk/ui/overlay/uicrop.h deleted file mode 100644 index ef822633..00000000 --- a/src/dusk/ui/overlay/uicrop.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/color.h" - -typedef struct { - color_t color; -} uicrop_t; - -extern uicrop_t UI_CROP; - -/** - * Initializes the crop bars to opaque black. - * - * @return Any error that occurs. - */ -errorret_t uiCropInit(void); - -/** - * Renders solid-color bars covering every area outside the - * current scan-safe region. No-ops when the scan area equals - * the full viewport. - * - * @return Any error that occurs. - */ -errorret_t uiCropDraw(void); diff --git a/src/dusk/ui/overlay/uifatalerror.c b/src/dusk/ui/overlay/uifatalerror.c deleted file mode 100644 index efc59171..00000000 --- a/src/dusk/ui/overlay/uifatalerror.c +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uifatalerror.h" -#include "ui/widget/uiframe.h" -#include "ui/widget/uibutton.h" -#include "engine/engine.h" -#include "util/memory.h" -#include "util/string.h" -#include "util/math.h" -#include "display/screen/screen.h" -#include "display/color.h" -#include "display/text/font.h" -#include "display/spritebatch/spritebatch.h" - -uifatalerror_t UI_FATAL_ERROR; - -void uiFatalErrorMenuSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - ENGINE.running = false; -} - -errorret_t uiFatalErrorInit(void) { - memoryZero(&UI_FATAL_ERROR, sizeof(uifatalerror_t)); - - uiLabelInit( - &UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageText, - UI_FATAL_ERROR.messageSprites, UI_FATAL_ERROR_MESSAGE_SPRITES_MAX - ); - - errorOk(); -} - -void uiFatalErrorOpen(const char_t *message) { - // This is the last line of defense - it must never itself fail, so a - // second fatal error while one is already showing is silently dropped - // rather than asserting. - if(UI_FATAL_ERROR.open) return; - if(message == NULL) message = UI_FATAL_ERROR_GENERIC_MESSAGE; - - stringCopy( - UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1 - ); - - const float_t wrapWidth = - (float_t)SCREEN.scanWidth * UI_FATAL_ERROR_MESSAGE_WRAP_RATIO; - uiLabelSetWrap( - &UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageWrapText, - UI_FATAL_ERROR_MESSAGE_MAX, wrapWidth - ); - uiLabelRebuffer(&UI_FATAL_ERROR.messageLabel); - - UI_FATAL_ERROR.open = true; - - uiMenuInit(&UI_FATAL_ERROR.menu, uiFatalErrorMenuSelected, NULL, NULL); - UI_FATAL_ERROR.items[0].type = UI_MENU_WIDGET_TYPE_BUTTON; - uiButtonInit(&UI_FATAL_ERROR.items[0].button, UI_FATAL_ERROR_QUIT_LABEL); - uiMenuSetItems( - &UI_FATAL_ERROR.menu, UI_FATAL_ERROR.items, UI_FATAL_ERROR_ITEM_COUNT, 1 - ); - // No way out but the QUIT button - this is a fatal, unrecoverable - // condition. - uiMenuSetDisableBack(&UI_FATAL_ERROR.menu, true); - uiMenuOpen(&UI_FATAL_ERROR.menu); -} - -bool_t uiFatalErrorIsOpen(void) { - return UI_FATAL_ERROR.open; -} - -errorret_t uiFatalErrorDraw(void) { - if(!UI_FATAL_ERROR.open) errorOk(); - - const float_t x = (float_t)SCREEN.scanX; - const float_t y = (float_t)SCREEN.scanY; - const float_t width = (float_t)SCREEN.scanWidth; - const float_t height = (float_t)SCREEN.scanHeight; - - errorChain(uiFrameDraw(x, y, width, height)); - - const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - const float_t contentWidth = mathMax( - (float_t)UI_FATAL_ERROR.messageLabel.width, - (float_t)UI_FATAL_ERROR.items[0].button.label.width - ); - const float_t contentHeight = (float_t)UI_FATAL_ERROR.messageLabel.height + - UI_FRAME_PADDING_Y + rowHeight; - - const float_t contentX = x + (width - contentWidth) * 0.5f; - const float_t contentY = y + (height - contentHeight) * 0.5f; - - uiLabelSetX(&UI_FATAL_ERROR.messageLabel, contentX); - uiLabelSetY(&UI_FATAL_ERROR.messageLabel, contentY); - errorChain(uiLabelRender(&UI_FATAL_ERROR.messageLabel, COLOR_WHITE)); - - const float_t menuY = contentY + - (float_t)UI_FATAL_ERROR.messageLabel.height + UI_FRAME_PADDING_Y; - errorChain(uiMenuDraw( - &UI_FATAL_ERROR.menu, contentX, menuY, contentWidth, rowHeight - )); - - errorChain(spriteBatchFlush()); - errorOk(); -} - -errorret_t uiFatalErrorDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/overlay/uifatalerror.h b/src/dusk/ui/overlay/uifatalerror.h deleted file mode 100644 index 85e8b6b1..00000000 --- a/src/dusk/ui/overlay/uifatalerror.h +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" -#include "ui/widget/uimenu.h" - -#define UI_FATAL_ERROR_MESSAGE_MAX 256 -#define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX -#define UI_FATAL_ERROR_MESSAGE_WRAP_RATIO 0.8f -#define UI_FATAL_ERROR_ITEM_COUNT 1 -#define UI_FATAL_ERROR_QUIT_LABEL "QUIT" -#define UI_FATAL_ERROR_GENERIC_MESSAGE \ - "An unexpected error occurred and the game must exit. " \ - "Please contact support if this issue persists." - -typedef struct { - bool_t open; - - uilabel_t messageLabel; - char_t messageText[UI_FATAL_ERROR_MESSAGE_MAX]; - char_t messageWrapText[UI_FATAL_ERROR_MESSAGE_MAX]; - spritebatchsprite_t messageSprites[UI_FATAL_ERROR_MESSAGE_SPRITES_MAX]; - - uimenu_t menu; - uimenuitem_t items[UI_FATAL_ERROR_ITEM_COUNT]; -} uifatalerror_t; - -extern uifatalerror_t UI_FATAL_ERROR; - -/** - * Initializes the fatal error overlay. - * - * @return Any error that occurs. - */ -errorret_t uiFatalErrorInit(void); - -/** - * Opens the fatal error overlay, a fullscreen, non-dismissible display - * for an unrecoverable error - the only way out is its QUIT button, - * which sets ENGINE.running to false. Intended as a last-resort, nicer- - * looking alternative to a raw assert crash for a genuinely fatal - * condition, so it must never itself fail: a second call while already - * open is silently ignored rather than asserting, and it draws above - * every other UI element. - * - * @param message The error text to display; copied internally, safe to - * be transient. Truncated to UI_FATAL_ERROR_MESSAGE_MAX - 1 characters. - * May be NULL, in which case UI_FATAL_ERROR_GENERIC_MESSAGE is shown - * instead - useful for callers with no specific error text to report - * (or that don't want to risk formatting/locale-lookup failures of their - * own right before calling this, the last line of defense). - */ -void uiFatalErrorOpen(const char_t *message); - -/** - * Returns whether the fatal error overlay is currently open. - * - * @returns True if open. - */ -bool_t uiFatalErrorIsOpen(void); - -/** - * Draws the fatal error overlay. No-op when not open. - * - * @return Any error that occurs. - */ -errorret_t uiFatalErrorDraw(void); - -/** - * Disposes of the fatal error overlay. - * - * @return Any error that occurs. - */ -errorret_t uiFatalErrorDispose(void); diff --git a/src/dusk/ui/overlay/uifullbox.c b/src/dusk/ui/overlay/uifullbox.c deleted file mode 100644 index d1751f0b..00000000 --- a/src/dusk/ui/overlay/uifullbox.c +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uifullbox.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "time/time.h" -#include "display/screen/screen.h" -#include "display/texture/texture.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" - -uifullbox_t UI_FULLBOX_UNDER; -uifullbox_t UI_FULLBOX_OVER; - -void uiFullboxInit(uifullbox_t *fullbox) { - assertNotNull(fullbox, "fullbox must not be NULL"); - memoryZero(fullbox, sizeof(uifullbox_t)); -} - -void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) { - assertNotNull(fullbox, "fullbox must not be NULL"); - if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) return; - - fullbox->time += delta; - if(fullbox->time >= fullbox->duration) { - fullbox->time = fullbox->duration; - if(fullbox->onTransitionEnd) { - fullbox->onTransitionEnd(fullbox, fullbox->onTransitionEndUser); - } - } -} - -color_t uiFullboxGetColor(const uifullbox_t *fullbox) { - if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) { - return fullbox->toColor; - } - float_t t = easingApply(fullbox->easing, fullbox->time / fullbox->duration); - return color4b( - (uint8_t)((float_t)fullbox->fromColor.r + - ((float_t)fullbox->toColor.r - (float_t)fullbox->fromColor.r) * t), - (uint8_t)((float_t)fullbox->fromColor.g + - ((float_t)fullbox->toColor.g - (float_t)fullbox->fromColor.g) * t), - (uint8_t)((float_t)fullbox->fromColor.b + - ((float_t)fullbox->toColor.b - (float_t)fullbox->fromColor.b) * t), - (uint8_t)((float_t)fullbox->fromColor.a + - ((float_t)fullbox->toColor.a - (float_t)fullbox->fromColor.a) * t) - ); -} - -errorret_t uiFullboxDraw(uifullbox_t *fullbox) { - assertNotNull(fullbox, "fullbox must not be NULL"); - - color_t color = uiFullboxGetColor(fullbox); - if(color.a == 0) errorOk(); - - spritebatchsprite_t sprite = { - .min = { 0.0f, 0.0f, 0.0f }, - .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t material = { - .unlit = { - .color = color, - .texture = &TEXTURE_WHITE - } - }; - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - return spriteBatchFlush(); -} - -void uiFullboxTransition( - uifullbox_t *fullbox, - color_t from, - color_t to, - float_t duration, - easingtype_t easing -) { - assertNotNull(fullbox, "fullbox must not be NULL"); - fullbox->fromColor = from; - fullbox->toColor = to; - fullbox->duration = duration; - fullbox->time = 0.0f; - fullbox->easing = easing; -} - -errorret_t uiFullboxUnderInit(void) { - uiFullboxInit(&UI_FULLBOX_UNDER); - errorOk(); -} - -errorret_t uiFullboxUnderUpdate(void) { - uiFullboxUpdate(&UI_FULLBOX_UNDER, TIME.delta); - errorOk(); -} - -errorret_t uiFullboxUnderDraw(void) { - return uiFullboxDraw(&UI_FULLBOX_UNDER); -} - -errorret_t uiFullboxOverInit(void) { - uiFullboxInit(&UI_FULLBOX_OVER); - errorOk(); -} - -errorret_t uiFullboxOverUpdate(void) { - uiFullboxUpdate(&UI_FULLBOX_OVER, TIME.delta); - errorOk(); -} - -errorret_t uiFullboxOverDraw(void) { - return uiFullboxDraw(&UI_FULLBOX_OVER); -} diff --git a/src/dusk/ui/overlay/uifullbox.h b/src/dusk/ui/overlay/uifullbox.h deleted file mode 100644 index f27a1466..00000000 --- a/src/dusk/ui/overlay/uifullbox.h +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/color.h" -#include "animation/easing.h" - -typedef struct uifullbox_s uifullbox_t; - -/** - * Callback fired once when a fullbox transition completes. - * - * @param fullbox The uifullbox_t the transition ran on. - * @param user The user pointer passed alongside the callback. - */ -typedef void (*uifullboxcallback_t)(uifullbox_t *fullbox, void *user); - -struct uifullbox_s { - color_t fromColor; - color_t toColor; - float_t duration; - float_t time; - easingtype_t easing; - uifullboxcallback_t onTransitionEnd; - void *onTransitionEndUser; -}; - -extern uifullbox_t UI_FULLBOX_UNDER; -extern uifullbox_t UI_FULLBOX_OVER; - -/** - * Initializes a fullbox, zeroing all fields. - * - * @param fullbox The fullbox to initialize. - */ -void uiFullboxInit(uifullbox_t *fullbox); - -/** - * Advances the fullbox transition. Fires onTransitionEnd once when the - * transition completes. Safe to call when no transition is running. - * - * @param fullbox The fullbox to update. - * @param delta Seconds elapsed since last update. - */ -void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta); - -/** - * Computes the fullbox's current color, interpolating between fromColor and - * toColor based on the transition's elapsed time and easing function. - * - * @param fullbox The fullbox to compute the color for. - * @return The current interpolated color. - */ -color_t uiFullboxGetColor(const uifullbox_t *fullbox); - -/** - * Renders the fullbox. Skipped entirely when the current alpha is zero. - * - * @param fullbox The fullbox to draw. - * @return Any error that occurs. - */ -errorret_t uiFullboxDraw(uifullbox_t *fullbox); - -/** - * Begins a color transition. - * - * @param fullbox The fullbox to transition. - * @param from Start color. - * @param to End color. - * @param duration Transition duration in seconds. - * @param easing Easing function to apply. - */ -void uiFullboxTransition( - uifullbox_t *fullbox, - color_t from, - color_t to, - float_t duration, - easingtype_t easing -); - -/** - * Init function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). - * - * @return Any error that occurs. - */ -errorret_t uiFullboxUnderInit(void); - -/** - * Update function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). - * - * @return Any error that occurs. - */ -errorret_t uiFullboxUnderUpdate(void); - -/** - * Draw function for UI_FULLBOX_UNDER (registered in UI_ELEMENTS). - * - * @return Any error that occurs. - */ -errorret_t uiFullboxUnderDraw(void); - -/** - * Init function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). - * - * @return Any error that occurs. - */ -errorret_t uiFullboxOverInit(void); - -/** - * Update function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). - * - * @return Any error that occurs. - */ -errorret_t uiFullboxOverUpdate(void); - -/** - * Draw function for UI_FULLBOX_OVER (registered in UI_ELEMENTS). - * - * @return Any error that occurs. - */ -errorret_t uiFullboxOverDraw(void); diff --git a/src/dusk/ui/overlay/uiloading.c b/src/dusk/ui/overlay/uiloading.c deleted file mode 100644 index 70927eac..00000000 --- a/src/dusk/ui/overlay/uiloading.c +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiloading.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "time/time.h" -#include "display/text/text.h" -#include "display/screen/screen.h" -#include "display/spritebatch/spritebatch.h" -#include "uifullbox.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" - -uiloading_t UI_LOADING; - -errorret_t uiLoadingInit(void) { - memoryZero(&UI_LOADING, sizeof(uiloading_t)); - - errorChain(localeManagerGetString( - "ui.loading.text", UI_LOADING.text, UI_LOADING_TEXT_MAX, 0 - )); - - errorOk(); -} - -errorret_t uiLoadingUpdate(void) { - if(UI_LOADING.duration > 0.0f && UI_LOADING.time < UI_LOADING.duration) { - UI_LOADING.time += TIME.delta; - if(UI_LOADING.time >= UI_LOADING.duration) { - UI_LOADING.time = UI_LOADING.duration; - if(UI_LOADING.onTransitionEnd) { - UI_LOADING.onTransitionEnd(&UI_LOADING, UI_LOADING.onTransitionEndUser); - } - } - } - errorOk(); -} - -errorret_t uiLoadingDraw(void) { - float_t alpha; - if(UI_LOADING.duration <= 0.0f || UI_LOADING.time >= UI_LOADING.duration) { - alpha = UI_LOADING.toAlpha; - } else { - float_t t = UI_LOADING.time / UI_LOADING.duration; - alpha = UI_LOADING.fromAlpha + - (UI_LOADING.toAlpha - UI_LOADING.fromAlpha) * t; - } - - if(alpha <= 0.0f) errorOk(); - - int32_t textW, textH; - textMeasure(UI_LOADING.text, &FONT_DEFAULT, &textW, &textH); - - float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - - (float_t)textW - UI_LOADING_MARGIN; - float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - - (float_t)textH - UI_LOADING_MARGIN; - - color_t color = COLOR_WHITE; - color.a = (uint8_t)(alpha * 255.0f); - - errorChain(textDraw(x, y, UI_LOADING.text, color, &FONT_DEFAULT)); - return spriteBatchFlush(); -} - -void uiLoadingTransition( - const float_t from, - const float_t to, - const uiloadingcallback_t callback, - void *user -) { - UI_LOADING.fromAlpha = from; - UI_LOADING.toAlpha = to; - UI_LOADING.duration = UI_LOADING_FADE_DURATION; - UI_LOADING.time = 0.0f; - UI_LOADING.onTransitionEnd = callback; - UI_LOADING.onTransitionEndUser = user; -} - -void uiLoadingShow(uiloadingcallback_t callback, void *user) { - uiLoadingTransition(0.0f, 1.0f, callback, user); - uiFullboxTransition( - &UI_FULLBOX_OVER, - COLOR_TRANSPARENT_BLACK, - COLOR_BLACK, - UI_LOADING_FADE_DURATION, - EASING_LINEAR - ); -} - -void uiLoadingHide(uiloadingcallback_t callback, void *user) { - uiLoadingTransition(1.0f, 0.0f, callback, user); - uiFullboxTransition( - &UI_FULLBOX_OVER, - COLOR_BLACK, - COLOR_TRANSPARENT_BLACK, - UI_LOADING_FADE_DURATION, - EASING_LINEAR - ); -} diff --git a/src/dusk/ui/overlay/uiloading.h b/src/dusk/ui/overlay/uiloading.h deleted file mode 100644 index 531d4f23..00000000 --- a/src/dusk/ui/overlay/uiloading.h +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define UI_LOADING_FADE_DURATION 0.5f -#define UI_LOADING_MARGIN 8.0f -#define UI_LOADING_TEXT_MAX 32 - -typedef struct uiloading_s uiloading_t; - -/** - * Callback fired once when a loading indicator fade transition completes. - * - * @param loading The uiloading_t the transition ran on. - * @param user The user pointer passed alongside the callback. - */ -typedef void (*uiloadingcallback_t)(uiloading_t *loading, void *user); - -struct uiloading_s { - float_t fromAlpha; - float_t toAlpha; - float_t duration; - float_t time; - uiloadingcallback_t onTransitionEnd; - void *onTransitionEndUser; - char_t text[UI_LOADING_TEXT_MAX]; -}; - -extern uiloading_t UI_LOADING; - -/** - * Initializes the loading indicator. - * - * @return Any error that occurs. - */ -errorret_t uiLoadingInit(void); - -/** - * Advances the loading indicator fade transition. Fires onTransitionEnd once - * when the transition completes. - * - * @return Any error that occurs. - */ -errorret_t uiLoadingUpdate(void); - -/** - * Draws the loading indicator. No-op when fully transparent. - * - * @return Error state. - */ -errorret_t uiLoadingDraw(void); - -/** - * Begins a loading indicator fade transition, replacing any transition - * already in progress along with its pending callback. - * - * @param from Starting alpha. - * @param to Ending alpha. - * @param callback Called when the transition completes. May be NULL. - * @param user Forwarded to the callback unchanged. - */ -void uiLoadingTransition( - const float_t from, - const float_t to, - const uiloadingcallback_t callback, - void *user -); - -/** - * Fades the loading indicator in. Invokes callback when fully visible. - * - * @param callback Called when the fade-in completes. May be NULL. - * @param user Forwarded to the callback unchanged. - */ -void uiLoadingShow(uiloadingcallback_t callback, void *user); - -/** - * Fades the loading indicator out. Invokes callback when fully hidden. - * - * @param callback Called when the fade-out completes. May be NULL. - * @param user Forwarded to the callback unchanged. - */ -void uiLoadingHide(uiloadingcallback_t callback, void *user); diff --git a/src/dusk/ui/rpg/CMakeLists.txt b/src/dusk/ui/rpg/CMakeLists.txt deleted file mode 100644 index eb3441de..00000000 --- a/src/dusk/ui/rpg/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uiemoji.c -) - - -add_subdirectory(textbox) \ No newline at end of file diff --git a/src/dusk/ui/rpg/textbox/CMakeLists.txt b/src/dusk/ui/rpg/textbox/CMakeLists.txt deleted file mode 100644 index e7787f2d..00000000 --- a/src/dusk/ui/rpg/textbox/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uitextbox.c - uitextboxmain.c - uitextboxmini.c - uitextboxminilist.c -) diff --git a/src/dusk/ui/rpg/textbox/uitextbox.c b/src/dusk/ui/rpg/textbox/uitextbox.c deleted file mode 100644 index 20b078e5..00000000 --- a/src/dusk/ui/rpg/textbox/uitextbox.c +++ /dev/null @@ -1,244 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitextbox.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "time/time.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" -#include "ui/widget/uiframe.h" - -void uiTextboxInit( - uitextbox_t *box, - char_t *text, - const uint32_t maxLength, - uitextboxline_t *lines, - const uint32_t linesMax -) { - assertNotNull(box, "Textbox cannot be NULL"); - assertNotNull(text, "Text buffer cannot be NULL"); - assertTrue(maxLength >= 1, "maxLength must be at least 1"); - assertNotNull(lines, "Lines buffer cannot be NULL"); - assertTrue(linesMax >= 1, "linesMax must be at least 1"); - memoryZero(box, sizeof(uitextbox_t)); - box->text = text; - box->maxLength = maxLength; - box->lines = lines; - box->linesMax = linesMax; -} - -void uiTextboxSetText(uitextbox_t *box, const char_t *text) { - assertNotNull(box, "Textbox cannot be NULL"); - assertNotNull(text, "Text cannot be NULL"); - stringCopy(box->text, text, box->maxLength); - box->currentPage = 0; - box->scroll = 0; - box->layoutWidth = 0.0f; - box->layoutHeight = 0.0f; -} - -void uiTextboxBuildLayout( - uitextbox_t *box, - const float_t width, - const float_t height -) { - assertNotNull(box, "Textbox cannot be NULL"); - - box->layoutWidth = width; - box->layoutHeight = height; - box->lineCount = 0; - box->pageCount = 1; - - float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; - float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; - - if(fontW <= 0.0f || fontH <= 0.0f) return; - - box->charsPerLine = (int32_t)(width / fontW); - box->linesPerPage = (int32_t)(height / (fontH + UI_TEXTBOX_LINE_SPACING)); - if(box->linesPerPage > UI_TEXTBOX_LINES_PER_PAGE_MAX) { - box->linesPerPage = UI_TEXTBOX_LINES_PER_PAGE_MAX; - } - - if(box->charsPerLine <= 0 || box->linesPerPage <= 0) return; - if(box->text[0] == '\0') return; - - char_t *src = box->text; - int32_t i = 0; - - while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) { - if(src[i] == '\t') { - i++; - int32_t rem = box->lineCount % box->linesPerPage; - int32_t pad = rem > 0 ? box->linesPerPage - rem : 0; - while(pad > 0 && box->lineCount < (int32_t)box->linesMax) { - box->lines[box->lineCount].start = i; - box->lines[box->lineCount].count = 0; - box->lineCount++; - pad--; - } - continue; - } - - int32_t lineStart = i; - int32_t lineWidth = 0; - - while(src[i] != '\0') { - char_t c = src[i]; - - if(c == '\n') { i++; break; } - if(c == '\t') break; - - if(c == ' ') { - int32_t wordLen = 0; - int32_t j = i + 1; - while( - src[j] != ' ' && src[j] != '\n' && - src[j] != '\t' && src[j] != '\0' - ) { - wordLen++; - j++; - } - - if(lineWidth > 0 && lineWidth + 1 + wordLen > box->charsPerLine) { - i++; - break; - } - - lineWidth++; - i++; - } else { - if(lineWidth >= box->charsPerLine) break; - lineWidth++; - i++; - } - } - - box->lines[box->lineCount].start = lineStart; - box->lines[box->lineCount].count = lineWidth; - box->lineCount++; - } - - if(box->lineCount == 0) { - box->pageCount = 1; - } else { - box->pageCount = - (box->lineCount + box->linesPerPage - 1) / box->linesPerPage; - } -} - -errorret_t uiTextboxUpdate(uitextbox_t *box) { - assertNotNull(box, "Textbox cannot be NULL"); - -#ifdef DUSK_TIME_DYNAMIC - if(TIME.dynamicUpdate) errorOk(); -#endif - - if(!uiTextboxPageIsComplete(box)) { - box->scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK; - } - - errorOk(); -} - -errorret_t uiTextboxDraw( - uitextbox_t *box, - const float_t x, - const float_t y, - const float_t width, - const float_t height -) { - assertNotNull(box, "Textbox cannot be NULL"); - - float_t startX = (float_t)UI_FRAME_START_X; - float_t startY = (float_t)UI_FRAME_START_Y; - float_t contentX = x + startX; - float_t contentY = y + startY; - float_t contentW = width - 2.0f * startX; - float_t contentH = height - 2.0f * startY; - - if(contentW != box->layoutWidth || contentH != box->layoutHeight) { - uiTextboxBuildLayout(box, contentW, contentH); - } - - errorChain(uiFrameDraw(x, y, width, height)); - errorChain(spriteBatchFlush()); - - if(box->lineCount == 0 || box->text[0] == '\0') errorOk(); - - float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; - float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; - - shadermaterial_t material = { - .unlit = { - .color = COLOR_WHITE, - .texture = FONT_DEFAULT.texture - } - }; - - int32_t pageFirst = box->currentPage * box->linesPerPage; - int32_t pageLast = pageFirst + box->linesPerPage; - if(pageLast > box->lineCount) pageLast = box->lineCount; - - int32_t charsLeft = box->scroll; - - for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) { - uitextboxline_t *line = &box->lines[li]; - int32_t visible = line->count < charsLeft ? line->count : charsLeft; - float_t lineY = contentY + - (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING); - - for(int32_t ci = 0; ci < visible; ci++) { - char_t c = box->text[line->start + ci]; - if(c == ' ') continue; - spritebatchsprite_t sprite = textGetSprite( - (vec2){ contentX + (float_t)ci * fontW, lineY }, - c, - &FONT_DEFAULT - ); - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - } - - charsLeft -= visible; - } - - errorChain(spriteBatchFlush()); - errorOk(); -} - -int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) { - assertNotNull(box, "Textbox cannot be NULL"); - int32_t first = box->currentPage * box->linesPerPage; - int32_t last = first + box->linesPerPage; - if(last > box->lineCount) last = box->lineCount; - int32_t total = 0; - for(int32_t i = first; i < last; i++) { - total += box->lines[i].count; - } - return total; -} - -bool_t uiTextboxPageIsComplete(const uitextbox_t *box) { - assertNotNull(box, "Textbox cannot be NULL"); - return box->scroll >= uiTextboxGetPageCharCount(box); -} - -bool_t uiTextboxHasNextPage(const uitextbox_t *box) { - assertNotNull(box, "Textbox cannot be NULL"); - return box->currentPage + 1 < box->pageCount; -} - -void uiTextboxNextPage(uitextbox_t *box) { - assertNotNull(box, "Textbox cannot be NULL"); - if(!uiTextboxHasNextPage(box)) return; - box->currentPage++; - box->scroll = 0; -} diff --git a/src/dusk/ui/rpg/textbox/uitextbox.h b/src/dusk/ui/rpg/textbox/uitextbox.h deleted file mode 100644 index 047e7e1f..00000000 --- a/src/dusk/ui/rpg/textbox/uitextbox.h +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4 -#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1 -#define UI_TEXTBOX_LINE_SPACING 0.0f - -typedef struct { - int32_t start; - int32_t count; -} uitextboxline_t; - -typedef struct { - char_t *text; - uint32_t maxLength; - - uitextboxline_t *lines; - uint32_t linesMax; - int32_t lineCount; - int32_t charsPerLine; - int32_t linesPerPage; - int32_t pageCount; - - // last dimensions used for layout; rebuild triggers when these change - float_t layoutWidth; - float_t layoutHeight; - - int32_t currentPage; - int32_t scroll; -} uitextbox_t; - -/** - * Initializes a textbox, zeroing all state and binding it to caller-owned - * text and line storage. - * - * @param box The textbox to initialize. - * @param text Caller-owned buffer the textbox copies its text into. - * @param maxLength Capacity of text, in characters. - * @param lines Caller-owned buffer the textbox lays lines out into. - * @param linesMax Capacity of lines, in entries. - */ -void uiTextboxInit( - uitextbox_t *box, - char_t *text, - const uint32_t maxLength, - uitextboxline_t *lines, - const uint32_t linesMax -); - -/** - * Copies text into the textbox and marks layout as dirty. - * Resets currentPage and scroll to 0. - * - * @param box The textbox to update. - * @param text Null-terminated source string. - */ -void uiTextboxSetText(uitextbox_t *box, const char_t *text); - -/** - * Rebuilds word-wrap and page layout for the given draw dimensions. - * Called automatically by uiTextboxDraw when width or height changes. - * - * @param box The textbox to rebuild. - * @param width Available content width in pixels. - * @param height Available content height in pixels. - */ -void uiTextboxBuildLayout( - uitextbox_t *box, - const float_t width, - const float_t height -); - -/** - * Advances the typewriter scroll by UI_TEXTBOX_SCROLL_CHARS_PER_TICK. - * Skipped on dynamic ticks. - * - * @param box The textbox to update. - * @returns Any error that occurs. - */ -errorret_t uiTextboxUpdate(uitextbox_t *box); - -/** - * Draws the textbox frame and visible text. Rebuilds layout automatically - * if width or height differs from the last draw call. - * - * @param box The textbox to draw. - * @param x Screen x position. - * @param y Screen y position. - * @param width Draw width in pixels. - * @param height Draw height in pixels. - * @returns Any error that occurs. - */ -errorret_t uiTextboxDraw( - uitextbox_t *box, - const float_t x, - const float_t y, - const float_t width, - const float_t height -); - -/** - * Returns the total visible char count for the current page. - * - * @param box The textbox to query. - * @returns Total chars on the current page. - */ -int32_t uiTextboxGetPageCharCount(const uitextbox_t *box); - -/** - * Returns true when scroll has fully revealed the current page. - * - * @param box The textbox to query. - * @returns True if the current page is fully visible. - */ -bool_t uiTextboxPageIsComplete(const uitextbox_t *box); - -/** - * Returns true when there is at least one more page after the current one. - * - * @param box The textbox to query. - * @returns True if a next page exists. - */ -bool_t uiTextboxHasNextPage(const uitextbox_t *box); - -/** - * Advances to the next page and resets scroll to 0. - * Has no effect if already on the last page. - * - * @param box The textbox to advance. - */ -void uiTextboxNextPage(uitextbox_t *box); diff --git a/src/dusk/ui/rpg/textbox/uitextboxmain.c b/src/dusk/ui/rpg/textbox/uitextboxmain.c deleted file mode 100644 index 09918b1f..00000000 --- a/src/dusk/ui/rpg/textbox/uitextboxmain.c +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitextboxmain.h" -#include "ui/focus/uifocus.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" -#include "ui/widget/uiframe.h" - -uitextboxmain_t UI_TEXTBOX_MAIN; -static uifocusitem_t *focusItem = NULL; - -errorret_t uiTextboxMainInit(void) { - uiTextboxInit( - &UI_TEXTBOX_MAIN.box, - UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX, - UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX - ); - errorOk(); -} - -void uiTextboxMainSetText(const char_t *text) { - uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text); - if(focusItem != NULL) return; - focusItem = uiFocusPush( - 1, 1, - uiTextboxMainFocusSelected, - NULL, - uiTextboxMainFocusClosed, - NULL, - NULL - ); -} - -errorret_t uiTextboxMainUpdate(void) { - if(focusItem == NULL) errorOk(); - return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box); -} - -errorret_t uiTextboxMainDraw(void) { - if(focusItem == NULL) errorOk(); - float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; - float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH + - (float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING + - 2.0f * (float_t)UI_FRAME_START_Y; - float_t w = (float_t)SCREEN.scanWidth; - float_t x = (float_t)SCREEN.scanX; - float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h; - errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h)); - - if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk(); - - float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth; - float_t contentX = x + (float_t)UI_FRAME_START_X; - float_t contentY = y + (float_t)UI_FRAME_START_Y; - float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X; - float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y; - - shadermaterial_t material = { - .unlit = { - .color = COLOR_WHITE, - .texture = FONT_DEFAULT.texture - } - }; - - spritebatchsprite_t caret = textGetSprite( - (vec2){ - contentX + contentW - fontW, - contentY + contentH - fontH - }, - 'v', - &FONT_DEFAULT - ); - errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material)); - errorChain(spriteBatchFlush()); - - errorOk(); -} - -bool_t uiTextboxMainPageIsComplete(void) { - return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box); -} - -bool_t uiTextboxMainHasNextPage(void) { - return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box); -} - -void uiTextboxMainNextPage(void) { - uiTextboxNextPage(&UI_TEXTBOX_MAIN.box); -} - -bool_t uiTextboxMainIsActive(void) { - return focusItem != NULL; -} - -bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) { - if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) { - UI_TEXTBOX_MAIN.box.scroll = - uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box); - return true; - } - - if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) { - uiTextboxNextPage(&UI_TEXTBOX_MAIN.box); - return true; - } - - uiFocusPopItem(focusItem); - return true; -} - -bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) { - focusItem = NULL; - return true; -} diff --git a/src/dusk/ui/rpg/textbox/uitextboxmain.h b/src/dusk/ui/rpg/textbox/uitextboxmain.h deleted file mode 100644 index e7eb83f6..00000000 --- a/src/dusk/ui/rpg/textbox/uitextboxmain.h +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "ui/rpg/textbox/uitextbox.h" -#include "ui/focus/uifocusitem.h" - -#define UI_TEXTBOX_MAIN_LINES 4 -#define UI_TEXTBOX_MAIN_TEXT_MAX 1024 -#define UI_TEXTBOX_MAIN_LINES_MAX 64 - -typedef struct { - uitextbox_t box; - char_t text[UI_TEXTBOX_MAIN_TEXT_MAX]; - uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX]; -} uitextboxmain_t; - -extern uitextboxmain_t UI_TEXTBOX_MAIN; - -/** - * Initializes UI_TEXTBOX_MAIN. - * - * @returns Any error that occurs. - */ -errorret_t uiTextboxMainInit(void); - -/** - * Copies text into UI_TEXTBOX_MAIN and resets page and scroll. - * - * @param text Null-terminated source string. - */ -void uiTextboxMainSetText(const char_t *text); - -/** - * Advances the typewriter scroll for UI_TEXTBOX_MAIN. - * - * @returns Any error that occurs. - */ -errorret_t uiTextboxMainUpdate(void); - -/** - * Draws UI_TEXTBOX_MAIN full-width at the bottom of the screen. - * Position and size are derived from SCREEN each call. - * - * @returns Any error that occurs. - */ -errorret_t uiTextboxMainDraw(void); - -/** - * Returns true when the current page is fully scrolled in. - * - * @returns True if the current page is complete. - */ -bool_t uiTextboxMainPageIsComplete(void); - -/** - * Returns true when at least one more page follows the current one. - * - * @returns True if a next page exists. - */ -bool_t uiTextboxMainHasNextPage(void); - -/** - * Advances UI_TEXTBOX_MAIN to the next page and resets scroll. - * Has no effect if already on the last page. - */ -void uiTextboxMainNextPage(void); - -/** - * Returns true when UI_TEXTBOX_MAIN has focus (is visible and active). - * - * @returns True if the textbox is currently active. - */ -bool_t uiTextboxMainIsActive(void); - -/** - * Internal focus callback - skip scroll or advance page or dismiss. - * - * @param item The active focus item. - * @returns True. - */ -bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item); - -/** - * Internal focus callback - clears the focus item pointer on dismiss. - * - * @param item The focus item being closed. - * @returns True. - */ -bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item); diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.c b/src/dusk/ui/rpg/textbox/uitextboxmini.c deleted file mode 100644 index 8ae38f34..00000000 --- a/src/dusk/ui/rpg/textbox/uitextboxmini.c +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitextboxmini.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "time/time.h" -#include "rpg/rpgcamera.h" -#include "display/text/text.h" -#include "display/screen/screen.h" -#include "ui/widget/uiframe.h" - -void uiTextboxMiniInit(uitextboxmini_t *mini) { - assertNotNull(mini, "Mini textbox cannot be NULL"); - memoryZero(mini, sizeof(uitextboxmini_t)); - uiTextboxInit( - &mini->box, - mini->text, UI_TEXTBOX_MINI_TEXT_MAX, - mini->lines, UI_TEXTBOX_MINI_LINES_MAX - ); -} - -void uiTextboxMiniShow( - uitextboxmini_t *mini, - const char_t *text, - const vec3 position, - const float_t duration, - uitextboxminiclosedcallback_t closed, - void *user -) { - assertNotNull(mini, "Mini textbox cannot be NULL"); - assertNotNull(text, "Text cannot be NULL"); - uiTextboxSetText(&mini->box, text); - glm_vec3_copy(position, mini->position); - - int32_t textWidth, textHeight; - textMeasure(text, &FONT_DEFAULT, &textWidth, &textHeight); - float_t width = (float_t)textWidth + 2.0f * (float_t)UI_FRAME_START_X; - mini->width = width < UI_TEXTBOX_MINI_WIDTH_MAX - ? width : UI_TEXTBOX_MINI_WIDTH_MAX; - - float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight; - float_t maxHeight = (float_t)UI_TEXTBOX_MINI_LINES_MAX * fontH + - (float_t)(UI_TEXTBOX_MINI_LINES_MAX - 1) * UI_TEXTBOX_LINE_SPACING + - 2.0f * (float_t)UI_FRAME_START_Y; - uiTextboxBuildLayout( - &mini->box, - mini->width - 2.0f * (float_t)UI_FRAME_START_X, - maxHeight - 2.0f * (float_t)UI_FRAME_START_Y - ); - int32_t lineCount = mini->box.lineCount > 0 ? mini->box.lineCount : 1; - mini->height = (float_t)lineCount * fontH + - (float_t)(lineCount - 1) * UI_TEXTBOX_LINE_SPACING + - 2.0f * (float_t)UI_FRAME_START_Y; - - mini->active = true; - mini->timer = duration; - mini->closed = closed; - mini->user = user; -} - -errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini) { - assertNotNull(mini, "Mini textbox cannot be NULL"); - if(!mini->active) errorOk(); - - errorChain(uiTextboxUpdate(&mini->box)); - - if(mini->timer != UI_TEXTBOX_MINI_DURATION_INFINITE) { - mini->timer -= TIME.delta; - if(mini->timer <= 0.0f) uiTextboxMiniClose(mini); - } - - errorOk(); -} - -errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini) { - assertNotNull(mini, "Mini textbox cannot be NULL"); - if(!mini->active) errorOk(); - - vec2 screenPos; - rpgCameraToScreen(mini->position, screenPos); - - if( - mini->clamp == UI_TEXTBOX_MINI_CLAMP_X || - mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH - ) { - float_t minX = (float_t)SCREEN.scanX; - float_t maxX = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - mini->width; - if(screenPos[0] < minX) screenPos[0] = minX; - if(screenPos[0] > maxX) screenPos[0] = maxX; - } - - if( - mini->clamp == UI_TEXTBOX_MINI_CLAMP_Y || - mini->clamp == UI_TEXTBOX_MINI_CLAMP_BOTH - ) { - float_t minY = (float_t)SCREEN.scanY; - float_t maxY = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - mini->height; - if(screenPos[1] < minY) screenPos[1] = minY; - if(screenPos[1] > maxY) screenPos[1] = maxY; - } - - return uiTextboxDraw( - &mini->box, screenPos[0], screenPos[1], mini->width, mini->height - ); -} - -bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini) { - assertNotNull(mini, "Mini textbox cannot be NULL"); - return mini->active; -} - -void uiTextboxMiniClose(uitextboxmini_t *mini) { - assertNotNull(mini, "Mini textbox cannot be NULL"); - if(!mini->active) return; - mini->active = false; - if(mini->closed != NULL) mini->closed(mini); -} diff --git a/src/dusk/ui/rpg/textbox/uitextboxmini.h b/src/dusk/ui/rpg/textbox/uitextboxmini.h deleted file mode 100644 index 16cf3f4b..00000000 --- a/src/dusk/ui/rpg/textbox/uitextboxmini.h +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "ui/rpg/textbox/uitextbox.h" - -#define UI_TEXTBOX_MINI_TEXT_MAX 128 -#define UI_TEXTBOX_MINI_LINES_MAX 2 -#define UI_TEXTBOX_MINI_WIDTH_MAX 160.0f - -// Magic duration value that keeps a mini textbox visible indefinitely -// instead of counting down and auto-closing. -#define UI_TEXTBOX_MINI_DURATION_INFINITE -1.0f - -typedef struct uitextboxmini_s uitextboxmini_t; -typedef void (*uitextboxminiclosedcallback_t)(const uitextboxmini_t *mini); - -/** - * Determines whether a mini textbox's screen position is clamped to stay - * fully on screen. Defaults to UI_TEXTBOX_MINI_CLAMP_NONE. - */ -typedef enum { - UI_TEXTBOX_MINI_CLAMP_NONE, - UI_TEXTBOX_MINI_CLAMP_X, - UI_TEXTBOX_MINI_CLAMP_Y, - UI_TEXTBOX_MINI_CLAMP_BOTH -} uitextboxminiclamp_t; - -typedef struct uitextboxmini_s { - uitextbox_t box; - char_t text[UI_TEXTBOX_MINI_TEXT_MAX]; - uitextboxline_t lines[UI_TEXTBOX_MINI_LINES_MAX]; - - vec3 position; - float_t width; - float_t height; - uitextboxminiclamp_t clamp; - bool_t active; - float_t timer; - - uitextboxminiclosedcallback_t closed; - void *user; -} uitextboxmini_t; - -/** - * Initializes a mini textbox, zeroing all state and binding its internal - * uitextbox_t to its own fixed-size text and line storage. - * - * @param mini The mini textbox to initialize. - */ -void uiTextboxMiniInit(uitextboxmini_t *mini); - -/** - * Shows a mini textbox with the given text at the given world position for - * the given duration. Resets the typewriter scroll, measures the text to - * size the box (width capped at UI_TEXTBOX_MINI_WIDTH_MAX, height derived - * from the resulting wrapped line count), and starts the visibility timer. - * - * @param mini The mini textbox to show. - * @param text Null-terminated source string. - * @param position World-space position the box is anchored to on screen. - * @param duration How long the mini textbox stays visible, in seconds, or - * UI_TEXTBOX_MINI_DURATION_INFINITE to never auto-close. - * @param closed Called once the timer elapses and the box closes. May be - * NULL. - * @param user Opaque pointer passed back through the closed callback. - */ -void uiTextboxMiniShow( - uitextboxmini_t *mini, - const char_t *text, - const vec3 position, - const float_t duration, - uitextboxminiclosedcallback_t closed, - void *user -); - -/** - * Advances the typewriter scroll and counts down the visibility timer. - * Closes the mini textbox and fires its closed callback once the timer - * elapses. Has no effect if not active or if the duration was set to - * UI_TEXTBOX_MINI_DURATION_INFINITE. - * - * @param mini The mini textbox to update. - * @returns Any error that occurs. - */ -errorret_t uiTextboxMiniUpdate(uitextboxmini_t *mini); - -/** - * Draws the mini textbox at its text-measured size, anchored on screen to - * its world-space position via the RPG camera. Has no effect if not - * active. - * - * @param mini The mini textbox to draw. - * @returns Any error that occurs. - */ -errorret_t uiTextboxMiniDraw(uitextboxmini_t *mini); - -/** - * Returns true when the mini textbox is currently visible. - * - * @param mini The mini textbox to query. - * @returns True if active. - */ -bool_t uiTextboxMiniIsActive(const uitextboxmini_t *mini); - -/** - * Immediately closes the mini textbox and fires its closed callback. - * Has no effect if not active. - * - * @param mini The mini textbox to close. - */ -void uiTextboxMiniClose(uitextboxmini_t *mini); diff --git a/src/dusk/ui/rpg/textbox/uitextboxminilist.c b/src/dusk/ui/rpg/textbox/uitextboxminilist.c deleted file mode 100644 index 070bc817..00000000 --- a/src/dusk/ui/rpg/textbox/uitextboxminilist.c +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitextboxminilist.h" - -uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT]; -uint8_t UI_TEXTBOX_MINI_LIST_NEXT; - -errorret_t uiTextboxMiniListInit(void) { - for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) { - uiTextboxMiniInit(&UI_TEXTBOX_MINI_LIST[i]); - } - UI_TEXTBOX_MINI_LIST_NEXT = 0; - errorOk(); -} - -uint8_t uiTextboxMiniListGetNext(void) { - uint8_t index = UI_TEXTBOX_MINI_LIST_NEXT; - UI_TEXTBOX_MINI_LIST_NEXT = - (UI_TEXTBOX_MINI_LIST_NEXT + 1) % UI_TEXTBOX_MINI_LIST_COUNT; - return index; -} - -errorret_t uiTextboxMiniListUpdate(void) { - for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) { - errorChain(uiTextboxMiniUpdate(&UI_TEXTBOX_MINI_LIST[i])); - } - errorOk(); -} - -errorret_t uiTextboxMiniListDraw(void) { - for(uint8_t i = 0; i < UI_TEXTBOX_MINI_LIST_COUNT; i++) { - errorChain(uiTextboxMiniDraw(&UI_TEXTBOX_MINI_LIST[i])); - } - errorOk(); -} diff --git a/src/dusk/ui/rpg/textbox/uitextboxminilist.h b/src/dusk/ui/rpg/textbox/uitextboxminilist.h deleted file mode 100644 index f3040b2c..00000000 --- a/src/dusk/ui/rpg/textbox/uitextboxminilist.h +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "ui/rpg/textbox/uitextboxmini.h" - -#define UI_TEXTBOX_MINI_LIST_COUNT 4 - -extern uitextboxmini_t UI_TEXTBOX_MINI_LIST[UI_TEXTBOX_MINI_LIST_COUNT]; -extern uint8_t UI_TEXTBOX_MINI_LIST_NEXT; - -/** - * Initializes all UI_TEXTBOX_MINI_LIST slots and resets - * UI_TEXTBOX_MINI_LIST_NEXT. - * - * @returns Any error that occurs. - */ -errorret_t uiTextboxMiniListInit(void); - -/** - * Returns the index of the next UI_TEXTBOX_MINI_LIST slot to use, cycling - * through all slots in round-robin order via UI_TEXTBOX_MINI_LIST_NEXT. - * - * @returns The next slot index. - */ -uint8_t uiTextboxMiniListGetNext(void); - -/** - * Updates all UI_TEXTBOX_MINI_LIST slots. - * - * @returns Any error that occurs. - */ -errorret_t uiTextboxMiniListUpdate(void); - -/** - * Draws all active UI_TEXTBOX_MINI_LIST slots. - * - * @returns Any error that occurs. - */ -errorret_t uiTextboxMiniListDraw(void); diff --git a/src/dusk/ui/rpg/uiemoji.c b/src/dusk/ui/rpg/uiemoji.c deleted file mode 100644 index 21a3cbf2..00000000 --- a/src/dusk/ui/rpg/uiemoji.c +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiemoji.h" -#include "display/spritebatch/spritebatch.h" -#include "time/time.h" -#include "util/memory.h" -#include "display/shader/shaderunlit.h" -#include "rpg/entity/entity.h" -#include "rpg/rpgcamera.h" - -uiemoji_t UI_EMOJI; - -errorret_t uiEmojiInit(void) { - memoryZero(&UI_EMOJI, sizeof(UI_EMOJI)); - errorOk(); -} - -void uiEmojiAdd( - const uint8_t entity, - const float_t duration, - const uiemojitype_t type -) { - uint8_t slot = 0xFF; - // Find an available slot if any - for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) { - if(UI_EMOJI.items[i].time <= 0.0f) { - slot = i; - break; - } - - if(UI_EMOJI.items[i].entity == entity) { - slot = i; - break; - } - - if(UI_EMOJI.items[i].type == UI_EMOJI_NULL) { - slot = i; - break; - } - } - - if(slot == 0xFF) { - // Whichever ends soonest - float_t minTime = UI_EMOJI.items[0].time; - slot = 0; - for(uint8_t i = 1; i < UI_EMOJI_ACTIVE_COUNT; i++) { - if(UI_EMOJI.items[i].time >= minTime) continue; - minTime = UI_EMOJI.items[i].time; - slot = i; - } - } - - uiemojiitem_t *item = &UI_EMOJI.items[slot]; - item->entity = entity; - item->time = duration; - item->type = type; -} - -errorret_t uiEmojiUpdate(void) { - for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) { - uiemojiitem_t *item = &UI_EMOJI.items[i]; - if(item->type == UI_EMOJI_NULL) continue; - - item->time -= TIME.delta; - - if(item->time <= 0.0f) { - item->type = UI_EMOJI_NULL; - item->entity = 0xFF; - } - } - - errorOk(); -} - -errorret_t uiEmojiDraw(void) { - spritebatchsprite_t sprites[UI_EMOJI_ACTIVE_COUNT]; - uint8_t count = 0; - - for(uint8_t i = 0; i < UI_EMOJI_ACTIVE_COUNT; i++) { - uiemojiitem_t *item = &UI_EMOJI.items[i]; - if(item->type == UI_EMOJI_NULL) continue; - - vec2 screenPos; - rpgCameraToScreen(ENTITIES[item->entity].renderPosition, screenPos); - - spritebatchsprite_t *sprite = &sprites[count++]; - sprite->min[0] = screenPos[0]; - sprite->min[1] = screenPos[1] - UI_EMOJI_SIZE; - sprite->min[2] = 0.0f; - sprite->max[0] = screenPos[0] + UI_EMOJI_SIZE; - sprite->max[1] = screenPos[1]; - sprite->max[2] = 0.0f; - sprite->uvMin[0] = 0.0f; - sprite->uvMin[1] = 0.0f; - sprite->uvMax[0] = 1.0f; - sprite->uvMax[1] = 1.0f; - } - - if(count == 0) errorOk(); - - shadermaterial_t mat = { - .unlit = { - .color = COLOR_WHITE, - .texture = NULL - } - }; - errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, mat)); - errorChain(spriteBatchFlush()); - errorOk(); -} - -errorret_t uiEmojiDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/rpg/uiemoji.h b/src/dusk/ui/rpg/uiemoji.h deleted file mode 100644 index 6dc8cc00..00000000 --- a/src/dusk/ui/rpg/uiemoji.h +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define UI_EMOJI_ACTIVE_COUNT 4 -#define UI_EMOJI_SIZE 32.0f - -typedef enum { - UI_EMOJI_NULL, - - UI_EMOJI_QUESTION_MARK, - UI_EMOJI_EXCLAMATION_MARK, - - UI_EMOJI_COUNT, -} uiemojitype_t; - -typedef struct { - uint8_t entity; - float_t time; - uiemojitype_t type; -} uiemojiitem_t; - -typedef struct { - uiemojiitem_t items[UI_EMOJI_ACTIVE_COUNT]; -} uiemoji_t; - -extern uiemoji_t UI_EMOJI; - -/** - * Initialize the UI emoji. - */ -errorret_t uiEmojiInit(void); - -/** - * Show a UI emoji for a specific entity. - * - * @param entity The entity to show the emoji for. - * @param duration The duration to display the emoji. - * @param type The type of emoji to display. - */ -void uiEmojiAdd( - const uint8_t entity, - const float_t duration, - const uiemojitype_t type -); - -/** - * Update the UI emoji. - */ -errorret_t uiEmojiUpdate(void); - -/** - * Draw the UI emoji. - */ -errorret_t uiEmojiDraw(void); - -/** - * Dispose of the UI emoji. - */ -errorret_t uiEmojiDispose(void); \ No newline at end of file diff --git a/src/dusk/ui/screen/CMakeLists.txt b/src/dusk/ui/screen/CMakeLists.txt deleted file mode 100644 index 6934dc3b..00000000 --- a/src/dusk/ui/screen/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -add_subdirectory(game) -add_subdirectory(mainmenu) -add_subdirectory(battle) -add_subdirectory(backpack) \ No newline at end of file diff --git a/src/dusk/ui/screen/backpack/CMakeLists.txt b/src/dusk/ui/screen/backpack/CMakeLists.txt deleted file mode 100644 index 570abb53..00000000 --- a/src/dusk/ui/screen/backpack/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uibackpack.c -) diff --git a/src/dusk/ui/screen/backpack/uibackpack.c b/src/dusk/ui/screen/backpack/uibackpack.c deleted file mode 100644 index 269b8f0d..00000000 --- a/src/dusk/ui/screen/backpack/uibackpack.c +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uibackpack.h" -#include "ui/widget/uiframe.h" -#include "rpg/item/backpack.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "assert/assert.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" - -uibackpack_t UI_BACKPACK; - -void uiBackpackTabChanged( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - const itemtype_t type = (itemtype_t)(index + 1); - const inventory_t *inventory = backpackGetInventory(type); - - errorCatch(uiItemListSetItemStacks( - &UI_BACKPACK.itemList, inventory->storage, inventory->storageSize - )); -} - -void uiBackpackTabSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - uiItemListOpen(&UI_BACKPACK.itemList); -} - -errorret_t uiBackpackInit(void) { - memoryZero(&UI_BACKPACK, sizeof(uibackpack_t)); - - errorChain(localeManagerGetString( - "ui.backpack.category_format", - UI_BACKPACK.categoryFormat, - UI_BACKPACK_CATEGORY_FORMAT_MAX, - 0 - )); - - MENU_BEGIN( - &UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs, - uiBackpackTabSelected, NULL, uiBackpackTabChanged - ); - const uint8_t tabCount = ITEM.typeCount - 1; - for(uint8_t i = 0; i < tabCount; i++) { - stringFormat( - UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1, - UI_BACKPACK.categoryFormat, i + 1 - ); - MENU_TAB(UI_BACKPACK.tabLabels[i]); - } - MENU_END(UI_BACKPACK.tabs, menuIndex); - - uiItemListInit( - &UI_BACKPACK.itemList, - UI_BACKPACK_ITEM_LIST_COLUMNS, UI_BACKPACK_ITEM_LIST_ROWS, 1, - NULL, NULL, NULL, NULL - ); - - errorOk(); -} - -errorret_t uiBackpackDraw(void) { - if(!uiMenuIsActive(&UI_BACKPACK.tabsMenu)) errorOk(); - - const float_t width = SCREEN.width; - const float_t height = SCREEN.height; - const float_t x = (float_t)SCREEN.scanX + - ((float_t)SCREEN.scanWidth - width) * 0.5f; - const float_t y = (float_t)SCREEN.scanY + - ((float_t)SCREEN.scanHeight - height) * 0.5f; - - errorChain(uiFrameDraw(x, y, width, height)); - - const float_t contentX = x + UI_FRAME_START_X; - const float_t contentY = y + UI_FRAME_START_Y; - const float_t contentWidth = width - (UI_FRAME_START_X * 2); - const float_t contentHeight = height - (UI_FRAME_START_Y * 2); - - errorChain(uiMenuDraw( - &UI_BACKPACK.tabsMenu, - contentX, - contentY, - contentWidth, - contentHeight - )); - - const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - const float_t listY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y; - - errorChain( - uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth) - ); - - errorChain(spriteBatchFlush()); - errorOk(); -} - -bool_t uiBackpackIsOpen(void) { - return uiMenuIsActive(&UI_BACKPACK.tabsMenu); -} - -void uiBackpackOpen(void) { - uiMenuOpen(&UI_BACKPACK.tabsMenu); -} - -void uiBackpackClose(void) { - uiMenuClose(&UI_BACKPACK.tabsMenu); -} - -errorret_t uiBackpackDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/screen/backpack/uibackpack.h b/src/dusk/ui/screen/backpack/uibackpack.h deleted file mode 100644 index f48c076d..00000000 --- a/src/dusk/ui/screen/backpack/uibackpack.h +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uimenu.h" -#include "ui/widget/uiitemlist.h" -#include "rpg/item/item.h" - -#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_DEF_COUNT_MAX - 1) -#define UI_BACKPACK_TAB_LABEL_MAX 32 -#define UI_BACKPACK_CATEGORY_FORMAT_MAX 32 -#define UI_BACKPACK_ITEM_LIST_COLUMNS 4 -#define UI_BACKPACK_ITEM_LIST_ROWS 5 - -typedef struct { - uimenu_t tabsMenu; - uimenuitem_t tabs[UI_BACKPACK_TAB_COUNT]; - char_t tabLabels[UI_BACKPACK_TAB_COUNT][UI_BACKPACK_TAB_LABEL_MAX]; - char_t categoryFormat[UI_BACKPACK_CATEGORY_FORMAT_MAX]; - - uiitemlist_t itemList; -} uibackpack_t; - -extern uibackpack_t UI_BACKPACK; - -/** - * Initializes the backpack panel and its item type tabs. - * - * @return Any error that occurs. - */ -errorret_t uiBackpackInit(void); - -/** - * Draws the backpack panel. No-op when not visible. - * - * @return Any error that occurs. - */ -errorret_t uiBackpackDraw(void); - -/** - * Returns true when the backpack panel is currently open. - * - * @returns True if open. - */ -bool_t uiBackpackIsOpen(void); - -/** - * Opens the backpack panel. No-op when already open. - */ -void uiBackpackOpen(void); - -/** - * Closes the backpack panel. No-op when already closed. - */ -void uiBackpackClose(void); - -/** - * Disposes of the backpack panel. - * - * @return Any error that occurs. - */ -errorret_t uiBackpackDispose(void); diff --git a/src/dusk/ui/screen/battle/CMakeLists.txt b/src/dusk/ui/screen/battle/CMakeLists.txt deleted file mode 100644 index 1cdef8a0..00000000 --- a/src/dusk/ui/screen/battle/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uibattlemenu.c - uibattlehud.c -) diff --git a/src/dusk/ui/screen/battle/uibattlehud.c b/src/dusk/ui/screen/battle/uibattlehud.c deleted file mode 100644 index 8793a0ac..00000000 --- a/src/dusk/ui/screen/battle/uibattlehud.c +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uibattlehud.h" -#include "rpg/battle/battle.h" -#include "rpg/battle/party.h" -#include "scene/battle/scenebattle.h" -#include "scene/scene.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "util/string.h" -#include "util/memory.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" - -#define UI_BATTLE_HUD_TEXT_MAX 32 -#define UI_BATTLE_HUD_LINE_HEIGHT 12.0f - -uibattlehud_t UI_BATTLE_HUD; - -errorret_t uiBattleHudInit(void) { - memoryZero(&UI_BATTLE_HUD, sizeof(uibattlehud_t)); - - errorChain(localeManagerGetString( - "ui.battle.hud.hp_format", - UI_BATTLE_HUD.hpFormat, - UI_BATTLE_HUD_FORMAT_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.battle.hud.mp_format", - UI_BATTLE_HUD.mpFormat, - UI_BATTLE_HUD_FORMAT_MAX, - 0 - )); - - errorOk(); -} - -errorret_t uiBattleHudDraw(void) { - if(BATTLE.state == BATTLE_STATE_NULL) errorOk(); - - scenebattle_t *battle = &SCENE.data.battle; - - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - const battlefighter_t *fighter = BATTLE.fighters[i]; - if(fighter == NULL) continue; - if(fighter->status == BATTLE_FIGHTER_STATUS_NULL) continue; - - vec3 position; - sceneBattleGetFighterPosition(i, position); - glm_vec3_add(position, (vec3){ 0.5f, 1.3f, 0.0f }, position); - - vec2 screenPos; - sceneBattleWorldToScreen(battle, position, screenPos); - - float_t lineY = screenPos[1]; - if(fighter->team == BATTLE_FIGHTER_TEAM_ALLY) { - errorChain(textDraw( - screenPos[0], lineY, partyGetMemberKey(fighter->id), COLOR_WHITE, NULL - )); - lineY += UI_BATTLE_HUD_LINE_HEIGHT; - } - - char_t hpText[UI_BATTLE_HUD_TEXT_MAX]; - stringFormat( - hpText, UI_BATTLE_HUD_TEXT_MAX - 1, UI_BATTLE_HUD.hpFormat, - fighter->stats.health, fighter->stats.healthMax - ); - errorChain(textDraw(screenPos[0], lineY, hpText, COLOR_WHITE, NULL)); - lineY += UI_BATTLE_HUD_LINE_HEIGHT; - - char_t mpText[UI_BATTLE_HUD_TEXT_MAX]; - stringFormat( - mpText, UI_BATTLE_HUD_TEXT_MAX - 1, UI_BATTLE_HUD.mpFormat, - fighter->stats.mp, fighter->stats.mpMax - ); - errorChain(textDraw(screenPos[0], lineY, mpText, COLOR_CYAN, NULL)); - } - - errorChain(spriteBatchFlush()); - errorOk(); -} diff --git a/src/dusk/ui/screen/battle/uibattlehud.h b/src/dusk/ui/screen/battle/uibattlehud.h deleted file mode 100644 index 47c38f17..00000000 --- a/src/dusk/ui/screen/battle/uibattlehud.h +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define UI_BATTLE_HUD_FORMAT_MAX 32 - -typedef struct { - char_t hpFormat[UI_BATTLE_HUD_FORMAT_MAX]; - char_t mpFormat[UI_BATTLE_HUD_FORMAT_MAX]; -} uibattlehud_t; - -extern uibattlehud_t UI_BATTLE_HUD; - -/** - * Loads the HP/MP format strings from the active locale. - * - * @return Any error that occurs. - */ -errorret_t uiBattleHudInit(void); - -/** - * Draws each living battle fighter's HP/MP as text above its position in - * the battle scene. No-op if no battle is active. - * - * @return An error if drawing failed, or errorOk() if it succeeded. - */ -errorret_t uiBattleHudDraw(void); diff --git a/src/dusk/ui/screen/battle/uibattlemenu.c b/src/dusk/ui/screen/battle/uibattlemenu.c deleted file mode 100644 index bc7bc077..00000000 --- a/src/dusk/ui/screen/battle/uibattlemenu.c +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uibattlemenu.h" -#include "ui/widget/uiframe.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/color.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" -#include "rpg/battle/party.h" - -uibattlemenu_t UI_BATTLE_MENU; - -void uiBattleMenuTargetSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - assertTrue(index < UI_BATTLE_MENU_TARGET_ITEM_COUNT, "index out of bounds"); - battleStateSelectionPlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]); - uiMenuClose(&UI_BATTLE_MENU.targetMenu); - uiMenuClose(&UI_BATTLE_MENU.actionMenu); -} - -void uiBattleMenuOpenTargets(void) { - battlefighter_t *current = battleStateSelectionGetCurrentFighter(); - if(current == NULL) return; - - const battlefighterteam_t enemyTeam = - current->team == BATTLE_FIGHTER_TEAM_ALLY ? - BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY; - - MENU_BEGIN( - &UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems, - uiBattleMenuTargetSelected, NULL, NULL - ); - - for(uint8_t i = 0; i < BATTLE_FIGHTER_POS_COUNT; i++) { - battlefighter_t *candidate = BATTLE.fighters[i]; - if(candidate == NULL) continue; - if(candidate->team != enemyTeam) continue; - if(!battleFighterIsAlive(candidate)) continue; - - stringFormat( - UI_BATTLE_MENU.targetLabels[menuIndex], - UI_BATTLE_MENU_TARGET_LABEL_MAX - 1, - UI_BATTLE_MENU.targetFormat, - candidate->id, candidate->stats.health, candidate->stats.healthMax - ); - UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i; - MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]); - } - - MENU_END(UI_BATTLE_MENU.targetItems, 1); - - uiMenuOpen(&UI_BATTLE_MENU.targetMenu); -} - -bool_t uiBattleMenuActionCancel(const uimenu_t *menu) { - battleStateSelectionGoBack(); - return true; -} - -void uiBattleMenuActionSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - switch(index) { - case UI_BATTLE_MENU_ACTION_INDEX_ATTACK: - uiBattleMenuOpenTargets(); - break; - - case UI_BATTLE_MENU_ACTION_INDEX_FLEE: - battleStateSelectionPlayerFlee(); - uiMenuClose(&UI_BATTLE_MENU.actionMenu); - break; - - default: - break; - } -} - -errorret_t uiBattleMenuInit(void) { - memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t)); - - errorChain(localeManagerGetString( - "ui.battle.menu.attack", - UI_BATTLE_MENU.attackLabel, - UI_BATTLE_MENU_ACTION_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.battle.menu.flee", - UI_BATTLE_MENU.fleeLabel, - UI_BATTLE_MENU_ACTION_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.battle.menu.target_format", - UI_BATTLE_MENU.targetFormat, - UI_BATTLE_MENU_TARGET_FORMAT_MAX, - 0 - )); - - MENU_BEGIN( - &UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems, - uiBattleMenuActionSelected, NULL, NULL - ); - MENU_BUTTON(UI_BATTLE_MENU.attackLabel); - MENU_BUTTON(UI_BATTLE_MENU.fleeLabel); - MENU_END(UI_BATTLE_MENU.actionItems, 1); - - uiMenuSetCancelCallback(&UI_BATTLE_MENU.actionMenu, uiBattleMenuActionCancel); - - errorOk(); -} - -errorret_t uiBattleMenuUpdate(void) { - if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk(); - - battlefighter_t *current = battleStateSelectionGetCurrentFighter(); - if(current == NULL) errorOk(); - if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk(); - - uiMenuOpen(&UI_BATTLE_MENU.actionMenu); - errorOk(); -} - -errorret_t uiBattleMenuDraw(void) { - if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk(); - - const float_t width = UI_BATTLE_MENU_WIDTH; - const float_t height = UI_BATTLE_MENU_HEIGHT; - const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width; - const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height; - - battlefighter_t *current = battleStateSelectionGetCurrentFighter(); - if(current != NULL) { - errorChain(textDraw( - x, y - UI_BATTLE_MENU_NAME_LINE_HEIGHT, - partyGetMemberKey(current->id), COLOR_WHITE, NULL - )); - } - - errorChain(uiFrameDraw(x, y, width, height)); - errorChain(uiMenuDraw( - &UI_BATTLE_MENU.actionMenu, - x + UI_FRAME_START_X, - y + UI_FRAME_START_Y, - width - (UI_FRAME_START_X * 2), - height - (UI_FRAME_START_Y * 2) - )); - - if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) { - const float_t targetY = y - height; - errorChain(uiFrameDraw(x, targetY, width, height)); - errorChain(uiMenuDraw( - &UI_BATTLE_MENU.targetMenu, - x + UI_FRAME_START_X, - targetY + UI_FRAME_START_Y, - width - (UI_FRAME_START_X * 2), - height - (UI_FRAME_START_Y * 2) - )); - } - - errorChain(spriteBatchFlush()); - errorOk(); -} diff --git a/src/dusk/ui/screen/battle/uibattlemenu.h b/src/dusk/ui/screen/battle/uibattlemenu.h deleted file mode 100644 index 6d531a74..00000000 --- a/src/dusk/ui/screen/battle/uibattlemenu.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uimenu.h" -#include "rpg/battle/battle.h" - -#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2 -#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0 -#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1 -#define UI_BATTLE_MENU_ACTION_LABEL_MAX 32 - -#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_POS_COUNT -#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32 -#define UI_BATTLE_MENU_TARGET_FORMAT_MAX 48 - -#define UI_BATTLE_MENU_WIDTH 160.0f -#define UI_BATTLE_MENU_HEIGHT 96.0f -#define UI_BATTLE_MENU_NAME_LINE_HEIGHT 12.0f - -typedef struct { - uimenu_t actionMenu; - uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT]; - char_t attackLabel[UI_BATTLE_MENU_ACTION_LABEL_MAX]; - char_t fleeLabel[UI_BATTLE_MENU_ACTION_LABEL_MAX]; - - uimenu_t targetMenu; - uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT]; - uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT]; - char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT] - [UI_BATTLE_MENU_TARGET_LABEL_MAX]; - char_t targetFormat[UI_BATTLE_MENU_TARGET_FORMAT_MAX]; -} uibattlemenu_t; - -extern uibattlemenu_t UI_BATTLE_MENU; - -/** - * Initializes the battle action/target menus. - * - * @return Any error that occurs. - */ -errorret_t uiBattleMenuInit(void); - -/** - * Updates the battle menu: opens the action menu whenever it becomes a - * player-controlled fighter's turn. - * - * @return Any error that occurs. - */ -errorret_t uiBattleMenuUpdate(void); - -/** - * Draws the battle action menu, and the target menu above it when - * open. No-op when the action menu isn't active. - * - * @return Any error that occurs. - */ -errorret_t uiBattleMenuDraw(void); diff --git a/src/dusk/ui/screen/game/CMakeLists.txt b/src/dusk/ui/screen/game/CMakeLists.txt deleted file mode 100644 index 4b79eda6..00000000 --- a/src/dusk/ui/screen/game/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uigamemenu.c -) diff --git a/src/dusk/ui/screen/game/uigamemenu.c b/src/dusk/ui/screen/game/uigamemenu.c deleted file mode 100644 index dbe26561..00000000 --- a/src/dusk/ui/screen/game/uigamemenu.c +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uigamemenu.h" -#include "ui/widget/uiframe.h" -#include "ui/dialog/uiconfirm.h" -#include "ui/screen/backpack/uibackpack.h" -#include "ui/rpg/textbox/uitextboxmain.h" -#include "util/memory.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" -#include "assert/assert.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" -#include "error/error.h" -#include "util/string.h" - -#define UI_GAME_MENU_INDEX_CHARACTERS 0 -#define UI_GAME_MENU_INDEX_ITEMS 1 - -uigamemenu_t UI_GAME_MENU; - -void uiGameMenuSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - switch(index) { - case UI_GAME_MENU_INDEX_ITEMS: - uiBackpackOpen(); - break; - default: - assertUnreachable("Unhandled game menu index"); - } -} - -errorret_t uiGameMenuInit(void) { - memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); - - errorChain(localeManagerGetString( - "ui.game_menu.characters", - UI_GAME_MENU.charactersLabel, - UI_GAME_MENU_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.items", - UI_GAME_MENU.itemsLabel, - UI_GAME_MENU_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.settings", - UI_GAME_MENU.settingsLabel, - UI_GAME_MENU_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_success", - UI_GAME_MENU.saveSuccessText, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_cancelled", - UI_GAME_MENU.saveCancelledText, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_unavailable", - UI_GAME_MENU.saveUnavailableText, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_temporary", - UI_GAME_MENU.saveTemporaryText, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_create_confirm", - UI_GAME_MENU.saveCreateConfirmText, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_failed_format", - UI_GAME_MENU.saveFailedFormat, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - errorChain(localeManagerGetString( - "ui.game_menu.save_check_failed_format", - UI_GAME_MENU.saveCheckFailedFormat, - UI_GAME_MENU_MESSAGE_MAX, - 0 - )); - - MENU_BEGIN( - &UI_GAME_MENU.menu, UI_GAME_MENU.items, uiGameMenuSelected, NULL, NULL - ); - MENU_BUTTON(UI_GAME_MENU.charactersLabel); - MENU_BUTTON(UI_GAME_MENU.itemsLabel); - MENU_BUTTON(UI_GAME_MENU.saveLabel); - - MENU_END(UI_GAME_MENU.items, 1); - - errorOk(); -} - -errorret_t uiGameMenuDraw(void) { - if(!uiMenuIsActive(&UI_GAME_MENU.menu)) errorOk(); - - const float_t width = UI_GAME_MENU_WIDTH; - const float_t height = (float_t)SCREEN.scanHeight; - const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width; - const float_t y = (float_t)SCREEN.scanY; - - errorChain(uiFrameDraw(x, y, width, height)); - errorChain(uiMenuDraw( - &UI_GAME_MENU.menu, - x + UI_FRAME_START_X, - y + UI_FRAME_START_Y, - width - (UI_FRAME_START_X * 2), - height - (UI_FRAME_START_Y * 2) - )); - - errorChain(spriteBatchFlush()); - errorOk(); -} - -bool_t uiGameMenuIsOpen(void) { - return uiMenuIsActive(&UI_GAME_MENU.menu); -} - -void uiGameMenuOpen() { - uiMenuOpen(&UI_GAME_MENU.menu); -} - -void uiGameMenuClose() { - uiMenuClose(&UI_GAME_MENU.menu); -} - -errorret_t uiGameMenuDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/screen/game/uigamemenu.h b/src/dusk/ui/screen/game/uigamemenu.h deleted file mode 100644 index 87ec2a4e..00000000 --- a/src/dusk/ui/screen/game/uigamemenu.h +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uimenu.h" - -#define UI_GAME_MENU_ITEM_COUNT 4 -#define UI_GAME_MENU_WIDTH 150.0f -#define UI_GAME_MENU_LABEL_MAX 32 -#define UI_GAME_MENU_MESSAGE_MAX 192 - -typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_GAME_MENU_ITEM_COUNT]; - char_t charactersLabel[UI_GAME_MENU_LABEL_MAX]; - char_t itemsLabel[UI_GAME_MENU_LABEL_MAX]; - char_t settingsLabel[UI_GAME_MENU_LABEL_MAX]; - char_t saveLabel[UI_GAME_MENU_LABEL_MAX]; - char_t saveSuccessText[UI_GAME_MENU_MESSAGE_MAX]; - char_t saveCancelledText[UI_GAME_MENU_MESSAGE_MAX]; - char_t saveUnavailableText[UI_GAME_MENU_MESSAGE_MAX]; - char_t saveTemporaryText[UI_GAME_MENU_MESSAGE_MAX]; - char_t saveCreateConfirmText[UI_GAME_MENU_MESSAGE_MAX]; - char_t saveFailedFormat[UI_GAME_MENU_MESSAGE_MAX]; - char_t saveCheckFailedFormat[UI_GAME_MENU_MESSAGE_MAX]; -} uigamemenu_t; - -extern uigamemenu_t UI_GAME_MENU; - -/** - * Initializes the game menu panel. - * - * @return Any error that occurs. - */ -errorret_t uiGameMenuInit(void); - -/** - * Draws the game menu panel. No-op when not visible. - * - * @return Any error that occurs. - */ -errorret_t uiGameMenuDraw(void); - -/** - * Returns true when the game menu panel is currently open. - * - * @returns True if open. - */ -bool_t uiGameMenuIsOpen(void); - -/** - * Opens the game menu panel. No-op when already open. - */ -void uiGameMenuOpen(); - -/** - * Closes the game menu panel. No-op when already closed. - */ -void uiGameMenuClose(); - -/** - * Disposes of the game menu panel. - * - * @return Any error that occurs. - */ -errorret_t uiGameMenuDispose(void); diff --git a/src/dusk/ui/screen/mainmenu/CMakeLists.txt b/src/dusk/ui/screen/mainmenu/CMakeLists.txt deleted file mode 100644 index 2231cb11..00000000 --- a/src/dusk/ui/screen/mainmenu/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uimainmenu.c -) diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c deleted file mode 100644 index 168a1d63..00000000 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uimainmenu.h" -#include "ui/widget/uiframe.h" -#include "util/memory.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" -#include "assert/assert.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" -#include "ui/dialog/save/uiselectsave.h" -#include "ui/overlay/uifatalerror.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "scene/scene.h" -#include "asset/asset.h" -#include "save/save.h" -#include "rpg/overworld/map.h" - -#define UI_MAIN_MENU_INDEX_START_GAME 0 -#define UI_MAIN_MENU_INDEX_OPTIONS 1 -#define UI_MAIN_MENU_INDEX_QUIT 2 - -uimainmenu_t UI_MAIN_MENU; - -cutscene_t *uiMainMenuLoadCutscene(void) { - return cutsceneCutsceneResolve("main_menu"); -} - -void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) { - if(slotIndex == UI_SELECT_SAVE_RESULT_NONE) { - cutsceneSystemLoad("cutscenes/main_menu.jsonc"); - return; - } - - SAVE.slotCurrent = slotIndex; - errorret_t initResult = saveSlotInit(&SAVE.slot); - if(errorIsNotOk(initResult)) { - errorCatch(errorPrint(initResult)); - uiFatalErrorOpen(initResult.state->message); - return; - } - - errorret_t result = saveLoadSlot(); - if(errorIsNotOk(result)) { - errorCatch(errorPrint(result)); - uiFatalErrorOpen(result.state->message); - return; - } - - assertStrLenMin( - SAVE.slot.cachedData.mapName, 1, "Save slot has no map name" - ); - errorret_t mapResult = mapSetMap(SAVE.slot.cachedData.mapName); - if(errorIsNotOk(mapResult)) { - errorCatch(errorPrint(mapResult)); - uiFatalErrorOpen(mapResult.state->message); - return; - } - - sceneSet(SCENE_TYPE_OVERWORLD); - - if(SAVE.slot.currentCutscene[0] != '\0') { - cutsceneSystemLoad(SAVE.slot.currentCutscene); - } -} - -void uiMainMenuOpenSelectSave(void *userData) { - uiSelectSaveOpen( - UI_SELECT_SAVE_TYPE_LOAD, uiMainMenuSelectSaveResult, NULL - ); -} - -void uiMainMenuStartGame(void) { - if(CUTSCENE_SYSTEM.scene == NULL) { - cutscene_t *cutscene = uiMainMenuLoadCutscene(); - if(cutscene == NULL) return; - cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "NEW_GAME"); - } else { - cutsceneSystemGoTo("NEW_GAME"); - } - - cutsceneSystemSetOnComplete(uiMainMenuOpenSelectSave); -} - -void uiMainMenuOptions(void) { - if(CUTSCENE_SYSTEM.scene == NULL) { - cutscene_t *cutscene = uiMainMenuLoadCutscene(); - if(cutscene == NULL) return; - cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "OPTIONS"); - } else { - cutsceneSystemGoTo("OPTIONS"); - } -} - -void uiMainMenuQuit(void) { - if(CUTSCENE_SYSTEM.scene == NULL) { - cutscene_t *cutscene = uiMainMenuLoadCutscene(); - if(cutscene == NULL) return; - cutsceneSystemStartCutsceneAndGoToMarker(cutscene, "QUIT"); - } else { - cutsceneSystemGoTo("QUIT"); - } -} - -void uiMainMenuSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - - switch(index) { - case UI_MAIN_MENU_INDEX_START_GAME: - uiMenuClose(&UI_MAIN_MENU.menu); - uiMainMenuStartGame(); - break; - - case UI_MAIN_MENU_INDEX_OPTIONS: - uiMainMenuOptions(); - break; - - case UI_MAIN_MENU_INDEX_QUIT: - uiMenuClose(&UI_MAIN_MENU.menu); - uiMainMenuQuit(); - break; - - default: - break; - } -} - -errorret_t uiMainMenuInit(void) { - memoryZero(&UI_MAIN_MENU, sizeof(uimainmenu_t)); - - errorChain(localeManagerGetString( - "main_menu.start_game", - UI_MAIN_MENU.startGameLabel, - UI_MAIN_MENU_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "main_menu.options", - UI_MAIN_MENU.optionsLabel, - UI_MAIN_MENU_LABEL_MAX, - 0 - )); - errorChain(localeManagerGetString( - "main_menu.quit", - UI_MAIN_MENU.quitLabel, - UI_MAIN_MENU_LABEL_MAX, - 0 - )); - - MENU_BEGIN( - &UI_MAIN_MENU.menu, UI_MAIN_MENU.items, uiMainMenuSelected, NULL, NULL - ); - MENU_BUTTON(UI_MAIN_MENU.startGameLabel); - MENU_BUTTON(UI_MAIN_MENU.optionsLabel); - MENU_BUTTON(UI_MAIN_MENU.quitLabel); - MENU_END(UI_MAIN_MENU.items, 1); - uiMenuSetDisableBack(&UI_MAIN_MENU.menu, true); - - errorOk(); -} - -void uiMainMenuOpen(void) { - if(uiMenuIsActive(&UI_MAIN_MENU.menu)) return; - uiMenuOpen(&UI_MAIN_MENU.menu); -} - -errorret_t uiMainMenuDraw(void) { - if(!uiMenuIsActive(&UI_MAIN_MENU.menu)) errorOk(); - - const float_t width = UI_MAIN_MENU_WIDTH; - const float_t height = UI_MAIN_MENU_HEIGHT; - const float_t x = - (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f; - const float_t y = - (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f; - - errorChain(uiFrameDraw(x, y, width, height)); - errorChain(uiMenuDraw( - &UI_MAIN_MENU.menu, - x + UI_FRAME_START_X, - y + UI_FRAME_START_Y, - width - (UI_FRAME_START_X * 2), - height - (UI_FRAME_START_Y * 2) - )); - - errorChain(spriteBatchFlush()); - errorOk(); -} - -errorret_t uiMainMenuDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.h b/src/dusk/ui/screen/mainmenu/uimainmenu.h deleted file mode 100644 index 82f9b9cb..00000000 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.h +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uimenu.h" - -typedef struct cutscene_s cutscene_t; - -#define UI_MAIN_MENU_ITEM_COUNT 3 -#define UI_MAIN_MENU_WIDTH 200.0f -#define UI_MAIN_MENU_HEIGHT 160.0f -#define UI_MAIN_MENU_LABEL_MAX 32 - -typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_MAIN_MENU_ITEM_COUNT]; - char_t startGameLabel[UI_MAIN_MENU_LABEL_MAX]; - char_t optionsLabel[UI_MAIN_MENU_LABEL_MAX]; - char_t quitLabel[UI_MAIN_MENU_LABEL_MAX]; -} uimainmenu_t; - -extern uimainmenu_t UI_MAIN_MENU; - -/** - * Initializes the main menu panel. - * - * @return Any error that occurs. - */ -errorret_t uiMainMenuInit(void); - -/** - * Opens the main menu panel. No-op when already open. - */ -void uiMainMenuOpen(void); - -/** - * Loads (or re-loads) the main menu cutscene asset (assets/cutscenes/ - * main_menu.jsonc, parsed directly at load time rather than compiled in) - * and returns it - the shared accessor for the "Start Game"/Options/Quit - * flows below to jump/restart it. Only one JSON-authored cutscene is ever - * resident at a time (see CUTSCENE_LOADED_ITEMS_MAX's doc comment in - * cutscenesystem.h), so this resolves fresh on every call rather than - * caching anything. - * - * @return The main menu cutscene, or NULL on failure (already reported via - * uiFatalErrorOpen). - */ -cutscene_t *uiMainMenuLoadCutscene(void); - -/** - * Starts the "Start Game" flow: jumps the already-running main menu - * cutscene to its NEW_GAME marker (or restarts it there if it already ran - * to completion, e.g. the player backed out of the save picker back to - * this menu) to load every save slot - retrying on error, prompting if no - * save device is found - then arms uiMainMenuOpenSelectSave to open the - * load-game picker once that succeeds. Called when the player selects - * Start Game. - */ -void uiMainMenuStartGame(void); - -/** - * Cutscene completion callback armed by uiMainMenuStartGame: opens the - * load-game picker once the NEW_GAME flow has finished loading every save - * slot. - * - * @param userData Unused - required by cutscenecallback_t's signature. - */ -void uiMainMenuOpenSelectSave(void *userData); - -/** - * Result callback for the load-game picker opened by - * uiMainMenuOpenSelectSave: re-opens this panel if backed out of without - * choosing a slot, otherwise starts the game with the chosen slot. - * - * @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE. - * @param user Unused - required by uiselectsaveresultcallback_t's signature. - */ -void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user); - -/** - * Draws the main menu panel, centered on screen. No-op when not active. - * - * @return Any error that occurs. - */ -errorret_t uiMainMenuDraw(void); - -/** - * Disposes of the main menu panel. - * - * @return Any error that occurs. - */ -errorret_t uiMainMenuDispose(void); diff --git a/src/dusk/ui/transition/CMakeLists.txt b/src/dusk/ui/transition/CMakeLists.txt deleted file mode 100644 index 0e365038..00000000 --- a/src/dusk/ui/transition/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uitransition.c - uitransitionfade.c -) diff --git a/src/dusk/ui/transition/uitransition.c b/src/dusk/ui/transition/uitransition.c deleted file mode 100644 index 32f5dce9..00000000 --- a/src/dusk/ui/transition/uitransition.c +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitransition.h" -#include "uitransitionfade.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "time/time.h" -#include "util/math.h" - -uitransition_t UI_TRANSITION; - -uitransitiondrawfunc_t UI_TRANSITION_TYPE_DRAW[UI_TRANSITION_TYPE_COUNT] = { - [UI_TRANSITION_TYPE_FADE] = uiTransitionFadeDraw -}; - -errorret_t uiTransitionInit(void) { - memoryZero(&UI_TRANSITION, sizeof(uitransition_t)); - - // uiTransitionStart((uitransitionstartparams_t){ - // .duration = 5.0f, - // .type = UI_TRANSITION_TYPE_FADE, - // .params = { - // .fade = { - // .easing = EASING_OUT_QUART, - // .fromColor = COLOR_TRANSPARENT_BLACK, - // .toColor = COLOR_BLACK - // } - // }, - // .finished = NULL, - // .user = NULL - // }); - - errorOk(); -} - -errorret_t uiTransitionUpdate(void) { - // Update time. - float_t newTime = UI_TRANSITION.data.time + TIME.delta; - - UI_TRANSITION.data.time = mathClamp( - newTime, - 0.0f, - UI_TRANSITION.data.duration - ); - UI_TRANSITION.data.t = ( - (UI_TRANSITION.data.duration <= 0.0f) ? - 1.0f : - (UI_TRANSITION.data.time / UI_TRANSITION.data.duration) - ); - - if(UI_TRANSITION.data.t >= 1.0f && UI_TRANSITION.finished) { - // Saved and cleared before firing so a callback that immediately - // starts another transition isn't clobbered by this function's own - // cleanup running after it - same reentrancy hazard as uiFocusPop, - // see src/dusk/ui/focus/uifocus.c. Without this, finished would also - // just keep re-firing every frame forever once t reaches 1.0. - uitransitioncallback_t finished = UI_TRANSITION.finished; - void *user = UI_TRANSITION.user; - UI_TRANSITION.finished = NULL; - finished(user); - } - - errorOk(); -} - -errorret_t uiTransitionDraw(void) { - uitransitiondrawfunc_t draw = UI_TRANSITION_TYPE_DRAW[UI_TRANSITION.type]; - if(!draw) errorOk(); - return draw(&UI_TRANSITION.data); -} - -void uiTransitionStart(const uitransitionstartparams_t info) { - assertTrue(info.type > UI_TRANSITION_TYPE_NULL, "Invalid type"); - assertTrue(info.type < UI_TRANSITION_TYPE_COUNT, "Invalid type"); - assertTrue(info.duration >= 0.0f, "Duration must be non-negative"); - - memoryZero(&UI_TRANSITION, sizeof(uitransition_t)); - - UI_TRANSITION.data.duration = info.duration; - UI_TRANSITION.type = info.type; - UI_TRANSITION.data.params = info.params; - UI_TRANSITION.finished = info.finished; - UI_TRANSITION.user = info.user; -} \ No newline at end of file diff --git a/src/dusk/ui/transition/uitransition.h b/src/dusk/ui/transition/uitransition.h deleted file mode 100644 index 305ec5c3..00000000 --- a/src/dusk/ui/transition/uitransition.h +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/color.h" -#include "animation/easing.h" -#include "uitransitiondata.h" - -typedef void (*uitransitioncallback_t)(void *user); - -typedef enum { - UI_TRANSITION_TYPE_NULL, - - UI_TRANSITION_TYPE_FADE, - - UI_TRANSITION_TYPE_COUNT -} uitransitiontype_t; - -extern uitransitiondrawfunc_t UI_TRANSITION_TYPE_DRAW[UI_TRANSITION_TYPE_COUNT]; - - -typedef struct { - uitransitiondata_t data; - uitransitiontype_t type; - uitransitioncallback_t finished; - void *user; -} uitransition_t; - -extern uitransition_t UI_TRANSITION; - -/** - * Initializes the transition overlay, zeroing all fields and wiring the - * onTransitionEnd event. - * - * @return Any error that occurs. - */ -errorret_t uiTransitionInit(void); - -/** - * Advances the transition. Fires onTransitionEnd once when it completes. - * Safe to call when no transition is running. - * - * @return Any error that occurs. - */ -errorret_t uiTransitionUpdate(void); - -/** - * Renders the transition overlay. Skipped entirely when the current alpha - * is zero. - * - * @return Any error that occurs. - */ -errorret_t uiTransitionDraw(void); - -typedef struct { - float_t duration; - uitransitiontype_t type; - uitransitionparams_t params; - uitransitioncallback_t finished; - void *user; -} uitransitionstartparams_t; - -/** - * Starts a transition described by an info struct. - * - * @param info Transition parameters. - */ -void uiTransitionStart(const uitransitionstartparams_t info); - diff --git a/src/dusk/ui/transition/uitransitiondata.h b/src/dusk/ui/transition/uitransitiondata.h deleted file mode 100644 index 37e8430f..00000000 --- a/src/dusk/ui/transition/uitransitiondata.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "uitransitionfade.h" - -typedef union uitransitionparams_u { - uitransitionfade_t fade; -} uitransitionparams_t; - -typedef struct uitransitiondata_s { - float_t time; - float_t duration; - float_t t; - uitransitionparams_t params; -} uitransitiondata_t; - -typedef errorret_t (*uitransitiondrawfunc_t)(const uitransitiondata_t *data); diff --git a/src/dusk/ui/transition/uitransitionfade.c b/src/dusk/ui/transition/uitransitionfade.c deleted file mode 100644 index 45e3168d..00000000 --- a/src/dusk/ui/transition/uitransitionfade.c +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitransition.h" -#include "assert/assert.h" -#include "display/screen/screen.h" -#include "display/texture/texture.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" - -errorret_t uiTransitionFadeDraw(const uitransitiondata_t *data) { - assertNotNull(data, "data must not be NULL"); - - float_t e = easingApply(data->params.fade.easing, data->t); - - float_t r = (float_t)( - data->params.fade.toColor.r - data->params.fade.fromColor.r - ) * e; - float_t g = (float_t)( - data->params.fade.toColor.g - data->params.fade.fromColor.g - ) * e; - float_t b = (float_t)( - data->params.fade.toColor.b - data->params.fade.fromColor.b - ) * e; - float_t a = (float_t)( - data->params.fade.toColor.a - data->params.fade.fromColor.a - ) * e; - - color_t color = color4b( - data->params.fade.fromColor.r + (uint8_t)r, - data->params.fade.fromColor.g + (uint8_t)g, - data->params.fade.fromColor.b + (uint8_t)b, - data->params.fade.fromColor.a + (uint8_t)a - ); - - if(color.a == 0) errorOk(); - - spritebatchsprite_t sprite = { - .min = { 0.0f, 0.0f, 0.0f }, - .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t material = { - .unlit = { - .color = color, - .texture = NULL - } - }; - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - return spriteBatchFlush(); -} diff --git a/src/dusk/ui/transition/uitransitionfade.h b/src/dusk/ui/transition/uitransitionfade.h deleted file mode 100644 index a1b2475a..00000000 --- a/src/dusk/ui/transition/uitransitionfade.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/color.h" -#include "animation/easing.h" - -typedef struct uitransitiondata_s uitransitiondata_t; - -typedef struct { - color_t fromColor; - color_t toColor; - easingtype_t easing; -} uitransitionfade_t; - -/** - * Renders a full-screen color overlay interpolated between fade->fromColor - * and fade->toColor using fade->easing. t is the raw normalized progress - * in [0, 1] before easing is applied. No-op when the resulting alpha is zero. - * - * @param fade The fade parameters. - * @return Any error that occurs. - */ -errorret_t uiTransitionFadeDraw(const uitransitiondata_t *fade); diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c deleted file mode 100644 index 2cd586d1..00000000 --- a/src/dusk/ui/ui.c +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "ui.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" -#include "ui/uielement.h" -#include "ui/uielementlist.h" -#include "ui/focus/uifocus.h" -#include "log/log.h" - -ui_t UI; - -errorret_t uiInit(void) { - memoryZero(&UI, sizeof(ui_t)); - uiFocusInit(); - - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementInit(element)); - element++; - } - - errorOk(); -} - -errorret_t uiUpdate(void) { - uiFocusUpdate(); - - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementUpdate(element)); - element++; - } - - errorOk(); -} - -errorret_t uiRender(void) { - const uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementDraw(element)); - - if(SPRITEBATCH.spriteCount > 0) { - logDebug("Finished UI element but unflushed sprites remain.\n"); - } - - element++; - } - - errorOk(); -} - -errorret_t uiDispose(void) { - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementDispose(element)); - element++; - } - errorOk(); -} - -errorret_t uiUpdateTranslations(void) { - uielement_t *element = &UI_ELEMENTS[0]; - while(!uiElementIsNull(element)) { - errorChain(uiElementUpdateTranslations(element)); - element++; - } - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/ui/ui.h b/src/dusk/ui/ui.h deleted file mode 100644 index 8d6a88ad..00000000 --- a/src/dusk/ui/ui.h +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - void *nothing; -} ui_t; - -extern ui_t UI; - -/** - * Initializes the UI system. - */ -errorret_t uiInit(void); - -/** - * Updates the UI system. - * - * @return Any error that occurs. - */ -errorret_t uiUpdate(void); - -/** - * Renders the UI system. - * - * @return Any error that occurs. - */ -errorret_t uiRender(void); - -/** - * Disposes of the UI system. - * - * @return Any error that occurs. - */ -errorret_t uiDispose(void); - -/** - * Notifies the UI system that the active locale has changed, so every - * registered element can re-fetch any localized strings it caches. - * - * @return Any error that occurs. - */ -errorret_t uiUpdateTranslations(void); \ No newline at end of file diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c deleted file mode 100644 index af4c8cf3..00000000 --- a/src/dusk/ui/uielement.c +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uielement.h" -#include "assert/assert.h" -#include "ui/widget/uiframe.h" -#include "engine/engine.h" - -bool_t uiElementIsNull(const uielement_t *element) { - return element->init == NULL && - element->update == NULL && - element->draw == NULL && - element->dispose == NULL && - element->updateTranslations == NULL; -} - -errorret_t uiElementInit(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - errorChain(uiFrameInit()); - if(element->init != NULL) errorChain(element->init()); - errorOk(); -} - -errorret_t uiElementUpdate(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->update != NULL) errorChain(element->update()); - errorOk(); -} - -errorret_t uiElementDraw(const uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->draw != NULL) errorChain(element->draw()); - errorOk(); -} - -errorret_t uiElementDispose(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->dispose != NULL) errorChain(element->dispose()); - errorChain(uiFrameDispose()); - errorOk(); -} - -errorret_t uiElementUpdateTranslations(uielement_t *element) { - assertNotNull(element, "element must not be NULL"); - if(element->updateTranslations != NULL) { - errorChain(element->updateTranslations()); - } - errorOk(); -} \ No newline at end of file diff --git a/src/dusk/ui/uielement.h b/src/dusk/ui/uielement.h deleted file mode 100644 index d4fdc74f..00000000 --- a/src/dusk/ui/uielement.h +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - errorret_t (*init)(); - errorret_t (*update)(); - errorret_t (*draw)(); - errorret_t (*dispose)(); - errorret_t (*updateTranslations)(); -} uielement_t; - -/** - * Returns true when all callbacks on the element are NULL, which marks - * the end of the UI_ELEMENTS array. - * - * @param element The element to test. - * @returns True if the element is the null terminator. - */ -bool_t uiElementIsNull(const uielement_t *element); - -/** - * Initializes a UI element, invoking its init callback if set. - * - * @param element The element to initialize. - * @return Any error that occurs. - */ -errorret_t uiElementInit(uielement_t *element); - -/** - * Updates a UI element, calling its update callback if set. - * - * @param element The element to update. - * @return Any error that occurs. - */ -errorret_t uiElementUpdate(uielement_t *element); - -/** - * Draws a UI element, calling its draw callback if set. - * - * @param element The element to render. - * @return Any error that occurs. - */ -errorret_t uiElementDraw(const uielement_t *element); - -/** - * Disposes of a UI element, invoking its dispose callback if set. - * - * @param element The element to dispose. - * @return Any error that occurs. - */ -errorret_t uiElementDispose(uielement_t *element); - -/** - * Notifies a UI element that the active locale has changed, calling its - * updateTranslations callback if set, so it can re-fetch any localized - * strings it caches. - * - * @param element The element to notify. - * @return Any error that occurs. - */ -errorret_t uiElementUpdateTranslations(uielement_t *element); diff --git a/src/dusk/ui/uielementlist.c b/src/dusk/ui/uielementlist.c deleted file mode 100644 index 9f3783dd..00000000 --- a/src/dusk/ui/uielementlist.c +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uielementlist.h" -#include "ui/widget/uiframe.h" -#include "ui/debug/uifps.h" -#include "ui/overlay/uifullbox.h" -#include "ui/overlay/uiloading.h" -#include "ui/overlay/uifatalerror.h" -#include "ui/debug/uiplayerpos.h" -#include "ui/debug/uicutscene.h" -#include "ui/overlay/uicrop.h" -#include "ui/transition/uitransition.h" -#include "ui/debug/uiconsole.h" -#include "ui/screen/game/uigamemenu.h" -#include "ui/screen/mainmenu/uimainmenu.h" -#include "ui/screen/battle/uibattlemenu.h" -#include "ui/screen/battle/uibattlehud.h" -#include "ui/screen/backpack/uibackpack.h" -#include "ui/dialog/uiconfirm.h" -#include "ui/widget/uimodal.h" -#include "ui/dialog/keyboard/uikeyboard.h" -#include "ui/dialog/save/uiselectsave.h" -#include "ui/rpg/textbox/uitextboxmain.h" -#include "ui/rpg/textbox/uitextboxminilist.h" -#include "ui/rpg/uiemoji.h" - -uielement_t UI_ELEMENTS[] = { - // =========================================================================== - // Non-Rendered Components, just needs init and deinit - // =========================================================================== - { - .init = uiFrameInit, - .dispose = uiFrameDispose - }, - - // =========================================================================== - // In-World Gameplay Components, sits below all UI but above gameplay. - // =========================================================================== - { - .init = uiEmojiInit, - .update = uiEmojiUpdate, - .draw = uiEmojiDraw, - .dispose = uiEmojiDispose - }, - - // =========================================================================== - // Low rendered components, usually gameplay non-world components. - // =========================================================================== - { - .init = uiFullboxUnderInit, - .update = uiFullboxUnderUpdate, - .draw = uiFullboxUnderDraw - }, - - // =========================================================================== - // Most menus and non-gameplay components. Hierarchy is very important. - // =========================================================================== - { - .init = uiGameMenuInit, - .draw = uiGameMenuDraw, - .dispose = uiGameMenuDispose - }, - { - .init = uiMainMenuInit, - .draw = uiMainMenuDraw, - .dispose = uiMainMenuDispose - }, - { - .init = uiBattleHudInit, - .draw = uiBattleHudDraw - }, - { - .init = uiBattleMenuInit, - .update = uiBattleMenuUpdate, - .draw = uiBattleMenuDraw - }, - { - .init = uiBackpackInit, - .draw = uiBackpackDraw, - .dispose = uiBackpackDispose - }, - - // =========================================================================== - // Modals, Popups and Dialogs, above most other UI things. - // =========================================================================== - { - .init = uiSelectSaveInit, - .draw = uiSelectSaveDraw, - .dispose = uiSelectSaveDispose - }, - { - .init = uiTextboxMainInit, - .update = uiTextboxMainUpdate, - .draw = uiTextboxMainDraw - }, - { - .init = uiTextboxMiniListInit, - .update = uiTextboxMiniListUpdate, - .draw = uiTextboxMiniListDraw - }, - { - .init = uiKeyboardInit, - .draw = uiKeyboardDraw, - .dispose = uiKeyboardDispose - }, - { - .init = uiModalInit, - .draw = uiModalDraw, - .dispose = uiModalDispose - }, - { - .init = uiConfirmInit, - .dispose = uiConfirmDispose - }, - - // =========================================================================== - // Overlayed components, can even outdraw all UI things. - // =========================================================================== - { - .init = uiTransitionInit, - .update = uiTransitionUpdate, - .draw = uiTransitionDraw - }, - { - .init = uiFullboxOverInit, - .update = uiFullboxOverUpdate, - .draw = uiFullboxOverDraw - }, - - { - .init = uiLoadingInit, - .update = uiLoadingUpdate, - .draw = uiLoadingDraw - }, - { - .init = uiFatalErrorInit, - .draw = uiFatalErrorDraw, - .dispose = uiFatalErrorDispose - }, - - // =========================================================================== - // Important rendering components, lives outside the primary game engine - // =========================================================================== - { - .init = uiCropInit, - .draw = uiCropDraw - }, - - // =========================================================================== - // Debug components, disregards everything else. - // =========================================================================== - { .init = uiConsoleInit, .draw = uiConsoleDraw }, - { .init = uiFPSInit, .draw = uiFPSDraw }, - { .init = uiPlayerPosInit, .draw = uiPlayerPosDraw }, - { .init = uiCutsceneInit, .draw = uiCutsceneDraw }, - - // =========================================================================== - // Null terminator - // =========================================================================== - { 0 } -}; diff --git a/src/dusk/ui/uielementlist.h b/src/dusk/ui/uielementlist.h deleted file mode 100644 index 11bbca33..00000000 --- a/src/dusk/ui/uielementlist.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "ui/uielement.h" - -extern uielement_t UI_ELEMENTS[]; diff --git a/src/dusk/ui/widget/CMakeLists.txt b/src/dusk/ui/widget/CMakeLists.txt deleted file mode 100644 index e15d658a..00000000 --- a/src/dusk/ui/widget/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - uibutton.c - uicheckbox.c - uiframe.c - uilabel.c - uitab.c - uislider.c - uidropdown.c - uiscrolling.c - uiitem.c - uiitemlist.c - uimenu.c - uimodal.c -) diff --git a/src/dusk/ui/widget/uibutton.c b/src/dusk/ui/widget/uibutton.c deleted file mode 100644 index 789676af..00000000 --- a/src/dusk/ui/widget/uibutton.c +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uibutton.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/color.h" - -void uiButtonInit(uibutton_t *button, const char_t *label) { - assertNotNull(button, "Button cannot be NULL"); - assertNotNull(label, "Label cannot be NULL"); - - memoryZero(button, sizeof(uibutton_t)); - uiLabelInit(&button->label, label, button->sprites, UI_BUTTON_SPRITES_MAX); - button->label.dirty = true; -} - -bool_t uiButtonIsHighlighted(const uibutton_t *button) { - assertNotNull(button, "Button cannot be NULL"); - return button->highlighted; -} - -void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) { - assertNotNull(button, "Button cannot be NULL"); - button->highlighted = highlighted; -} - -bool_t uiButtonIsActive(const uibutton_t *button) { - assertNotNull(button, "Button cannot be NULL"); - return button->active; -} - -void uiButtonSetActive(uibutton_t *button, const bool_t active) { - assertNotNull(button, "Button cannot be NULL"); - button->active = active; -} - -errorret_t uiButtonDraw( - uibutton_t *button, - const float_t x, - const float_t y -) { - assertNotNull(button, "Button cannot be NULL"); - - uiLabelSetX(&button->label, x); - uiLabelSetY(&button->label, y); - - return uiLabelRender( - &button->label, - button->active ? COLOR_GREEN : - button->highlighted ? COLOR_RED : - COLOR_WHITE - ); -} diff --git a/src/dusk/ui/widget/uibutton.h b/src/dusk/ui/widget/uibutton.h deleted file mode 100644 index 87cd1a21..00000000 --- a/src/dusk/ui/widget/uibutton.h +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" - -#define UI_BUTTON_SPRITES_MAX 32 - -typedef struct { - uilabel_t label; - spritebatchsprite_t sprites[UI_BUTTON_SPRITES_MAX]; - bool_t highlighted; - // Independent of highlighted (which the focus system drives based on - // what's currently focused) - for a button that also needs to show a - // toggled-on state regardless of focus, e.g. the keyboard's CAPS/SHIFT - // keys. - bool_t active; -} uibutton_t; - -/** - * Initializes a button. - * - * @param button The button to initialize. - * @param label Display label; never written to, safe to be a caller- - * owned or immutable string. - */ -void uiButtonInit(uibutton_t *button, const char_t *label); - -/** - * Returns whether the button is highlighted. - * - * @param button The button to query. - * @returns True if highlighted. - */ -bool_t uiButtonIsHighlighted(const uibutton_t *button); - -/** - * Sets the highlighted state of the button. - * - * @param button The button to update. - * @param highlighted The new highlighted state. - */ -void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted); - -/** - * Returns whether the button is active. - * - * @param button The button to query. - * @returns True if active. - */ -bool_t uiButtonIsActive(const uibutton_t *button); - -/** - * Sets the active state of the button - a toggled-on indicator drawn - * the same way as highlighted, but independent of focus. - * - * @param button The button to update. - * @param active The new active state. - */ -void uiButtonSetActive(uibutton_t *button, const bool_t active); - -/** - * Draws the button at the given screen position. - * - * @param button The button to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. - */ -errorret_t uiButtonDraw( - uibutton_t *button, - const float_t x, - const float_t y -); diff --git a/src/dusk/ui/widget/uicheckbox.c b/src/dusk/ui/widget/uicheckbox.c deleted file mode 100644 index f9cc8a55..00000000 --- a/src/dusk/ui/widget/uicheckbox.c +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uicheckbox.h" -#include "util/memory.h" -#include "display/text/text.h" -#include "display/color.h" - -void uiCheckboxInit( - uicheckbox_t *checkbox, - const char_t *label -) { - memoryZero(checkbox, sizeof(uicheckbox_t)); - checkbox->label = label; -} - -bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) { - return checkbox->checked; -} - -void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) { - checkbox->checked = checked; -} - -void uiCheckboxToggle(uicheckbox_t *checkbox) { - uiCheckboxSetChecked(checkbox, !uiCheckboxIsChecked(checkbox)); -} - -bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox) { - return checkbox->highlighted; -} - -void uiCheckboxSetHighlighted( - uicheckbox_t *checkbox, const bool_t highlighted -) { - checkbox->highlighted = highlighted; -} - -errorret_t uiCheckboxDraw( - const uicheckbox_t *checkbox, - const float_t x, - const float_t y -) { - color_t color = checkbox->highlighted ? COLOR_RED : COLOR_WHITE; - const char_t *mark = checkbox->checked ? "Y " : "N "; - errorChain(textDraw(x, y, mark, color, &FONT_DEFAULT)); - - int32_t markW, markH; - textMeasure(mark, &FONT_DEFAULT, &markW, &markH); - errorChain(textDraw( - x + (float_t)markW, y, checkbox->label, color, &FONT_DEFAULT - )); - - errorOk(); -} diff --git a/src/dusk/ui/widget/uicheckbox.h b/src/dusk/ui/widget/uicheckbox.h deleted file mode 100644 index b32d52cc..00000000 --- a/src/dusk/ui/widget/uicheckbox.h +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct uicheckbox_s { - const char_t *label; - bool_t checked; - bool_t highlighted; -} uicheckbox_t; - -/** - * Initializes a checkbox. - * - * @param checkbox The checkbox to initialize. - * @param label Display label. - */ -void uiCheckboxInit( - uicheckbox_t *checkbox, - const char_t *label -); - -/** - * Returns whether the checkbox is checked. - * - * @param checkbox The checkbox to query. - * @returns True if checked. - */ -bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox); - -/** - * Sets the checked state of the checkbox. - * - * @param checkbox The checkbox to update. - * @param checked The new checked state. - */ -void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked); - -/** - * Toggles the checked state of the checkbox. - * - * @param checkbox The checkbox to toggle. - */ -void uiCheckboxToggle(uicheckbox_t *checkbox); - -/** - * Returns whether the checkbox is highlighted. - * - * @param checkbox The checkbox to query. - * @returns True if highlighted. - */ -bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox); - -/** - * Sets the highlighted state of the checkbox. - * - * @param checkbox The checkbox to update. - * @param highlighted The new highlighted state. - */ -void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted); - -/** - * Draws the checkbox at the given screen position. - * - * @param checkbox The checkbox to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. - */ -errorret_t uiCheckboxDraw( - const uicheckbox_t *checkbox, - const float_t x, - const float_t y -); diff --git a/src/dusk/ui/widget/uidropdown.c b/src/dusk/ui/widget/uidropdown.c deleted file mode 100644 index ae72bd4e..00000000 --- a/src/dusk/ui/widget/uidropdown.c +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uidropdown.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/text/text.h" -#include "display/color.h" - -void uiDropdownInit( - uidropdown_t *dropdown, - const char_t *label, - const char_t *const *options, - const uint8_t optionCount, - const uint8_t selectedIndex -) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - assertNotNull(label, "Label cannot be NULL"); - assertNotNull(options, "Options cannot be NULL"); - assertTrue(optionCount > 0, "Dropdown must have at least one option"); - - memoryZero(dropdown, sizeof(uidropdown_t)); - dropdown->label = label; - dropdown->options = options; - dropdown->optionCount = optionCount; - dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0; -} - -uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - return dropdown->selectedIndex; -} - -const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - return dropdown->options[dropdown->selectedIndex]; -} - -void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - assertTrue(index < dropdown->optionCount, "Dropdown index out of range"); - dropdown->selectedIndex = index; -} - -void uiDropdownStepNext(uidropdown_t *dropdown) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - dropdown->selectedIndex = - (dropdown->selectedIndex + 1) % dropdown->optionCount; -} - -void uiDropdownStepPrev(uidropdown_t *dropdown) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - dropdown->selectedIndex = dropdown->selectedIndex == 0 ? - dropdown->optionCount - 1 : dropdown->selectedIndex - 1; -} - -bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - return dropdown->highlighted; -} - -void uiDropdownSetHighlighted( - uidropdown_t *dropdown, - const bool_t highlighted -) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - dropdown->highlighted = highlighted; -} - -errorret_t uiDropdownDraw( - const uidropdown_t *dropdown, - const float_t x, - const float_t y -) { - assertNotNull(dropdown, "Dropdown cannot be NULL"); - - color_t color = dropdown->highlighted ? COLOR_RED : COLOR_WHITE; - - errorChain(textDraw(x, y, dropdown->label, color, &FONT_DEFAULT)); - - int32_t labelW, labelH; - textMeasure(dropdown->label, &FONT_DEFAULT, &labelW, &labelH); - - char_t valueText[UI_DROPDOWN_VALUE_TEXT_MAX]; - stringFormat( - valueText, UI_DROPDOWN_VALUE_TEXT_MAX - 1, "< %s >", - uiDropdownGetSelectedOption(dropdown) - ); - - errorChain(textDraw( - x + (float_t)labelW + UI_DROPDOWN_GAP, y, valueText, color, &FONT_DEFAULT - )); - - errorOk(); -} diff --git a/src/dusk/ui/widget/uidropdown.h b/src/dusk/ui/widget/uidropdown.h deleted file mode 100644 index 0c6884ac..00000000 --- a/src/dusk/ui/widget/uidropdown.h +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define UI_DROPDOWN_GAP 4.0f -#define UI_DROPDOWN_VALUE_TEXT_MAX 64 - -typedef struct { - const char_t *label; - const char_t *const *options; - uint8_t optionCount; - uint8_t selectedIndex; - bool_t highlighted; -} uidropdown_t; - -/** - * Initializes a dropdown. - * - * @param dropdown The dropdown to initialize. - * @param label Display label. - * @param options Array of option display strings; caller-owned, must - * outlive the dropdown. - * @param optionCount Number of entries in options. Must be > 0. - * @param selectedIndex Initial selected option index, clamped to - * [0, optionCount - 1]. - */ -void uiDropdownInit( - uidropdown_t *dropdown, - const char_t *label, - const char_t *const *options, - const uint8_t optionCount, - const uint8_t selectedIndex -); - -/** - * Returns the index of the currently selected option. - * - * @param dropdown The dropdown to query. - * @returns The selected option index. - */ -uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown); - -/** - * Returns the currently selected option's display string. - * - * @param dropdown The dropdown to query. - * @returns The selected option string. - */ -const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown); - -/** - * Sets the selected option by index. - * - * @param dropdown The dropdown to update. - * @param index The new selected option index. Must be < optionCount. - */ -void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index); - -/** - * Selects the next option, wrapping around to the first option past - * the last. - * - * @param dropdown The dropdown to update. - */ -void uiDropdownStepNext(uidropdown_t *dropdown); - -/** - * Selects the previous option, wrapping around to the last option - * before the first. - * - * @param dropdown The dropdown to update. - */ -void uiDropdownStepPrev(uidropdown_t *dropdown); - -/** - * Returns whether the dropdown is highlighted. - * - * @param dropdown The dropdown to query. - * @returns True if highlighted. - */ -bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown); - -/** - * Sets the highlighted state of the dropdown. - * - * @param dropdown The dropdown to update. - * @param highlighted The new highlighted state. - */ -void uiDropdownSetHighlighted( - uidropdown_t *dropdown, - const bool_t highlighted -); - -/** - * Draws the dropdown at the given screen position: label, then the - * currently selected option surrounded by cycle arrows. - * - * @param dropdown The dropdown to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. - */ -errorret_t uiDropdownDraw( - const uidropdown_t *dropdown, - const float_t x, - const float_t y -); diff --git a/src/dusk/ui/widget/uiframe.c b/src/dusk/ui/widget/uiframe.c deleted file mode 100644 index 5202b3e1..00000000 --- a/src/dusk/ui/widget/uiframe.c +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiframe.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/texture/texture.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" -#include "display/color.h" - -uiframe_t UI_FRAME; - -errorret_t uiFrameInit(void) { - memoryZero(&UI_FRAME, sizeof(uiframe_t)); - - // Init texture. - color_t border = color4b(0, 50, 255, 255); - color_t center = color4b(0, 100, 220, 240); - - for(uint8_t y = 0; y < UI_FRAME_TEXTURE_HEIGHT_POW2; y++) { - for(uint8_t x = 0; x < UI_FRAME_TEXTURE_WIDTH_POW2; x++) { - color_t c; - if(x >= UI_FRAME_TEXTURE_WIDTH || y >= UI_FRAME_TEXTURE_HEIGHT) { - c = COLOR_TRANSPARENT; - } else if( - y < UI_FRAME_TILE_HEIGHT || y >= UI_FRAME_TILE_HEIGHT * 2 || - x < UI_FRAME_TILE_WIDTH || x >= UI_FRAME_TILE_WIDTH * 2 - ) { - c = border; - } else { - c = center; - } - UI_FRAME.pixels[y * UI_FRAME_TEXTURE_WIDTH_POW2 + x] = c; - } - } - - errorChain(textureInit( - &UI_FRAME.texture, - UI_FRAME_TEXTURE_WIDTH_POW2, UI_FRAME_TEXTURE_HEIGHT_POW2, - TEXTURE_FORMAT_RGBA, - (texturedata_t){ .rgbaColors = UI_FRAME.pixels } - )); - - UI_FRAME.tileset.tileWidth = UI_FRAME_TILE_WIDTH; - UI_FRAME.tileset.tileHeight = UI_FRAME_TILE_HEIGHT; - UI_FRAME.tileset.columns = 3; - UI_FRAME.tileset.rows = 3; - UI_FRAME.tileset.tileCount = 9; - UI_FRAME.tileset.uv[0] = - (float_t)UI_FRAME_TILE_WIDTH / UI_FRAME_TEXTURE_WIDTH_POW2; - UI_FRAME.tileset.uv[1] = - (float_t)UI_FRAME_TILE_HEIGHT / UI_FRAME_TEXTURE_HEIGHT_POW2; - - errorOk(); -} - -errorret_t uiFrameDraw( - const float_t x, - const float_t y, - const float_t width, - const float_t height -) { - shadermaterial_t material = { - .unlit = { - .color = COLOR_WHITE, - .texture = &UI_FRAME.texture - } - }; - spritebatchsprite_t sprites[9]; - float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH; - float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT; - - sprites[0] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 0, 0, - x, y, tileW, tileH - ); - sprites[1] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 1, 0, - x + tileW, y, width - (tileW * 2.0f), tileH - ); - sprites[2] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 2, 0, - x + width - tileW, y, tileW, tileH - ); - sprites[3] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 0, 1, - x, y + tileH, tileW, height - (tileH * 2.0f) - ); - sprites[4] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 1, 1, - x + tileW, y + tileH, - width - (tileW * 2.0f), height - (tileH * 2.0f) - ); - sprites[5] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 2, 1, - x + width - tileW, y + tileH, tileW, height - (tileH * 2.0f) - ); - sprites[6] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 0, 2, - x, y + height - tileH, tileW, tileH - ); - sprites[7] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 1, 2, - x + tileW, y + height - tileH, width - (tileW * 2.0f), tileH - ); - sprites[8] = spriteBatchSpriteTilesetPosition( - &UI_FRAME.tileset, 2, 2, - x + width - tileW, y + height - tileH, tileW, tileH - ); - - return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material); -} - -errorret_t uiFrameDispose(void) { - return textureDispose(&UI_FRAME.texture); -} diff --git a/src/dusk/ui/widget/uiframe.h b/src/dusk/ui/widget/uiframe.h deleted file mode 100644 index 626afd64..00000000 --- a/src/dusk/ui/widget/uiframe.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/texture/texture.h" -#include "display/texture/tileset.h" - -#define UI_FRAME_BORDER_WIDTH 6 -#define UI_FRAME_BORDER_HEIGHT 6 -#define UI_FRAME_PADDING_X 2 -#define UI_FRAME_PADDING_Y 2 -#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X) -#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y) -#define UI_FRAME_TILE_WIDTH 1 -#define UI_FRAME_TILE_HEIGHT 1 -#define UI_FRAME_TEXTURE_WIDTH (UI_FRAME_TILE_WIDTH * 3) -#define UI_FRAME_TEXTURE_HEIGHT (UI_FRAME_TILE_HEIGHT * 3) -#define UI_FRAME_TEXTURE_WIDTH_POW2 4 -#define UI_FRAME_TEXTURE_HEIGHT_POW2 4 - -typedef struct { - tileset_t tileset; - texture_t texture; - color_t pixels[UI_FRAME_BORDER_WIDTH * UI_FRAME_BORDER_HEIGHT]; -} uiframe_t; - -extern uiframe_t UI_FRAME; - -/** - * Initializes the global UI_FRAME: builds the dummy texture and - * configures the tileset. Call once after displayInit(). - * - * @return Any error that occurs. - */ -errorret_t uiFrameInit(void); - -/** - * Draws UI_FRAME using 9-slice rendering from its 3x3 tileset. - * Pushes quads to the sprite batch without flushing. - * - * @param x Screen x position. - * @param y Screen y position. - * @param width Total width of the frame. - * @param height Total height of the frame. - * @return Any error that occurs. - */ -errorret_t uiFrameDraw( - const float_t x, - const float_t y, - const float_t width, - const float_t height -); - -/** - * Disposes of the global UI_FRAME, releasing its GPU texture. - * - * @return Any error that occurs. - */ -errorret_t uiFrameDispose(void); diff --git a/src/dusk/ui/widget/uiitem.c b/src/dusk/ui/widget/uiitem.c deleted file mode 100644 index 9e73b676..00000000 --- a/src/dusk/ui/widget/uiitem.c +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiitem.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/text/text.h" -#include "display/color.h" - -#define UI_ITEM_LABEL_MAX 48 - -errorret_t uiItemInit( - uiitem_t *item, - const itemid_t itemId, - const uint8_t quantity -) { - assertNotNull(item, "Item cannot be NULL"); - memoryZero(item, sizeof(uiitem_t)); - item->item = itemId; - item->quantity = quantity; - - if(itemId == ITEM_ID_NULL) errorOk(); - - errorChain(itemGetName(itemId, item->nameLabel, UI_ITEM_NAME_LABEL_MAX)); - - errorOk(); -} - -itemid_t uiItemGetItem(const uiitem_t *item) { - assertNotNull(item, "Item cannot be NULL"); - return item->item; -} - -uint8_t uiItemGetQuantity(const uiitem_t *item) { - assertNotNull(item, "Item cannot be NULL"); - return item->quantity; -} - -void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity) { - assertNotNull(item, "Item cannot be NULL"); - item->quantity = quantity; -} - -bool_t uiItemIsHighlighted(const uiitem_t *item) { - assertNotNull(item, "Item cannot be NULL"); - return item->highlighted; -} - -void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted) { - assertNotNull(item, "Item cannot be NULL"); - item->highlighted = highlighted; -} - -errorret_t uiItemDraw( - const uiitem_t *item, - const float_t x, - const float_t y -) { - assertNotNull(item, "Item cannot be NULL"); - if(item->item == ITEM_ID_NULL) errorOk(); - - const color_t color = item->highlighted ? COLOR_RED : COLOR_WHITE; - - char_t text[UI_ITEM_LABEL_MAX]; - stringFormat( - text, UI_ITEM_LABEL_MAX - 1, - "%s x%u", item->nameLabel, item->quantity - ); - - errorChain(textDraw(x, y, text, color, &FONT_DEFAULT)); - errorOk(); -} diff --git a/src/dusk/ui/widget/uiitem.h b/src/dusk/ui/widget/uiitem.h deleted file mode 100644 index 787884c8..00000000 --- a/src/dusk/ui/widget/uiitem.h +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "rpg/item/item.h" - -#define UI_ITEM_NAME_LABEL_MAX 32 - -typedef struct { - itemid_t item; - uint8_t quantity; - bool_t highlighted; - char_t nameLabel[UI_ITEM_NAME_LABEL_MAX]; -} uiitem_t; - -/** - * Initializes an item widget, resolving the item's localized name. - * - * @param item The item widget to initialize. - * @param itemId The item ID to display. ITEM_ID_NULL renders as an - * empty slot. - * @param quantity The stack quantity to display. - * @return Any error that occurs. - */ -errorret_t uiItemInit( - uiitem_t *item, - const itemid_t itemId, - const uint8_t quantity -); - -/** - * Returns the item ID this widget displays. - * - * @param item The item widget to query. - * @returns The item ID. - */ -itemid_t uiItemGetItem(const uiitem_t *item); - -/** - * Returns the stack quantity this widget displays. - * - * @param item The item widget to query. - * @returns The quantity. - */ -uint8_t uiItemGetQuantity(const uiitem_t *item); - -/** - * Sets the stack quantity this widget displays. - * - * @param item The item widget to update. - * @param quantity The new quantity. - */ -void uiItemSetQuantity(uiitem_t *item, const uint8_t quantity); - -/** - * Returns whether the item widget is highlighted. - * - * @param item The item widget to query. - * @returns True if highlighted. - */ -bool_t uiItemIsHighlighted(const uiitem_t *item); - -/** - * Sets the highlighted state of the item widget. - * - * @param item The item widget to update. - * @param highlighted The new highlighted state. - */ -void uiItemSetHighlighted(uiitem_t *item, const bool_t highlighted); - -/** - * Draws the item widget at the given screen position: item name and - * quantity. No-op for an empty (ITEM_ID_NULL) slot. - * - * @param item The item widget to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. - */ -errorret_t uiItemDraw( - const uiitem_t *item, - const float_t x, - const float_t y -); diff --git a/src/dusk/ui/widget/uiitemlist.c b/src/dusk/ui/widget/uiitemlist.c deleted file mode 100644 index 2ab2f62a..00000000 --- a/src/dusk/ui/widget/uiitemlist.c +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiitemlist.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/text/text.h" - -void uiItemListInit( - uiitemlist_t *list, - const uint8_t columns, - const uint8_t rows, - const uint8_t itemColumns, - uiitemlistselectedcallback_t selected, - uiitemlistchangedcallback_t changed, - uiitemlistclosedcallback_t closed, - uiitemlistcolumncallback_t columnDraw -) { - assertNotNull(list, "Item list cannot be NULL"); - assertTrue(columns > 0, "Item list columns must be > 0"); - assertTrue(rows > 0, "Item list rows must be > 0"); - assertTrue(itemColumns > 0, "Item list itemColumns must be > 0"); - - memoryZero(list, sizeof(uiitemlist_t)); - list->columns = columns; - list->rows = rows; - list->itemColumns = itemColumns; - list->selected = selected; - list->changed = changed; - list->closed = closed; - list->columnDraw = columnDraw; - - uiScrollingInit(&list->scroll); -} - -void uiItemListSetItems( - uiitemlist_t *list, - const uiitem_t *items, - const uint8_t itemCount -) { - assertNotNull(list, "Item list cannot be NULL"); - assertTrue( - itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list" - ); - - memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount); - list->itemCount = itemCount; -} - -errorret_t uiItemListSetItemStacks( - uiitemlist_t *list, - const inventorystack_t *stacks, - const uint8_t stackCount -) { - assertNotNull(list, "Item list cannot be NULL"); - assertTrue( - stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list" - ); - - for(uint8_t i = 0; i < stackCount; i++) { - errorChain( - uiItemInit(&list->items[i], stacks[i].item, stacks[i].quantity) - ); - } - list->itemCount = stackCount; - - errorOk(); -} - -void uiItemListOpen(uiitemlist_t *list) { - assertNotNull(list, "Item list cannot be NULL"); - if(list->focusItem != NULL) return; - - list->focusItem = uiFocusPush( - list->columns, list->rows, - uiItemListFocusSelected, - uiItemListFocusChanged, - uiItemListFocusClosed, - NULL, - list - ); -} - -void uiItemListClose(uiitemlist_t *list) { - assertNotNull(list, "Item list cannot be NULL"); - if(list->focusItem == NULL) return; - uiFocusPopItem(list->focusItem); - list->focusItem = NULL; -} - -bool_t uiItemListIsActive(const uiitemlist_t *list) { - assertNotNull(list, "Item list cannot be NULL"); - return list->focusItem != NULL; -} - -errorret_t uiItemListDraw( - const uiitemlist_t *list, - const float_t x, - const float_t y, - const float_t width -) { - assertNotNull(list, "Item list cannot be NULL"); - if(list->itemCount == 0) errorOk(); - - const float_t colStep = width / (float_t)list->columns; - const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - const float_t itemColStep = colStep / (float_t)list->itemColumns; - - const uint8_t visibleMax = list->columns * list->rows; - const uint8_t drawCount = - list->itemCount < visibleMax ? list->itemCount : visibleMax; - - for(uint8_t i = 0; i < drawCount; i++) { - const uint8_t col = i % list->columns; - const uint8_t row = i / list->columns; - - const float_t ix = x + (float_t)col * colStep; - const float_t iy = y + (float_t)row * rowHeight; - - errorChain(uiItemDraw(&list->items[i], ix, iy)); - - for(uint8_t c = 1; c < list->itemColumns; c++) { - if(list->columnDraw == NULL) continue; - errorChain(list->columnDraw( - list, &list->items[i], c, ix + (float_t)c * itemColStep, iy - )); - } - } - - errorOk(); -} - -bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uiitemlist_t *list = (uiitemlist_t *)focusItem->user; - if(list->selected == NULL) return true; - - const uint8_t index = focusItem->y * list->columns + focusItem->x; - if(index >= list->itemCount) return true; - - list->selected(list, index, &list->items[index]); - return true; -} - -bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uiitemlist_t *list = (uiitemlist_t *)focusItem->user; - - const uint8_t focusIndex = focusItem->y * list->columns + focusItem->x; - for(uint8_t i = 0; i < list->itemCount; i++) { - uiItemSetHighlighted(&list->items[i], i == focusIndex); - } - - if(list->changed == NULL) return true; - if(focusIndex >= list->itemCount) return true; - - list->changed(list, focusIndex, &list->items[focusIndex]); - return true; -} - -bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uiitemlist_t *list = (uiitemlist_t *)focusItem->user; - list->focusItem = NULL; - - for(uint8_t i = 0; i < list->itemCount; i++) { - uiItemSetHighlighted(&list->items[i], false); - } - - if(list->closed != NULL) list->closed(list); - return true; -} diff --git a/src/dusk/ui/widget/uiitemlist.h b/src/dusk/ui/widget/uiitemlist.h deleted file mode 100644 index 26b3e350..00000000 --- a/src/dusk/ui/widget/uiitemlist.h +++ /dev/null @@ -1,200 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uiitem.h" -#include "ui/widget/uiscrolling.h" -#include "ui/focus/uifocus.h" -#include "rpg/item/inventory.h" - -#define UI_ITEM_LIST_CAPACITY_MAX 40 - -typedef struct uiitemlist_s uiitemlist_t; - -typedef void (*uiitemlistselectedcallback_t)( - const uiitemlist_t *list, - const uint8_t index, - const uiitem_t *item -); - -typedef void (*uiitemlistchangedcallback_t)( - const uiitemlist_t *list, - const uint8_t index, - const uiitem_t *item -); - -typedef void (*uiitemlistclosedcallback_t)(const uiitemlist_t *list); - -/** - * Called to draw an extra per-item info column (columnIndex >= 1) - * beyond the item's own default uiItemDraw rendering -- e.g. a shop - * menu drawing a price alongside the item name. - * - * @param list The item list being drawn. - * @param item The item slot being drawn. - * @param columnIndex The info column being drawn (>= 1). - * @param x Screen x position for this column. - * @param y Screen y position for this column. - * @return Any error that occurs. - */ -typedef errorret_t (*uiitemlistcolumncallback_t)( - const uiitemlist_t *list, - const uiitem_t *item, - const uint8_t columnIndex, - const float_t x, - const float_t y -); - -struct uiitemlist_s { - uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX]; - uint8_t itemCount; - - // Grid layout: how many item slots wide/tall the list displays. - uint8_t columns; - uint8_t rows; - - // How many info columns are rendered per item slot. 1 means only the - // item's own default name/quantity is drawn; anything beyond that is - // rendered via columnDraw. - uint8_t itemColumns; - - uiscrolling_t scroll; - uifocusitem_t *focusItem; - - uiitemlistselectedcallback_t selected; - uiitemlistchangedcallback_t changed; - uiitemlistclosedcallback_t closed; - uiitemlistcolumncallback_t columnDraw; - - void *user; -}; - -/** - * Initializes an item list. - * - * @param list The item list to initialize. - * @param columns Number of item slots per row. - * @param rows Number of item slot rows. - * @param itemColumns Number of info columns rendered per item slot. - * @param selected Called when an item slot is selected. - * @param changed Called when the focused item slot changes. - * @param closed Called when the item list is closed. - * @param columnDraw Called to draw each extra info column (index >= 1); - * may be NULL if itemColumns is 1. - */ -void uiItemListInit( - uiitemlist_t *list, - const uint8_t columns, - const uint8_t rows, - const uint8_t itemColumns, - uiitemlistselectedcallback_t selected, - uiitemlistchangedcallback_t changed, - uiitemlistclosedcallback_t closed, - uiitemlistcolumncallback_t columnDraw -); - -/** - * Sets the items displayed by the list directly, copying them into - * the list's own storage. - * - * @param list The item list to update. - * @param items The items to display. - * @param itemCount Number of entries in items. Must be <= - * UI_ITEM_LIST_CAPACITY_MAX. - */ -void uiItemListSetItems( - uiitemlist_t *list, - const uiitem_t *items, - const uint8_t itemCount -); - -/** - * Sets the items displayed by the list from an array of item stacks, - * internally creating a uiitem_t for each stack. - * - * @param list The item list to update. - * @param stacks The item stacks to display. - * @param stackCount Number of entries in stacks. Must be <= - * UI_ITEM_LIST_CAPACITY_MAX. - * @return Any error that occurs. - */ -errorret_t uiItemListSetItemStacks( - uiitemlist_t *list, - const inventorystack_t *stacks, - const uint8_t stackCount -); - -/** - * Pushes the item list onto the UI focus stack, making it navigable. - * No-op if already open. - * - * @param list The item list to open. - */ -void uiItemListOpen(uiitemlist_t *list); - -/** - * Pops the item list from the UI focus stack. No-op if already closed. - * - * @param list The item list to close. - */ -void uiItemListClose(uiitemlist_t *list); - -/** - * Returns whether the item list is currently on the UI focus stack. - * - * @param list The item list to query. - * @returns True if active. - */ -bool_t uiItemListIsActive(const uiitemlist_t *list); - -/** - * Draws the item list's grid of item slots at the given position. - * Only the first columns * rows items are drawn. - * - * @param list The item list to draw. - * @param x Screen x position. - * @param y Screen y position. - * @param width Content width, used to lay out columns. - * @return Any error that occurs. - */ -errorret_t uiItemListDraw( - const uiitemlist_t *list, - const float_t x, - const float_t y, - const float_t width -); - -/** - * Internal focus callback -- forwards selection to the list's selected - * handler. - * - * @param focusItem The active focus item; user field must point to - * uiitemlist_t. - * @returns True. - */ -bool_t uiItemListFocusSelected(const uifocusitem_t *focusItem); - -/** - * Internal focus callback -- updates item highlights and fires - * changed. - * - * @param focusItem The active focus item; user field must point to - * uiitemlist_t. - * @returns True. - */ -bool_t uiItemListFocusChanged(const uifocusitem_t *focusItem); - -/** - * Internal focus callback -- clears focusItem and fires the closed - * handler. - * - * @param focusItem The active focus item; user field must point to - * uiitemlist_t. - * @returns True. - */ -bool_t uiItemListFocusClosed(const uifocusitem_t *focusItem); diff --git a/src/dusk/ui/widget/uilabel.c b/src/dusk/ui/widget/uilabel.c deleted file mode 100644 index 48109474..00000000 --- a/src/dusk/ui/widget/uilabel.c +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uilabel.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "display/text/text.h" -#include "display/spritebatch/spritebatch.h" -#include "display/shader/shaderunlit.h" - -void uiLabelInit( - uilabel_t *label, - const char_t *text, - spritebatchsprite_t *sprites, - const int32_t spritesMax -) { - assertNotNull(text, "Text buffer cannot be NULL"); - assertNotNull(sprites, "Sprites buffer cannot be NULL"); - - memoryZero(label, sizeof(uilabel_t)); - label->text = text; - label->sprites = sprites; - label->spritesMax = spritesMax; - label->dirty = true; -} - -void uiLabelSetWrap( - uilabel_t *label, - char_t *wrapBuffer, - const uint32_t wrapBufferMax, - const float_t wrapWidth -) { - assertNotNull(label, "Label cannot be NULL"); - - // A NULL wrapBuffer always disables wrapping outright, regardless of - // wrapWidth - so callers can unconditionally pass NULL to turn wrapping - // off without also having to zero out whatever width they were using. - // Enabling it (a non-NULL buffer) always requires a positive width. - if(wrapBuffer != NULL) { - assertTrue(wrapWidth > 0.0f, "Wrap width must be greater than 0"); - assertTrue(wrapBufferMax >= 1, "Wrap buffer max must be at least 1"); - } - - label->wrapBuffer = wrapBuffer; - label->wrapBufferMax = wrapBufferMax; - label->wrapWidth = wrapBuffer == NULL ? 0.0f : wrapWidth; - label->dirty = true; -} - -void uiLabelSetX(uilabel_t *label, const float_t x) { - if(label->x == x) return; - label->x = x; - label->dirty = true; -} - -float_t uiLabelGetX(const uilabel_t *label) { - return label->x; -} - -void uiLabelSetY(uilabel_t *label, const float_t y) { - if(label->y == y) return; - label->y = y; - label->dirty = true; -} - -float_t uiLabelGetY(const uilabel_t *label) { - return label->y; -} - -void uiLabelRebuffer(uilabel_t *label) { - if(!label->dirty) return; - - const char_t *renderText = label->text; - if(label->wrapWidth > 0.0f) { - assertNotNull( - label->wrapBuffer, "Wrap buffer cannot be NULL when wrapping is enabled" - ); - stringCopy(label->wrapBuffer, label->text, label->wrapBufferMax); - textWrap(label->wrapBuffer, &FONT_DEFAULT, label->wrapWidth); - renderText = label->wrapBuffer; - } - - textMeasure(renderText, &FONT_DEFAULT, &label->width, &label->height); - - int32_t charIndex = 0; - float_t posX = label->x; - float_t posY = label->y; - label->spriteCount = textBuffer( - label->x, label->y, renderText, &FONT_DEFAULT, - label->sprites, label->spritesMax, - &charIndex, &posX, &posY - ); - - label->dirty = false; -} - -errorret_t uiLabelRender(uilabel_t *label, const color_t color) { - uiLabelRebuffer(label); - if(label->spriteCount == 0) errorOk(); - - shadermaterial_t material = { - .unlit = { - .color = color, - .texture = FONT_DEFAULT.texture - } - }; - errorChain(spriteBatchBuffer( - label->sprites, label->spriteCount, &SHADER_UNLIT, material - )); - - errorOk(); -} - -void uiLabelDispose(uilabel_t *label) { - memoryZero(label, sizeof(uilabel_t)); -} diff --git a/src/dusk/ui/widget/uilabel.h b/src/dusk/ui/widget/uilabel.h deleted file mode 100644 index edc16fab..00000000 --- a/src/dusk/ui/widget/uilabel.h +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/spritebatch/spritebatchsprite.h" -#include "display/color.h" - -typedef struct { - const char_t *text; - spritebatchsprite_t *sprites; - int32_t spritesMax; - uint32_t spriteCount; - int32_t width; - int32_t height; - float_t x; - float_t y; - bool_t dirty; - - // Wrapping - disabled while wrapWidth is 0 (the default). Enabling it - // requires wrapBuffer, a caller-owned scratch buffer at least as large as - // text: label->text is never written to directly (see uiLabelInit), so - // uiLabelRebuffer instead copies it into wrapBuffer and word-wraps that - // copy in place (via textWrap) before measuring/buffering it. - char_t *wrapBuffer; - uint32_t wrapBufferMax; - float_t wrapWidth; -} uilabel_t; - -/** - * Initializes a label. The caller owns the text buffer and writes its - * contents directly (setting label->dirty afterwards); call - * uiLabelRebuffer or uiLabelRender to rebuild the cached sprites. - * - * @param label The label to initialize. - * @param text Caller-owned, null-terminated string the label reads from - * directly; never written to by the label. Cannot be NULL. - * @param sprites Caller-owned sprite buffer the label writes cached - * glyph sprites into. Cannot be NULL. - * @param spritesMax Number of sprites the sprites buffer can hold. - */ -void uiLabelInit( - uilabel_t *label, - const char_t *text, - spritebatchsprite_t *sprites, - const int32_t spritesMax -); - -/** - * Sets the screen x position of the label. - * - * @param label The label to update. - * @param x Screen x position. - */ -void uiLabelSetX(uilabel_t *label, const float_t x); - -/** - * Returns the screen x position of the label. - * - * @param label The label to query. - * @returns The label's x position. - */ -float_t uiLabelGetX(const uilabel_t *label); - -/** - * Sets the screen y position of the label. - * - * @param label The label to update. - * @param y Screen y position. - */ -void uiLabelSetY(uilabel_t *label, const float_t y); - -/** - * Returns the screen y position of the label. - * - * @param label The label to query. - * @returns The label's y position. - */ -float_t uiLabelGetY(const uilabel_t *label); - -/** - * Enables or disables word-wrap on the label and marks it dirty. Pass - * wrapBuffer NULL to disable wrapping outright (wrapWidth is then - * ignored); enabling it requires both a non-NULL wrapBuffer and a - * wrapWidth greater than 0. - * - * @param label The label to configure. - * @param wrapBuffer Caller-owned scratch buffer, at least as large as the - * label's text, or NULL to disable wrapping. - * @param wrapBufferMax Capacity of wrapBuffer, including the null - * terminator. - * @param wrapWidth Maximum line width in pixels; must be greater than 0 - * when wrapBuffer is non-NULL. - */ -void uiLabelSetWrap( - uilabel_t *label, - char_t *wrapBuffer, - const uint32_t wrapBufferMax, - const float_t wrapWidth -); - -/** - * Rebuffers the label's cached sprites from its current text at its - * current x/y position. No-op unless the label is dirty. Called - * internally by uiLabelRender. - * - * @param label The label to rebuffer. - */ -void uiLabelRebuffer(uilabel_t *label); - -/** - * Renders the label's cached sprites at its current screen position. - * Rebuffers first if the label is dirty (text or position changed). - * - * @param label The label to render. - * @param color The color to render the text in. - * @return Any error that occurs. - */ -errorret_t uiLabelRender(uilabel_t *label, const color_t color); - -/** - * Disposes of a label. - * - * @param label The label to dispose. - */ -void uiLabelDispose(uilabel_t *label); diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c deleted file mode 100644 index 6737aff5..00000000 --- a/src/dusk/ui/widget/uimenu.c +++ /dev/null @@ -1,444 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uimenu.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/text/text.h" -#include "display/color.h" -#include "ui/widget/uibutton.h" -#include "ui/widget/uitab.h" -#include "ui/widget/uislider.h" -#include "ui/widget/uidropdown.h" - -void uiMenuInit( - uimenu_t *menu, - uimenuselectedcallback_t selected, - uimenuclosedcallback_t closed, - uimenuchangedcallback_t changed -) { - assertNotNull(menu, "Menu cannot be NULL"); - memoryZero(menu, sizeof(uimenu_t)); - menu->selected = selected; - menu->closed = closed; - menu->changed = changed; -} - -void uiMenuSetItems( - uimenu_t *menu, - const uimenuitem_t *items, - const uint8_t itemCount, - const uint8_t columns -) { - assertNotNull(menu, "Menu cannot be NULL"); - assertNotNull(items, "Items cannot be NULL"); - assertTrue(itemCount > 0, "Item count must be > 0"); - assertTrue(columns > 0, "Columns must be > 0"); - menu->items = (uimenuitem_t *)items; - menu->itemCount = itemCount; - menu->columns = columns; -} - -void uiMenuSetPosition(uimenu_t *menu, const uint8_t x, const uint8_t y) { - assertNotNull(menu, "Menu cannot be NULL"); - if(menu->focusItem == NULL) return; - uiFocusSetPosition(menu->focusItem, x, y); -} - -void uiMenuOpen(uimenu_t *menu) { - assertNotNull(menu, "Menu cannot be NULL"); - assertNotNull(menu->items, "Menu items cannot be NULL"); - assertTrue(menu->itemCount > 0, "Menu item count must be > 0"); - assertTrue(menu->columns > 0, "Menu columns must be > 0"); - if(menu->focusItem != NULL) return; - - uint8_t focusable = uiMenuFocusableCount(menu); - uint8_t rows = focusable > 0 ? - (focusable + menu->columns - 1) / menu->columns : 1; - menu->focusItem = uiFocusPush( - menu->columns, rows, - uiMenuFocusSelected, - uiMenuFocusChanged, - uiMenuFocusClosed, - uiMenuFocusDirection, - menu - ); - menu->focusItem->disableBack = menu->disableBack; - menu->focusItem->cancel = menu->cancel != NULL ? uiMenuFocusCancel : NULL; -} - -void uiMenuClose(uimenu_t *menu) { - assertNotNull(menu, "Menu cannot be NULL"); - if(menu->focusItem == NULL) return; - uiFocusPopItem(menu->focusItem); - menu->focusItem = NULL; -} - -void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack) { - assertNotNull(menu, "Menu cannot be NULL"); - menu->disableBack = disableBack; - if(menu->focusItem != NULL) menu->focusItem->disableBack = disableBack; -} - -void uiMenuSetCancelCallback(uimenu_t *menu, uimenucancelcallback_t cancel) { - assertNotNull(menu, "Menu cannot be NULL"); - menu->cancel = cancel; - if(menu->focusItem != NULL) { - menu->focusItem->cancel = cancel != NULL ? uiMenuFocusCancel : NULL; - } -} - -bool_t uiMenuIsActive(const uimenu_t *menu) { - assertNotNull(menu, "Menu cannot be NULL"); - return menu->focusItem != NULL; -} - -errorret_t uiMenuDraw( - const uimenu_t *menu, - const float_t x, - const float_t y, - const float_t width, - const float_t height -) { - assertNotNull(menu, "Menu cannot be NULL"); - if(menu->itemCount == 0) errorOk(); - - float_t colStep = width / (float_t)menu->columns; - float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - - uint8_t col = 0; - uint8_t row = 0; - - for(uint8_t i = 0; i < menu->itemCount; i++) { - uimenuitem_t *item = &menu->items[i]; - - if(item->type == UI_MENU_WIDGET_TYPE_LABEL) { - if(col > 0) { row++; col = 0; } - errorChain(textDraw( - x, - y + (float_t)row * rowHeight, - item->label, - COLOR_WHITE, - &FONT_DEFAULT - )); - row++; - continue; - } - - if(item->type == UI_MENU_WIDGET_TYPE_SPACER) { - if(col > 0) { row++; col = 0; } - row++; - continue; - } - - float_t ix = x + (float_t)col * colStep; - float_t iy = y + (float_t)row * rowHeight; - - switch(item->type) { - case UI_MENU_WIDGET_TYPE_CHECKBOX: - errorChain(uiCheckboxDraw(&item->checkbox, ix, iy)); - break; - - case UI_MENU_WIDGET_TYPE_BUTTON: - errorChain(uiButtonDraw(&item->button, ix, iy)); - break; - - case UI_MENU_WIDGET_TYPE_TAB: - errorChain(uiTabDraw(&item->tab, ix, iy)); - break; - - case UI_MENU_WIDGET_TYPE_SLIDER: - errorChain(uiSliderDraw(&item->slider, ix, iy)); - break; - - case UI_MENU_WIDGET_TYPE_DROPDOWN: - errorChain(uiDropdownDraw(&item->dropdown, ix, iy)); - break; - - default: - break; - } - - col++; - if(col >= menu->columns) { col = 0; row++; } - } - - errorOk(); -} - -bool_t uiMenuFocusSelected(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uimenu_t *menu = (uimenu_t *)focusItem->user; - if(menu->selected == NULL) return true; - - uint8_t slot = focusItem->y * menu->columns + focusItem->x; - uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - if(index == 0xFF) return true; - - menu->selected(menu, index, &menu->items[index]); - return true; -} - -bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uimenu_t *menu = (uimenu_t *)focusItem->user; - - uint8_t focusSlot = focusItem->y * menu->columns + focusItem->x; - uint8_t slot = 0; - - for(uint8_t i = 0; i < menu->itemCount; i++) { - uimenuitem_t *item = &menu->items[i]; - if( - item->type == UI_MENU_WIDGET_TYPE_LABEL || - item->type == UI_MENU_WIDGET_TYPE_SPACER - ) continue; - uiMenuItemSetHighlighted(item, slot == focusSlot); - slot++; - } - - if(menu->changed == NULL) return true; - uint8_t index = uiMenuFocusSlotToIndex(menu, focusSlot); - if(index == 0xFF) return true; - - menu->changed(menu, index, &menu->items[index]); - return true; -} - -bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uimenu_t *menu = (uimenu_t *)focusItem->user; - menu->focusItem = NULL; - - for(uint8_t i = 0; i < menu->itemCount; i++) { - uiMenuItemSetHighlighted(&menu->items[i], false); - } - - if(menu->closed != NULL) menu->closed(menu); - return true; -} - -bool_t uiMenuFocusDirection( - const uifocusitem_t *focusItem, - const uifocusdirection_t direction -) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uimenu_t *menu = (uimenu_t *)focusItem->user; - - if( - direction == UI_FOCUS_DIRECTION_LEFT || - direction == UI_FOCUS_DIRECTION_RIGHT - ) { - uint8_t slot = focusItem->y * menu->columns + focusItem->x; - uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - - if(index != 0xFF) { - uimenuitem_t *item = &menu->items[index]; - bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT; - - if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) { - if(right) { - uiSliderStepUp(&item->slider); - } else { - uiSliderStepDown(&item->slider); - } - return true; - } - - if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { - uiCheckboxToggle(&item->checkbox); - return true; - } - - if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) { - if(right) { - uiDropdownStepNext(&item->dropdown); - } else { - uiDropdownStepPrev(&item->dropdown); - } - return true; - } - } - } - - return uiMenuFocusSkipBlanks(menu, focusItem, direction); -} - -bool_t uiMenuItemIsBlank(const uimenuitem_t *item) { - return - item->type == UI_MENU_WIDGET_TYPE_BUTTON && - item->button.label.text[0] == '\0'; -} - -bool_t uiMenuFocusSkipBlanks( - uimenu_t *menu, - const uifocusitem_t *focusItem, - const uifocusdirection_t direction -) { - int8_t dx = 0; - int8_t dy = 0; - for( - const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; - m->action != INPUT_ACTION_NULL; - m++ - ) { - if(m->direction != direction) continue; - dx = m->dx; - dy = m->dy; - break; - } - if(dx == 0 && dy == 0) return false; - - // Horizontal: a straight scan along the row. There's no "nearest" - // ambiguity here - the next non-blank cell in the pressed direction is - // always the right one, wrapping within the row as needed. - if(dy == 0) { - int16_t x = focusItem->x; - for(uint8_t i = 0; i < menu->columns; i++) { - x += dx; - if(x < 0) x += menu->columns; - x %= menu->columns; - - uint8_t slot = focusItem->y * menu->columns + (uint8_t)x; - uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - // Treat a missing item (label/spacer slot) the same as a blank - // button - both get skipped over transparently. - if(index != 0xFF && !uiMenuItemIsBlank(&menu->items[index])) { - uiMenuSetPosition(menu, (uint8_t)x, focusItem->y); - return true; - } - } - return false; - } - - // Vertical: search every cell strictly further in the pressed - // direction (no wrap - the rows below are only ever "below", never - // "above via the far edge") and land on whichever non-blank cell has - // the smallest combined row+column distance from where we moved from. - // This lets an exact column match several rows away beat an - // approximate match one row away, while still reaching something like - // SPACE from a key with nothing directly below it. Ties on total - // distance favor the smaller column distance - e.g. moving straight - // down two rows onto an exact column match beats landing one row down - // one column over, even though both are three cells away. - int16_t rowStart = dy > 0 ? focusItem->y + 1 : 0; - int16_t rowEnd = dy > 0 ? focusItem->rows - 1 : focusItem->y - 1; - - bool_t found = false; - uint16_t bestDistance = 0; - uint8_t bestColDistance = 0; - uint8_t bestX = 0; - uint8_t bestY = 0; - - for(int16_t yy = rowStart; yy <= rowEnd; yy++) { - uint8_t rowDistance = dy > 0 ? - (uint8_t)(yy - focusItem->y) : (uint8_t)(focusItem->y - yy); - - for(uint8_t xx = 0; xx < menu->columns; xx++) { - uint8_t colDistance = xx > focusItem->x ? - xx - focusItem->x : focusItem->x - xx; - uint16_t distance = rowDistance + colDistance; - if(found && distance > bestDistance) continue; - if(found && distance == bestDistance && colDistance >= bestColDistance) { - continue; - } - - uint8_t slot = (uint8_t)yy * menu->columns + xx; - uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - if(index == 0xFF || uiMenuItemIsBlank(&menu->items[index])) continue; - - found = true; - bestDistance = distance; - bestColDistance = colDistance; - bestX = xx; - bestY = (uint8_t)yy; - } - } - - // Nothing further in that direction at all (already on the edge row) - - // wrap to the opposite edge as a last resort, landing on its nearest - // column. This only ever kicks in when the non-wrapped search above - // found literally nothing, so it can't hijack a real nearby match the - // way wrapping during the main search could. - if(!found && focusItem->rows > 1) { - uint8_t wrapY = dy > 0 ? 0 : focusItem->rows - 1; - for(uint8_t xx = 0; xx < menu->columns; xx++) { - uint8_t colDistance = xx > focusItem->x ? - xx - focusItem->x : focusItem->x - xx; - if(found && colDistance >= bestDistance) continue; - - uint8_t slot = wrapY * menu->columns + xx; - uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - if(index == 0xFF || uiMenuItemIsBlank(&menu->items[index])) continue; - - found = true; - bestDistance = colDistance; - bestX = xx; - bestY = wrapY; - } - } - - if(found) { - uiMenuSetPosition(menu, bestX, bestY); - return true; - } - - // Nothing non-blank in that direction at all - fall through to the - // default single-step move. - return false; -} - -bool_t uiMenuFocusCancel(const uifocusitem_t *focusItem) { - assertNotNull(focusItem, "Focus item cannot be NULL"); - assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - uimenu_t *menu = (uimenu_t *)focusItem->user; - if(menu->cancel == NULL) return false; - return menu->cancel(menu); -} - -uint8_t uiMenuFocusableCount(const uimenu_t *menu) { - assertNotNull(menu, "Menu cannot be NULL"); - uint8_t count = 0; - for(uint8_t i = 0; i < menu->itemCount; i++) { - uimenuwidgettype_t t = menu->items[i].type; - if(t != UI_MENU_WIDGET_TYPE_LABEL && t != UI_MENU_WIDGET_TYPE_SPACER) { - count++; - } - } - return count; -} - -uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) { - assertNotNull(menu, "Menu cannot be NULL"); - uint8_t focusable = 0; - for(uint8_t i = 0; i < menu->itemCount; i++) { - uimenuwidgettype_t t = menu->items[i].type; - if(t == UI_MENU_WIDGET_TYPE_LABEL || t == UI_MENU_WIDGET_TYPE_SPACER) { - continue; - } - if(focusable == slot) return i; - focusable++; - } - return 0xFF; -} - -void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) { - if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { - uiCheckboxSetHighlighted(&item->checkbox, highlighted); - } else if(item->type == UI_MENU_WIDGET_TYPE_BUTTON) { - uiButtonSetHighlighted(&item->button, highlighted); - } else if(item->type == UI_MENU_WIDGET_TYPE_TAB) { - uiTabSetActive(&item->tab, highlighted); - } else if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) { - uiSliderSetHighlighted(&item->slider, highlighted); - } else if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) { - uiDropdownSetHighlighted(&item->dropdown, highlighted); - } -} diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h deleted file mode 100644 index 88abc38f..00000000 --- a/src/dusk/ui/widget/uimenu.h +++ /dev/null @@ -1,386 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/focus/uifocus.h" -#include "ui/widget/uibutton.h" -#include "ui/widget/uicheckbox.h" -#include "ui/widget/uitab.h" -#include "ui/widget/uislider.h" -#include "ui/widget/uidropdown.h" - -#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX - -typedef struct uimenu_s uimenu_t; - -typedef enum { - UI_MENU_WIDGET_TYPE_NONE, - UI_MENU_WIDGET_TYPE_LABEL, - UI_MENU_WIDGET_TYPE_SPACER, - UI_MENU_WIDGET_TYPE_CHECKBOX, - UI_MENU_WIDGET_TYPE_BUTTON, - UI_MENU_WIDGET_TYPE_TAB, - UI_MENU_WIDGET_TYPE_SLIDER, - UI_MENU_WIDGET_TYPE_DROPDOWN, -} uimenuwidgettype_t; - -typedef struct { - uimenuwidgettype_t type; - union { - char_t *label; - uicheckbox_t checkbox; - uibutton_t button; - uitab_t tab; - uislider_t slider; - uidropdown_t dropdown; - }; -} uimenuitem_t; - -typedef void (*uimenuselectedcallback_t)( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -); - -typedef void (*uimenuchangedcallback_t)( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -); - -typedef void (*uimenuclosedcallback_t)(const uimenu_t *menu); - -/** - * Callback invoked when INPUT_ACTION_CANCEL is pressed while the menu is - * focused - see uiMenuSetCancelCallback. - * - * @param menu The menu that was cancelled. - * @returns True if fully handled (skips the default disableBack/pop - * behavior); false to fall through to it. - */ -typedef bool_t (*uimenucancelcallback_t)(const uimenu_t *menu); - -typedef struct uimenu_s { - uimenuitem_t *items; - uint8_t itemCount; - uint8_t columns; - uifocusitem_t *focusItem; - - uimenuselectedcallback_t selected; - uimenuclosedcallback_t closed; - uimenuchangedcallback_t changed; - uimenucancelcallback_t cancel; - - void *user; - - // See uiMenuSetDisableBack. - bool_t disableBack; -} uimenu_t; - -/** - * Initializes a menu, clearing all items and focus state. - * - * @param menu The menu to initialize. - * @param items The list of items to display in the menu. - * @param itemCount The number of items in the list. - * @param columns The number of columns to display the items in. - * @param selected The callback to invoke when an item is selected. - * @param closed The callback to invoke when the menu is closed. - * @param changed The callback to invoke when the menu changes. - */ -void uiMenuInit( - uimenu_t *menu, - uimenuselectedcallback_t selected, - uimenuclosedcallback_t closed, - uimenuchangedcallback_t changed -); - -/** - * Sets the items to display in the menu. - * - * @param menu The menu to update. - * @param items The list of items to display in the menu. - * @param itemCount The number of items in the list. - * @param columns The number of columns to display the items in. - */ -void uiMenuSetItems( - uimenu_t *menu, - const uimenuitem_t *items, - const uint8_t itemCount, - const uint8_t columns -); - -/** - * Sets the position of the menu on the screen. - * - * @param menu The menu to position. - * @param x The x-coordinate to position the menu at. - * @param y The y-coordinate to position the menu at. - */ -void uiMenuSetPosition(uimenu_t *menu, const uint8_t x, const uint8_t y); - -/** - * Pushes a menu onto the UI focus stack, making it the active menu. - * - * @param menu The menu to push. - */ -void uiMenuOpen(uimenu_t *menu); - -/** - * Pops a menu from the UI focus stack, removing it from the active menu. - * - * @param menu The menu to pop. - */ -void uiMenuClose(uimenu_t *menu); - -/** - * Sets whether pressing back/cancel is ignored while this menu is the - * topmost focus item - it simply does nothing, rather than closing the - * menu. Does not affect a deliberate uiMenuClose() call. Safe to call - * before or after uiMenuOpen(). - * - * @param menu The menu to update. - * @param disableBack True to ignore back/cancel input for this menu. - */ -void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack); - -/** - * Sets a callback given first refusal of INPUT_ACTION_CANCEL while this - * menu is the topmost focus item, before disableBack/the default pop is - * applied. Safe to call before or after uiMenuOpen(). - * - * @param menu The menu to update. - * @param cancel The callback to install, or NULL to remove it and - * restore default disableBack/pop behavior. - */ -void uiMenuSetCancelCallback(uimenu_t *menu, uimenucancelcallback_t cancel); - -/** - * Returns whether the menu is currently active (on the UI focus stack). - * - * @param menu The menu to query. - * @returns True if the menu is active. - */ -bool_t uiMenuIsActive(const uimenu_t *menu); - -/** - * Draws the menu at the specified position and size. - * - * @param menu The menu to draw. - * @param x The x-coordinate to draw the menu at. - * @param y The y-coordinate to draw the menu at. - * @param width The width of the menu. - * @param height The height of the menu. - * @returns An error code indicating success or failure. - */ -errorret_t uiMenuDraw( - const uimenu_t *menu, - const float_t x, - const float_t y, - const float_t width, - const float_t height -); - -/** - * Returns the number of focusable (non-label) items in the menu. - * - * @param menu The menu to query. - * @returns Count of non-label items. - */ -uint8_t uiMenuFocusableCount(const uimenu_t *menu); - -/** - * Maps a flat focus slot index to the corresponding item array index, - * skipping over label items which are not focusable. - * - * @param menu The menu to query. - * @param slot The focus slot index (y * columns + x). - * @returns The item array index, or 0xFF if out of range. - */ -uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot); - -/** - * Sets the highlighted/active state on a menu item, dispatching to the - * matching widget's setter based on its type. No-op for label/spacer - * items and any other type with no highlight concept. - * - * @param item The item to update. - * @param highlighted The new highlighted/active state. - */ -void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted); - -/** - * Internal focus callback - forwards selection to the menu's selected - * handler. - * - * @param focusItem The active focus item; user field must point to uimenu_t. - * @returns True. - */ -bool_t uiMenuFocusSelected(const uifocusitem_t *focusItem); - -/** - * Internal focus callback - updates widget highlights and fires changed. - * - * @param focusItem The active focus item; user field must point to uimenu_t. - * @returns True. - */ -bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem); - -/** - * Internal focus callback - clears focusItem and fires the closed handler. - * - * @param focusItem The active focus item; user field must point to uimenu_t. - * @returns True. - */ -bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem); - -/** - * Internal focus callback - gives the focused item's widget a chance to - * handle LEFT/RIGHT itself (e.g. a slider adjusting its value); failing - * that, moves to the next non-blank cell in the given direction (see - * uiMenuFocusSkipBlanks) rather than the default single-step move, which - * would happily land on a deliberately-blank layout cell (e.g. a - * UI_MENU_WIDGET_TYPE_BUTTON with an empty label, used as filler). - * - * @param focusItem The active focus item; user field must point to uimenu_t. - * @param direction The direction that was pressed or held. - * @returns True if handled (a widget adjusted itself, or focus moved to - * a non-blank cell); false to fall through to the default single-step - * move (only when every cell along this line is blank). - */ -bool_t uiMenuFocusDirection( - const uifocusitem_t *focusItem, - const uifocusdirection_t direction -); - -/** - * True for a menu item that should be transparently skipped during - * d-pad navigation - a UI_MENU_WIDGET_TYPE_BUTTON with an empty label, - * used purely to hold a grid cell's position (the menu/focus grid has no - * concept of a row narrower than its column count, so short rows are - * padded with these instead). Labels/spacers are unaffected by this - - * they're already excluded from the focus grid entirely. - * - * @param item The item to check. - * @returns True if this item is blank filler. - */ -bool_t uiMenuItemIsBlank(const uimenuitem_t *item); - -/** - * Moves focusItem to a non-blank cell (see uiMenuItemIsBlank) in the - * given direction, wrapping the grid as many times as needed - so, from - * the menu's perspective, a blank cell simply isn't there. - * - * LEFT/RIGHT scan straight along the row, since there's no ambiguity - * about which non-blank cell is "next" (wrapping within the row as - * needed). UP/DOWN instead considers every cell strictly further in the - * pressed direction and lands on whichever non-blank cell has the - * smallest combined row+column distance from the one moved from - e.g. - * moving down off a densely-packed row onto a mostly-blank one lands on - * whatever's closest instead of only ever reaching a cell that happens - * to share the exact same column, while an exact column match a couple - * of rows down still wins over an approximate match one row down. Ties - * on total distance favor the smaller column distance, so a straight - * move down onto an exact column match beats an equally-far diagonal - * one. This search never wraps in either axis while there's still a - * real cell further in the pressed direction - column/row 0 is never - * treated as adjacent to the far edge just because the grid wraps in - * general. Only once already on the edge row (nothing further to search - * at all) does it fall back to wrapping to the opposite edge's nearest - * column, so pressing up from the top row still goes somewhere instead - * of doing nothing. - * - * Gives up (returning false, so the default single-step move applies - * instead) only if every cell on the grid is blank. - * - * @param menu The menu to move within. - * @param focusItem The active focus item; must belong to menu. - * @param direction The direction that was pressed or held. - * @returns True if a non-blank cell was found and focus moved there. - */ -bool_t uiMenuFocusSkipBlanks( - uimenu_t *menu, - const uifocusitem_t *focusItem, - const uifocusdirection_t direction -); - -/** - * Internal focus callback - forwards INPUT_ACTION_CANCEL to the menu's - * cancel handler, if one is set. - * - * @param focusItem The active focus item; user field must point to uimenu_t. - * @returns The cancel handler's result, or false if none is set. - */ -bool_t uiMenuFocusCancel(const uifocusitem_t *focusItem); - -// Helper macros -#define MENU_BEGIN(menuPtr, itemsArray, selected, closed, changed) \ - uimenu_t *menu = (menuPtr); \ - uiMenuInit(menu, selected, closed, changed); \ - menu->items = (itemsArray); \ - uint8_t menuIndex = 0; \ - uint8_t menuCapacity = sizeof(itemsArray) / sizeof((itemsArray)[0]) - -#define MENU_LABEL(text) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \ - menu->items[menuIndex].label = text; \ - ++menuIndex - -#define MENU_SPACER() \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SPACER; \ - ++menuIndex - -#define MENU_CHECKBOX(label) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_CHECKBOX; \ - uiCheckboxInit(&menu->items[menuIndex].checkbox, label); \ - ++menuIndex - -#define MENU_BUTTON(label) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_BUTTON; \ - uiButtonInit(&menu->items[menuIndex].button, label); \ - ++menuIndex - -#define MENU_TAB(label) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_TAB; \ - uiTabInit(&menu->items[menuIndex].tab, label); \ - ++menuIndex - -#define MENU_SLIDER_FLOAT(label, value, min, max, step) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \ - uiSliderInitFloat( \ - &menu->items[menuIndex].slider, label, value, min, max, step \ - ); \ - ++menuIndex - -#define MENU_SLIDER_INT(label, value, min, max, step) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \ - uiSliderInitInt( \ - &menu->items[menuIndex].slider, label, value, min, max, step \ - ); \ - ++menuIndex - -#define MENU_DROPDOWN(label, options, optionCount, selectedIndex) \ - assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \ - menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_DROPDOWN; \ - uiDropdownInit( \ - &menu->items[menuIndex].dropdown, label, options, optionCount, \ - selectedIndex \ - ); \ - ++menuIndex - -#define MENU_END(itemsArray, columns) \ - uiMenuSetItems(menu, (itemsArray), menuIndex, (columns)) - -//EOF diff --git a/src/dusk/ui/widget/uimodal.c b/src/dusk/ui/widget/uimodal.c deleted file mode 100644 index 9276f26b..00000000 --- a/src/dusk/ui/widget/uimodal.c +++ /dev/null @@ -1,264 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uimodal.h" -#include "ui/widget/uiframe.h" -#include "ui/focus/uifocus.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include "util/math.h" -#include "display/screen/screen.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "display/texture/texture.h" -#include "display/shader/shaderunlit.h" -#include "locale/localemanager.h" -#include "asset/loader/locale/assetlocaleloader.h" - -#define UI_MODAL_BACKDROP_COLOR color4b(0, 0, 0, 160) - -uimodal_t UI_MODAL; - -// Focus item backing a message-only modal (no options) - lets a plain -// MODAL still block on player confirmation. NULL whenever a -// message-only modal isn't open. Mirrors uitextboxmain.c's pattern. -static uifocusitem_t *messageFocus = NULL; - -void uiModalSelected( - const uimenu_t *menu, - const uint8_t index, - const uimenuitem_t *item -) { - UI_MODAL.result = index; - uiModalClose(NULL); -} - -void uiModalClosed(const uimenu_t *menu) { - if(UI_MODAL.callback != NULL) { - UI_MODAL.callback(UI_MODAL.result, UI_MODAL.user); - } -} - -bool_t uiModalMessageFocusSelected(const uifocusitem_t *item) { - uiFocusPopItem(messageFocus); - return true; -} - -bool_t uiModalMessageFocusClosed(const uifocusitem_t *item) { - messageFocus = NULL; - UI_MODAL.open = false; - uiModalClosed(NULL); - return true; -} - -errorret_t uiModalInit(void) { - memoryZero(&UI_MODAL, sizeof(uimodal_t)); - - uiLabelInit( - &UI_MODAL.titleLabel, UI_MODAL.titleText, - UI_MODAL.titleSprites, UI_MODAL_TITLE_SPRITES_MAX - ); - uiLabelInit( - &UI_MODAL.messageLabel, UI_MODAL.messageText, - UI_MODAL.messageSprites, UI_MODAL_MESSAGE_SPRITES_MAX - ); - - errorOk(); -} - -errorret_t uiModalDraw(void) { - if(!UI_MODAL.open) errorOk(); - - bool_t hasOptions = UI_MODAL.menu.itemCount > 0; - - spritebatchsprite_t backdropSprite = { - .min = { 0.0f, 0.0f, 0.0f }, - .max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t backdropMaterial = { - .unlit = { - .color = UI_MODAL_BACKDROP_COLOR, - .texture = &TEXTURE_WHITE - } - }; - errorChain( - spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial) - ); - errorChain(spriteBatchFlush()); - - float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; - float_t contentWidth = mathMax( - mathMax( - (float_t)UI_MODAL.titleLabel.width, (float_t)UI_MODAL.messageLabel.width - ), - UI_MODAL_MIN_WIDTH - (UI_FRAME_START_X * 2) - ); - float_t width = contentWidth + (UI_FRAME_START_X * 2); - float_t height = (UI_FRAME_START_Y * 2) - + (UI_MODAL.hasTitle ? - (float_t)UI_MODAL.titleLabel.height + UI_FRAME_PADDING_Y : 0.0f) - + (float_t)UI_MODAL.messageLabel.height - + (hasOptions ? UI_FRAME_PADDING_Y + rowHeight : 0.0f); - - float_t x = (float_t)SCREEN.scanX + - ((float_t)SCREEN.scanWidth - width) * 0.5f; - float_t y = (float_t)SCREEN.scanY + - ((float_t)SCREEN.scanHeight - height) * 0.5f; - - errorChain(uiFrameDraw(x, y, width, height)); - - float_t contentX = x + UI_FRAME_START_X; - float_t contentY = y + UI_FRAME_START_Y; - - float_t messageY = contentY; - if(UI_MODAL.hasTitle) { - uiLabelSetX(&UI_MODAL.titleLabel, contentX); - uiLabelSetY(&UI_MODAL.titleLabel, contentY); - errorChain(uiLabelRender(&UI_MODAL.titleLabel, COLOR_WHITE)); - - messageY = contentY + (float_t)UI_MODAL.titleLabel.height + - UI_FRAME_PADDING_Y; - } - - uiLabelSetX(&UI_MODAL.messageLabel, contentX); - uiLabelSetY(&UI_MODAL.messageLabel, messageY); - errorChain(uiLabelRender(&UI_MODAL.messageLabel, COLOR_WHITE)); - - if(hasOptions) { - float_t buttonsY = messageY + (float_t)UI_MODAL.messageLabel.height + - UI_FRAME_PADDING_Y; - errorChain( - uiMenuDraw(&UI_MODAL.menu, contentX, buttonsY, contentWidth, rowHeight) - ); - } - - errorChain(spriteBatchFlush()); - errorOk(); -} - -void uiModalLocalize( - const char_t *key, - char_t *buffer, - const size_t bufferSize -) { - if(key[0] == '\0') { - buffer[0] = '\0'; - return; - } - - errorret_t result = localeManagerGetString(key, buffer, bufferSize, 0); - if(errorIsNotOk(result)) { - errorCatch(result); - stringCopy(buffer, key, bufferSize - 1); - } -} - -bool_t uiModalIsOpen(void) { - return UI_MODAL.open; -} - -uint8_t uiModalGetResult(void) { - return UI_MODAL.result; -} - -void uiModalOpen( - const char_t *title, - const char_t *message, - const char_t **options, - const uint8_t optionCount, - uimodaloptioncallback_t callback, - uimodalopenedcallback_t onOpen, - void *user -) { - assertNotNull(message, "Message cannot be NULL"); - assertTrue( - options != NULL || optionCount == 0, - "Options cannot be NULL when optionCount > 0" - ); - assertTrue( - optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal" - ); - - float_t maxContentWidth = - ((float_t)SCREEN.scanWidth * UI_MODAL_MAX_WIDTH_RATIO) - - (UI_FRAME_START_X * 2.0f); - - UI_MODAL.hasTitle = title != NULL; - if(UI_MODAL.hasTitle) { - uiModalLocalize(title, UI_MODAL.titleText, UI_MODAL_TITLE_TEXT_MAX); - textWrap(UI_MODAL.titleText, &FONT_DEFAULT, maxContentWidth); - } else { - UI_MODAL.titleText[0] = '\0'; - } - UI_MODAL.titleLabel.dirty = true; - uiLabelRebuffer(&UI_MODAL.titleLabel); - - uiModalLocalize(message, UI_MODAL.messageText, UI_MODAL_MESSAGE_TEXT_MAX); - textWrap(UI_MODAL.messageText, &FONT_DEFAULT, maxContentWidth); - UI_MODAL.messageLabel.dirty = true; - uiLabelRebuffer(&UI_MODAL.messageLabel); - - UI_MODAL.callback = callback; - UI_MODAL.onOpen = onOpen; - UI_MODAL.user = user; - UI_MODAL.result = UI_MODAL_RESULT_NONE; - UI_MODAL.open = true; - - if(options != NULL) { - MENU_BEGIN( - &UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL - ); - for(uint8_t i = 0; i < optionCount; i++) { - uiModalLocalize( - options[i], UI_MODAL.optionsText[i], UI_MODAL_OPTION_TEXT_MAX - ); - MENU_BUTTON(UI_MODAL.optionsText[i]); - } - MENU_END(UI_MODAL.options, menuIndex); - - // A dialog with options must be dismissed by picking one, not by - // backing out of it - see uiMenuSetDisableBack. - uiMenuSetDisableBack(&UI_MODAL.menu, true); - uiMenuOpen(&UI_MODAL.menu); - } else { - uiMenuInit(&UI_MODAL.menu, uiModalSelected, uiModalClosed, NULL); - messageFocus = uiFocusPush( - 1, 1, - uiModalMessageFocusSelected, NULL, uiModalMessageFocusClosed, - NULL, NULL - ); - } - - if(UI_MODAL.onOpen != NULL) { - UI_MODAL.onOpen(UI_MODAL.user); - } -} - -void uiModalClose(uimodalclosedcallback_t callback) { - if(!UI_MODAL.open) return; - UI_MODAL.open = false; - - if(uiMenuIsActive(&UI_MODAL.menu)) { - uiMenuClose(&UI_MODAL.menu); - } else if(messageFocus != NULL) { - uiFocusPopItem(messageFocus); - } else { - uiModalClosed(NULL); - } - - if(callback != NULL) { - callback(UI_MODAL.user); - } -} - -errorret_t uiModalDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/widget/uimodal.h b/src/dusk/ui/widget/uimodal.h deleted file mode 100644 index 2a10b13f..00000000 --- a/src/dusk/ui/widget/uimodal.h +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uilabel.h" -#include "ui/widget/uimenu.h" - -#define UI_MODAL_TITLE_TEXT_MAX 64 -#define UI_MODAL_TITLE_SPRITES_MAX UI_MODAL_TITLE_TEXT_MAX -#define UI_MODAL_MESSAGE_TEXT_MAX 256 -#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX -#define UI_MODAL_OPTIONS_MAX 4 -#define UI_MODAL_OPTION_TEXT_MAX 32 -#define UI_MODAL_MIN_WIDTH 160.0f -// Title/message text wraps at word boundaries to stay within this -// fraction of the screen's scan width, so long (e.g. translated) -// strings don't run off-screen. -#define UI_MODAL_MAX_WIDTH_RATIO 0.8f -#define UI_MODAL_RESULT_NONE 0xFF - -/** - * Callback invoked once a modal is dismissed. - * - * @param optionIndex Index into the options array passed to uiModalOpen - * that was selected. A dialog with options ignores back/cancel input - * (see uiMenuSetDisableBack) and must be dismissed by picking one, so - * UI_MODAL_RESULT_NONE only reaches here via a direct uiModalClose() - * call made before any option was picked. - * @param user Arbitrary pointer passed to uiModalOpen. - */ -typedef void (*uimodaloptioncallback_t)(const uint8_t optionIndex, void *user); - -/** - * Callback invoked when a modal opens. Fired immediately for now, but - * gives a hook point for a future open transition to key off of. - * - * @param user Arbitrary pointer passed to uiModalOpen. - */ -typedef void (*uimodalopenedcallback_t)(void *user); - -/** - * Callback invoked when a modal closes, passed to uiModalClose. - * - * @param user Arbitrary pointer passed to uiModalOpen. - */ -typedef void (*uimodalclosedcallback_t)(void *user); - -typedef struct { - uilabel_t titleLabel; - char_t titleText[UI_MODAL_TITLE_TEXT_MAX]; - spritebatchsprite_t titleSprites[UI_MODAL_TITLE_SPRITES_MAX]; - - uilabel_t messageLabel; - char_t messageText[UI_MODAL_MESSAGE_TEXT_MAX]; - spritebatchsprite_t messageSprites[UI_MODAL_MESSAGE_SPRITES_MAX]; - - uimenu_t menu; - uimenuitem_t options[UI_MODAL_OPTIONS_MAX]; - char_t optionsText[UI_MODAL_OPTIONS_MAX][UI_MODAL_OPTION_TEXT_MAX]; - - uimodaloptioncallback_t callback; - uimodalopenedcallback_t onOpen; - void *user; - uint8_t result; - bool_t open; - bool_t hasTitle; -} uimodal_t; - -extern uimodal_t UI_MODAL; - -/** - * Initializes the modal dialog. - * - * @return Any error that occurs. - */ -errorret_t uiModalInit(void); - -/** - * Draws the modal dialog: a semi-transparent black backdrop covering the - * whole screen, then its own centered frame with the title, message, and - * option buttons. No-op when not open. - * - * @return Any error that occurs. - */ -errorret_t uiModalDraw(void); - -/** - * Returns true when the modal dialog is currently open. - * - * @returns True if open. - */ -bool_t uiModalIsOpen(void); - -/** - * Returns the result of the most recently dismissed modal. - * - * @returns The selected option index, or UI_MODAL_RESULT_NONE. - */ -uint8_t uiModalGetResult(void); - -/** - * Opens the modal dialog with the given title, message, and options. - * callback is invoked exactly once with the selected option. A dialog - * with options > 0 disables back/cancel (see uiMenuSetDisableBack), so - * it must be dismissed by picking one. - * - * title, message, and each option are treated as locale message IDs - * first: if one matches an entry in the active locale file its - * translated string is used, otherwise the string itself is displayed - * verbatim. See uiModalLocalize. - * - * @param title Display title, or a locale message ID; copied - * internally, safe to be transient. May be NULL for a dialog with no - * title row. - * @param message Display message, or a locale message ID; copied - * internally, safe to be transient. - * @param options Array of option label strings/locale message IDs; - * copied internally, safe to be transient. May be NULL for a - * message-only dialog with no option buttons, in which case optionCount - * must be 0. - * @param optionCount Number of options, from 0 to UI_MODAL_OPTIONS_MAX. - * @param callback Called with the result once the dialog closes. May be - * NULL. - * @param onOpen Called once the modal has opened. May be NULL. - * @param user Arbitrary pointer passed through to callback and onOpen, - * and to whatever callback is later passed to uiModalClose. - */ -void uiModalOpen( - const char_t *title, - const char_t *message, - const char_t **options, - const uint8_t optionCount, - uimodaloptioncallback_t callback, - uimodalopenedcallback_t onOpen, - void *user -); - -/** - * Closes the modal dialog. No-op when already closed. - * - * @param callback Called with the modal's user pointer once the dialog - * has closed, after the option-result callback set by uiModalOpen. May - * be NULL. Not called if the modal was already closed. - */ -void uiModalClose(uimodalclosedcallback_t callback); - -/** - * Disposes of the modal dialog. - * - * @return Any error that occurs. - */ -errorret_t uiModalDispose(void); - -/** - * Resolves display text for uiModalOpen: if key matches a locale - * message ID in the active locale file, buffer is filled with its - * translated string, otherwise key is copied into buffer verbatim. An - * empty key (e.g. an omitted JSON title/message, which defaults to "" - * rather than NULL - see cutscenemodal_t/cutscenemodaloptionsmarkers_t) - * is left as an empty buffer rather than looked up, since - * assetLocaleGetString rejects an empty message ID outright. - * - * @param key Display string or locale message ID. - * @param buffer Destination buffer. - * @param bufferSize Size of buffer, in bytes. - */ -void uiModalLocalize( - const char_t *key, - char_t *buffer, - const size_t bufferSize -); diff --git a/src/dusk/ui/widget/uiscrolling.c b/src/dusk/ui/widget/uiscrolling.c deleted file mode 100644 index 516caf00..00000000 --- a/src/dusk/ui/widget/uiscrolling.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uiscrolling.h" -#include "assert/assert.h" -#include "util/memory.h" - -void uiScrollingInit(uiscrolling_t *scrolling) { - assertNotNull(scrolling, "Scrolling container cannot be NULL"); - memoryZero(scrolling, sizeof(uiscrolling_t)); -} - -void uiScrollingEnsureVisible( - uiscrolling_t *scrolling, - const uint8_t index, - const uint8_t itemCount, - const uint8_t visibleCount -) { - assertNotNull(scrolling, "Scrolling container cannot be NULL"); - assertTrue(visibleCount > 0, "Visible count must be > 0"); - - if(index < scrolling->offset) { - scrolling->offset = index; - } else if(index >= (uint8_t)(scrolling->offset + visibleCount)) { - scrolling->offset = (uint8_t)(index - visibleCount + 1); - } - - uint8_t maxOffset = itemCount > visibleCount ? itemCount - visibleCount : 0; - if(scrolling->offset > maxOffset) scrolling->offset = maxOffset; -} diff --git a/src/dusk/ui/widget/uiscrolling.h b/src/dusk/ui/widget/uiscrolling.h deleted file mode 100644 index da47729a..00000000 --- a/src/dusk/ui/widget/uiscrolling.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - uint8_t offset; -} uiscrolling_t; - -/** - * Initializes a scrolling container with a zero offset. - * - * @param scrolling The scrolling container to initialize. - */ -void uiScrollingInit(uiscrolling_t *scrolling); - -/** - * Scrolls by the minimum amount needed to bring index within the visible - * window of visibleCount rows, then clamps so the window never scrolls - * past the end of itemCount rows. - * - * @param scrolling The scrolling container to update. - * @param index Row index that must end up visible. - * @param itemCount Total number of rows in the list. - * @param visibleCount Number of rows visible at once. - */ -void uiScrollingEnsureVisible( - uiscrolling_t *scrolling, - const uint8_t index, - const uint8_t itemCount, - const uint8_t visibleCount -); diff --git a/src/dusk/ui/widget/uislider.c b/src/dusk/ui/widget/uislider.c deleted file mode 100644 index 5b30bfe9..00000000 --- a/src/dusk/ui/widget/uislider.c +++ /dev/null @@ -1,244 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uislider.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include "util/string.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "display/texture/texture.h" -#include "display/shader/shaderunlit.h" - -void uiSliderInitFloat( - uislider_t *slider, - const char_t *label, - const float_t value, - const float_t min, - const float_t max, - const float_t step -) { - assertNotNull(slider, "Slider cannot be NULL"); - assertNotNull(label, "Label cannot be NULL"); - assertTrue(max > min, "Slider max must be greater than min"); - assertTrue(step > 0.0f, "Slider step must be greater than zero"); - - memoryZero(slider, sizeof(uislider_t)); - slider->label = label; - slider->type = UI_SLIDER_TYPE_FLOAT; - slider->min.f = min; - slider->max.f = max; - slider->step.f = step; - slider->value.f = mathClamp(value, min, max); -} - -void uiSliderInitInt( - uislider_t *slider, - const char_t *label, - const int32_t value, - const int32_t min, - const int32_t max, - const int32_t step -) { - assertNotNull(slider, "Slider cannot be NULL"); - assertNotNull(label, "Label cannot be NULL"); - assertTrue(max > min, "Slider max must be greater than min"); - assertTrue(step > 0, "Slider step must be greater than zero"); - - memoryZero(slider, sizeof(uislider_t)); - slider->label = label; - slider->type = UI_SLIDER_TYPE_INT; - slider->min.i = min; - slider->max.i = max; - slider->step.i = step; - slider->value.i = mathClamp(value, min, max); -} - -float_t uiSliderGetFloat(const uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - return slider->type == UI_SLIDER_TYPE_INT ? - (float_t)slider->value.i : slider->value.f; -} - -int32_t uiSliderGetInt(const uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - assertTrue( - slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider" - ); - return slider->value.i; -} - -void uiSliderSetFloat(uislider_t *slider, const float_t value) { - assertNotNull(slider, "Slider cannot be NULL"); - assertTrue( - slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider" - ); - slider->value.f = mathClamp(value, slider->min.f, slider->max.f); -} - -void uiSliderSetInt(uislider_t *slider, const int32_t value) { - assertNotNull(slider, "Slider cannot be NULL"); - assertTrue( - slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider" - ); - slider->value.i = mathClamp(value, slider->min.i, slider->max.i); -} - -void uiSliderStepUp(uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - if(slider->type == UI_SLIDER_TYPE_INT) { - int32_t next = slider->value.i + slider->step.i; - uiSliderSetInt(slider, next > slider->max.i ? slider->min.i : next); - } else { - float_t next = slider->value.f + slider->step.f; - uiSliderSetFloat(slider, next > slider->max.f ? slider->min.f : next); - } -} - -void uiSliderStepDown(uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - if(slider->type == UI_SLIDER_TYPE_INT) { - int32_t next = slider->value.i - slider->step.i; - uiSliderSetInt(slider, next < slider->min.i ? slider->max.i : next); - } else { - float_t next = slider->value.f - slider->step.f; - uiSliderSetFloat(slider, next < slider->min.f ? slider->max.f : next); - } -} - -float_t uiSliderGetRatio(const uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - if(slider->type == UI_SLIDER_TYPE_INT) { - if(slider->max.i == slider->min.i) return 0.0f; - return (float_t)(slider->value.i - slider->min.i) / - (float_t)(slider->max.i - slider->min.i); - } - - if(slider->max.f == slider->min.f) return 0.0f; - return (slider->value.f - slider->min.f) / (slider->max.f - slider->min.f); -} - -int32_t uiSliderGetStepCount(const uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - if(slider->type != UI_SLIDER_TYPE_INT || slider->step.i <= 0) return 0; - return (slider->max.i - slider->min.i) / slider->step.i; -} - -bool_t uiSliderIsHighlighted(const uislider_t *slider) { - assertNotNull(slider, "Slider cannot be NULL"); - return slider->highlighted; -} - -void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) { - assertNotNull(slider, "Slider cannot be NULL"); - slider->highlighted = highlighted; -} - -errorret_t uiSliderDraw( - const uislider_t *slider, - const float_t x, - const float_t y -) { - assertNotNull(slider, "Slider cannot be NULL"); - - color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE; - - errorChain(textDraw(x, y, slider->label, color, &FONT_DEFAULT)); - - int32_t labelW, labelH; - textMeasure(slider->label, &FONT_DEFAULT, &labelW, &labelH); - - float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP; - float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f; - - spritebatchsprite_t trackSprite = { - .min = { trackX, trackY, 0.0f }, - .max = { - trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f - }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t trackMaterial = { - .unlit = { - .color = COLOR_DARK_GRAY, - .texture = &TEXTURE_WHITE - } - }; - errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial)); - errorChain(spriteBatchFlush()); - - float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider); - if(fillWidth > 0.0f) { - spritebatchsprite_t fillSprite = { - .min = { trackX, trackY, 0.0f }, - .max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t fillMaterial = { - .unlit = { - .color = color, - .texture = &TEXTURE_WHITE - } - }; - errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial)); - errorChain(spriteBatchFlush()); - } - - int32_t stepCount = uiSliderGetStepCount(slider); - if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) { - for(int32_t i = 0; i <= stepCount; i++) { - float_t markerX = trackX + - UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount; - spritebatchsprite_t markerSprite = { - .min = { - markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f, - trackY - UI_SLIDER_STEP_MARKER_OVERHANG, - 0.0f - }, - .max = { - markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f, - trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG, - 0.0f - }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t markerMaterial = { - .unlit = { - .color = COLOR_WHITE, - .texture = &TEXTURE_WHITE - } - }; - errorChain( - spriteBatchBuffer(&markerSprite, 1, &SHADER_UNLIT, markerMaterial) - ); - errorChain(spriteBatchFlush()); - } - } - - char_t valueText[UI_SLIDER_VALUE_TEXT_MAX]; - if(slider->type == UI_SLIDER_TYPE_INT) { - stringFormat( - valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%d", slider->value.i - ); - } else { - stringFormat( - valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%.2f", slider->value.f - ); - } - - errorChain(textDraw( - trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y, - valueText, color, &FONT_DEFAULT - )); - - errorOk(); -} diff --git a/src/dusk/ui/widget/uislider.h b/src/dusk/ui/widget/uislider.h deleted file mode 100644 index d53dee00..00000000 --- a/src/dusk/ui/widget/uislider.h +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define UI_SLIDER_TRACK_WIDTH 80.0f -#define UI_SLIDER_TRACK_HEIGHT 4.0f -#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f -#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f -#define UI_SLIDER_GAP 4.0f -#define UI_SLIDER_VALUE_TEXT_MAX 16 - -/** - * Number of discrete steps below which an int slider renders individual - * step markers on its track instead of a smooth fill. - */ -#define UI_SLIDER_STEP_MARKERS_MAX 10 - -typedef enum { - UI_SLIDER_TYPE_FLOAT, - UI_SLIDER_TYPE_INT, - - UI_SLIDER_TYPE_COUNT -} uislidertype_t; - -typedef union { - float_t f; - int32_t i; -} uislidervalue_t; - -typedef struct { - const char_t *label; - uislidertype_t type; - uislidervalue_t value; - uislidervalue_t min; - uislidervalue_t max; - uislidervalue_t step; - bool_t highlighted; -} uislider_t; - -/** - * Initializes a floating point slider. - * - * @param slider The slider to initialize. - * @param label Display label. - * @param value Initial value, clamped to [min, max]. - * @param min Minimum value. - * @param max Maximum value. - * @param step The amount a single step moves the value by. - */ -void uiSliderInitFloat( - uislider_t *slider, - const char_t *label, - const float_t value, - const float_t min, - const float_t max, - const float_t step -); - -/** - * Initializes an integer slider. - * - * @param slider The slider to initialize. - * @param label Display label. - * @param value Initial value, clamped to [min, max]. - * @param min Minimum value. - * @param max Maximum value. - * @param step The amount a single step moves the value by. - */ -void uiSliderInitInt( - uislider_t *slider, - const char_t *label, - const int32_t value, - const int32_t min, - const int32_t max, - const int32_t step -); - -/** - * Returns the slider's current value as a float. Works for both slider - * types; int values are widened to float. - * - * @param slider The slider to query. - * @returns The current value. - */ -float_t uiSliderGetFloat(const uislider_t *slider); - -/** - * Returns the slider's current value as an int. Only valid for - * UI_SLIDER_TYPE_INT sliders. - * - * @param slider The slider to query. - * @returns The current value. - */ -int32_t uiSliderGetInt(const uislider_t *slider); - -/** - * Sets the slider's value, clamped to [min, max]. Only valid for - * UI_SLIDER_TYPE_FLOAT sliders. - * - * @param slider The slider to update. - * @param value The new value. - */ -void uiSliderSetFloat(uislider_t *slider, const float_t value); - -/** - * Sets the slider's value, clamped to [min, max]. Only valid for - * UI_SLIDER_TYPE_INT sliders. - * - * @param slider The slider to update. - * @param value The new value. - */ -void uiSliderSetInt(uislider_t *slider, const int32_t value); - -/** - * Moves the slider's value up by one step, wrapping around to its min - * if the step would exceed its max. - * - * @param slider The slider to update. - */ -void uiSliderStepUp(uislider_t *slider); - -/** - * Moves the slider's value down by one step, wrapping around to its - * max if the step would go below its min. - * - * @param slider The slider to update. - */ -void uiSliderStepDown(uislider_t *slider); - -/** - * Returns the slider's current value normalized to a 0..1 range based - * on its min/max. - * - * @param slider The slider to query. - * @returns The normalized value. - */ -float_t uiSliderGetRatio(const uislider_t *slider); - -/** - * Returns the number of discrete steps between min and max. Always 0 - * for float sliders. - * - * @param slider The slider to query. - * @returns The step count. - */ -int32_t uiSliderGetStepCount(const uislider_t *slider); - -/** - * Returns whether the slider is highlighted. - * - * @param slider The slider to query. - * @returns True if highlighted. - */ -bool_t uiSliderIsHighlighted(const uislider_t *slider); - -/** - * Sets the highlighted state of the slider. - * - * @param slider The slider to update. - * @param highlighted The new highlighted state. - */ -void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted); - -/** - * Draws the slider at the given screen position: label, then track, - * then the current value as text. Int sliders with fewer than - * UI_SLIDER_STEP_MARKERS_MAX discrete steps render a marker per step - * instead of a plain track. - * - * @param slider The slider to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. - */ -errorret_t uiSliderDraw( - const uislider_t *slider, - const float_t x, - const float_t y -); diff --git a/src/dusk/ui/widget/uitab.c b/src/dusk/ui/widget/uitab.c deleted file mode 100644 index 12073ef2..00000000 --- a/src/dusk/ui/widget/uitab.c +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uitab.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "display/text/text.h" -#include "display/color.h" -#include "display/spritebatch/spritebatch.h" -#include "display/texture/texture.h" -#include "display/shader/shaderunlit.h" - -void uiTabInit(uitab_t *tab, const char_t *label) { - assertNotNull(tab, "Tab cannot be NULL"); - assertNotNull(label, "Label cannot be NULL"); - memoryZero(tab, sizeof(uitab_t)); - tab->label = label; -} - -bool_t uiTabIsActive(const uitab_t *tab) { - assertNotNull(tab, "Tab cannot be NULL"); - return tab->active; -} - -void uiTabSetActive(uitab_t *tab, const bool_t active) { - assertNotNull(tab, "Tab cannot be NULL"); - tab->active = active; -} - -errorret_t uiTabDraw( - const uitab_t *tab, - const float_t x, - const float_t y -) { - assertNotNull(tab, "Tab cannot be NULL"); - - int32_t labelW, labelH; - textMeasure(tab->label, &FONT_DEFAULT, &labelW, &labelH); - - spritebatchsprite_t sprite = { - .min = { x, y, 0.0f }, - .max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f }, - .uvMin = { 0.0f, 0.0f }, - .uvMax = { 1.0f, 1.0f } - }; - shadermaterial_t material = { - .unlit = { - .color = tab->active ? COLOR_GREEN : COLOR_RED, - .texture = &TEXTURE_WHITE - } - }; - errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); - errorChain(spriteBatchFlush()); - - errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT)); - - errorOk(); -} diff --git a/src/dusk/ui/widget/uitab.h b/src/dusk/ui/widget/uitab.h deleted file mode 100644 index 678f54b6..00000000 --- a/src/dusk/ui/widget/uitab.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - const char_t *label; - bool_t active; -} uitab_t; - -/** - * Initializes a tab. - * - * @param tab The tab to initialize. - * @param label Display label. - */ -void uiTabInit(uitab_t *tab, const char_t *label); - -/** - * Returns whether the tab is active. - * - * @param tab The tab to query. - * @returns True if active. - */ -bool_t uiTabIsActive(const uitab_t *tab); - -/** - * Sets the active state of the tab. - * - * @param tab The tab to update. - * @param active The new active state. - */ -void uiTabSetActive(uitab_t *tab, const bool_t active); - -/** - * Draws the tab at the given screen position. Active tabs draw with a - * green background; inactive tabs draw with a red background. - * - * @param tab The tab to draw. - * @param x Screen x position. - * @param y Screen y position. - * @return Any error that occurs. - */ -errorret_t uiTabDraw( - const uitab_t *tab, - const float_t x, - const float_t y -); diff --git a/src/dusk/util/CMakeLists.txt b/src/dusk/util/CMakeLists.txt deleted file mode 100644 index 62fb7902..00000000 --- a/src/dusk/util/CMakeLists.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - array.c - crypt.c - endian.c - memory.c - random.c - string.c - math.c - sort.c - ref.c -) \ No newline at end of file diff --git a/src/dusk/util/array.c b/src/dusk/util/array.c deleted file mode 100644 index 214f8410..00000000 --- a/src/dusk/util/array.c +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "array.h" -#include "assert/assert.h" -#include "util/memory.h" - -void arrayReverse( - void *array, - const size_t count, - const size_t size -) { - assertNotNull(array, "Cannot reverse NULL memory."); - assertTrue(size > 0, "Element size must be greater than zero."); - - if(count == 0) return; - - // For half the array length, swap elements - uint8_t *start = (uint8_t*)array; - uint8_t *end = start + (count - 1) * size; - uint8_t *temp = (uint8_t*)memoryAllocate(size); - - // Floorf - size_t i; - for(i = 0; i < count / 2; i++) { - // Swap start and end - memoryCopy(temp, start, size); - memoryCopy(start, end, size); - memoryCopy(end, temp, size); - start += size; - end -= size; - } - - memoryFree(temp); -} \ No newline at end of file diff --git a/src/dusk/util/array.h b/src/dusk/util/array.h deleted file mode 100644 index ff2d5390..00000000 --- a/src/dusk/util/array.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Reverses the order of elements in an array. - * - * @param array The array to reverse. - * @param count The number of elements in the array. - * @param size The size of each element (the stride). - */ -void arrayReverse( - void *array, - const size_t count, - const size_t size -); \ No newline at end of file diff --git a/src/dusk/util/crypt.c b/src/dusk/util/crypt.c deleted file mode 100644 index 32734e31..00000000 --- a/src/dusk/util/crypt.c +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "util/crypt.h" - -void cryptCRC32Step(uint32_t *crc, const uint8_t byte) { - *crc ^= byte; - for(int j = 0; j < 8; j++) { - *crc = (*crc >> 1) ^ (0xEDB88320u & -(*crc & 1u)); - } -} - -uint32_t cryptCRC32Begin(void) { - return 0xFFFFFFFF; -} - -void cryptCRC32Update(uint32_t *crc, const void *data, const size_t size) { - const uint8_t *bytes = (const uint8_t *)data; - for(size_t i = 0; i < size; i++) { - cryptCRC32Step(crc, bytes[i]); - } -} - -uint32_t cryptCRC32End(const uint32_t crc) { - return ~crc; -} - -uint32_t cryptCRC32(const void *data, const size_t size) { - uint32_t crc = cryptCRC32Begin(); - cryptCRC32Update(&crc, data, size); - return cryptCRC32End(crc); -} diff --git a/src/dusk/util/crypt.h b/src/dusk/util/crypt.h deleted file mode 100644 index 971bfa7d..00000000 --- a/src/dusk/util/crypt.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Computes a CRC32 checksum over a block of data. - * - * @param data Pointer to the data to checksum. - * @param size Number of bytes to checksum. - * @return The CRC32 checksum. - */ -uint32_t cryptCRC32(const void *data, const size_t size); - -/** - * Returns the initial CRC32 accumulator value. - * - * @return The initial accumulator. - */ -uint32_t cryptCRC32Begin(void); - -/** - * Feeds a single byte into a running CRC32 accumulator. - * - * @param crc Pointer to the current accumulator (updated in place). - * @param byte The byte to feed in. - */ -void cryptCRC32Step(uint32_t *crc, const uint8_t byte); - -/** - * Feeds bytes into a running CRC32 accumulator. - * - * @param crc Pointer to the current accumulator (updated in place). - * @param data Pointer to the data to feed in. - * @param size Number of bytes to process. - */ -void cryptCRC32Update(uint32_t *crc, const void *data, const size_t size); - -/** - * Finalizes a running CRC32 accumulator and returns the checksum. - * - * @param crc The current accumulator value. - * @return The final CRC32 checksum. - */ -uint32_t cryptCRC32End(const uint32_t crc); diff --git a/src/dusk/util/endian.c b/src/dusk/util/endian.c deleted file mode 100644 index 4f225b01..00000000 --- a/src/dusk/util/endian.c +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "util/endian.h" -#include "util/memory.h" - -bool_t isHostLittleEndian(void) { - #if defined(DUSK_PLATFORM_ENDIAN_LITTLE) - return true; - #elif defined(DUSK_PLATFORM_ENDIAN_BIG) - return false; - #else - uint32_t value = ENDIAN_MAGIC; - uint8_t *bytePtr = (uint8_t *)&value; - return bytePtr[0] == 0x04; - #endif - -} - -uint32_t endianLittleToHost32(uint32_t value) { - #if defined(DUSK_PLATFORM_ENDIAN_LITTLE) - return value; - #elif defined(DUSK_PLATFORM_ENDIAN_BIG) - // Perform conversion below - #else - if(isHostLittleEndian()) { - return value; - } - #endif - - return ( - ((value & 0x000000FF) << 24) | - ((value & 0x0000FF00) << 8) | - ((value & 0x00FF0000) >> 8) | - ((value & 0xFF000000) >> 24) - ); -} - -uint16_t endianLittleToHost16(uint16_t value) { - #if defined(DUSK_PLATFORM_ENDIAN_LITTLE) - return value; - #elif defined(DUSK_PLATFORM_ENDIAN_BIG) - // Perform conversion below - #else - if(isHostLittleEndian()) { - return value; - } - #endif - - return (uint16_t)(((value & 0x00FF) << 8) | - ((value & 0xFF00) >> 8)); -} - -uint64_t endianLittleToHost64(uint64_t value) { - #if defined(DUSK_PLATFORM_ENDIAN_LITTLE) - return value; - #elif defined(DUSK_PLATFORM_ENDIAN_BIG) - // Perform conversion below - #else - if(isHostLittleEndian()) { - return value; - } - #endif - - return ( - ((value & 0x00000000000000FFULL) << 56) | - ((value & 0x000000000000FF00ULL) << 40) | - ((value & 0x0000000000FF0000ULL) << 24) | - ((value & 0x00000000FF000000ULL) << 8) | - ((value & 0x000000FF00000000ULL) >> 8) | - ((value & 0x0000FF0000000000ULL) >> 24) | - ((value & 0x00FF000000000000ULL) >> 40) | - ((value & 0xFF00000000000000ULL) >> 56) - ); -} - -float_t endianLittleToHostFloat(float_t value) { - #if defined(DUSK_PLATFORM_ENDIAN_LITTLE) - return value; - #elif defined(DUSK_PLATFORM_ENDIAN_BIG) - // Perform conversion below - #else - if(isHostLittleEndian()) { - return value; - } - #endif - - uint32_t temp; - float_t result; - memoryCopy(&temp, &value, sizeof(uint32_t)); - temp = endianLittleToHost32(temp); - memoryCopy(&result, &temp, sizeof(uint32_t)); - - return result; -} - diff --git a/src/dusk/util/endian.h b/src/dusk/util/endian.h deleted file mode 100644 index a5a397f1..00000000 --- a/src/dusk/util/endian.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#if !defined(DUSK_PLATFORM_ENDIAN_LITTLE) && !defined(DUSK_PLATFORM_ENDIAN_BIG) - #warning "Platform does not define endianness, requiring runtime check." -#endif - - -static const uint32_t ENDIAN_MAGIC = 0x01020304; - -/** - * Checks if the host system is little-endian. - * - * @return true if the host is little-endian, false otherwise. - */ -bool_t isHostLittleEndian(void); - -/** - * Converts a 32-bit integer from little-endian to host byte order. - * - * @param value The little-endian value to convert. - * @return The value in host byte order. - */ -uint32_t endianLittleToHost32(uint32_t value); - -/** - * Converts a 16-bit integer from little-endian to host byte order. - * @param value The little-endian value to convert. - * @return The value in host byte order. - */ -uint16_t endianLittleToHost16(uint16_t value); - -/** - * Converts a 64-bit integer from little-endian to host byte order. - * @param value The little-endian value to convert. - * @return The value in host byte order. - */ -uint64_t endianLittleToHost64(uint64_t value); - -/** - * Converts a float from little-endian to host byte order. - * - * @param value The little-endian value to convert. - * @return The value in host byte order. - */ -float_t endianLittleToHostFloat(float_t value); \ No newline at end of file diff --git a/src/dusk/util/math.c b/src/dusk/util/math.c deleted file mode 100644 index 01bb0168..00000000 --- a/src/dusk/util/math.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "math.h" - -uint32_t mathNextPowTwo(uint32_t value) { - if(value == 0) return 1; // Handle zero case - if(value >= 0x80000000) return 1; // Handle overflow case - - value--; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - return value + 1; -} - -float_t mathModFloat(float_t x, float_t y) { - float_t result = fmodf(x, y); - if(result < 0) result += y; - return result; -} - -float_t mathLerp(float_t a, float_t b, float_t t) { - return a + t * (b - a); -} \ No newline at end of file diff --git a/src/dusk/util/math.h b/src/dusk/util/math.h deleted file mode 100644 index 80239eca..00000000 --- a/src/dusk/util/math.h +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#define MATH_PI M_PI - -/** - * Finds the next power of two greater than or equal to the given value. - * - * @param value The value to find the next power of two for. - * @return The next power of two greater than or equal to the value. - */ -uint32_t mathNextPowTwo(uint32_t value); - -/** - * Returns the maximum of two values. - * - * @param a The first value. - * @param b The second value. - * @return The maximum of the two values. - */ -#define mathMax(a, b) ((a) > (b) ? (a) : (b)) - -/** - * Returns the minimum of two values. - * - * @param a The first value. - * @param b The second value. - * @return The minimum of the two values. - */ -#define mathMin(a, b) ((a) < (b) ? (a) : (b)) - -/** - * Clamps a value between a lower and upper bound. - * - * @param x The value to clamp. - * @param lower The lower bound. - * @param upper The upper bound. - * @return The clamped value. - */ -#define mathClamp(x, lower, upper) (mathMin(upper, mathMax(lower, x))) - -/** - * Integer ceiling division: ceil(n / d) using only integer arithmetic. - * - * @param n The dividend. - * @param d The divisor. - * @return The ceiling of n divided by d. - */ -#define mathCeilDiv(n, d) (((n) + (d) - 1) / (d)) - -/** - * Returns the absolute value of a number. - * - * @param amt The number to get the absolute value of. - * @return The absolute value of the number. - */ -#define mathAbs(amt) ((amt) < 0 ? -(amt) : (amt)) - -/** - * Performs a modulus operation that always returns a non-negative result, even - * if the dividend is negative. - * - * @param x The dividend. - * @param y The divisor. - * @return The result of the modulus operation. - */ -float_t mathModFloat(float_t x, float_t y); - -/** - * Linearly interpolates between two values. - * - * @param a The start value. - * @param b The end value. - * @param t The interpolation factor, between 0 and 1. - * @return The interpolated value. - */ -float_t mathLerp(float_t a, float_t b, float_t t); \ No newline at end of file diff --git a/src/dusk/util/memory.c b/src/dusk/util/memory.c deleted file mode 100644 index 0f5edb3d..00000000 --- a/src/dusk/util/memory.c +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "memory.h" -#include "assert/assert.h" -#include "util/math.h" -#include "log/log.h" - -#ifdef DUSK_TRACK_MEMORY - -// Fixed-size, pre-allocated table of live memoryAllocate() calls, keyed by -// pointer - deliberately never grows (backed by static storage, not -// malloc) so the tracker itself can never allocate, recurse or leak. Raise -// this if it's ever exhausted (see the assert in memoryAllocateTracked). -#define MEMORY_TRACK_MAX 16384 - -typedef struct { - void *ptr; - const char_t *file; - int_t line; - size_t size; -} memorytrackentry_t; - -static memorytrackentry_t MEMORY_TRACK_ENTRIES[MEMORY_TRACK_MAX]; -static size_t MEMORY_TRACK_COUNT = 0; - -#endif - -size_t memoryGetAllocatedCount(void) { - return MEMORY_POINTERS_IN_USE; -} - -void memoryTrack(void *ptr) { - assertNotNull(ptr, "Cannot track NULL pointer."); - MEMORY_POINTERS_IN_USE++; -} - -#ifdef DUSK_TRACK_MEMORY - -void * memoryAllocateTracked( - const size_t size, const char_t *file, const int_t line -) { - assertTrue(size > 0, "Cannot allocate 0 bytes of memory."); - void *ptr = malloc(size); - assertNotNull(ptr, "Memory allocation failed."); - memoryTrack(ptr); - - assertTrue( - MEMORY_TRACK_COUNT < MEMORY_TRACK_MAX, - "Memory tracking table is full - raise MEMORY_TRACK_MAX." - ); - MEMORY_TRACK_ENTRIES[MEMORY_TRACK_COUNT++] = (memorytrackentry_t){ - .ptr = ptr, - .file = file, - .line = line, - .size = size - }; - - return ptr; -} - -#else - -void * memoryAllocate(const size_t size) { - assertTrue(size > 0, "Cannot allocate 0 bytes of memory."); - void *ptr = malloc(size); - assertNotNull(ptr, "Memory allocation failed."); - memoryTrack(ptr); - return ptr; -} - -#endif - -void * memoryAlign(size_t alignment, size_t size) { - assertTrue(alignment > 0, "Alignment must be greater than 0."); - assertTrue(size > 0, "Cannot allocate 0 bytes of memory."); - void *ptr = memalign(alignment, size); - assertNotNull(ptr, "Aligned memory allocation failed."); - memoryTrack(ptr); - return ptr; -} - -void memoryCheckUnfreed(void) { - size_t count = memoryGetAllocatedCount(); - if(count == 0) { - logDebug("memoryCheckUnfreed: no unfreed allocations.\n"); - return; - } - -#ifdef DUSK_TRACK_MEMORY - logDebug("memoryCheckUnfreed: %zu unfreed allocation(s):\n", count); - - size_t trackedBytes = 0; - for(size_t i = 0; i < MEMORY_TRACK_COUNT; i++) { - memorytrackentry_t *entry = &MEMORY_TRACK_ENTRIES[i]; - logDebug( - " - %zu bytes allocated at %s:%d\n", - entry->size, entry->file, entry->line - ); - trackedBytes += entry->size; - } - logDebug(" (%zu bytes across %zu tracked allocations)\n", - trackedBytes, MEMORY_TRACK_COUNT - ); - - // memoryAlign()/memoryTrack() callers aren't recorded into - // MEMORY_TRACK_ENTRIES yet, so the tracked count can fall short of the - // real total - call that out rather than silently under-reporting. - size_t untracked = count - MEMORY_TRACK_COUNT; - if(untracked > 0) { - logDebug( - " + %zu more unfreed allocation(s) with no call site recorded " - "(from memoryAlign()/memoryTrack())\n", - untracked - ); - } -#else - logDebug( - "memoryCheckUnfreed: %zu unfreed allocation(s) - rebuild with " - "DUSK_TRACK_MEMORY=ON for call site details.\n", - count - ); -#endif -} - -void memoryFree(void *ptr) { - assertNotNull(ptr, "Cannot free NULL memory."); - -#ifdef DUSK_TRACK_MEMORY - // Only pointers from memoryAllocate() are in here (memoryAlign/ - // memoryTrack callers aren't tracked yet), so a miss here is expected, - // not an error - just leave the table alone. - for(size_t i = 0; i < MEMORY_TRACK_COUNT; i++) { - if(MEMORY_TRACK_ENTRIES[i].ptr == ptr) { - // Swap-remove - order doesn't matter for leak tracking. - MEMORY_TRACK_ENTRIES[i] = MEMORY_TRACK_ENTRIES[--MEMORY_TRACK_COUNT]; - break; - } - } -#endif - - free(ptr); - MEMORY_POINTERS_IN_USE--; -} - -void memoryCopy(void *dest, const void *src, const size_t size) { - assertNotNull(dest, "Cannot copy to NULL memory."); - assertNotNull(src, "Cannot copy from NULL memory."); - assertTrue(size > 0, "Cannot copy 0 bytes of memory."); - assertTrue(dest != src, "Cannot copy memory to itself."); - - // Check for overlapping regions - assertTrue( - ((uint8_t*)dest + size <= (uint8_t*)src) || - ((uint8_t*)src + size <= (uint8_t*)dest), - "Source and destination memory regions overlap." - ); - - memcpy(dest, src, size); -} - -void memorySet(void *dest, const uint8_t value, const size_t size) { - assertNotNull(dest, "Cannot set NULL memory."); - assertTrue(size > 0, "Cannot set 0 bytes of memory."); - memset(dest, value, size); -} - -void memoryZero(void *dest, const size_t size) { - memorySet(dest, 0, size); -} - -void memoryCopyRangeSafe( - void *dest, - const void *start, - const void *end, - const size_t sizeMax -) { - assertFalse(start == end, "Start and end pointers are the same."); - assertTrue(end > start, "End pointer is not after start pointer."); - - size_t copy = (size_t)end - (size_t)start; - assertTrue(copy <= sizeMax, "Size of memory to copy is too large."); - memoryCopy(dest, start, copy); -} - -void memoryMove(void *dest, const void *src, const size_t size) { - assertNotNull(dest, "Cannot move to NULL memory."); - assertNotNull(src, "Cannot move from NULL memory."); - assertTrue(size > 0, "Cannot move 0 bytes of memory."); - assertTrue(dest != src, "Cannot move memory to itself."); - memmove(dest, src, size); -} - -int_t memoryCompare( - const void *a, - const void *b, - const size_t size -) { - assertNotNull(a, "Cannot compare NULL memory."); - assertNotNull(b, "Cannot compare NULL memory."); - assertTrue(size >= 0, "Cannot compare negative bytes of memory."); - - if(size == 0) return 0; // Comparing 0 bytes is always equal - - return memcmp(a, b, size); -} - -void memoryReallocate(void **ptr, const size_t size) { - assertNotNull(ptr, "Cannot reallocate NULL pointer."); - assertTrue(size > 0, "Cannot reallocate to 0 bytes of memory."); - void *newPointer = memoryAllocate(size); - assertNotNull(newPointer, "Memory reallocation failed."); - memoryFree(*ptr); - *ptr = newPointer; -} - -void memoryCopyInterleaved( - void *dest, - const size_t destStride, - const void *src, - const size_t srcStride, - const size_t elementSize, - const size_t count -) { - assertNotNull(dest, "Cannot copy to NULL memory."); - assertNotNull(src, "Cannot copy from NULL memory."); - assertTrue(elementSize > 0, "Element size must be > 0."); - assertTrue(destStride >= elementSize, "Dest stride must be >= element size."); - assertTrue(srcStride >= elementSize, "Src stride must be >= element size."); - - uint8_t *d = (uint8_t *)dest; - const uint8_t *s = (const uint8_t *)src; - for(size_t i = 0; i < count; i++) { - memoryCopy(d, s, elementSize); - d += destStride; - s += srcStride; - } -} - -void memoryResize(void **ptr, const size_t oldSize, const size_t newSize) { - assertNotNull(ptr, "Cannot resize NULL pointer."); - assertTrue(newSize > 0, "Cannot resize to 0 bytes of memory."); - assertTrue(oldSize > 0, "Old size cannot be 0 bytes."); - if(newSize == oldSize) return; - - void *newPointer = memoryAllocate(newSize); - assertNotNull(newPointer, "Memory resizing failed."); - memoryCopy(newPointer, *ptr, mathMin(oldSize, newSize)); - memoryFree(*ptr); - *ptr = newPointer; -} \ No newline at end of file diff --git a/src/dusk/util/memory.h b/src/dusk/util/memory.h deleted file mode 100644 index 94171a32..00000000 --- a/src/dusk/util/memory.h +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -static size_t MEMORY_POINTERS_IN_USE = 0; - -/** - * Gets the number of currently allocated memory blocks. - * - * @return The number of allocated memory blocks. - */ -size_t memoryGetAllocatedCount(void); - -/** - * Track a pointer that was malloc'd outside of the dusk engine. - * - * @param ptr The pointer to track. - */ -void memoryTrack(void *ptr); - -/** - * Reports (via logDebug) whether any memoryAllocate()-family allocation is - * still outstanding - intended to be called once, near shutdown, after - * every subsystem has had a chance to dispose (see engineDispose). With - * DUSK_TRACK_MEMORY defined, prints exactly where (file:line) and how many - * bytes each still-unfreed allocation came from; without it, there's - * nowhere to report a call site from, so it only prints how many blocks - * remain outstanding. - */ -void memoryCheckUnfreed(void); - -#ifdef DUSK_TRACK_MEMORY - -/** - * Allocates memory, recording the call site (file/line) into a small - * fixed-size tracking table so a block still allocated at shutdown (see - * engineDispose's memoryGetAllocatedCount() check) can be traced back to - * where it came from. memoryFree removes the matching entry again once the - * pointer is freed. Only called via the memoryAllocate(size) macro below - * when DUSK_TRACK_MEMORY is defined - callers keep writing - * memoryAllocate(size) exactly as without it. - * - * @param size The size of the memory to allocate. - * @param file Call site file name (__FILE__). - * @param line Call site line number (__LINE__). - * @return The allocated memory. - */ -void * memoryAllocateTracked( - const size_t size, const char_t *file, const int_t line -); - -#define memoryAllocate(size) \ - memoryAllocateTracked((size), __FILE__, __LINE__) - -#else - -/** - * Allocates memory. - * - * @param size The size of the memory to allocate. - * @return The allocated memory. - */ -void * memoryAllocate(const size_t size); - -#endif - -/** - * Allocates aligned memory. - * - * @param alignment The alignment of the memory to allocate. - * @param size The size of the memory to allocate. - * @return The allocated memory. - */ -void * memoryAlign(size_t alignment, size_t size); - -/** - * Frees memory. - * - * @param ptr The memory to free. - */ -void memoryFree(void *ptr); - -/** - * Copies memory. - * - * @param dest The destination to copy to. - * @param src The source to copy from. - * @param size The size of the memory to copy. - */ -void memoryCopy(void *dest, const void *src, const size_t size); - -/** - * Sets memory. - * - * @param dest The destination to set. - * @param value The value to set. - * @param size The size of the memory to set. - */ -void memorySet(void *dest, const uint8_t value, const size_t size); - -/** - * Zeroes memory. - * - * @param dest The destination to zero. - * @param size The size of the memory to zero. - */ -void memoryZero(void *dest, const size_t size); - -/** - * Copies memory, ensuring that the memory range is as expected, typically this - * is done if you're trying to reshape data in a buffer. Extremely useful in - * copying data to a shader buffer. - * - * @param dest The destination to copy to. - * @param start The start of the source to copy from. - * @param end The end of the source to copy from. - * @param sizeMax The maximum size of the memory to copy. - */ -void memoryCopyRangeSafe( - void *dest, - const void *start, - const void *end, - const size_t sizeMax -); - -/** - * Moves memory. - * - * @param dest The destination to move to. - * @param src The source to move from. - * @param size The size of the memory to move. - */ -void memoryMove(void *dest, const void *src, const size_t size); - -/** - * Compares memory. - * - * @param a The first memory to compare. - * @param b The second memory to compare. - * @param size The size of the memory to compare. - * @return 0 if the memory is equal, < 0 if a < b, > 0 if a > b. - */ -int_t memoryCompare( - const void *a, - const void *b, - const size_t size -); - -/** - * Reallocates memory. - * - * @param ptr The pointer to the memory to reallocate. - * @param size The new size of the memory. - */ -void memoryReallocate(void **ptr, const size_t size); - -/** - * Reallocates memory, but copies existing data to the new memory. - * - * @param ptr The pointer to the memory to resize. - * @param oldSize The old size of the memory. - * @param newSize The new size of the memory. - */ -void memoryResize(void **ptr, const size_t oldSize, const size_t newSize); - -/** - * Scatter-copies elements from a contiguous source array into a strided - * destination. For each of count elements, copies elementSize bytes from - * src[i * srcStride] into dest[i * destStride]. - * - * @param dest Base destination pointer. - * @param destStride Byte stride between destination slots. - * @param src Base source pointer. - * @param srcStride Byte stride between source elements. - * @param elementSize Bytes to copy per element. - * @param count Number of elements to copy. - */ -void memoryCopyInterleaved( - void *dest, - const size_t destStride, - const void *src, - const size_t srcStride, - const size_t elementSize, - const size_t count -); \ No newline at end of file diff --git a/src/dusk/util/random.c b/src/dusk/util/random.c deleted file mode 100644 index c7974029..00000000 --- a/src/dusk/util/random.c +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "random.h" -#include - -float_t randomFloat(const float_t min, const float_t max) { - return min + ((float_t)rand() / (float_t)RAND_MAX) * (max - min); -} - -int_t randomInt(const int_t min, const int_t max) { - return min + (rand() % (max - min)); -} diff --git a/src/dusk/util/random.h b/src/dusk/util/random.h deleted file mode 100644 index 1fba6946..00000000 --- a/src/dusk/util/random.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Returns a random float between min (inclusive) and max (exclusive). - * - * @param min Lower bound. - * @param max Upper bound. - * @returns A random float in [min, max). - */ -float_t randomFloat(const float_t min, const float_t max); - -/** - * Returns a random integer between min (inclusive) and max (exclusive). - * - * @param min Lower bound. - * @param max Upper bound. - * @returns A random integer in [min, max). - */ -int_t randomInt(const int_t min, const int_t max); diff --git a/src/dusk/util/ref.c b/src/dusk/util/ref.c deleted file mode 100644 index faf84a9c..00000000 --- a/src/dusk/util/ref.c +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "ref.h" -#include "memory.h" -#include "assert/assert.h" - -void refInit( - ref_t *ref, - void *data, - refcallback_t onLock, - refcallback_t onUnlock, - refcallback_t onAllUnlocked -) { - assertNotNull(ref, "Ref cannot be NULL."); - memoryZero(ref, sizeof(ref_t)); - ref->count = 0; - ref->data = data; - ref->onLock = onLock; - ref->onUnlock = onUnlock; - ref->onAllUnlocked = onAllUnlocked; -} - -void refLock(ref_t *ref) { - assertNotNull(ref, "Ref cannot be NULL."); - assertTrue(ref->count >= 0, "Cannot lock a ref with negative count."); - ref->count++; - if(ref->onLock != NULL) ref->onLock(ref); -} - -bool_t refUnlock(ref_t *ref) { - assertNotNull(ref, "Ref cannot be NULL."); - assertTrue(ref->count >= 0, "Cannot unlock a ref with negative count."); - - ref->count--; - - if(ref->count > 0) { - if(ref->onUnlock != NULL) ref->onUnlock(ref); - return false; - } - - void *data = ref->data; - refcallback_t onAllUnlocked = ref->onAllUnlocked; - memoryZero(ref, sizeof(ref_t)); - ref->data = data; - if(onAllUnlocked != NULL) onAllUnlocked(ref); - return true; -} diff --git a/src/dusk/util/ref.h b/src/dusk/util/ref.h deleted file mode 100644 index e2c41af5..00000000 --- a/src/dusk/util/ref.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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 ref_s ref_t; -typedef void (*refcallback_t)(ref_t *ref); - -typedef struct ref_s { - uint32_t count; - void *data; - refcallback_t onLock; - refcallback_t onUnlock; - refcallback_t onAllUnlocked; -} ref_t; - -/** - * Initializes a ref with a count of 1. - * - * @param ref The ref to initialize. - * @param data Opaque context pointer accessible to all callbacks. - * @param onLock Called each time the ref is locked. May be NULL. - * @param onUnlock Called each time the ref is unlocked (count still > 0). - * May be NULL. - * @param onAllUnlocked Called when the count reaches zero. Responsible for - * any cleanup. May be NULL. - */ -void refInit( - ref_t *ref, - void *data, - refcallback_t onLock, - refcallback_t onUnlock, - refcallback_t onAllUnlocked -); - -/** - * Increments the lock count. - * - * @param ref The ref to lock. - */ -void refLock(ref_t *ref); - -/** - * Decrements the lock count. When it reaches zero onAllUnlocked is called - * (if set) and the ref struct is zeroed. - * - * @param ref The ref to unlock. - * @return true if the count reached zero. - */ -bool_t refUnlock(ref_t *ref); diff --git a/src/dusk/util/sort.c b/src/dusk/util/sort.c deleted file mode 100644 index 3e184bbb..00000000 --- a/src/dusk/util/sort.c +++ /dev/null @@ -1,71 +0,0 @@ -#include -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "sort.h" -#include "assert/assert.h" -#include "util/memory.h" - -void sortBubble( - void *array, - const size_t count, - const size_t size, - const sortcompare_t compare -) { - assertNotNull(array, "Cannot sort NULL memory."); - assertTrue(count > 0, "Cannot sort 0 elements."); - assertTrue(size > 0, "Element size must be greater than zero."); - assertNotNull(compare, "Compare function cannot be NULL."); - - // Create variables - uint8_t *temp = (uint8_t*)memoryAllocate(size); - bool_t swapped; - uint8_t *a, *b; - size_t i; - - do { - swapped = false; - for(i = 1; i < count; i++) { - a = array + (i - 1) * size; - b = array + i * size; - if(compare(a, b) > 0) { - // Swap - memoryCopy(temp, a, size); - memoryCopy(a, b, size); - memoryCopy(b, temp, size); - swapped = true; - } - } - } while(swapped); - - memoryFree(temp); -} - -void sortQuick( - void *array, - const size_t count, - const size_t size, - const sortcompare_t compare -) { - assertNotNull(array, "Cannot sort NULL memory."); - assertTrue(count > 0, "Cannot sort 0 elements."); - assertTrue(size > 0, "Element size must be greater than zero."); - assertNotNull(compare, "Compare function cannot be NULL."); - - // qsort expects a comparator returning int, which matches sortcompare_t - qsort(array, count, size, compare); -} - -int sortArrayU8Compare(const void *a, const void *b) { - uint8_t valA = *(const uint8_t*)a; - uint8_t valB = *(const uint8_t*)b; - return (valA > valB) - (valA < valB); -} - -void sortArrayU8(uint8_t *array, const size_t count) { - return sort((void*)array, count, sizeof(uint8_t), sortArrayU8Compare); -} \ No newline at end of file diff --git a/src/dusk/util/sort.h b/src/dusk/util/sort.h deleted file mode 100644 index 203bcc0b..00000000 --- a/src/dusk/util/sort.h +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef int_t (*sortcompare_t)(const void*, const void*); - -/** - * Sorts an array using the bubble sort algorithm. - * - * @param array The array to sort. - * @param count The number of elements in the array. - * @param size The size of each element. - * @param compare The comparison function. - */ -void sortBubble( - void *array, - const size_t count, - const size_t size, - const sortcompare_t compare -); - -/** - * Sorts an array using the quicksort algorithm. - * - * @param array The array to sort. - * @param count The number of elements in the array. - * @param size The size of each element. - * @param compare The comparison function. - */ -void sortQuick( - void *array, - const size_t count, - const size_t size, - const sortcompare_t compare -); - -/** - * Preferred sort function. - * - * @see sortQuick - */ -#define sort sortQuick - -/** - * Comparison function for uint8_t arrays. - * - * @param a First element. - * @param b Second element. - * @return Negative if a < b, zero if a == b, positive if a > b. - */ -int sortArrayU8Compare(const void *a, const void *b); - -/** - * Sorts an array of uint8_t in ascending order. - * - * @param array The array to sort. - * @param count The number of elements in the array. - */ -void sortArrayU8(uint8_t *array, const size_t count); \ No newline at end of file diff --git a/src/dusk/util/string.c b/src/dusk/util/string.c deleted file mode 100644 index 783a23cd..00000000 --- a/src/dusk/util/string.c +++ /dev/null @@ -1,248 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "string.h" -#include "assert/assert.h" -#include "util/memory.h" - -bool_t stringIsWhitespace(const char_t c) { - return isspace(c); -} - -void stringCopy(char_t *dest, const char_t *src, const size_t destSize) { - assertNotNull(dest, "dest must not be NULL"); - assertNotNull(src, "src must not be NULL"); - assertTrue(destSize > 0, "destSize must be greater than 0"); - assertStrLenMax(src, destSize, "src is too long"); - assertTrue(dest != src, "Cannot copy string to itself."); - memoryCopy(dest, src, strlen(src) + 1); -} - -int stringCompare(const char_t *str1, const char_t *str2) { - assertNotNull(str1, "str1 must not be NULL"); - assertNotNull(str2, "str2 must not be NULL"); - return strcmp(str1, str2); -} - -bool_t stringEquals(const char_t *str1, const char_t *str2) { - return stringCompare(str1, str2) == 0; -} - -int stringCompareInsensitive(const char_t *str1, const char_t *str2) { - assertNotNull(str1, "str1 must not be NULL"); - assertNotNull(str2, "str2 must not be NULL"); - return strcasecmp(str1, str2); -} - -void stringTrim(char_t *str) { - assertNotNull(str, "str must not be NULL"); - - // Trim leading whitespace - char_t *start = str; - while(stringIsWhitespace(*start)) start++; - - // Trim trailing whitespace - char_t *end = start + strlen(start) - 1; - while(end >= start && stringIsWhitespace(*end)) end--; - - // Null-terminate the string - *(end + 1) = '\0'; - - // Move trimmed string to the original buffer - if(start != str) memmove(str, start, end - start + 2); -} - -char_t * stringFindLastChar(const char_t *str, const char_t c) { - assertNotNull(str, "str must not be NULL"); - char_t *last = NULL; - for(const char_t *p = str; *p != '\0'; p++) { - if(*p == c) { - last = (char_t *)p; - } - } - return last; -} - -int32_t stringFormat( - char_t *dest, - const size_t destSize, - const char_t *format, - ... -) { - va_list args; - va_start(args, format); - int32_t len = stringFormatVA(dest, destSize, format, args); - va_end(args); - return len; -} - -int32_t stringFormatVA( - char_t *dest, - const size_t destSize, - const char_t *format, - va_list args -) { - assertNotNull(format, "format must not be NULL"); - - if(dest == NULL) { - int32_t ret = vsnprintf(NULL, 0, format, args); - assertTrue(ret >= 0, "Failed to format string."); - return ret; - } - - assertTrue(destSize > 0, "destSize must be greater than 0"); - int32_t ret = vsnprintf(dest, destSize, format, args); - assertTrue(ret >= 0, "Failed to format string."); - assertTrue(ret < destSize, "Formatted string is too long."); - return ret; -} - -bool_t stringToI32(const char_t *str, int32_t *out) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(out, "out must not be NULL"); - - // Empty string check - if(str[0] == '\0') return false; - - // Leading or trailing whitespace check - if(stringIsWhitespace(str[0])) return false; - if(stringIsWhitespace(str[strlen(str) - 1])) return false; - - char_t *endptr; - errno = 0; - long int result = strtol(str, &endptr, 10); - if(errno != 0 || *endptr != '\0') { - return false; - } - - // Within INT32 range - if(result < INT32_MIN || result > INT32_MAX) { - return false; - } - - *out = (int32_t)result; - return true; -} - -bool_t stringToI64(const char_t *str, int64_t *out) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(out, "out must not be NULL"); - - // Empty string check - if(str[0] == '\0') return false; - - // Leading or trailing whitespace check - if(stringIsWhitespace(str[0])) return false; - if(stringIsWhitespace(str[strlen(str) - 1])) return false; - - char_t *endptr; - errno = 0; - long long int result = strtoll(str, &endptr, 10); - if(errno != 0 || *endptr != '\0') { - return false; - } - *out = (int64_t)result; - return true; -} - -bool_t stringToI16(const char_t *str, int16_t *out) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(out, "out must not be NULL"); - - // Empty string check - if(str[0] == '\0') return false; - - // Leading or trailing whitespace check - if(stringIsWhitespace(str[0])) return false; - if(stringIsWhitespace(str[strlen(str) - 1])) return false; - - char_t *endptr; - errno = 0; - long int result = strtol(str, &endptr, 10); - if( - errno != 0 || - *endptr != '\0' || - result < INT16_MIN || - result > INT16_MAX - ) { - return false; - } - *out = (int16_t)result; - return true; -} - -bool_t stringToU16(const char_t *str, uint16_t *out) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(out, "out must not be NULL"); - - // Empty string check - if(str[0] == '\0') return false; - - // Leading or trailing whitespace check - if(stringIsWhitespace(str[0])) return false; - if(stringIsWhitespace(str[strlen(str) - 1])) return false; - - char_t *endptr; - errno = 0; - unsigned long int result = strtoul(str, &endptr, 10); - if(errno != 0 || *endptr != '\0' || result > UINT16_MAX) { - return false; - } - *out = (uint16_t)result; - return true; -} - -bool_t stringToF32(const char_t *str, float_t *out) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(out, "out must not be NULL"); - - // Empty string check - if(str[0] == '\0') return false; - - // Leading or trailing whitespace check - if(stringIsWhitespace(str[0])) return false; - if(stringIsWhitespace(str[strlen(str) - 1])) return false; - - char_t *endptr; - errno = 0; - float_t result = strtof(str, &endptr); - if(errno != 0 || *endptr != '\0') { - return false; - } - *out = result; - return true; -} - -bool_t stringEndsWith(const char_t *str, const char_t *suffix) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(suffix, "suffix must not be NULL"); - - size_t strLen = strlen(str); - size_t suffixLen = strlen(suffix); - if(suffixLen > strLen) return false; - return strcmp(str + strLen - suffixLen, suffix) == 0; -} - -bool_t stringEndsWithCaseInsensitive( - const char_t *str, - const char_t *suffix -) { - assertNotNull(str, "str must not be NULL"); - assertNotNull(suffix, "suffix must not be NULL"); - - size_t strLen = strlen(str); - size_t suffixLen = strlen(suffix); - if(suffixLen > strLen) return false; - return strcasecmp(str + strLen - suffixLen, suffix) == 0; -} - -bool_t stringIncludesString(const char_t *haystack, const char_t *needle) { - assertNotNull(haystack, "haystack must not be NULL"); - assertNotNull(needle, "needle must not be NULL"); - - return strstr(haystack, needle) != NULL; -} \ No newline at end of file diff --git a/src/dusk/util/string.h b/src/dusk/util/string.h deleted file mode 100644 index 77a6c5ba..00000000 --- a/src/dusk/util/string.h +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Determines if a character is whitespace. - * - * @param c The character to check. - * @return TRUE if the character is whitespace, FALSE otherwise. - */ -bool_t stringIsWhitespace(const char_t c); - -/** - * Copies a string from src to dest, ensuring the dest string is null-terminated - * and does not exceed the specified size. - * - * @param dest The destination string. - * @param src The source string. - * @param destSize The size of the destination string exc. null terminator. - */ -void stringCopy(char_t *dest, const char_t *src, const size_t destSize); - -/** - * Compares two strings. - * - * @param str1 The first string. - * @param str2 The second string. - * @return 0 if the strings are equal, -1 if str1 is less than str2, 1 if str1 - * is greater than str2. - */ -int stringCompare(const char_t *str1, const char_t *str2); - -/** - * Compares two strings for equality. - * - * @param str1 The first string. - * @param str2 The second string. - * @return TRUE if the strings are equal, FALSE otherwise. - */ -bool_t stringEquals(const char_t *str1, const char_t *str2); - -/** - * Compares two strings, ignoring case. - * - * @param str1 The first string. - * @param str2 The second string. - * @return 0 if the strings are equal, -1 if str1 is less than str2, 1 if str1 - * is greater than str2. - */ -int stringCompareInsensitive(const char_t *str1, const char_t *str2); - -/** - * Trims whitespace from the beginning and end of a string. - * - * @param str The string to trim. - */ -void stringTrim(char_t *str); - -/** - * Finds the last occurrence of a character in a string. - * - * @param str The string to search. - * @param c The character to find. - * @return A pointer to the last occurrence of the character in the string, or - * NULL if not found. - */ -char_t * stringFindLastChar(const char_t *str, const char_t c); - -/** - * Formats a string. - * - * @param dest The destination string, or NULL to get the length of the - * formatted string. - * @param destSize The size of the destination string exc. null terminator, can - * be anything if dest is NULL. - * @param format The format string. - * @param ... The arguments to format. - * @return The number of characters written. - */ -int32_t stringFormat( - char_t *dest, - const size_t destSize, - const char_t *format, - ... -); - -/** - * Formats a string using a va_list. - * - * @param dest The destination string, or NULL to get the length of the - * formatted string. - * @param destSize The size of the destination string exc. null terminator, can - * be anything if dest is NULL. - * @param format The format string. - * @param args The va_list of arguments. - * @return The number of characters written. - */ -int32_t stringFormatVA( - char_t *dest, - const size_t destSize, - const char_t *format, - va_list args -); - -/** - * Converts a string to an integer. - * - * @param str The string to convert. - * @param out The output integer. - * @return TRUE if the conversion was successful, FALSE otherwise. - */ -bool_t stringToI32(const char_t *str, int32_t *out); - -/** - * Converts a string to a signed 64-bit integer. - * - * @param str The string to convert. - * @param out The output signed integer. - * @return TRUE if the conversion was successful, FALSE otherwise. - */ -bool_t stringToI64(const char_t *str, int64_t *out); - -/** - * Converts a string to a signed 16-bit integer. - * - * @param str The string to convert. - * @param out The output signed integer. - * @return TRUE if the conversion was successful, FALSE otherwise. - */ -bool_t stringToI16(const char_t *str, int16_t *out); - -/** - * Converts a string to an unsigned 16-bit integer. - * - * @param str The string to convert. - * @param out The output unsigned integer. - * @return TRUE if the conversion was successful, FALSE otherwise. - */ -bool_t stringToU16(const char_t *str, uint16_t *out); - -/** - * Converts a string to a float. - * - * @param str The string to convert. - * @param out The output float. - * @return TRUE if the conversion was successful, FALSE otherwise. - */ -bool_t stringToF32(const char_t *str, float_t *out); - -/** - * Determines if a string ends with a specified suffix. - * - * @param str The string to check. - * @param suffix The suffix to check for. - * @return TRUE if the string ends with the suffix, FALSE otherwise. - */ -bool_t stringEndsWith(const char_t *str, const char_t *suffix); - -/** - * Determines if a string ends with a specified suffix, ignoring case. - * - * @param str The string to check. - * @param suffix The suffix to check for. - * @return TRUE if the string ends with the suffix, FALSE otherwise. - */ -bool_t stringEndsWithCaseInsensitive(const char_t *str, const char_t *suffix); - -/** - * Determines if a string includes a specified substring. - * - * @param haystack The string to check. - * @param needle The substring to check for. - * @return TRUE if the string includes the substring, FALSE otherwise. - */ -bool_t stringIncludesString(const char_t *haystack, const char_t *needle); \ No newline at end of file diff --git a/src/duskdolphin/CMakeLists.txt b/src/duskdolphin/CMakeLists.txt deleted file mode 100644 index 9aa02ee2..00000000 --- a/src/duskdolphin/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Sources -target_sources(${DUSK_BINARY_TARGET_NAME} - PUBLIC -) - -# Subdirs -add_subdirectory(asset) -add_subdirectory(audio) -add_subdirectory(log) -add_subdirectory(display) -add_subdirectory(input) -if(DUSK_NETWORK) - add_subdirectory(network) -endif() -add_subdirectory(save) -add_subdirectory(system) -add_subdirectory(time) \ No newline at end of file diff --git a/src/duskdolphin/asset/CMakeLists.txt b/src/duskdolphin/asset/CMakeLists.txt deleted file mode 100644 index 32883cf8..00000000 --- a/src/duskdolphin/asset/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO") - target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - assetdolphindvd.c - ) -else() - target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - assetdolphinfat.c - ) -endif() diff --git a/src/duskdolphin/asset/assetdolphindvd.c b/src/duskdolphin/asset/assetdolphindvd.c deleted file mode 100644 index b0c984f5..00000000 --- a/src/duskdolphin/asset/assetdolphindvd.c +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetdolphindvd.h" -#include "asset/asset.h" -#include "asset/assetdsk.h" -#include "util/string.h" -#include "util/memory.h" - -#define ASSET_DOLPHIN_DVD_SECTOR_SIZE 2048u -#define ASSET_DOLPHIN_DVD_PVD_SECTOR 16u - -errorret_t assetInitDolphinDVD(void) { - DVD_Init(); - DVD_Mount(); - - // ISO 9660 Primary Volume Descriptor is at sector 16. - u8 *pvd = (u8 *)assetDolphinDVDRead( - (s64)(ASSET_DOLPHIN_DVD_PVD_SECTOR * ASSET_DOLPHIN_DVD_SECTOR_SIZE), - ASSET_DOLPHIN_DVD_SECTOR_SIZE - ); - if(!pvd) errorThrow("Failed to read ISO 9660 PVD."); - - // Sanity-check: type=1, identifier="CD001" - if(pvd[0] != 1 || pvd[1] != 'C' || pvd[2] != 'D' || - pvd[3] != '0' || pvd[4] != '0' || pvd[5] != '1') { - memoryFree(pvd); - errorThrow("Not a valid ISO 9660 disc."); - } - - // Root Directory Record starts at PVD+156. - // ISO 9660 stores multi-byte fields in both byte orders; use the BE copies - // (offset +6 for LBA, +14 for size) since the GameCube is big-endian. - u32 rootLBA = assetDolphinDVDReadBigEndian32(pvd + 156 + 6); - u32 rootSize = assetDolphinDVDReadBigEndian32(pvd + 156 + 14); - memoryFree(pvd); - - u8 *dir = (u8 *)assetDolphinDVDRead( - (s64)rootLBA * ASSET_DOLPHIN_DVD_SECTOR_SIZE, rootSize - ); - if(!dir) errorThrow("Failed to read ISO 9660 root directory."); - - // Scan directory records for dusk.dsk. - // ISO 9660 level-1 names are uppercase with a ";1" version suffix, e.g. - // "DUSK.DSK;1". We strip the suffix before comparing case-insensitively. - u32 fileLBA = 0, fileSize = 0; - u32 pos = 0; - while(pos < rootSize) { - u8 recLen = dir[pos]; - if(recLen == 0) { - // Sector padding - skip to the start of the next sector. - pos = ( - pos + (ASSET_DOLPHIN_DVD_SECTOR_SIZE - 1u) - ) & ~(ASSET_DOLPHIN_DVD_SECTOR_SIZE - 1u); - continue; - } - - u8 flags = dir[pos + 25]; - u8 nameLen = dir[pos + 32]; - - if(!(flags & 0x02) && nameLen > 1) { // skip directories and "." / ".." - const char_t *isoName = (const char_t *)(dir + pos + 33); - - // Build a null-terminated copy of the base name (strip ";N" version). - char_t baseName[32]; - u8 baseLen = 0; - while(baseLen < nameLen && isoName[baseLen] != ';' && baseLen < 31) { - baseName[baseLen] = isoName[baseLen]; - baseLen++; - } - baseName[baseLen] = '\0'; - - if(stringCompareInsensitive(baseName, ASSET_FILE_NAME) == 0) { - fileLBA = assetDolphinDVDReadBigEndian32(dir + pos + 6); - fileSize = assetDolphinDVDReadBigEndian32(dir + pos + 14); - break; - } - } - pos += recLen; - } - memoryFree(dir); - - if(!fileLBA) errorThrow("Failed to find asset file on ISO."); - - u8 *data = (u8 *)assetDolphinDVDRead( - (s64)fileLBA * ASSET_DOLPHIN_DVD_SECTOR_SIZE, fileSize - ); - if(!data) errorThrow("Failed to read asset file from ISO."); - - errorret_t ret = assetDskOpenFromBuffer(data, fileSize, &ASSET.zip); - if(errorIsNotOk(ret)) { - memoryFree(data); - errorChain(ret); - } - - ASSET.platform.dskData = data; - errorOk(); -} - -void *assetDolphinDVDRead(const s64 offset, const u32 size) { - u32 padded = ASSET_DOLPHIN_DVD_ALIGN_UP(size); - void *buf = memoryAlign(ASSET_DOLPHIN_DVD_ALIGN, padded); - if(!buf) return NULL; - DCInvalidateRange(buf, padded); - static dvdcmdblk block; - if(DVD_ReadAbs(&block, buf, padded, offset) <= 0) { - memoryFree(buf); - return NULL; - } - return buf; -} - -u32 assetDolphinDVDReadBigEndian32(const u8 *p) { - return ((u32)p[0] << 24) | ((u32)p[1] << 16) | ((u32)p[2] << 8) | (u32)p[3]; -} - -errorret_t assetDisposeDolphinDVD(void) { - if(ASSET.platform.dskData != NULL) { - memoryFree(ASSET.platform.dskData); - ASSET.platform.dskData = NULL; - } - errorOk(); -} diff --git a/src/duskdolphin/asset/assetdolphindvd.h b/src/duskdolphin/asset/assetdolphindvd.h deleted file mode 100644 index 19197e25..00000000 --- a/src/duskdolphin/asset/assetdolphindvd.h +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include -#include - -/** Alignment for Dolphin DVD asset reads. */ -#define ASSET_DOLPHIN_DVD_ALIGN 32u - -/** - * Gets the next multiple of ASSET_DOLPHIN_DVD_ALIGN greater than or equal to n. - * This is used to ensure that DVD reads are properly aligned and padded. - * - * @param n The number to align. - * @return The aligned number. - */ -#define ASSET_DOLPHIN_DVD_ALIGN_UP(n) \ - (((u32)(n) + ASSET_DOLPHIN_DVD_ALIGN - 1u) & ~(ASSET_DOLPHIN_DVD_ALIGN - 1u)) - -typedef struct { - // Whole dusk.dsk blob read from the ISO, kept alive for as long as - // ASSET.zip is open since it's a non-owning window - // into it (see assetDskOpenFromBuffer). Freed in assetDisposeDolphinDVD. - uint8_t *dskData; -} assetdolphindvd_t; - -/** - * Initializes the Dolphin DVD asset system. - * - * This will; - * - Initialize the DVD subsystem and mount the disc. - * - Read the disc header to find the FST location. - * - Read the FST and find the offset and length of the "data" file. - * - Store the offset and length for later use when loading assets. - * - * @return An errorret_t indicating success or failure. - */ -errorret_t assetInitDolphinDVD(void); - -/** - * Reads a block of data from the DVD at the specified offset and size. - * The returned buffer is aligned to 32 bytes and padded. The caller is - * responsible for freeing the buffer. - * - * @param offset The offset on the DVD to read from. - * @param size The number of bytes to read. - * @return A pointer to the buffer, or NULL on failure. - */ -void * assetDolphinDVDRead(const s64 offset, const u32 size); - -/** - * Reads a big-endian 32-bit integer from the given data. - * - * @param data The data to read from. - * @return The big-endian 32-bit integer as a host-endian uint32_t. - */ -u32 assetDolphinDVDReadBigEndian32(const u8 *data); - -/** - * Disposes of the Dolphin DVD asset system. - * - * @return An errorret_t indicating success or failure. - */ -errorret_t assetDisposeDolphinDVD(void); \ No newline at end of file diff --git a/src/duskdolphin/asset/assetdolphinfat.c b/src/duskdolphin/asset/assetdolphinfat.c deleted file mode 100644 index 32b0413e..00000000 --- a/src/duskdolphin/asset/assetdolphinfat.c +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "assetdolphinfat.h" -#include "asset/asset.h" -#include "asset/assetdsk.h" -#include "util/string.h" -#include -#include -#include -#include -#include -#include -#include -#include - -errorret_t assetInitDolphinFAT(void) { - if(!fatInitDefault()) errorThrow("Failed to initialize FAT filesystem."); - - char_t **dolphinSearchPath = (char_t **)ASSET_DOLPHIN_PATHS; - char_t foundPath[ASSET_DOLPHIN_FAT_PATH_MAX]; - foundPath[0] = '\0'; - do { - DIR *pdir = opendir(*dolphinSearchPath); - if(pdir == NULL) continue; - - while(true) { - struct dirent* pent = readdir(pdir); - if(pent == NULL) break; - - if(stringCompareInsensitive(pent->d_name, ASSET_FILE_NAME) != 0) continue; - - snprintf( - foundPath, - ASSET_DOLPHIN_FAT_PATH_MAX, - "%s/%s", - *dolphinSearchPath, - ASSET_FILE_NAME - ); - // Save devices with a real filesystem path (Wii SD) reuse this as - // their base directory - see ASSET.baseDirectory's own comment. - stringCopy( - ASSET.baseDirectory, *dolphinSearchPath, ASSET_BASE_DIRECTORY_MAX - ); - break; - } - - closedir(pdir); - if(foundPath[0] != '\0') break; - } while(*(++dolphinSearchPath) != NULL); - - if(foundPath[0] == '\0') - errorThrow("Failed to find asset file on FAT filesystem."); - - errorChain(assetDskOpenFromPath(foundPath, &ASSET.zip)); - errorOk(); -} - -errorret_t assetDisposeDolphinFAT(void) { - errorOk(); -} diff --git a/src/duskdolphin/asset/assetdolphinfat.h b/src/duskdolphin/asset/assetdolphinfat.h deleted file mode 100644 index ded5d023..00000000 --- a/src/duskdolphin/asset/assetdolphinfat.h +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -#define ASSET_DOLPHIN_FAT_PATH_MAX FILENAME_MAX - -static const char_t *ASSET_DOLPHIN_PATHS[] = { - "/", - "/Dusk", - "/dusk", - "/DUSK", - "/apps", - "/apps/Dusk", - "/apps/dusk", - "/apps/DUSK", - ".", - "./", - "./Dusk", - "./dusk", - "./DUSK", - "./apps", - "./apps/Dusk", - "./apps/dusk", - "./apps/DUSK", - NULL -}; - -typedef struct { - uint8_t nothing; -} assetdolphinfat_t; - -/** - * Initializes the Dolphin FAT asset system. - * - * FAT being the general term for the FAT filesystem, on Wii this would likely - * be the SD Card, on Gamecube it can be something like the SD Gecko or the - * Slot2 SD Card. - * - * As far as I know this doesn't (currently) work on the CUBEODE? - * - * @return An errorret_t indicating success or failure. - */ -errorret_t assetInitDolphinFAT(void); - -/** - * Disposes of the Dolphin FAT asset system. - * - * @return An errorret_t indicating success or failure. - */ -errorret_t assetDisposeDolphinFAT(void); \ No newline at end of file diff --git a/src/duskdolphin/asset/assetplatform.h b/src/duskdolphin/asset/assetplatform.h deleted file mode 100644 index 5ef73420..00000000 --- a/src/duskdolphin/asset/assetplatform.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -#ifdef DUSK_DOLPHIN_BUILD_ISO - #include "assetdolphindvd.h" - #define assetInitPlatform assetInitDolphinDVD - #define assetDisposePlatform assetDisposeDolphinDVD - typedef assetdolphindvd_t assetplatform_t; -#else - #include "assetdolphinfat.h" - #define assetInitPlatform assetInitDolphinFAT - #define assetDisposePlatform assetDisposeDolphinFAT - typedef assetdolphinfat_t assetplatform_t; -#endif - diff --git a/src/duskdolphin/audio/CMakeLists.txt b/src/duskdolphin/audio/CMakeLists.txt deleted file mode 100644 index a692cdb5..00000000 --- a/src/duskdolphin/audio/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# 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 - audiodolphin.c - audiostreamdolphin.c -) - -# No hardware MP3 decoder used on Dolphin (see audiostreammp3decoder.h's -# own comment on why libogc's MP3Player wrapper isn't a fit) - uses the -# shared libmad-based software backend + decode-ahead ring in -# src/duskmad instead (also used by Linux), see that directory's own -# CMakeLists.txt for how it's sourced/linked. diff --git a/src/duskdolphin/audio/audiodolphin.c b/src/duskdolphin/audio/audiodolphin.c deleted file mode 100644 index b9823a5e..00000000 --- a/src/duskdolphin/audio/audiodolphin.c +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiodolphin.h" -#include - -errorret_t audioDolphinInit() { - ASND_Init(); - // ASND_Init() starts the sound engine paused - unpause immediately so - // voices actually play once configured. - ASND_Pause(0); - - errorOk(); -} - -errorret_t audioDolphinUpdate() { - errorOk(); -} - -errorret_t audioDolphinDispose() { - ASND_End(); - - errorOk(); -} diff --git a/src/duskdolphin/audio/audiodolphin.h b/src/duskdolphin/audio/audiodolphin.h deleted file mode 100644 index d8ab17d7..00000000 --- a/src/duskdolphin/audio/audiodolphin.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -/** - * Initializes the GameCube/Wii-specific audio subsystem state. - * - * @return Error state if any. - */ -errorret_t audioDolphinInit(); - -/** - * Updates the GameCube/Wii-specific audio subsystem state. Called once per - * frame. - * - * @return Error state if any. - */ -errorret_t audioDolphinUpdate(); - -/** - * Disposes the GameCube/Wii-specific audio subsystem state. - * - * @return Error state if any. - */ -errorret_t audioDolphinDispose(); diff --git a/src/duskdolphin/audio/audioplatform.h b/src/duskdolphin/audio/audioplatform.h deleted file mode 100644 index 3cd1a1da..00000000 --- a/src/duskdolphin/audio/audioplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audiodolphin.h" - -#define audioPlatformInit audioDolphinInit -#define audioPlatformUpdate audioDolphinUpdate -#define audioPlatformDispose audioDolphinDispose diff --git a/src/duskdolphin/audio/audiostreamdolphin.c b/src/duskdolphin/audio/audiostreamdolphin.c deleted file mode 100644 index 9708d363..00000000 --- a/src/duskdolphin/audio/audiostreamdolphin.c +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreamdolphin.h" -#include "audio/stream/audiostream.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include - -errorret_t audioStreamDolphinInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - const s32 voiceId = ASND_GetFirstUnusedVoice(); - if(voiceId == SND_INVALID) { - errorThrow("No free ASND voice available."); - } - - const size_t frameSize = stream->channels * sizeof(int16_t); - const size_t rawBytes = AUDIO_DOLPHIN_WINDOW_FRAMES * frameSize; - const size_t alignedBytes = (rawBytes + 31) & ~((size_t) 31); - - stream->platform.voiceId = voiceId; - stream->platform.bufferCapacityBytes = alignedBytes; - stream->platform.buffer[0] = (uint8_t *) memoryAlign(32, alignedBytes); - stream->platform.buffer[1] = (uint8_t *) memoryAlign(32, alignedBytes); - stream->platform.nextBufferIndex = 0; - stream->platform.primed = false; - stream->platform.position = 0; - stream->platform.endFrame = 0; - stream->platform.volumeLeft = 0; - stream->platform.volumeRight = 0; - - errorChain(audioStreamMp3RingInitIfNeeded(stream)); - - errorOk(); -} - -errorret_t audioStreamDolphinDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - // Must happen first: audioStreamMp3Dispose() (called right after this - // function returns, from the shared audioStreamDispose()) tears down - // stream->mp3's decoder/file - the decode thread must be fully parked - // before that, not just before this function's own ASND/buffer cleanup - // below. - errorChain(audioStreamMp3RingDisposeIfNeeded(stream)); - - ASND_StopVoice(stream->platform.voiceId); - memoryFree(stream->platform.buffer[0]); - memoryFree(stream->platform.buffer[1]); - - errorOk(); -} - -errorret_t audioStreamDolphinBuffer(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - const size_t totalFrames = audioStreamGetTotalFrames(stream); - - const size_t startFrame = mathMin(stream->startFrame, totalFrames); - stream->startFrame = 0; - stream->seeking = false; - - // This pass's stop point - the current loop segment's end, or the true - // end of the clip if not looping (or if startFrame already landed past - // the loop end, e.g. a seek into an outro). See - // audioStreamComputeEndFrame()'s own comment. - const size_t endFrame = - audioStreamComputeEndFrame(stream, startFrame, totalFrames); - - float_t leftFactor, rightFactor; - audioStreamGetPanFactors(stream, &leftFactor, &rightFactor); - const float_t baseVolume = stream->volume; - stream->platform.volumeLeft = - (s32) mathClamp(baseVolume * leftFactor * MAX_VOLUME, 0, MAX_VOLUME); - stream->platform.volumeRight = - (s32) mathClamp(baseVolume * rightFactor * MAX_VOLUME, 0, MAX_VOLUME); - - // A fresh pass discards whatever windows were previously queued (both - // ASND buffer slots) rather than letting it play out - a natural loop - // restart costs a small gap here (unlike Linux, which deliberately - // leaves its own tail queued for gapless looping); acceptable for now, - // see this backend's own history of iterating on click/gap reduction - // once real hardware confirms it's actually audible. - ASND_StopVoice(stream->platform.voiceId); - stream->platform.nextBufferIndex = 0; - stream->platform.primed = false; - - // For MP3 streams, the background decode thread reads via the same - // stream->mp3 decoder/file audioStreamSeek() below is about to mutate - - // must be fully stopped first, every time a new pass begins (initial - // play, explicit seek, or loop restart), not just once at stream - // teardown. Blocks until that's guaranteed and resets the ring to empty - // for this fresh pass. - errorChain(audioStreamMp3RingStopIfNeeded(stream)); - - errorChain(audioStreamSeek(stream, startFrame)); - stream->platform.position = startFrame; - stream->platform.endFrame = endFrame; - - // Launch this pass's decode thread now that position/endFrame are - // finalized - it starts filling the ring immediately in the background, - // ahead of audioStreamDolphinFeed() below even asking for anything. - errorChain(audioStreamMp3RingStartIfNeeded(stream, startFrame, endFrame)); - - // Prime only the first buffer here - the second window can't be queued - // back-to-back with this one, since ASND_SetVoice() leaves VOICE_UPDATE - // set until its own audio-DMA-interrupt tick picks it up, and - // ASND_AddVoice() refuses to queue anything while that's still pending - // (see audioStreamDolphinFeed()'s own comment). Later polls queue - // everything after this. For MP3 streams this first call will likely - // find the ring still empty (the decode thread just started) and do - // nothing - the next few audioStreamDolphinIsFinished() polls pick it - // up once decode has caught up. - errorChain(audioStreamDolphinFeed(stream)); - - errorOk(); -} - -errorret_t audioStreamDolphinFeed(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->platform.position >= stream->platform.endFrame) { - errorOk(); - } - - // The very first window of a pass always has room (nothing's queued - // yet) - every window after that needs an actual free slot, which only - // opens up once ASND has finished consuming the previous "primary" - // buffer and promoted the "secondary" one in its place. This is only a - // fast-path skip, not a full guarantee ASND_AddVoice() will actually - // succeed below - see that call's own comment. - // - // ASND_TestVoiceBufferReady() returns SND_OK (0) when ready and - // SND_BUSY (1) when not - opposite of a plain boolean. Comparing - // explicitly against SND_OK matters: treating the raw result as a bool - // reads a genuinely-ready voice's SND_OK (0) as false/not-ready, - // silently skipping every ASND_AddVoice() call forever after the first - // window - confirmed the hard way (only ever heard a clip's first - // buffered window, nothing ever played after it). - const bool_t ready = - ASND_TestVoiceBufferReady(stream->platform.voiceId) == SND_OK; - if(stream->platform.primed && !ready) { - errorOk(); - } - - const size_t frameSize = stream->channels * sizeof(int16_t); - const size_t framesRemaining = - stream->platform.endFrame - stream->platform.position; - const size_t framesToRead = mathMin( - (size_t) AUDIO_DOLPHIN_WINDOW_FRAMES, framesRemaining - ); - - uint8_t *buffer = stream->platform.buffer[stream->platform.nextBufferIndex]; - size_t framesRead = 0; - bool_t notReadyYet = false; - // For MP3 streams, drains already-decoded PCM out of the background ring - // instead of decoding directly - the expensive part (asset I/O + libmad - // decode) already happened on the decode thread, off the main thread. - // See audiostreammp3ring.h's own top-of-file comment for why. For - // everything else, this is just a plain audioStreamRead(). - errorChain(audioStreamReadForPlayback( - stream, (int16_t *) buffer, framesToRead, &framesRead, ¬ReadyYet - )); - if(notReadyYet) { - // Decode hasn't caught up to a full window yet - not an error, just - // try again next poll. - errorOk(); - } - - const size_t bytesRead = framesRead * frameSize; - const size_t alignedBytes = (bytesRead + 31) & ~((size_t) 31); - if(alignedBytes > bytesRead) { - // Pad up to ASND's required 32-byte alignment - only matters for - // whatever the final (possibly short) window of a pass is, since - // every full window is already a multiple of 32 bytes by - // construction (bufferCapacityBytes itself is pre-aligned). - memoryZero(buffer + bytesRead, alignedBytes - bytesRead); - } - - if(!stream->platform.primed) { - const s32 format = - stream->channels == 1 ? VOICE_MONO_16BIT : VOICE_STEREO_16BIT; - const s32 pitch = (s32) stream->sampleRate; - const s32 result = ASND_SetVoice( - stream->platform.voiceId, format, pitch, 0, - buffer, (s32) alignedBytes, - stream->platform.volumeLeft, stream->platform.volumeRight, NULL - ); - if(result != SND_OK) { - errorThrow("Failed to set ASND voice: %d", result); - } - stream->platform.primed = true; - } else { - // ASND_SetVoice() leaves VOICE_UPDATE set in this voice's flags until - // ASND's own audio-DMA-interrupt tick actually picks up the change - - // ASND_AddVoice() refuses to queue anything while that (or its own - // VOICE_UPDATEADD, set the same way on a successful call here) is - // still pending, returning SND_INVALID rather than SND_BUSY for it - // despite this being a purely transient, expected condition, not a - // real usage error. ASND_TestVoiceBufferReady() above doesn't catch - // this - it only checks whether the buffer slot itself is empty, a - // completely separate piece of state from these flags - so treat - // *any* non-OK result here as "not ready yet, retry on the next - // poll" rather than a hard failure: don't advance position or - // rotate the buffer index, just leave this same window to be reread - // and requeued next time. - const s32 result = ASND_AddVoice( - stream->platform.voiceId, buffer, (s32) alignedBytes - ); - if(result != SND_OK) { - errorOk(); - } - } - - stream->platform.position += framesRead; - stream->platform.nextBufferIndex = 1 - stream->platform.nextBufferIndex; - - errorOk(); -} - -bool_t audioStreamDolphinIsFinished(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->platform.position < stream->platform.endFrame) { - errorret_t ret = audioStreamDolphinFeed(stream); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - return true; // Can't recover - let the shared layer end/loop it. - } - return false; - } - - // Nothing left to read for this pass - finished once ASND has actually - // drained both buffer slots too, not just when we stopped feeding it. - return ASND_StatusVoice(stream->platform.voiceId) == SND_UNUSED; -} diff --git a/src/duskdolphin/audio/audiostreamdolphin.h b/src/duskdolphin/audio/audiostreamdolphin.h deleted file mode 100644 index f0afff80..00000000 --- a/src/duskdolphin/audio/audiostreamdolphin.h +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "audiostreammp3ring.h" - -typedef struct audiostream_s audiostream_t; - -// How many frames of PCM each of the two rotating buffers holds - just -// needs to be comfortably longer than one Update() tick's worth of real -// time, with margin, so polling once per frame never lets ASND run dry. -// Sized in frames (not bytes) to match PSP/Linux's own window constants; -// at 44100Hz mono this is ~371ms per buffer, ~743ms of total lookahead -// across both. Deliberately NOT "read the whole segment" (this backend's -// original design, inherited from its ansnd-based predecessor) - that -// allocated size scaled with asset length and crashed GameCube/Wii's -// limited RAM (24MB/88MB total) outright on a real multi-minute WAV. -#define AUDIO_DOLPHIN_WINDOW_FRAMES 16384 - -typedef struct { - // Allocated ASND voice slot (0-15, see ASND_GetFirstUnusedVoice()). - int32_t voiceId; - - // Two fixed-size, 32-byte-aligned scratch buffers (ASND requires this - // alignment), allocated once in Init() and reused for the stream's - // whole lifetime - never resized/reallocated per pass, unlike this - // backend's previous whole-segment-per-Buffer()-call design. Rotated - // as a classic double buffer: one plays via ASND_SetVoice() (the - // "primary" slot), the other is queued ahead via ASND_AddVoice() (the - // "secondary" slot) - see audioStreamDolphinFeed()'s own comment for - // the exact protocol. - uint8_t *buffer[2]; - - // Byte capacity of each buffer[] slot - fixed at Init() from this - // stream's channel count (frames * channels * sizeof(int16_t), padded - // to 32 bytes). - size_t bufferCapacityBytes; - - // Which buffer[] slot audioStreamDolphinFeed() fills next - alternates - // between 0 and 1 every time it successfully hands one to ASND. - size_t nextBufferIndex; - - // Set once audioStreamDolphinBuffer() has handed the very first window - // of a pass to ASND_SetVoice() - every window after that goes through - // ASND_AddVoice() instead, which requires an already-active voice. - // Cleared at the start of every new pass. - bool_t primed; - - // This pass's own read cursor and stop point, in frames relative to - // the start of the decoded stream - mirrors audiostreamlinux_t.position/ - // endFrame exactly (this backend now streams bounded windows on demand - // just like Linux/PSP, rather than buffering a whole segment up front). - size_t position; - size_t endFrame; - - // This pass's per-voice volume, resolved once in audioStreamDolphinBuffer() - // (pan/volume are only ever set via ASND_SetVoice(), not per-window - // ASND_AddVoice() calls) and reused by every audioStreamDolphinFeed() - // call for that pass's first (ASND_SetVoice()) window. - int32_t volumeLeft; - int32_t volumeRight; - - // Background decode-ahead ring, shared with Linux - only initialized/used - // for AUDIO_STREAM_TYPE_MP3 streams (PCM/WAV reads are cheap plain - // copies and don't need this). See audiostreammp3ring.h's own comment. - audiostreammp3ring_t mp3Ring; -} audiostreamdolphin_t; - -/** - * Initializes the GameCube/Wii-specific playback state of an audio stream: - * allocates an ASND voice slot (ASND_GetFirstUnusedVoice()) and this - * stream's two fixed-size double-buffer windows (sized from its channel - * count, AUDIO_DOLPHIN_WINDOW_FRAMES frames each). For MP3 streams, also - * initializes the background decode-ahead ring (audioStreamMp3RingInit()) - - * see audiostreammp3ring.h's own comment. - * - * @param stream The audio stream to initialize. - * @return Error state if any. - */ -errorret_t audioStreamDolphinInit(audiostream_t *stream); - -/** - * Disposes the GameCube/Wii-specific playback state of an audio stream: - * for MP3 streams, first fully stops and frees the background decode ring - * (audioStreamMp3RingDispose()) - must happen before the shared MP3 - * layer tears down its decoder/file - then stops the voice (releasing the - * slot back to ASND_GetFirstUnusedVoice()) and frees its two buffers. - * - * @param stream The audio stream to dispose. - * @return Error state if any. - */ -errorret_t audioStreamDolphinDispose(audiostream_t *stream); - -/** - * Starts a new playback pass: seeks the stream's decode position to - * stream->startFrame, determines this pass's loop-segment end, resolves - * this pass's volume/pan, stops whatever the voice was doing before (so a - * fresh pass never plays out stale queued windows from the last one). For - * MP3 streams, also stops the previous pass's background decode thread - * (audioStreamMp3RingStop(), which must complete before the seek - * below - see its own comment) and starts a fresh one for this pass - * (audioStreamMp3RingStart()) once position/endFrame are finalized. - * Finally primes the first window via audioStreamDolphinFeed() - for MP3 - * streams this may find the ring still empty and do nothing yet; the next - * few polls pick it up once decode has caught up. - * - * @param stream The audio stream to output. - * @return Error state if any. - */ -errorret_t audioStreamDolphinBuffer(audiostream_t *stream); - -/** - * Reads and queues one bounded window (AUDIO_DOLPHIN_WINDOW_FRAMES) of - * PCM data, if (and only if) ASND's voice actually has room for another - * one right now: the very first call of a pass always does (there's - * nothing queued yet - this hands it to ASND_SetVoice()), and every call - * after that only proceeds once ASND_TestVoiceBufferReady() confirms the - * voice's secondary buffer slot has been freed (handed to - * ASND_AddVoice()). A no-op once this pass's platform.position has - * reached platform.endFrame. - * - * For MP3 streams, the PCM data comes from draining the background decode - * ring (audioStreamMp3RingDrain()) instead of decoding directly - - * see audiostreammp3ring.h's own comment for why. - * - * @param stream The audio stream to feed. - * @return Error state if any. - */ -errorret_t audioStreamDolphinFeed(audiostream_t *stream); - -/** - * Checks whether the stream's voice has finished playing all of this - * pass's data. As a side effect, tops up the voice's buffers (via - * audioStreamDolphinFeed()) whenever there's room and more of the current - * pass left to read, rather than reporting finished early. - * - * @param stream The audio stream to check. - * @return true if playback has finished, false otherwise. - */ -bool_t audioStreamDolphinIsFinished(audiostream_t *stream); diff --git a/src/duskdolphin/audio/audiostreammp3decoder.h b/src/duskdolphin/audio/audiostreammp3decoder.h deleted file mode 100644 index f39288d6..00000000 --- a/src/duskdolphin/audio/audiostreammp3decoder.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -// GameCube/Wii have no usable hardware MP3 decoder for this engine's -// architecture (libogc's MP3Player wraps libmad but drives its own -// internal audio output, bypassing the ASND-based pipeline -// audiostreamdolphin.c already owns - not a fit here), so Dolphin uses the -// same shared libmad-based software backend as Linux, driven directly at -// the mad_stream/mad_frame/mad_synth level instead of through MP3Player - -// see that header's own documentation for the actual struct/interface. -// Conveniently, libmad ships as part of libogc itself (gc/mad.h) rather -// than needing to be fetched separately, unlike on Linux. -#include "audiostreammp3decodersw.h" diff --git a/src/duskdolphin/audio/audiostreamplatform.h b/src/duskdolphin/audio/audiostreamplatform.h deleted file mode 100644 index 0f86b66a..00000000 --- a/src/duskdolphin/audio/audiostreamplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audiostreamdolphin.h" - -typedef audiostreamdolphin_t audiostreamplatform_t; - -#define audioStreamPlatformInit audioStreamDolphinInit -#define audioStreamPlatformDispose audioStreamDolphinDispose -#define audioStreamPlatformBuffer audioStreamDolphinBuffer -#define audioStreamPlatformIsFinished audioStreamDolphinIsFinished diff --git a/src/duskdolphin/display/CMakeLists.txt b/src/duskdolphin/display/CMakeLists.txt deleted file mode 100644 index 88bf05f8..00000000 --- a/src/duskdolphin/display/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# 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 - displaydolphin.c - # debug.c -) - -# Subdirs -add_subdirectory(framebuffer) -add_subdirectory(mesh) -add_subdirectory(texture) -add_subdirectory(shader) \ No newline at end of file diff --git a/src/duskdolphin/display/displaydolphin.c b/src/duskdolphin/display/displaydolphin.c deleted file mode 100644 index 7980dca9..00000000 --- a/src/duskdolphin/display/displaydolphin.c +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/display.h" -#include "util/memory.h" -#include "engine/engine.h" - -errorret_t displayInitDolphin(void) { - VIDEO_Init(); - DISPLAY.screenMode = VIDEO_GetPreferredMode(NULL); - DISPLAY.frameBuffer[0] = MEM_K0_TO_K1( - SYS_AllocateFramebuffer(DISPLAY.screenMode) - ); - if(DISPLAY.frameBuffer[0] == NULL) { - errorThrow("Failed to allocate framebuffer"); - } - DISPLAY.frameBuffer[1] = MEM_K0_TO_K1( - SYS_AllocateFramebuffer(DISPLAY.screenMode) - ); - if(DISPLAY.frameBuffer[1] == NULL) { - errorThrow("Failed to allocate framebuffer"); - } - VIDEO_Configure(DISPLAY.screenMode); - - VIDEO_SetNextFramebuffer(DISPLAY.frameBuffer[DISPLAY.whichFrameBuffer]); - // VIDEO_SetPostRetraceCallback(copy_buffers); - VIDEO_SetBlack(FALSE); - VIDEO_Flush(); - VIDEO_WaitVSync(); - if(DISPLAY.screenMode->viTVMode & VI_NON_INTERLACE) VIDEO_WaitVSync(); - - DISPLAY.fifoBuffer = memalign(32, DISPLAY_DOLPHIN_FIFO_SIZE); - if(DISPLAY.fifoBuffer == NULL) { - errorThrow("Failed to allocate FIFO buffer"); - } - memoryZero(DISPLAY.fifoBuffer, DISPLAY_DOLPHIN_FIFO_SIZE); - - GX_Init(DISPLAY.fifoBuffer, DISPLAY_DOLPHIN_FIFO_SIZE); - - // This seems to be mostly related to interlacing vs progressive - GX_SetViewport( - 0, 0, - DISPLAY.screenMode->fbWidth, DISPLAY.screenMode->efbHeight, - 0, 1 - ); - float_t yscale = GX_GetYScaleFactor( - DISPLAY.screenMode->efbHeight, DISPLAY.screenMode->xfbHeight - ); - uint32_t xfbHeight = GX_SetDispCopyYScale(yscale); - GX_SetScissor( - 0, 0, - DISPLAY.screenMode->fbWidth, DISPLAY.screenMode->efbHeight - ); - GX_SetDispCopySrc( - 0, 0, - DISPLAY.screenMode->fbWidth, DISPLAY.screenMode->efbHeight - ); - GX_SetDispCopyDst(DISPLAY.screenMode->fbWidth, xfbHeight); - GX_SetCopyFilter( - DISPLAY.screenMode->aa, - DISPLAY.screenMode->sample_pattern, - GX_TRUE, - DISPLAY.screenMode->vfilter - ); - GX_SetFieldMode( - DISPLAY.screenMode->field_rendering, - ( - (DISPLAY.screenMode->viHeight == 2 * DISPLAY.screenMode->xfbHeight) ? - GX_ENABLE : - GX_DISABLE - ) - ); - - GX_SetDispCopyGamma(GX_GM_1_0); - GX_SetColorUpdate(GX_TRUE); - - // Without this, the EFB's Z-buffer format/compression is left at whatever - // GX_Init() defaulted to, so depth testing and the GX_MAX_Z24 clear value - // used every frame in frameBufferClearDolphin aren't guaranteed to line up - // with the actual EFB Z format. - GX_SetPixelFmt(GX_PF_RGB8_Z24, GX_ZC_LINEAR); - - // Describe mesh vertex format. - GX_ClearVtxDesc(); - GX_SetVtxDesc(GX_VA_POS, GX_INDEX16); - GX_SetVtxDesc(GX_VA_TEX0, GX_INDEX16); - GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); - GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); - - errorOk(); -} - -errorret_t displayUpdateDolphin(void) { - ENGINE.running = SYS_MainLoop(); - errorOk(); -} - -errorret_t displaySetStateDolphin(displaystate_t state) { - if(state.flags & DISPLAY_STATE_FLAG_CULL) { - GX_SetCullMode(GX_CULL_FRONT); - } else { - GX_SetCullMode(GX_CULL_NONE); - } - - if(state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) { - GX_SetZMode(GX_TRUE, GX_LEQUAL, GX_FALSE); - } else { - GX_SetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE); - } - - if(state.flags & DISPLAY_STATE_FLAG_BLEND) { - GX_SetBlendMode( - GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR - ); - } else { - GX_SetBlendMode( - GX_BM_NONE, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR - ); - } - - errorOk(); -} - -errorret_t displaySwapDolphin(void) { - GX_DrawDone(); - - DISPLAY.whichFrameBuffer ^= 1; - GX_CopyDisp(DISPLAY.frameBuffer[DISPLAY.whichFrameBuffer], GX_TRUE); - VIDEO_SetNextFramebuffer(DISPLAY.frameBuffer[DISPLAY.whichFrameBuffer]); - VIDEO_Flush(); - VIDEO_WaitVSync(); - - errorOk(); -} \ No newline at end of file diff --git a/src/duskdolphin/display/displaydolphin.h b/src/duskdolphin/display/displaydolphin.h deleted file mode 100644 index 8b992c8b..00000000 --- a/src/duskdolphin/display/displaydolphin.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/displaystate.h" - -#define DISPLAY_DOLPHIN_FIFO_SIZE (256*1024) - -typedef struct { - void *frameBuffer[2]; // Double-Bufferred - int_t whichFrameBuffer; - GXRModeObj *screenMode; - void *fifoBuffer; -} displaydolphin_t; - -/** - * Initializes the display system on Dolphin. - */ -errorret_t displayInitDolphin(void); - -/** - * Tells the display system to actually draw the frame on Dolphin. - */ -errorret_t displayUpdateDolphin(void); - -/** - * Swaps the display buffers on Dolphin. - */ -errorret_t displaySwapDolphin(void); - -/** - * Sets the display state on Dolphin. - */ -errorret_t displaySetStateDolphin(displaystate_t state); \ No newline at end of file diff --git a/src/duskdolphin/display/displayplatform.h b/src/duskdolphin/display/displayplatform.h deleted file mode 100644 index ed9b61a9..00000000 --- a/src/duskdolphin/display/displayplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "displaydolphin.h" - -#define displayPlatformInit displayInitDolphin -#define displayPlatformUpdate displayUpdateDolphin -#define displayPlatformSwap displaySwapDolphin -#define displayPlatformSetState displaySetStateDolphin - -typedef displaydolphin_t displayplatform_t; \ No newline at end of file diff --git a/src/duskdolphin/display/framebuffer/CMakeLists.txt b/src/duskdolphin/display/framebuffer/CMakeLists.txt deleted file mode 100644 index 1f0c0ca1..00000000 --- a/src/duskdolphin/display/framebuffer/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - framebufferdolphin.c -) \ No newline at end of file diff --git a/src/duskdolphin/display/framebuffer/framebufferdolphin.c b/src/duskdolphin/display/framebuffer/framebufferdolphin.c deleted file mode 100644 index 42860c31..00000000 --- a/src/duskdolphin/display/framebuffer/framebufferdolphin.c +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/framebuffer/framebuffer.h" -#include "display/display.h" -#include "assert/assert.h" -#include "system/systemdolphin.h" - -errorret_t frameBufferInitBackBufferDolphin(void) { - errorOk(); -} - -uint32_t frameBufferGetWidthDolphin(const framebufferdolphin_t *framebuffer) { - assertNotNull(framebuffer, "Cannot get width of NULL framebuffer."); - return DISPLAY.screenMode->fbWidth; -} - -uint32_t frameBufferGetHeightDolphin(const framebufferdolphin_t *framebuffer) { - assertNotNull(framebuffer, "Cannot get height of NULL framebuffer."); - return DISPLAY.screenMode->efbHeight; -} - -float_t frameBufferGetAspectDolphin(const framebufferdolphin_t *framebuffer) { - assertNotNull(framebuffer, "Cannot get aspect of NULL framebuffer."); - switch(systemGetAspectRatioDolphin()) { - case CONF_ASPECT_16_9: - return 16.0f / 9.0f; - case CONF_ASPECT_4_3: - return 4.0f / 3.0f; - default: - return ( - (float_t)DISPLAY.screenMode->fbWidth / - (float_t)DISPLAY.screenMode->efbHeight - ); - } -} - -errorret_t frameBufferBindDolphin(framebufferdolphin_t *framebuffer) { - assertNotNull(framebuffer, "Cannot bind NULL framebuffer."); - assertTrue( - framebuffer == &FRAMEBUFFER_BACKBUFFER, - "Cannot bind framebuffer that is not the back buffer." - ); - - GX_InvVtxCache(); - GX_InvalidateTexAll(); - GX_SetZMode(GX_FALSE, GX_ALWAYS, GX_FALSE); - - GX_SetViewport( - 0, 0, - frameBufferGetWidth(framebuffer), - frameBufferGetHeight(framebuffer), - 0, 1 - ); - - errorOk(); -} - -void frameBufferClearDolphin(const uint8_t flags, const color_t color) { - GX_SetCopyClear( - (GXColor){ color.r, color.g, color.b, color.a }, - GX_MAX_Z24 - ); -} \ No newline at end of file diff --git a/src/duskdolphin/display/framebuffer/framebufferdolphin.h b/src/duskdolphin/display/framebuffer/framebufferdolphin.h deleted file mode 100644 index fd6616ec..00000000 --- a/src/duskdolphin/display/framebuffer/framebufferdolphin.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/color.h" -#include "error/error.h" - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - #error "Dolphin does not support dynamic display sizes." -#endif - -typedef struct { - uint8_t nothing; -} framebufferdolphin_t; - -/** - * Initializes the backbuffer framebuffer. (Dolphin implementation). - */ -errorret_t frameBufferInitBackBufferDolphin(void); - -/** - * Gets the height of the framebuffer. (Dolphin implementation). - * - * @param framebuffer The framebuffer to get the height of. - * @return The height of the framebuffer, or 0 if the framebuffer is NULL. - */ -uint32_t frameBufferGetWidthDolphin(const framebufferdolphin_t *framebuffer); - -/** - * Gets the width of the framebuffer. (Dolphin implementation). - * - * @param framebuffer The framebuffer to get the width of. - * @return The width of the framebuffer, or 0 if the framebuffer is NULL. - */ -uint32_t frameBufferGetHeightDolphin(const framebufferdolphin_t *framebuffer); - -/** - * Gets the aspect ratio of the framebuffer. (Dolphin implementation). Taking - * the Wii aspect setting into consideration. - * - * @param framebuffer The framebuffer to get the aspect ratio of. - * @return The aspect ratio of the framebuffer. - */ -float_t frameBufferGetAspectDolphin(const framebufferdolphin_t *framebuffer); - -/** - * Binds the framebuffer for rendering. (Dolphin implementation). - * - * @param framebuffer The framebuffer to bind. - * @return Either error or not. - */ -errorret_t frameBufferBindDolphin(framebufferdolphin_t *framebuffer); - -/** - * Clears the framebuffer with the specified flags and color. - * - * @param flags The clear flags. - * @param color The clear color. - */ -void frameBufferClearDolphin(const uint8_t flags, const color_t color); \ No newline at end of file diff --git a/src/duskdolphin/display/framebuffer/framebufferplatform.h b/src/duskdolphin/display/framebuffer/framebufferplatform.h deleted file mode 100644 index 3e491f8a..00000000 --- a/src/duskdolphin/display/framebuffer/framebufferplatform.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "framebufferdolphin.h" - -typedef framebufferdolphin_t framebufferplatform_t; - -#define frameBufferPlatformInitBackBuffer frameBufferInitBackBufferDolphin -#define frameBufferPlatformGetWidth frameBufferGetWidthDolphin -#define frameBufferPlatformGetHeight frameBufferGetHeightDolphin -#define frameBufferPlatformGetAspect frameBufferGetAspectDolphin -#define frameBufferPlatformBind frameBufferBindDolphin -#define frameBufferPlatformClear frameBufferClearDolphin diff --git a/src/duskdolphin/display/mesh/CMakeLists.txt b/src/duskdolphin/display/mesh/CMakeLists.txt deleted file mode 100644 index 00c265ef..00000000 --- a/src/duskdolphin/display/mesh/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - meshdolphin.c -) \ No newline at end of file diff --git a/src/duskdolphin/display/mesh/meshdolphin.c b/src/duskdolphin/display/mesh/meshdolphin.c deleted file mode 100644 index 95dd3d9b..00000000 --- a/src/duskdolphin/display/mesh/meshdolphin.c +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/mesh/mesh.h" -#include "display/texture/texture.h" -#include "assert/assert.h" -#include "display/shader/shader.h" - -errorret_t meshInitDolphin( - meshdolphin_t *mesh, - const meshprimitivetypedolphin_t primitiveType, - const int32_t vertexCount, - const meshvertex_t *vertices -) { - assertNotNull(mesh, "Mesh cannot be null."); - assertNotNull(vertices, "Vertices cannot be null."); - assertTrue(vertexCount > 0, "Vertex count must be greater than 0."); - - mesh->primitiveType = primitiveType; - mesh->vertexCount = vertexCount; - mesh->vertices = vertices; - - errorOk(); -} - -errorret_t meshDrawDolphin( - const meshdolphin_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -) { - assertNotNull(mesh, "Mesh cannot be NULL."); - assertTrue(vertexOffset >= 0, "Vertex offset must be >= 0"); - assertTrue(vertexCount > 0, "Vertex count must be > 0"); - assertTrue( - vertexOffset + vertexCount <= mesh->vertexCount, - "Requested vertex range is invalid" - ); - assertTrue(vertexCount <= UINT16_MAX, "Vertex count exceeds GX limit"); - - // Matches vertex format described in displaydolphin.c - assertTrue(offsetof(meshvertex_t, uv) == 0, "uv offset wrong"); - assertTrue(offsetof(meshvertex_t, pos) == 8, "pos offset wrong"); - - // Flush vertex data to GPU. This is required before drawing with GX. - DCFlushRange( - (void*)&mesh->vertices[vertexOffset], - sizeof(meshvertex_t) * vertexCount - ); - - // Update matrix. - errorChain(shaderUpdateMVPDolphin()); - - const uint8_t stride = (uint8_t)sizeof(meshvertex_t); - GX_SetArray(GX_VA_POS, (void*)&mesh->vertices[vertexOffset].pos[0], stride); - GX_SetArray(GX_VA_TEX0, (void*)&mesh->vertices[vertexOffset].uv[0], stride); - - GX_InvVtxCache(); - - GX_Begin(mesh->primitiveType, GX_VTXFMT0, (uint16_t)vertexCount); - for(uint16_t i = 0; i < (uint16_t)vertexCount; ++i) { - GX_Position1x16(i); - GX_TexCoord1x16(i); - } - GX_End(); - - errorOk(); -} - -int32_t meshGetVertexCountDolphin(const meshdolphin_t *mesh) { - assertNotNull(mesh, "Mesh cannot be NULL."); - return mesh->vertexCount; -} - -errorret_t meshDisposeDolphin(meshdolphin_t *mesh) { - errorOk(); -} \ No newline at end of file diff --git a/src/duskdolphin/display/mesh/meshdolphin.h b/src/duskdolphin/display/mesh/meshdolphin.h deleted file mode 100644 index ec7e8a32..00000000 --- a/src/duskdolphin/display/mesh/meshdolphin.h +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/mesh/meshvertex.h" -#include "error/error.h" - -typedef enum { - MESH_PRIMITIVE_TYPE_TRIANGLES = GX_TRIANGLES, - MESH_PRIMITIVE_TYPE_LINES = GX_LINES, - MESH_PRIMITIVE_TYPE_POINTS = GX_POINTS, -} meshprimitivetypedolphin_t; - -typedef struct { - const meshvertex_t *vertices; - int32_t vertexCount; - meshprimitivetypedolphin_t primitiveType; -} meshdolphin_t; - -/** - * Initializes a mesh. - * - * @param mesh The mesh to initialize. - * @param primitiveType The Dolphin primitive type (e.g., GX_TRIANGLES). - * @param vertexCount The number of vertices in the mesh. - * @param vertices The vertex data for the mesh. - * @return An error indicating success or failure. - */ -errorret_t meshInitDolphin( - meshdolphin_t *mesh, - const meshprimitivetypedolphin_t primitiveType, - const int32_t vertexCount, - const meshvertex_t *vertices -); - -/** - * Draws a mesh. - * - * @param mesh The mesh to draw. - * @param vertexOffset The offset in the vertex array to start drawing from. - * @param vertexCount The number of vertices to draw. If -1, draws all vertices. - * @return An error indicating success or failure. - */ -errorret_t meshDrawDolphin( - const meshdolphin_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -); - -/** - * Gets the vertex count of a mesh. - * - * @param mesh The mesh to get the vertex count from. - * @return The vertex count of the mesh. - */ -int32_t meshGetVertexCountDolphin(const meshdolphin_t *mesh); - -/** - * Disposes of a mesh, freeing any associated resources. - * - * @param mesh The mesh to dispose of. - * @return An error indicating success or failure. - */ -errorret_t meshDisposeDolphin(meshdolphin_t *mesh); \ No newline at end of file diff --git a/src/duskdolphin/display/mesh/meshplatform.h b/src/duskdolphin/display/mesh/meshplatform.h deleted file mode 100644 index f3fb4c2c..00000000 --- a/src/duskdolphin/display/mesh/meshplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "meshdolphin.h" - -#define meshInitPlatform meshInitDolphin -#define meshDrawPlatform meshDrawDolphin -#define meshGetVertexCountPlatform meshGetVertexCountDolphin -#define meshDisposePlatform meshDisposeDolphin -typedef meshprimitivetypedolphin_t meshprimitivetypeplatform_t; -typedef meshdolphin_t meshplatform_t; \ No newline at end of file diff --git a/src/duskdolphin/display/shader/CMakeLists.txt b/src/duskdolphin/display/shader/CMakeLists.txt deleted file mode 100644 index 2addb51e..00000000 --- a/src/duskdolphin/display/shader/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - shaderdolphin.c - shaderunlitdolphin.c -) \ No newline at end of file diff --git a/src/duskdolphin/display/shader/shaderdolphin.c b/src/duskdolphin/display/shader/shaderdolphin.c deleted file mode 100644 index cc381b02..00000000 --- a/src/duskdolphin/display/shader/shaderdolphin.c +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "util/memory.h" -#include "util/string.h" -#include "display/shader/shaderunlit.h" -#include "assert/assert.h" -#include "log/log.h" - -shaderdolphin_t *SHADER_BOUND; - -errorret_t shaderInitDolphin( - shaderdolphin_t *shader, - const shaderdefinitiondolphin_t *def -) { - assertNotNull(shader, "Shader must not be null"); - assertNotNull(def, "Shader definition must not be null"); - - memoryZero(shader, sizeof(shaderdolphin_t)); - - shader->definition = def; - - glm_mat4_identity(shader->view); - glm_mat4_identity(shader->proj); - glm_mat4_identity(shader->model); - shader->dirtyMatrix = ( - SHADER_DOLPHIN_DIRTY_MODEL | - SHADER_DOLPHIN_DIRTY_PROJ | - SHADER_DOLPHIN_DIRTY_VIEW - ); - - errorOk(); -} - -errorret_t shaderBindDolphin(shaderdolphin_t *shader) { - assertNotNull(shader, "Shader must not be null"); - - SHADER_BOUND = shader; - - - GX_LoadProjectionMtx( - shader->dolphinProj, - shader->isProjectionPerspective ? GX_PERSPECTIVE : GX_ORTHOGRAPHIC - ); - GX_LoadPosMtxImm(shader->dolphinModelView, GX_PNMTX0); - - errorOk(); -} - -errorret_t shaderSetMatrixDolphin( - shaderdolphin_t *shader, - const char_t *name, - mat4 mat -) { - assertNotNull(shader, "Shader must not be null"); - assertNotNull(name, "Uniform name must not be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - - if(stringCompare(name, SHADER_UNLIT_PROJECTION) == 0) { - shader->dirtyMatrix |= SHADER_DOLPHIN_DIRTY_PROJ; - glm_mat4_copy(mat, shader->proj); - - } else if(stringCompare(name, SHADER_UNLIT_VIEW) == 0) { - shader->dirtyMatrix |= SHADER_DOLPHIN_DIRTY_VIEW; - glm_mat4_copy(mat, shader->view); - - } else if(stringCompare(name, SHADER_UNLIT_MODEL) == 0) { - shader->dirtyMatrix |= SHADER_DOLPHIN_DIRTY_MODEL; - glm_mat4_copy(mat, shader->model); - - } else { - assertUnreachable("Cannot use a custom matrix on dolphin."); - } - - errorOk(); -} - -errorret_t shaderSetTextureDolphin( - shaderdolphin_t *shader, - const char_t *name, - texture_t *texture -) { - assertNotNull(shader, "Shader must not be null"); - assertNotNull(name, "Uniform name must not be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - - if(texture == NULL) { - GX_SetNumChans(1); - GX_SetChanCtrl( - GX_COLOR0A0, - GX_DISABLE, - GX_SRC_REG, - GX_SRC_REG, - GX_LIGHTNULL, - GX_DF_NONE, - GX_AF_NONE - ); - GX_SetChanAmbColor(GX_COLOR0A0, (GXColor){0,0,0,0}); - - GX_SetNumTexGens(0); - GX_SetNumTevStages(1); - GX_SetTevOrder(GX_TEVSTAGE0, GX_TEXCOORDNULL, GX_TEXMAP_NULL, GX_COLOR0A0); - GX_SetTevOp(GX_TEVSTAGE0, GX_PASSCLR); - - GX_SetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_CLEAR); - GX_SetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); - - errorOk(); - } - - // Add channel for vertex color - GX_LoadTexObj(&texture->texObj, GX_TEXMAP0); - GX_SetNumChans(1); - GX_SetChanCtrl( - GX_COLOR0A0, - GX_DISABLE, - GX_SRC_REG, - GX_SRC_REG, - GX_LIGHTNULL, - GX_DF_NONE, - GX_AF_NONE - ); - - // One set of UVs - GX_SetNumTexGens(1); - GX_SetTexCoordGen( - GX_TEXCOORD0, - GX_TG_MTX2x4, - GX_TG_TEX0, - GX_IDENTITY - ); - - // Basically the shader setup - switch(texture->format) { - case TEXTURE_FORMAT_RGBA: - // One TEV stage: vertex color * texture color - GX_SetNumTevStages(1); - GX_SetTevOp(GX_TEVSTAGE0, GX_MODULATE); - GX_SetTevOrder( - GX_TEVSTAGE0, - GX_TEXCOORD0, - GX_TEXMAP0, - GX_COLOR0A0 - ); - GX_SetBlendMode( - GX_BM_BLEND, - GX_BL_SRCALPHA, - GX_BL_INVSRCALPHA, - GX_LO_CLEAR - ); - GX_SetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); - break; - - default: - assertUnreachable("Unknown texture format in meshDraw"); - break; - } - - errorOk(); -} - -errorret_t shaderSetColorDolphin( - shaderdolphin_t *shader, - const char_t *name, - color_t color -) { - assertNotNull(shader, "Shader must not be null"); - assertNotNull(name, "Uniform name must not be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - - GX_SetChanMatColor(GX_COLOR0A0, (GXColor){ - color.r, - color.g, - color.b, - color.a - }); - - errorOk(); -} - -errorret_t shaderDisposeDolphin(shaderdolphin_t *shader) { - assertNotNull(shader, "Shader must not be null"); - - SHADER_BOUND = NULL; - - errorOk(); -} - -errorret_t shaderUpdateMVPDolphin() { - assertNotNull(SHADER_BOUND, "Shader must not be null"); - - // Any changes? - if(SHADER_BOUND->dirtyMatrix == 0) errorOk(); - - // Need to update projection? - if((SHADER_BOUND->dirtyMatrix & SHADER_DOLPHIN_DIRTY_PROJ) != 0) { - shaderMat4ToMtx44(SHADER_BOUND->proj, SHADER_BOUND->dolphinProj); - - // Fix projection Z mapping between GLM and GX. - float A = SHADER_BOUND->dolphinProj[2][2]; - float B = SHADER_BOUND->dolphinProj[2][3]; - SHADER_BOUND->dolphinProj[2][2] = 0.5f * (A + 1.0f); - SHADER_BOUND->dolphinProj[2][3] = 0.5f * B; - - // Is this perspective or ortho originally? Dolphin cares for some reason. - const float_t epsilon = 0.0001f; - SHADER_BOUND->isProjectionPerspective = ( - fabsf(SHADER_BOUND->proj[3][2]) > epsilon && - fabsf(SHADER_BOUND->proj[3][3]) < epsilon - ); - - GX_LoadProjectionMtx( - SHADER_BOUND->dolphinProj, - SHADER_BOUND->isProjectionPerspective ? GX_PERSPECTIVE : GX_ORTHOGRAPHIC - ); - } - - // Need to update view or model? - bool_t mvDirt = false; - if((SHADER_BOUND->dirtyMatrix & SHADER_DOLPHIN_DIRTY_VIEW) != 0) { - mvDirt = true; - } - - if((SHADER_BOUND->dirtyMatrix & SHADER_DOLPHIN_DIRTY_MODEL) != 0) { - mvDirt = true; - } - - // Set Model/View Matrix - if(mvDirt) { - glm_mat4_mul( - SHADER_BOUND->view, SHADER_BOUND->model, SHADER_BOUND->modelView - ); - shaderMat4ToMtx(SHADER_BOUND->modelView, SHADER_BOUND->dolphinModelView); - GX_LoadPosMtxImm(SHADER_BOUND->dolphinModelView, GX_PNMTX0); - } - - SHADER_BOUND->dirtyMatrix = 0; - errorOk(); -} - -void shaderMat4ToMtx44(const mat4 inGlmMatrix, Mtx44 outGXMatrix) { - assertNotNull(inGlmMatrix, "Input matrix cannot be null"); - assertNotNull(outGXMatrix, "Output matrix cannot be null"); - - for(int row = 0; row < 4; ++row) { - for(int col = 0; col < 4; ++col) { - outGXMatrix[row][col] = inGlmMatrix[col][row]; - } - } -} - -void shaderMat4ToMtx(const mat4 inGlmMatrix, Mtx outGXMatrix) { - assertNotNull(inGlmMatrix, "Input matrix cannot be null"); - assertNotNull(outGXMatrix, "Output matrix cannot be null"); - - guMtxIdentity(outGXMatrix); - - for(int row = 0; row < 3; ++row) { - for(int col = 0; col < 4; ++col) { - outGXMatrix[row][col] = inGlmMatrix[col][row]; - } - } -} \ No newline at end of file diff --git a/src/duskdolphin/display/shader/shaderdolphin.h b/src/duskdolphin/display/shader/shaderdolphin.h deleted file mode 100644 index b6439435..00000000 --- a/src/duskdolphin/display/shader/shaderdolphin.h +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/texture/texture.h" - -typedef union shadermaterial_u shadermaterial_t; -typedef struct shaderdolphin_s shaderdolphin_t; - -typedef struct { - errorret_t (*setMaterial)( - shaderdolphin_t *shader, - const shadermaterial_t *material - ); -} shaderdefinitiondolphin_t; - -typedef struct shaderdolphin_s { - shaderdefinitiondolphin_t *definition; - - mat4 view; - mat4 proj; - mat4 model; - mat4 modelView; - - Mtx dolphinProj; - Mtx dolphinModelView; - - bool_t isProjectionPerspective; - uint_fast8_t dirtyMatrix; -} shaderdolphin_t; - -#define SHADER_DOLPHIN_DIRTY_MODEL (1 << 0) -#define SHADER_DOLPHIN_DIRTY_PROJ (1 << 1) -#define SHADER_DOLPHIN_DIRTY_VIEW (1 << 2) - -extern shaderdolphin_t *SHADER_BOUND; - -/** - * Initializes a dolphin shader. Basically a render parameter pipeline. - * - * @param shader Shader to initialize. - * @param def Definition of the shader to initialize with. - * @return Error code if failure. - */ -errorret_t shaderInitDolphin( - shaderdolphin_t *shader, - const shaderdefinitiondolphin_t *def -); - -/** - * Binds a dolphin shader. Basically sets the render parameters for rendering. - * - * @param shader Shader to bind. - * @return Error code if failure. - */ -errorret_t shaderBindDolphin(shaderdolphin_t *shader); - -/** - * Sets a matrix uniform in the dolphin shader. Basically does nothing. - * - * @param shader Shader to set the matrix in. - * @param name Name of the uniform to set. - * @param matrix Matrix to set. - * @return Error code if failure. - */ -errorret_t shaderSetMatrixDolphin( - shaderdolphin_t *shader, - const char_t *name, - mat4 matrix -); - -/** - * Sets a texture uniform in the dolphin shader. Basically does nothing. - * - * @param shader Shader to set the texture in. - * @param name Name of the uniform to set. - * @param texture Texture to set. - * @return Error code if failure. - */ -errorret_t shaderSetTextureDolphin( - shaderdolphin_t *shader, - const char_t *name, - texture_t *texture -); - -/** - * Sets a color uniform in the dolphin shader. - * - * @param shader Shader to set the color in. - * @param name Name of the uniform to set. - * @param color Color to set. - * @return Error code if failure. - */ -errorret_t shaderSetColorDolphin( - shaderdolphin_t *shader, - const char_t *name, - color_t color -); - -/** - * Disposes a dolphin shader. Basically does nothing. - * - * @param shader Shader to dispose. - * @return Error code if failure. - */ -errorret_t shaderDisposeDolphin(shaderdolphin_t *shader); - -/** - * Internal Dolphin method, called right before a draw call to perform a matrix - * update and recalc. - * - * @return Error code if failure. - */ -errorret_t shaderUpdateMVPDolphin(); - -/** - * Converts a glm style column major mat4 to a GX compatible row major Mtx44. - * - * @param in Matrix to convert. - * @param out Output converted matrix. - */ -void shaderMat4ToMtx44(const mat4 in, Mtx44 out); - -/** - * Converts a glm style column major mat4 to a GX compatible row major Mtx. - * - * @param inGlmMatrix Matrix to convert. - * @param outGXMatrix Output converted matrix. - */ -void shaderMat4ToMtx(const mat4 inGlmMatrix, Mtx outGXMatrix); \ No newline at end of file diff --git a/src/duskdolphin/display/shader/shaderplatform.h b/src/duskdolphin/display/shader/shaderplatform.h deleted file mode 100644 index 071b84ba..00000000 --- a/src/duskdolphin/display/shader/shaderplatform.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "shaderdolphin.h" - -typedef shaderdolphin_t shaderplatform_t; -typedef shaderdefinitiondolphin_t shaderdefinitionplatform_t; - -#define shaderInitPlatform shaderInitDolphin -#define shaderBindPlatform shaderBindDolphin -#define shaderSetMatrixPlatform shaderSetMatrixDolphin -#define shaderSetTexturePlatform shaderSetTextureDolphin -#define shaderSetColorPlatform shaderSetColorDolphin -#define shaderDisposePlatform shaderDisposeDolphin \ No newline at end of file diff --git a/src/duskdolphin/display/shader/shaderunlitdolphin.c b/src/duskdolphin/display/shader/shaderunlitdolphin.c deleted file mode 100644 index c0e854ac..00000000 --- a/src/duskdolphin/display/shader/shaderunlitdolphin.c +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/shader/shaderunlit.h" - -shaderdefinition_t SHADER_UNLIT_DEFINITION = { - .setMaterial = shaderUnlitSetMaterial -}; \ No newline at end of file diff --git a/src/duskdolphin/display/texture/CMakeLists.txt b/src/duskdolphin/display/texture/CMakeLists.txt deleted file mode 100644 index 3315ce6f..00000000 --- a/src/duskdolphin/display/texture/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - texturedolphin.c -) \ No newline at end of file diff --git a/src/duskdolphin/display/texture/texturedolphin.c b/src/duskdolphin/display/texture/texturedolphin.c deleted file mode 100644 index 1d8e8f17..00000000 --- a/src/duskdolphin/display/texture/texturedolphin.c +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/texture/texture.h" -#include "assert/assert.h" -#include "util/memory.h" - -errorret_t textureInitDolphin( - texturedolphin_t *texture, - const int32_t width, - const int32_t height, - const textureformatdolphin_t format, - const texturedata_t data -) { - switch(format) { - case TEXTURE_FORMAT_RGBA: { - size_t rgbaSize = width * height * sizeof(uint8_t) * 4; - - // Dolphin takes the RGBA data as 4x4 tiled layout. - texture->rgba = memoryAllocate(rgbaSize); - - for(uint32_t y = 0; y < height; ++y) { - for(uint32_t x = 0; x < width; ++x) { - const int_t src = y * width + x; - - const int_t tileX = x >> 2; - const int_t tileY = y >> 2; - const int_t tilesPerRow = width >> 2; - const int_t tileIndex = tileY * tilesPerRow + tileX; - const int_t inTile = ((y & 3) << 2) + (x & 3); - const int_t tileBase = tileIndex * 64; - - color_t col = data.rgbaColors[src]; - - // AR plane - texture->rgba[tileBase + inTile * 2 + 0] = col.a; - texture->rgba[tileBase + inTile * 2 + 1] = col.r; - - // GB plane - texture->rgba[tileBase + 32 + inTile * 2 + 0] = col.g; - texture->rgba[tileBase + 32 + inTile * 2 + 1] = col.b; - } - } - DCFlushRange(texture->rgba, rgbaSize); - GX_InitTexObj( - &texture->texObj, - texture->rgba, - width, height, - format, - GX_REPEAT, GX_REPEAT, - GX_FALSE - ); - - DCFlushRange(texture->rgba, rgbaSize); - GX_InvalidateTexAll(); - - GX_InitTexObjLOD( - &texture->texObj, - GX_NEAR, GX_NEAR, - 0.0f, 0.0f, 0.0f, - GX_FALSE, - GX_FALSE, - GX_ANISO_1 - ); - break; - } - - case TEXTURE_FORMAT_PALETTE: { - assertUnreachable("Paletted textures not yet implemented for Dolphin"); - break; - } - - default: { - assertUnreachable("Unsupported texture format for Dolphin"); - break; - } - } - - errorOk(); -} - - -errorret_t textureDisposeDolphin(texturedolphin_t *texture) { - switch(texture->format) { - case TEXTURE_FORMAT_RGBA: { - if(texture->rgba) { - memoryFree(texture->rgba); - texture->rgba = NULL; - } - break; - } - - // case TEXTURE_FORMAT_RGB4A3: { - // assertUnreachable("RGB4A3 texture format not yet implemented"); - // } - - // case TEXTURE_FORMAT_RGB5: { - // assertUnreachable("RGB5 texture format not yet implemented"); - // } - - // case TEXTURE_FORMAT_PALETTE: { - // assertUnreachable("Paletted textures not yet implemented for Dolphin"); - // break; - // } - - default: { - assertUnreachable("Unsupported texture format for Dolphin"); - break; - } - } - errorOk(); -} \ No newline at end of file diff --git a/src/duskdolphin/display/texture/texturedolphin.h b/src/duskdolphin/display/texture/texturedolphin.h deleted file mode 100644 index 88fa095d..00000000 --- a/src/duskdolphin/display/texture/texturedolphin.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef union texturedata_u texturedata_t; - -typedef enum { - TEXTURE_FORMAT_RGBA = GX_TF_RGBA8, - TEXTURE_FORMAT_PALETTE = GX_TF_CI8, - // TEXTURE_FORMAT_RGB4A3 = GX_TF_RGB5A3, - // TEXTURE_FORMAT_RGB5 = GX_TF_RGB565, -} textureformatdolphin_t; - -typedef struct { - GXTexObj texObj; - textureformatdolphin_t format; - int32_t width; - int32_t height; - - union { - uint8_t *rgba; - // u16 *rgba; - }; -} texturedolphin_t; - -/** - * Initializes a texture. - * - * @param texture The texture to initialize. - * @param width The width of the texture. - * @param height The height of the texture. - * @param format The format of the texture (e.g., GX_TF_RGBA8, GX_TF_CI8). - * @param data The data for the texture, the format changes per format. - * @return An error if the texture failed to initialize, otherwise success. - */ -errorret_t textureInitDolphin( - texturedolphin_t *texture, - const int32_t width, - const int32_t height, - const textureformatdolphin_t format, - const texturedata_t data -); - -/** - * Binds a texture for rendering. Providing NULL will unbind any texture. - * - * @param texture The texture to bind. - * @return An error if the texture failed to bind, otherwise success. - */ -errorret_t textureBindDolphin(texturedolphin_t *texture); - -/** - * Disposes a texture. - * - * @param texture The texture to dispose. - * @return An error if the texture failed to dispose, otherwise success. - */ -errorret_t textureDisposeDolphin(texturedolphin_t *texture); \ No newline at end of file diff --git a/src/duskdolphin/display/texture/textureplatform.h b/src/duskdolphin/display/texture/textureplatform.h deleted file mode 100644 index 33585f7a..00000000 --- a/src/duskdolphin/display/texture/textureplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "texturedolphin.h" - -typedef textureformatdolphin_t textureformatplatform_t; -typedef texturedolphin_t textureplatform_t; - -#define textureInitPlatform textureInitDolphin -#define textureBindPlatform textureBindDolphin -#define textureDisposePlatform textureDisposeDolphin \ No newline at end of file diff --git a/src/duskdolphin/duskplatform.h b/src/duskdolphin/duskplatform.h deleted file mode 100644 index 48b85832..00000000 --- a/src/duskdolphin/duskplatform.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -#include -#include -#include - -#define consoleInit consoleInitDolhpin - -#ifdef DUSK_GAMECUBE - #define CONF_ASPECT_4_3 0 - #define CONF_ASPECT_16_9 1 - #define CONF_GetAspectRatio() CONF_ASPECT_4_3 - - #define CONF_GetLanguage() SYS_GetLanguage() -#endif \ No newline at end of file diff --git a/src/duskdolphin/input/CMakeLists.txt b/src/duskdolphin/input/CMakeLists.txt deleted file mode 100644 index b88e182e..00000000 --- a/src/duskdolphin/input/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - inputdolphin.c -) - -# Subdirs \ No newline at end of file diff --git a/src/duskdolphin/input/inputdolphin.c b/src/duskdolphin/input/inputdolphin.c deleted file mode 100644 index c9fb4869..00000000 --- a/src/duskdolphin/input/inputdolphin.c +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "input/input.h" -#include "assert/assert.h" -#include "log/log.h" -#include "util/string.h" - -inputbuttondata_t INPUT_BUTTON_DATA[] = { - #ifdef DUSK_INPUT_GAMEPAD - { .name = "a", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_A } }, - { .name = "b", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_B } }, - { .name = "x", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_X } }, - { .name = "y", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_Y } }, - { .name = "start", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_START } }, - { .name = "up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_UP } }, - { .name = "down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_DOWN } }, - { .name = "left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_LEFT } }, - { .name = "right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_RIGHT } }, - { .name = "l", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_TRIGGER_L } }, - { .name = "r", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_TRIGGER_R } }, - { .name = "z", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_TRIGGER_Z } }, - { .name = "menu", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = PAD_BUTTON_MENU } }, - - { .name = "lstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_LEFT_Y, .positive = true } } }, - { .name = "lstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_LEFT_Y, .positive = false } } }, - { .name = "lstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_LEFT_X, .positive = false } } }, - { .name = "lstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_LEFT_X, .positive = true } } }, - - { .name = "rstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_C_X, .positive = true } } }, - { .name = "rstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_C_X, .positive = false } } }, - { .name = "rstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_C_Y, .positive = true } } }, - { .name = "rstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_C_Y, .positive = false } } }, - { .name = "lstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = INPUT_GAMEPAD_AXIS_LEFT_Y, .positive = false } } }, - { .name = "ltrigger", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = INPUT_GAMEPAD_AXIS_TRIGGER_LEFT, - .positive = true } } }, - { .name = "rtrigger", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = INPUT_GAMEPAD_AXIS_TRIGGER_RIGHT, - .positive = true } } }, - #endif - - { .name = NULL } -}; - -errorret_t inputInitDolphin(void) { - PAD_Init(); - - #define X(buttonName, buttonAction) \ - inputBind(inputButtonGetByName(buttonName), buttonAction); - - #if defined(DUSK_GAMECUBE) - X("up", INPUT_ACTION_UP); - X("down", INPUT_ACTION_DOWN); - X("left", INPUT_ACTION_LEFT); - X("right", INPUT_ACTION_RIGHT); - X("lstick_up", INPUT_ACTION_UP); - X("lstick_down", INPUT_ACTION_DOWN); - X("lstick_left", INPUT_ACTION_LEFT); - X("lstick_right", INPUT_ACTION_RIGHT); - - X("a", INPUT_ACTION_ACCEPT); - X("b", INPUT_ACTION_CANCEL); - X("z", INPUT_ACTION_CONSOLE); - X("start", INPUT_ACTION_RAGEQUIT); - X("start", INPUT_ACTION_PAUSE); - - #elif defined(DUSK_WII) - X("up", INPUT_ACTION_UP); - X("down", INPUT_ACTION_DOWN); - X("left", INPUT_ACTION_LEFT); - X("right", INPUT_ACTION_RIGHT); - X("lstick_up", INPUT_ACTION_UP); - X("lstick_down", INPUT_ACTION_DOWN); - X("lstick_left", INPUT_ACTION_LEFT); - X("lstick_right", INPUT_ACTION_RIGHT); - - X("a", INPUT_ACTION_ACCEPT); - X("b", INPUT_ACTION_CANCEL); - X("z", INPUT_ACTION_CONSOLE); - X("start", INPUT_ACTION_RAGEQUIT); - X("start", INPUT_ACTION_PAUSE); - - // TODO: Wiimote, USB Keyboard, probably more. - - #else - #error "Unknown Dolphin platform?" - #endif - - #undef X - - errorOk(); -} - -void inputUpdateDolphin(void) { - PAD_ScanPads(); - - #ifdef DUSK_INPUT_GAMEPAD - for(uint8_t i = 0; i < INPUT_DOLPHIN_PAD_COUNT; i++) { - INPUT.platform.padState[i] = PAD_ButtonsHeld(i); - - INPUT.platform.pads[i][INPUT_GAMEPAD_AXIS_LEFT_X] = PAD_StickX(i); - INPUT.platform.pads[i][INPUT_GAMEPAD_AXIS_LEFT_Y] = PAD_StickY(i); - INPUT.platform.pads[i][INPUT_GAMEPAD_AXIS_C_X] = PAD_SubStickX(i); - INPUT.platform.pads[i][INPUT_GAMEPAD_AXIS_C_Y] = PAD_SubStickY(i); - INPUT.platform.pads[i][INPUT_GAMEPAD_AXIS_TRIGGER_LEFT]=PAD_TriggerL(i); - INPUT.platform.pads[i][INPUT_GAMEPAD_AXIS_TRIGGER_RIGHT]=PAD_TriggerR(i); - // Because gamecube sticks are hexagons this is going to be weird. - } - #endif -} - -float_t inputButtonGetValueDolphin(const inputbutton_t button) { - switch(button.type) { - #ifdef DUSK_INPUT_GAMEPAD - case INPUT_BUTTON_TYPE_GAMEPAD: { - if(INPUT.platform.padState[0] & button.gpButton) return 1.0f; - return 0.0f; - } - - case INPUT_BUTTON_TYPE_GAMEPAD_AXIS: { - float_t axis = INPUT.platform.pads[0][button.gpAxis.axis]; - float_t value = axis / 128.0f; - if(!button.gpAxis.positive) value = -value; - value = inputDeadzone(value, inputGetDeadzoneDolphin(button)); - return value; - } - #endif - - default: { - assertUnreachable("Unknown input button type"); - return 0.0f; - } - } -} - -float_t inputGetDeadzoneDolphin(const inputbutton_t button) { - return 0.2f; -} \ No newline at end of file diff --git a/src/duskdolphin/input/inputdolphin.h b/src/duskdolphin/input/inputdolphin.h deleted file mode 100644 index 2f130051..00000000 --- a/src/duskdolphin/input/inputdolphin.h +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "error/error.h" - -#define INPUT_DOLPHIN_PAD_COUNT PAD_CHANMAX -#define INPUT_DOLPHIN_AXIS(value) ((float_t)(value) / 128.0f) - -typedef struct inputbutton_s inputbutton_t; - -#ifdef DUSK_INPUT_POINTER - #error "Wii not implemented" -#endif - -#ifdef DUSK_INPUT_KEYBOARD - #error "Keyboard not implemented" -#endif - -#ifdef DUSK_INPUT_GAMEPAD - // TODO: Can I support wiimote, gamecube, pro controller, etc? - - typedef u16 inputgamepadbuttondolphin_t; - typedef enum { - INPUT_GAMEPAD_AXIS_LEFT_X, - INPUT_GAMEPAD_AXIS_LEFT_Y, - INPUT_GAMEPAD_AXIS_C_X, - INPUT_GAMEPAD_AXIS_C_Y, - INPUT_GAMEPAD_AXIS_TRIGGER_LEFT, - INPUT_GAMEPAD_AXIS_TRIGGER_RIGHT, - - INPUT_GAMEPAD_AXIS_COUNT - } inputgamepadaxisdolphin_t; -#endif - -typedef struct { - #ifdef DUSK_INPUT_GAMEPAD - int padState[INPUT_DOLPHIN_PAD_COUNT]; - float_t pads[INPUT_DOLPHIN_PAD_COUNT][INPUT_GAMEPAD_AXIS_COUNT]; - #endif -} inputdolphin_t; - -/** - * Initializes the input system for Dolphin. - * - * @return An error code if initialization fails. - */ -errorret_t inputInitDolphin(void); - -/** - * Updates the input state for Dolphin. - */ -void inputUpdateDolphin(void); - -/** - * Returns the input value (between 0 and 1) of the given button. - * - * @param button The button to get the value of. - * @return The value of the button, between 0 and 1. - */ -float_t inputButtonGetValueDolphin(const inputbutton_t button); - -/** - * Returns the deadzone for the given button. - * - * @param button The button to get the deadzone of. - * @return The deadzone for the button, between 0 and 1. - */ -float_t inputGetDeadzoneDolphin(const inputbutton_t button); \ No newline at end of file diff --git a/src/duskdolphin/input/inputplatform.h b/src/duskdolphin/input/inputplatform.h deleted file mode 100644 index 4be4317d..00000000 --- a/src/duskdolphin/input/inputplatform.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "inputdolphin.h" - -#ifdef DUSK_INPUT_GAMEPAD - typedef inputgamepadbuttondolphin_t inputgamepadbuttonplatform_t; - typedef inputgamepadaxisdolphin_t inputgamepadaxissplatform_t; -#endif - -typedef inputdolphin_t inputplatform_t; - -#define inputInitPlatform inputInitDolphin -#define inputUpdatePlatform inputUpdateDolphin -#define inputButtonGetValuePlatform inputButtonGetValueDolphin \ No newline at end of file diff --git a/src/duskdolphin/log/CMakeLists.txt b/src/duskdolphin/log/CMakeLists.txt deleted file mode 100644 index e6535a76..00000000 --- a/src/duskdolphin/log/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - log.c -) \ No newline at end of file diff --git a/src/duskdolphin/log/log.c b/src/duskdolphin/log/log.c deleted file mode 100644 index 7cd7b7e1..00000000 --- a/src/duskdolphin/log/log.c +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "log/log.h" -#include "log/logdolphin.h" -#include "display/display.h" -#include -#include - -#if DUSK_DOLPHIN_BUILD_TYPE == DOL - #include - #include - - #define LOG_DEBUG_PATH "/apps/Dusk/debug.log" - #define LOG_ERROR_PATH "/apps/Dusk/error.log" - - static bool_t fatTried = false; - static bool_t fatReady = false; - - void logInitFAT(void) { - if(fatTried) return; - fatTried = true; - fatReady = fatInitDefault(); - } -#endif - -void logDebug(const char_t *message, ...) { - va_list args; - va_start(args, message); - - // Print to stdout - va_list copy; - va_copy(copy, args); - vfprintf(stdout, message, copy); - va_end(copy); - fflush(stdout); - - #if DUSK_DOLPHIN_BUILD_TYPE == DOL - // Print to file - logInitFAT(); - if(fatReady) { - FILE *file = fopen(LOG_DEBUG_PATH, "a"); - if(file) { - va_copy(copy, args); - vfprintf(file, message, copy); - va_end(copy); - fclose(file); - } - } - #endif - - va_end(args); -} - -void logError(const char_t *message, ...) { - va_list args; - va_start(args, message); - - // Write to file before displaying on screen - #if DUSK_DOLPHIN_BUILD_TYPE == DOL - logInitFAT(); - if(fatReady) { - FILE *file = fopen(LOG_ERROR_PATH, "a"); - if(file) { - va_list copy; - va_copy(copy, args); - vfprintf(file, message, copy); - va_end(copy); - fclose(file); - } - } - #endif - - // Either create graphics, or hijack the displays' graphics. - GXRModeObj *rmode = NULL; - void *framebuffer; - - if(DISPLAY.frameBuffer[0]) { - console_init( - DISPLAY.frameBuffer[0], - 20, - 20, - DISPLAY.screenMode->fbWidth, - DISPLAY.screenMode->xfbHeight, - DISPLAY.screenMode->fbWidth * VI_DISPLAY_PIX_SZ - ); - } else { - VIDEO_Init(); - rmode = VIDEO_GetPreferredMode(NULL); - framebuffer = MEM_K0_TO_K1(SYS_AllocateFramebuffer(rmode)); - console_init( - framebuffer, - 20, - 20, - rmode->fbWidth, - rmode->xfbHeight, - rmode->fbWidth*VI_DISPLAY_PIX_SZ - ); - VIDEO_Configure(rmode); - VIDEO_SetNextFramebuffer(framebuffer); - VIDEO_SetBlack(FALSE); - VIDEO_Flush(); - VIDEO_WaitVSync(); - if(rmode->viTVMode&VI_NON_INTERLACE) VIDEO_WaitVSync(); - } - - // Printf - va_list copy; - va_copy(copy, args); - vprintf(message, copy); - va_end(copy); - va_end(args); - - // PAD_Init is idempotent - safe even if inputInit already called it, - // and handles the case where the error occurred before inputInit ran. - PAD_Init(); - while(SYS_MainLoop()) { - PAD_ScanPads(); - // START button matches the RAGEQUIT bind - allows exit on GC controller - // when there is no Wiimote HOME button available. - if(PAD_ButtonsDown(0) & PAD_BUTTON_START) break; - VIDEO_WaitVSync(); - } - - // Exit cleanly to HBC regardless of engine state. engineDispose() is not - // called here because the engine may be partially initialized. - exit(0); -} \ No newline at end of file diff --git a/src/duskdolphin/log/logdolphin.h b/src/duskdolphin/log/logdolphin.h deleted file mode 100644 index ba820ba4..00000000 --- a/src/duskdolphin/log/logdolphin.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#if DUSK_DOLPHIN_BUILD_TYPE == DOL - /** - * Lazily mounts the FAT filesystem (via fatInitDefault) the first time - * it's needed, so logDebug/logError can write to a file. Safe to call - * repeatedly - only the first call actually attempts the mount. - */ - void logInitFAT(void); -#endif diff --git a/src/duskdolphin/network/CMakeLists.txt b/src/duskdolphin/network/CMakeLists.txt deleted file mode 100644 index e514c3b9..00000000 --- a/src/duskdolphin/network/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - networkdolphin.c -) diff --git a/src/duskdolphin/network/networkdolphin.c b/src/duskdolphin/network/networkdolphin.c deleted file mode 100644 index 580031fb..00000000 --- a/src/duskdolphin/network/networkdolphin.c +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "network/network.h" -#include "util/memory.h" -#include "assert/assert.h" - -errorret_t networkDolphinInit() { - // s32 ret = net_init(); - // if(ret < 0) errorThrow("Failed to init network stack: %d", ret); - errorOk(); -} - -errorret_t networkDolphinUpdate() { - errorOk(); -} - -errorret_t networkDolphinDispose() { - // #ifdef DUSK_WII - // net_deinit(); - // #endif - errorOk(); -} - -bool_t networkDolphinIsConnected() { - return NETWORK.state == NETWORK_STATE_CONNECTED; -} - -void networkDolphinRequestConnection( - void (*onConnected)(void *user), - void (*onFailed)(errorret_t error, void *user), - void (*onDisconnect)(errorret_t error, void *user), - void *user -) { - assertTrue( - NETWORK.state == NETWORK_STATE_CONNECTING, - "Network host should be in a connecting state." - ); - - NETWORK.platform.onConnected = onConnected; - NETWORK.platform.onFailed = onFailed; - NETWORK.platform.onConnectedUser = user; - - memoryZero(NETWORK.platform.ip, sizeof(NETWORK.platform.ip)); - memoryZero(NETWORK.platform.netmask, sizeof(NETWORK.platform.netmask)); - memoryZero(NETWORK.platform.gateway, sizeof(NETWORK.platform.gateway)); - - // Negotiate DHCP using the Wi-Fi settings saved in Wii System Menu. - // This call blocks until the interface is configured or times out. - #ifdef DUSK_WII - s32 ret = if_config( - NETWORK.platform.ip, - NETWORK.platform.netmask, - NETWORK.platform.gateway, - true - ); - #else - s32 ret = -1; - #endif - - if(ret >= 0) { - NETWORK.state = NETWORK_STATE_CONNECTED; - assertNotNull( - NETWORK.platform.onConnected, - "Network platform onConnected callback should be set." - ); - NETWORK.platform.onConnected(NETWORK.platform.onConnectedUser); - } else { - NETWORK.state = NETWORK_STATE_DISCONNECTED; - assertNotNull( - NETWORK.platform.onFailed, - "Network platform onFailed callback should be set." - ); - errorret_t error = errorThrowImpl( - &NETWORK.errorState, - ERROR_NOT_OK, - __FILE__, __func__, __LINE__, - "Failed to connect to network" - ); - NETWORK.platform.onFailed(error, NETWORK.platform.onConnectedUser); - } -} - -void networkDolphinRequestDisconnection( - void (*onComplete)(void *user), - void *user -) { - assertTrue( - NETWORK.state == NETWORK_STATE_DISCONNECTING, - "Network host should be in a disconnecting state." - ); - - NETWORK.state = NETWORK_STATE_DISCONNECTED; - onComplete(user); -} - -networkinfo_t networkDolphinGetInfo() { - networkinfo_t info; - memoryZero(&info, sizeof(networkinfo_t)); - - info.type = NETWORK_TYPE_IPV4; - int_t ret = sscanf( - NETWORK.platform.ip, - "%hhu.%hhu.%hhu.%hhu", - &info.ipv4.ip[0], - &info.ipv4.ip[1], - &info.ipv4.ip[2], - &info.ipv4.ip[3] - ); - assertTrue(ret == 4, "Failed to parse IP address"); - - return info; -} \ No newline at end of file diff --git a/src/duskdolphin/network/networkdolphin.h b/src/duskdolphin/network/networkdolphin.h deleted file mode 100644 index 993219a0..00000000 --- a/src/duskdolphin/network/networkdolphin.h +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "network/networkinfo.h" -#include - -#define NETWORK_DOLPHIN_IP_MAX 16 - -typedef struct { - char_t ip[NETWORK_DOLPHIN_IP_MAX]; - char_t netmask[NETWORK_DOLPHIN_IP_MAX]; - char_t gateway[NETWORK_DOLPHIN_IP_MAX]; - - void *onConnectedUser; - void (*onConnected)(void *user); - void (*onFailed)(errorret_t error, void *user); -} networkdolphin_t; - -/** - * Initializes the Wii network stack via IOS. Does not connect; use - * networkDolphinRequestConnection for that. - * - * @return Error state (if any). - */ -errorret_t networkDolphinInit(); - -/** - * Called each frame. No-op on Wii since connection is synchronous. - * - * @return Error state (if any). - */ -errorret_t networkDolphinUpdate(); - -/** - * Disposes the Wii network stack. - * - * @return Error state (if any). - */ -errorret_t networkDolphinDispose(); - -/** - * Returns true if the Wii is connected to a network. - * - * @return True if connected. - */ -bool_t networkDolphinIsConnected(); - -/** - * Requests the Wii to connect to the network using the Wi-Fi settings saved - * in the Wii System Menu. Blocks until connected or failed. - * - * @param onConnected Callback on successful connection. - * @param onFailed Callback if connection fails. - * @param onDisconnect Callback when connection is later lost. - * @param user User data passed to all callbacks. - */ -void networkDolphinRequestConnection( - void (*onConnected)(void *user), - void (*onFailed)(errorret_t error, void *user), - void (*onDisconnect)(errorret_t error, void *user), - void *user -); - -/** - * Requests the Wii to disconnect from the network. - * - * @param onComplete Callback when disconnection is complete. - * @param user User data passed to the callback. - */ -void networkDolphinRequestDisconnection( - void (*onComplete)(void *user), - void *user -); - -/** - * Returns the current network IP information. - * - * @return Network info for the active connection. - */ -networkinfo_t networkDolphinGetInfo(); diff --git a/src/duskdolphin/network/networkplatform.h b/src/duskdolphin/network/networkplatform.h deleted file mode 100644 index c46b13fe..00000000 --- a/src/duskdolphin/network/networkplatform.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "networkdolphin.h" - -#define networkPlatformInit networkDolphinInit -#define networkPlatformUpdate networkDolphinUpdate -#define networkPlatformDispose networkDolphinDispose -#define networkPlatformIsConnected networkDolphinIsConnected -#define networkPlatformRequestConnection networkDolphinRequestConnection -#define networkPlatformRequestDisconnection networkDolphinRequestDisconnection -#define networkPlatformGetInfo networkDolphinGetInfo - -typedef networkdolphin_t networkplatform_t; diff --git a/src/duskdolphin/save/CMakeLists.txt b/src/duskdolphin/save/CMakeLists.txt deleted file mode 100644 index 4b7de25d..00000000 --- a/src/duskdolphin/save/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Only one save device implementation is ever compiled in - it must match -# whichever one savedeviceplatform.h dispatches to. In particular, the NAND -# (ISFS) device can't even compile on GameCube - libogc's isfs.h declares -# nothing at all outside of Wii (HW_RVL) builds. -if(DUSK_TARGET_SYSTEM STREQUAL "wii" AND DUSK_SAVE_WII_METHOD STREQUAL "NAND") - target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - savedevicedolphinnand.c - ) -elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" AND DUSK_SAVE_WII_METHOD STREQUAL "SD") - target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - savedevicedolphinsd.c - ) -else() - # GameCube, or Wii with DUSK_SAVE_WII_METHOD=CARD. - target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - savedevicedolphincard.c - ) -endif() diff --git a/src/duskdolphin/save/savedevicedolphincard.c b/src/duskdolphin/save/savedevicedolphincard.c deleted file mode 100644 index ce102d9e..00000000 --- a/src/duskdolphin/save/savedevicedolphincard.c +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/savedevice.h" -#include "save/savedevicedolphincard.h" -#include "save/save.h" -#include "assert/assert.h" -#include "util/memory.h" - -errorret_t saveDeviceDolphinCardInit(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - - device->state = SAVE_DEVICE_STATE_UNKNOWN; - - // Each savedevice_t in SAVE.devices maps to one physical memory - // card slot - device 0 is slot A, device 1 is slot B, determined by this - // device's position in that array (the same index math save.c - // itself uses to identify a device). - uint8_t index = (uint8_t)(device - &SAVE.devices[0]); - device->platform.channel = index == 0 ? CARD_SLOTA : CARD_SLOTB; - - errorOk(); -} - -errorret_t saveDeviceDolphinCardUpdate(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - errorOk(); -} - -void saveDeviceDolphinCardCheckAvailability(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - assertTrue( - device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, - "Device state incorrect?" - ); - - savedeviceplatform_t *platform = &device->platform; - - // CARD_Init sets up card_inited, the per-channel control blocks (wait - // queues, alarms) CARD_Mount reads, and initializes the DSP (needed for - // the card unlock sequence) - safe to call more than once, and cheap - // enough to just always call before every mount attempt. - int32_t result = CARD_Init(SAVE_DEVICE_DOLPHIN_GAME_CODE, NULL); - if(result < 0) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.dolphin.card_init_failed"; - return saveDeviceFireCallback(device); - } - - // Slot A gets polled automatically by the IPL at boot, but slot B does - // not - CARD_Mount() alone reliably reports CARD_ERROR_NOCARD for a - // present slot B card unless CARD_Probe() is used to explicitly detect - // it first (matches the probe-then-mount pattern in every official - // devkitPro CARD sample, not something slot A happens to need too). - do { - result = CARD_Probe(platform->channel); - } while(result == CARD_ERROR_BUSY); - - if(result < 0) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = saveDeviceDolphinCardErrorReasonKey(result); - return saveDeviceFireCallback(device); - } - - do { - result = CARD_Mount(platform->channel, platform->cardBuffer, NULL); - } while(result == CARD_ERROR_BUSY); - - if(result < 0) { - platform->mounted = false; - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = saveDeviceDolphinCardErrorReasonKey(result); - return saveDeviceFireCallback(device); - } - platform->mounted = true; - - if(!saveDeviceDolphinCardHasFreeSpace(platform->channel)) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.dolphin.card_full"; - return saveDeviceFireCallback(device); - } - - device->state = SAVE_DEVICE_STATE_AVAILABLE; - device->reasonKey = NULL; - saveDeviceFireCallback(device); -} - -const char_t *saveDeviceDolphinCardErrorReasonKey(const int32_t result) { - switch(result) { - case CARD_ERROR_NOCARD: return "save.dolphin.no_card"; - case CARD_ERROR_WRONGDEVICE: return "save.dolphin.wrong_device"; - case CARD_ERROR_BROKEN: return "save.dolphin.broken"; - default: return "save.dolphin.mount_failed"; - } -} - -bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) { - uint16_t blockCount = 0; - if(CARD_GetBlockCount(channel, &blockCount) < 0) return false; - - uint32_t sectorSize = 0; - if(CARD_GetSectorSize(channel, §orSize) < 0 || sectorSize == 0) { - return false; - } - - // showall=true - free space is a card-wide resource, not just what this - // game itself has already written. - card_dir entries[CARD_MAXFILES]; - int32_t count = 0; - int32_t dirResult = CARD_GetDirectory(channel, entries, &count, true); - // A totally empty card (no files at all yet) reports CARD_ERROR_NOFILE - // here rather than success with count 0 - not a real error, and by far - // the most common state for a card nobody has saved to yet. - if(dirResult < 0 && dirResult != CARD_ERROR_NOFILE) return false; - if(dirResult == CARD_ERROR_NOFILE) count = 0; - if(count >= CARD_MAXFILES) return false;// No free file entry left either. - - uint32_t usedBlocks = 0; - for(int32_t i = 0; i < count; i++) { - usedBlocks += (entries[i].filelen + sectorSize - 1) / sectorSize; - } - - return usedBlocks < (uint32_t)blockCount; -} - -savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot( - const int32_t channel, - const char_t *filename, - const bool_t keepBuffer -) { - savedevicedolphincardslot_t result = {0}; - - card_file file; - if(CARD_Open(channel, filename, &file) < 0) return result; - - uint32_t len = (uint32_t)file.len; - uint8_t *buffer = memoryAlign(32, len); - int32_t readResult = CARD_Read(&file, buffer, len, 0); - CARD_Close(&file); - - if(readResult < 0 || !saveDeviceRawIsValid(buffer, len, &result.generation)) { - memoryFree(buffer); - return result; - } - - result.valid = true; - result.len = len; - if(keepBuffer) { - result.buffer = buffer; - } else { - memoryFree(buffer); - } - return result; -} - -errorret_t saveDeviceDolphinCardDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(buffer, "buffer cannot be null"); - - int32_t channel = device->platform.channel; - - savedevicedolphincardslot_t slot0 = saveDeviceDolphinCardProbeSlot( - channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0, false - ); - savedevicedolphincardslot_t slot1 = saveDeviceDolphinCardProbeSlot( - channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1, false - ); - - // Target whichever slot is NOT the currently valid+newest one, so a crash - // partway through this write can never touch the copy a load would use. - const char_t *targetFilename; - if(slot0.valid && slot1.valid) { - targetFilename = slot0.generation <= slot1.generation ? - SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 : SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1; - } else if(slot0.valid) { - targetFilename = SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1; - } else { - targetFilename = SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0; - } - - uint32_t sectorSize = 0; - if(CARD_GetSectorSize(channel, §orSize) < 0 || sectorSize == 0) { - errorThrow("Failed to query memory card sector size"); - } - - // CARD_Create's size must be a sector-size multiple, and CARD_Read always - // reports that full (possibly padded) size back via file.len - harmless, - // since saveDeviceRawIsValid() tolerates trailing padding beyond the - // header's declared payload length. - uint32_t paddedSize = - (uint32_t)(((size + sectorSize - 1) / sectorSize) * sectorSize); - uint8_t *paddedBuffer = memoryAlign(32, paddedSize); - memorySet(paddedBuffer, 0, paddedSize); - memoryCopy(paddedBuffer, buffer, size); - - // The target file may already exist from an earlier generation at a - // different size - memory card files can't be resized, so it has to be - // deleted and recreated. This only ever touches the stale/inactive slot, - // never the one saveDeviceDolphinCardDataRead would currently pick. - CARD_Delete(channel, targetFilename);// Not an error if it doesn't exist. - - card_file file; - int32_t createResult = CARD_Create( - channel, targetFilename, paddedSize, &file - ); - if(createResult < 0) { - memoryFree(paddedBuffer); - errorThrow("Failed to create save data file: %s", targetFilename); - } - - int32_t writeResult = CARD_Write(&file, paddedBuffer, paddedSize, 0); - CARD_Close(&file); - memoryFree(paddedBuffer); - if(writeResult < 0) { - errorThrow("Failed to write save data: %s", targetFilename); - } - - errorOk(); -} - -errorret_t saveDeviceDolphinCardDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(outBuffer, "outBuffer cannot be null"); - assertNotNull(outSize, "outSize cannot be null"); - - int32_t channel = device->platform.channel; - - savedevicedolphincardslot_t slot0 = saveDeviceDolphinCardProbeSlot( - channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0, true - ); - savedevicedolphincardslot_t slot1 = saveDeviceDolphinCardProbeSlot( - channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1, true - ); - - savedevicedolphincardslot_t *chosen = NULL; - if(slot0.valid && slot1.valid) { - chosen = slot0.generation >= slot1.generation ? &slot0 : &slot1; - } else if(slot0.valid) { - chosen = &slot0; - } else if(slot1.valid) { - chosen = &slot1; - } - - if(chosen == NULL) { - // Neither slot has ever been saved to (or both failed validation) - - // not an error, leave the destination as-is. - *outBuffer = NULL; - *outSize = 0; - errorOk(); - } - - if(chosen != &slot0 && slot0.buffer != NULL) memoryFree(slot0.buffer); - if(chosen != &slot1 && slot1.buffer != NULL) memoryFree(slot1.buffer); - - *outBuffer = chosen->buffer; - *outSize = chosen->len; - errorOk(); -} - -errorret_t saveDeviceDolphinCardDispose(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - if(device->platform.mounted) { - CARD_Unmount(device->platform.channel); - device->platform.mounted = false; - } - errorOk(); -} diff --git a/src/duskdolphin/save/savedevicedolphincard.h b/src/duskdolphin/save/savedevicedolphincard.h deleted file mode 100644 index 889d279a..00000000 --- a/src/duskdolphin/save/savedevicedolphincard.h +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include - -// GameCube always uses this device (slot A + slot B). On Wii it's also used -// when DUSK_SAVE_WII_METHOD_CARD is selected - libogc's CARD API -// transparently redirects to Wii's NAND-based GameCube memory card -// emulation, so the exact same code path works on both. -#ifndef SAVE_DEVICE_DOLPHIN_GAME_CODE - #define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK" -#endif - -// Memory cards have no rename primitive and files can't be resized once -// created, so a single "current" file can't be replaced atomically the way -// the other backends do. Instead saveDeviceDolphinCardDataWrite() ping-pongs -// between these two fixed files - every store targets whichever one is NOT -// the currently valid+newest copy (by saveDeviceRawIsValid()'s generation -// counter), so a crash mid-write can only ever corrupt the copy nobody -// would load from; the other one stays intact as the fallback. -#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 - #define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 "DUSKSAVE0" -#endif -#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1 - #define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1 "DUSKSAVE1" -#endif - -typedef struct { - // Set from this device's index into SAVE.devices - device 0 is - // CARD_SLOTA, device 1 is CARD_SLOTB. - int32_t channel; - uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32))); - bool_t mounted; -} savedeviceplatform_t; - -typedef struct savedevice_s savedevice_t; - -/** - * Initializes the save device platform. - * - * @param device The save device platform to initialize. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinCardInit(savedevice_t *device); - -/** - * Updates the save device platform. - * - * @param device The save device platform to update. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinCardUpdate(savedevice_t *device); - -/** - * Requests the device to check its availability, this will call the callback - * whence completed. Mounts the memory card in this device's slot and - * confirms there's at least one free block and one free file entry left to - * write a save into. - * - * @param device The save device platform to check availability. - */ -void saveDeviceDolphinCardCheckAvailability(savedevice_t *device); - -/** - * Maps a libogc CARD_ERROR_* result (from CARD_Probe or CARD_Mount) to the - * matching reasonKey. - * - * @param result The card_errors result code to map. - * @return The matching reasonKey, or a generic fallback if unrecognized. - */ -const char_t *saveDeviceDolphinCardErrorReasonKey(const int32_t result); - -/** - * Returns whether the mounted memory card in the given slot has at least - * one block of free space and one free file entry remaining, by summing - * the block usage of every file already on the card (CARD_GetDirectory) - * against the card's total block count (CARD_GetBlockCount) - libogc - * doesn't expose a single "free blocks remaining" call. - * - * @param channel The CARD slot (CARD_SLOTA or CARD_SLOTB) to check. Must - * already be mounted. - * @return True if there's room to create at least one more save file. - */ -bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel); - -/** - * Disposes of the save device platform. - * - * @param device The save device platform to dispose. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinCardDispose(savedevice_t *device); - -// One probe result for one of the two ping-pong slots. -typedef struct { - bool_t valid; - uint32_t generation; - uint8_t *buffer;// only set if keepBuffer was requested, else NULL - uint32_t len; -} savedevicedolphincardslot_t; - -/** - * Opens the given card file (if it exists) and validates it as a raw save - * blob via saveDeviceRawIsValid(). CARD_Read requires a 32-byte aligned - * buffer, so the read (and any buffer kept for the caller) uses memoryAlign - * rather than memoryAllocate. If keepBuffer is false, or the file doesn't - * exist/validate, no buffer is left allocated. - * - * @param channel The CARD slot (CARD_SLOTA or CARD_SLOTB) to probe. - * @param filename The card file name to open. - * @param keepBuffer Whether to keep the read buffer allocated on success. - * @return The probe result - valid is false if the file doesn't exist or - * doesn't validate as a raw save blob. - */ -savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot( - const int32_t channel, - const char_t *filename, - const bool_t keepBuffer -); - -/** - * Writes the combined save data blob out to whichever of the two fixed - * card files (see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0/1 above) is not - * currently the valid+newest copy - ping-pong in place of the atomic - * rename the other backends use, since the CARD API has neither a rename - * nor a way to resize an existing file. - * - * @param device The save device to write to. - * @param buffer The raw bytes to write. - * @param size The number of bytes to write. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinCardDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -); - -/** - * Reads whichever of the two fixed card files is valid and has the higher - * generation counter. If neither file exists or validates, this is not an - * error - *outBuffer is set to NULL and *outSize to 0. - * - * @param device The save device to read from. - * @param outBuffer Receives a memoryAlign'd (32-byte) buffer the caller - * must free with memoryFree, or NULL if nothing has been saved yet. - * @param outSize Receives the number of bytes in *outBuffer. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinCardDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -); diff --git a/src/duskdolphin/save/savedevicedolphinnand.c b/src/duskdolphin/save/savedevicedolphinnand.c deleted file mode 100644 index 52e0a42a..00000000 --- a/src/duskdolphin/save/savedevicedolphinnand.c +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/savedevice.h" -#include "save/savedevicedolphinnand.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" - -#define SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX 64 - -errorret_t saveDeviceDolphinNandInit(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - - device->state = SAVE_DEVICE_STATE_UNKNOWN; - - errorOk(); -} - -errorret_t saveDeviceDolphinNandUpdate(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - errorOk(); -} - -void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - assertTrue( - device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, - "Device state incorrect?" - ); - - int32_t result = ISFS_Initialize(); - if(result != ISFS_OK) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.dolphin.nand_init_failed"; - return saveDeviceFireCallback(device); - } - device->platform.initialized = true; - - // Best-effort create - libogc's isfs.h doesn't expose a named "already - // exists" result, so the real confirmation is the ISFS_GetUsage() call - // below rather than this return value. - ISFS_CreateDir( - SAVE_DEVICE_DOLPHIN_NAND_DIR, 0, ISFS_OPEN_RW, ISFS_OPEN_RW, ISFS_OPEN_RW - ); - - uint32_t usedBlocks = 0; - uint32_t usedInodes = 0; - result = ISFS_GetUsage( - SAVE_DEVICE_DOLPHIN_NAND_DIR, &usedBlocks, &usedInodes - ); - if(result != ISFS_OK) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.dolphin.nand_access_denied"; - return saveDeviceFireCallback(device); - } - - device->state = SAVE_DEVICE_STATE_AVAILABLE; - device->reasonKey = NULL; - saveDeviceFireCallback(device); -} - -errorret_t saveDeviceDolphinNandGetDataPath( - char_t *dest, - const size_t destSize -) { - assertNotNull(dest, "dest cannot be null"); - - stringFormat( - dest, destSize, "%s/%s", - SAVE_DEVICE_DOLPHIN_NAND_DIR, SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME - ); - - errorOk(); -} - -errorret_t saveDeviceDolphinNandDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(buffer, "buffer cannot be null"); - - char_t finalPath[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX]; - errorChain(saveDeviceDolphinNandGetDataPath(finalPath, sizeof(finalPath))); - - char_t tempPath[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX]; - stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); - - // Best-effort create - unlike POSIX's O_CREAT, ISFS_Open can't create a - // file that doesn't exist yet, so ISFS_CreateFile has to run first. Best - // effort since a leftover temp file from a previous crash is fine to - // clobber. - ISFS_Delete(tempPath); - int32_t createResult = ISFS_CreateFile( - tempPath, 0, ISFS_OPEN_RW, ISFS_OPEN_RW, ISFS_OPEN_RW - ); - if(createResult != ISFS_OK) { - errorThrow("Failed to create save data file: %s", tempPath); - } - - int32_t fd = ISFS_Open(tempPath, ISFS_OPEN_WRITE); - if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath); - - int32_t written = ISFS_Write(fd, buffer, (u32)size); - ISFS_Close(fd); - if(written < 0 || (size_t)written != size) { - errorThrow("Failed to write save data: %s", tempPath); - } - - // Atomic replace: the temp file is fully written before it ever takes the - // final name, so a crash/power-loss here can never leave a half-written - // save behind - at worst the previous save survives untouched. - ISFS_Delete(finalPath);// Not an error if it doesn't exist yet. - if(ISFS_Rename(tempPath, finalPath) != ISFS_OK) { - errorThrow("Failed to finalize save data: %s", finalPath); - } - - errorOk(); -} - -errorret_t saveDeviceDolphinNandDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(outBuffer, "outBuffer cannot be null"); - assertNotNull(outSize, "outSize cannot be null"); - - char_t path[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX]; - errorChain(saveDeviceDolphinNandGetDataPath(path, sizeof(path))); - - int32_t fd = ISFS_Open(path, ISFS_OPEN_READ); - if(fd < 0) { - // No save yet - not an error. - *outBuffer = NULL; - *outSize = 0; - errorOk(); - } - - fstats stats; - if(ISFS_GetFileStats(fd, &stats) != ISFS_OK) { - ISFS_Close(fd); - errorThrow("Failed to stat save data: %s", path); - } - - size_t size = (size_t)stats.file_length; - uint8_t *buffer = memoryAllocate(size); - - int32_t readBytes = ISFS_Read(fd, buffer, (u32)size); - ISFS_Close(fd); - if(readBytes < 0 || (size_t)readBytes != size) { - memoryFree(buffer); - errorThrow("Failed to read save data: %s", path); - } - - *outBuffer = buffer; - *outSize = size; - errorOk(); -} - -errorret_t saveDeviceDolphinNandDispose(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - if(device->platform.initialized) { - ISFS_Deinitialize(); - device->platform.initialized = false; - } - errorOk(); -} diff --git a/src/duskdolphin/save/savedevicedolphinnand.h b/src/duskdolphin/save/savedevicedolphinnand.h deleted file mode 100644 index aea2c572..00000000 --- a/src/duskdolphin/save/savedevicedolphinnand.h +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include - -// A private namespace under /title on the Wii's internal NAND, used purely -// as ISFS storage - not a real installed title/ticket. Only reachable -// because the Homebrew Channel's grants full hardware access, -// which also disables IOS's normal ES rights checking for the running app; -// this hasn't been confirmed against real hardware yet (see project memory -// for the caveat - Dolphin's NAND emulation may not reproduce the same -// rights-check behavior as a real Wii/vWii). -#ifndef SAVE_DEVICE_DOLPHIN_NAND_DIR - #define SAVE_DEVICE_DOLPHIN_NAND_DIR "/title/00010001/44555348/data" -#endif -#ifndef SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME - #define SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME "save.dat" -#endif - -typedef struct { - bool_t initialized; -} savedeviceplatform_t; - -typedef struct savedevice_s savedevice_t; - -/** - * Initializes the save device platform. - * - * @param device The save device platform to initialize. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinNandInit(savedevice_t *device); - -/** - * Updates the save device platform. - * - * @param device The save device platform to update. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinNandUpdate(savedevice_t *device); - -/** - * Requests the device to check its availability, this will call the callback - * whence completed. Brings up ISFS and confirms our private save directory - * exists (creating it on first run). - * - * @param device The save device platform to check availability. - */ -void saveDeviceDolphinNandCheckAvailability(savedevice_t *device); - -/** - * Disposes of the save device platform. - * - * @param device The save device platform to dispose. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinNandDispose(savedevice_t *device); - -/** - * Builds the absolute path to the combined save data file. - * - * @param dest The destination buffer to write the path into. - * @param destSize The size of the destination buffer, exc. null terminator. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinNandGetDataPath( - char_t *dest, - const size_t destSize -); - -/** - * Writes the combined save data blob out to the NAND, replacing it - * atomically (write to a temp file, then swap it in via ISFS_Rename) so a - * crash mid-write can never leave a half-written file behind. - * - * @param device The save device to write to. - * @param buffer The raw bytes to write. - * @param size The number of bytes to write. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinNandDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -); - -/** - * Reads the combined save data blob in from the NAND. If no save data - * exists yet, this is not an error - *outBuffer is set to NULL and - * *outSize to 0. - * - * @param device The save device to read from. - * @param outBuffer Receives a memoryAllocate'd buffer the caller must free - * with memoryFree, or NULL if nothing has been saved yet. - * @param outSize Receives the number of bytes in *outBuffer. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinNandDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -); diff --git a/src/duskdolphin/save/savedevicedolphinsd.c b/src/duskdolphin/save/savedevicedolphinsd.c deleted file mode 100644 index 32074889..00000000 --- a/src/duskdolphin/save/savedevicedolphinsd.c +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/savedevice.h" -#include "save/savedevicedolphinsd.h" -#include "asset/asset.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include -#include -#include - -#define SAVE_DEVICE_DOLPHIN_SD_PATH_MAX 128 - -errorret_t saveDeviceDolphinSDInit(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - - device->state = SAVE_DEVICE_STATE_UNKNOWN; - - errorOk(); -} - -errorret_t saveDeviceDolphinSDUpdate(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - errorOk(); -} - -void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - assertTrue( - device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, - "Device state incorrect?" - ); - - // Safe to call more than once - returns true if already mounted. No - // directory to create here - ASSET.baseDirectory is already known to - // exist, since dusk.dsk was just successfully opened from it. - if(!fatInitDefault()) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.dolphin.sd_fat_init_failed"; - return saveDeviceFireCallback(device); - } - - device->state = SAVE_DEVICE_STATE_AVAILABLE; - device->reasonKey = NULL; - saveDeviceFireCallback(device); -} - -errorret_t saveDeviceDolphinSDGetDataPath( - char_t *dest, - const size_t destSize -) { - assertNotNull(dest, "dest cannot be null"); - - stringFormat( - dest, destSize, "%s/%s", - ASSET.baseDirectory, SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME - ); - - errorOk(); -} - -errorret_t saveDeviceDolphinSDDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(buffer, "buffer cannot be null"); - - char_t finalPath[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX]; - errorChain(saveDeviceDolphinSDGetDataPath(finalPath, sizeof(finalPath))); - - char_t tempPath[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX]; - stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); - - FILE *file = fopen(tempPath, "wb"); - if(file == NULL) { - errorThrow("Failed to open save data for writing: %s", tempPath); - } - - size_t written = fwrite(buffer, 1, size, file); - fclose(file); - if(written != size) errorThrow("Failed to write save data: %s", tempPath); - - // Atomic replace: the temp file is fully written before it ever takes the - // final name, so a crash/power-loss here can never leave a half-written - // save behind - at worst the previous save survives untouched. - remove(finalPath);// Not an error if it doesn't exist yet. - if(rename(tempPath, finalPath) != 0) { - errorThrow("Failed to finalize save data: %s", finalPath); - } - - errorOk(); -} - -errorret_t saveDeviceDolphinSDDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(outBuffer, "outBuffer cannot be null"); - assertNotNull(outSize, "outSize cannot be null"); - - char_t path[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX]; - errorChain(saveDeviceDolphinSDGetDataPath(path, sizeof(path))); - - struct stat st; - if(stat(path, &st) != 0) { - // No save yet - not an error. - *outBuffer = NULL; - *outSize = 0; - errorOk(); - } - - size_t size = (size_t)st.st_size; - uint8_t *buffer = memoryAllocate(size); - - FILE *file = fopen(path, "rb"); - if(file == NULL) { - memoryFree(buffer); - errorThrow("Failed to open save data for reading: %s", path); - } - - size_t readBytes = fread(buffer, 1, size, file); - fclose(file); - if(readBytes != size) { - memoryFree(buffer); - errorThrow("Failed to read save data: %s", path); - } - - *outBuffer = buffer; - *outSize = size; - errorOk(); -} - -errorret_t saveDeviceDolphinSDDispose(savedevice_t *device) { - errorOk(); -} diff --git a/src/duskdolphin/save/savedevicedolphinsd.h b/src/duskdolphin/save/savedevicedolphinsd.h deleted file mode 100644 index bc6622eb..00000000 --- a/src/duskdolphin/save/savedevicedolphinsd.h +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -// Data file lives directly in ASSET.baseDirectory (the same directory -// dusk.dsk was found in - see its own comment) rather than a separate -// fixed directory, so it doesn't need creating: that directory is already -// known to exist, since dusk.dsk was just successfully opened from it. -#ifndef SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME - #define SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME "save.dat" -#endif - -typedef struct { - void *nothing; -} savedeviceplatform_t; - -typedef struct savedevice_s savedevice_t; - -/** - * Initializes the save device platform. - * - * @param device The save device platform to initialize. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinSDInit(savedevice_t *device); - -/** - * Updates the save device platform. - * - * @param device The save device platform to update. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinSDUpdate(savedevice_t *device); - -/** - * Requests the device to check its availability, this will call the callback - * whence completed. Initializes libfat - the save directory itself is - * ASSET.baseDirectory, already known to exist. - * - * @param device The save device platform to check availability. - */ -void saveDeviceDolphinSDCheckAvailability(savedevice_t *device); - -/** - * Disposes of the save device platform. - * - * @param device The save device platform to dispose. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinSDDispose(savedevice_t *device); - -/** - * Builds the absolute path to the combined save data file. - * - * @param dest The destination buffer to write the path into. - * @param destSize The size of the destination buffer, exc. null terminator. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinSDGetDataPath(char_t *dest, const size_t destSize); - -/** - * Writes the combined save data blob out to the SD card, replacing it - * atomically (write to a temp file, then swap it in) so a crash mid-write - * can never leave a half-written file behind. - * - * @param device The save device to write to. - * @param buffer The raw bytes to write. - * @param size The number of bytes to write. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinSDDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -); - -/** - * Reads the combined save data blob in from the SD card. If no save data - * exists yet, this is not an error - *outBuffer is set to NULL and - * *outSize to 0. - * - * @param device The save device to read from. - * @param outBuffer Receives a memoryAllocate'd buffer the caller must free - * with memoryFree, or NULL if nothing has been saved yet. - * @param outSize Receives the number of bytes in *outBuffer. - * @return Error state if any. - */ -errorret_t saveDeviceDolphinSDDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -); diff --git a/src/duskdolphin/save/savedeviceplatform.h b/src/duskdolphin/save/savedeviceplatform.h deleted file mode 100644 index 857f2d9d..00000000 --- a/src/duskdolphin/save/savedeviceplatform.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -// GameCube only ever has the CARD_SLOTA/CARD_SLOTB memory card devices. -// Wii additionally picks between three storage methods, selected by -// DUSK_SAVE_WII_METHOD in cmake/targets/wii.cmake: -// - NAND (default): real internal storage via ISFS, see -// savedevicedolphinnand.h for the caveats around this. -// - CARD: reuses the exact same GameCube memory card code, since libogc's -// CARD API transparently redirects to Wii's NAND-based GameCube memory -// card emulation. -// - SD: the SD card via libfat, same mechanism this project already uses -// for Dolphin/Wii asset loading (see assetdolphinfat.c). -#if defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_NAND) - #include "savedevicedolphinnand.h" - #define SAVE_DEVICE_COUNT 1 - #define saveDevicePlatformInit saveDeviceDolphinNandInit - #define saveDevicePlatformUpdate saveDeviceDolphinNandUpdate - #define saveDeviceCheckAvailabilityPlatform \ - saveDeviceDolphinNandCheckAvailability - #define saveDevicePlatformDispose saveDeviceDolphinNandDispose - - // ISFS_Rename exists, so NAND uses the same write-temp-then-rename - // pattern as PSP/Linux - see SAVE_DEVICE_DATA_RAW in save/savedevice.h. - #define SAVE_DEVICE_DATA_RAW - #define saveDeviceDataWritePlatform saveDeviceDolphinNandDataWrite - #define saveDeviceDataReadPlatform saveDeviceDolphinNandDataRead -#elif defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_SD) - #include "savedevicedolphinsd.h" - #define SAVE_DEVICE_COUNT 1 - #define saveDevicePlatformInit saveDeviceDolphinSDInit - #define saveDevicePlatformUpdate saveDeviceDolphinSDUpdate - #define saveDeviceCheckAvailabilityPlatform \ - saveDeviceDolphinSDCheckAvailability - #define saveDevicePlatformDispose saveDeviceDolphinSDDispose - - // libfat gives standard POSIX rename(), so SD uses the same - // write-temp-then-rename pattern as PSP/Linux - see SAVE_DEVICE_DATA_RAW - // in save/savedevice.h. - #define SAVE_DEVICE_DATA_RAW - #define saveDeviceDataWritePlatform saveDeviceDolphinSDDataWrite - #define saveDeviceDataReadPlatform saveDeviceDolphinSDDataRead -#else - #include "savedevicedolphincard.h" - #define SAVE_DEVICE_COUNT 2 - #define saveDevicePlatformInit saveDeviceDolphinCardInit - #define saveDevicePlatformUpdate saveDeviceDolphinCardUpdate - #define saveDeviceCheckAvailabilityPlatform \ - saveDeviceDolphinCardCheckAvailability - #define saveDevicePlatformDispose saveDeviceDolphinCardDispose - - // No rename or resize primitive on the CARD API, so this ping-pongs - // between two fixed files instead - see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 - // in savedevicedolphincard.h and SAVE_DEVICE_DATA_RAW in save/savedevice.h. - #define SAVE_DEVICE_DATA_RAW - #define saveDeviceDataWritePlatform saveDeviceDolphinCardDataWrite - #define saveDeviceDataReadPlatform saveDeviceDolphinCardDataRead -#endif diff --git a/src/duskdolphin/script/module/moduleplatformdolphin.h b/src/duskdolphin/script/module/moduleplatformdolphin.h deleted file mode 100644 index 6a19105c..00000000 --- a/src/duskdolphin/script/module/moduleplatformdolphin.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "script/module/modulebase.h" - -static void modulePlatformDolphin(void) { - moduleBaseEval("var DOLPHIN = true;\n"); -} diff --git a/src/duskdolphin/script/module/moduleplatformplatform.h b/src/duskdolphin/script/module/moduleplatformplatform.h deleted file mode 100644 index b02e99e4..00000000 --- a/src/duskdolphin/script/module/moduleplatformplatform.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "moduleplatformdolphin.h" - -#define modulePlatformPlatform modulePlatformDolphin \ No newline at end of file diff --git a/src/duskdolphin/system/CMakeLists.txt b/src/duskdolphin/system/CMakeLists.txt deleted file mode 100644 index 679b705d..00000000 --- a/src/duskdolphin/system/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - systemdolphin.c -) diff --git a/src/duskdolphin/system/systemdolphin.c b/src/duskdolphin/system/systemdolphin.c deleted file mode 100644 index 429a5ade..00000000 --- a/src/duskdolphin/system/systemdolphin.c +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "systemdolphin.h" -#include "input/input.h" -#include "util/string.h" -#include "assert/assert.h" -#include "console/console.h" - -errorret_t systemInitDolphin(void) { - // Routes newlib stdout/stderr (printf, and this project's logDebug/ - // logError) through OSReport, which Dolphin's logger captures under the - // OSREPORT category - without this, guest console output goes nowhere. - SYS_STDIO_Report(true); - - char_t *names[] = { - "4:3", - "16:9", - "Unknown" - }; - - int32_t ratio = systemGetAspectRatioDolphin(); - uint8_t i = ( - ratio == CONF_ASPECT_4_3 ? 0 : - ratio == CONF_ASPECT_16_9 ? 1 : - 2 - ); - consolePrint("Dolphin Aspect Ratio: %s", names[i]); - - errorOk(); -} - -systemdialogtype_t systemGetActiveDialogTypeDolphin(void) { - return SYSTEM_DIALOG_TYPE_NONE; -} - -int32_t systemGetAspectRatioDolphin(void) { - return CONF_GetAspectRatio(); -} - -int32_t systemGetLanguageDolphin(void) { - return CONF_GetLanguage(); -} - -const localeinfo_t * systemGetLocaleDolphin(void) { - switch(systemGetLanguageDolphin()) { - case SYS_LANG_JAPANESE: return &LOCALE_INFO_JP_JP; - case SYS_LANG_SPANISH: return &LOCALE_INFO_ES_MX; - default: return &LOCALE_DEFAULT; - } -} \ No newline at end of file diff --git a/src/duskdolphin/system/systemdolphin.h b/src/duskdolphin/system/systemdolphin.h deleted file mode 100644 index c6fa29b1..00000000 --- a/src/duskdolphin/system/systemdolphin.h +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "system/system.h" - -/** - * Initializes the Dolphin system module. - * - * @return Error code indicating success or failure. - */ -errorret_t systemInitDolphin(void); - -/** - * Returns which Dolphin system dialog is currently open (if any). - * - * @return Currently open system dialog type. - */ -systemdialogtype_t systemGetActiveDialogTypeDolphin(void); - -/** - * Returns either CONF_ASPECT_4_3 or CONF_ASPECT_16_9 depending on the aspect - * ratio of the system. I do believe that Gamecube will only ever return 4:3. - * - * Refer to; - * https://github.com/devkitPro/libogc/blob/ - * 20d90e944b83c8991538e88b00b1e5f309428e85/gc/ogc/conf.h#L190 - * - * @return Aspect ratio of the system. - */ -int32_t systemGetAspectRatioDolphin(void); - -/** - * Returns the language the system is set to. This is used for things like - * locale management, to try to match the system language if possible. - * - * Refer to; - * https://github.com/devkitPro/libogc/blob/ - * 20d90e944b83c8991538e88b00b1e5f309428e85/gc/ogc/conf.h#L190 - * - * On gamecube, refer to; - * https://libogc.devkitpro.org/system_8h.html - * - * @return System language. - */ -int32_t systemGetLanguageDolphin(void); - -/** - * Returns the locale matching the system's configured language, falling - * back to LOCALE_DEFAULT if the system language has no matching locale. - * - * @return The current locale. - */ -const localeinfo_t * systemGetLocaleDolphin(void); - -// There's actually a tonne more things Wii can return, this is it for now -// though. \ No newline at end of file diff --git a/src/duskdolphin/system/systemplatform.h b/src/duskdolphin/system/systemplatform.h deleted file mode 100644 index dda9116d..00000000 --- a/src/duskdolphin/system/systemplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "system/systemdolphin.h" - -#define systemInitPlatform systemInitDolphin -#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeDolphin -#define systemGetLocalePlatform systemGetLocaleDolphin \ No newline at end of file diff --git a/src/duskdolphin/time/CMakeLists.txt b/src/duskdolphin/time/CMakeLists.txt deleted file mode 100644 index 8a69ff22..00000000 --- a/src/duskdolphin/time/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - timedolphin.c -) diff --git a/src/duskdolphin/time/timedolphin.c b/src/duskdolphin/time/timedolphin.c deleted file mode 100644 index bd7318d1..00000000 --- a/src/duskdolphin/time/timedolphin.c +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "time/timedolphin.h" - -#define SYS_Time __SYS_GetSystemTime -#define PPCTicksToUs ticks_to_microsecs - -double_t timeGetRealDolphin(void) { - // "Returns time in ticks since 2000" - u64 timeInUs = PPCTicksToUs(SYS_Time()); - double_t timeSeconds = (double_t)timeInUs / 1000000.0; - - // Time to adjust time from 2000 to 1970, in seconds - double_t timeOffset = 946684800.0; - - timeSeconds += timeOffset; - return timeSeconds; -} - -double_t timeGetRealTimeZoneDolphin(void) { - return 0.0; -} \ No newline at end of file diff --git a/src/duskdolphin/time/timedolphin.h b/src/duskdolphin/time/timedolphin.h deleted file mode 100644 index 1fde9079..00000000 --- a/src/duskdolphin/time/timedolphin.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Returns the real current time, in seconds since January 1, 1970. - * - * @return The real current time, in seconds since January 1, 1970. - */ -double_t timeGetRealDolphin(void); - -/** - * Returns the real time zone offset in seconds from UTC. - * - * @return The real time zone offset in seconds from UTC. - */ -double_t timeGetRealTimeZoneDolphin(void); \ No newline at end of file diff --git a/src/duskdolphin/time/timeplatform.h b/src/duskdolphin/time/timeplatform.h deleted file mode 100644 index d5ad4939..00000000 --- a/src/duskdolphin/time/timeplatform.h +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "time/timedolphin.h" - -#define timeGetRealPlatform timeGetRealDolphin -#define timeGetRealTimeZonePlatform timeGetRealTimeZoneDolphin \ No newline at end of file diff --git a/src/duskgl/CMakeLists.txt b/src/duskgl/CMakeLists.txt deleted file mode 100644 index d7391b81..00000000 --- a/src/duskgl/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Subdirs -add_subdirectory(display) -add_subdirectory(error) \ No newline at end of file diff --git a/src/duskgl/assert/assertgl.h b/src/duskgl/assert/assertgl.h deleted file mode 100644 index db80b5d4..00000000 --- a/src/duskgl/assert/assertgl.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "assert/assert.h" -#include "error/errorgl.h" - -#define assertNoGLError(message) \ - assertTrue(errorIsOk(errorGLCheck()), message) - -// EOF \ No newline at end of file diff --git a/src/duskgl/display/CMakeLists.txt b/src/duskgl/display/CMakeLists.txt deleted file mode 100644 index cdeee63e..00000000 --- a/src/duskgl/display/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# 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 - displaygl.c -) - -# Subdirs -add_subdirectory(framebuffer) -add_subdirectory(texture) -add_subdirectory(mesh) -add_subdirectory(shader) \ No newline at end of file diff --git a/src/duskgl/display/displaygl.c b/src/duskgl/display/displaygl.c deleted file mode 100644 index d3120c09..00000000 --- a/src/duskgl/display/displaygl.c +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "displaygl.h" - -errorret_t displayOpenGLInit(void) { - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - errorChain(errorGLCheck()); - - #if DUSK_OPENGL_LEGACY - glDisable(GL_LIGHTING);// PSP defaults this on? - errorChain(errorGLCheck()); - glShadeModel(GL_SMOOTH); // Fixes color on PSP? - errorChain(errorGLCheck()); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - errorChain(errorGLCheck()); - glEnableClientState(GL_VERTEX_ARRAY); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} \ No newline at end of file diff --git a/src/duskgl/display/displaygl.h b/src/duskgl/display/displaygl.h deleted file mode 100644 index 2944f227..00000000 --- a/src/duskgl/display/displaygl.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/errorgl.h" - -/** - * Initializes the OpenGL specific contexts for rendering. - * - * @return An errorret_t indicating success or failure of the initialization. - */ -errorret_t displayOpenGLInit(void); \ No newline at end of file diff --git a/src/duskgl/display/framebuffer/CMakeLists.txt b/src/duskgl/display/framebuffer/CMakeLists.txt deleted file mode 100644 index d04ebbb8..00000000 --- a/src/duskgl/display/framebuffer/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - framebuffergl.c -) \ No newline at end of file diff --git a/src/duskgl/display/framebuffer/framebuffergl.c b/src/duskgl/display/framebuffer/framebuffergl.c deleted file mode 100644 index 95eecfa3..00000000 --- a/src/duskgl/display/framebuffer/framebuffergl.c +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/display.h" -#include "display/framebuffer/framebuffer.h" -#include "assert/assertgl.h" -#include "util/memory.h" - -errorret_t frameBufferGLInitBackBuffer(void) { - errorOk(); -} - -uint32_t frameBufferGLGetWidth(const framebuffer_t *framebuffer) { - if(framebuffer == NULL) { - return 0; - } - - if(framebuffer == &FRAMEBUFFER_BACKBUFFER) { - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - int32_t windowWidth, windowHeight; - SDL_GetWindowSize(DISPLAY.window, &windowWidth, &windowHeight); - return windowWidth; - #else - return DUSK_DISPLAY_WIDTH; - #endif - } - - return framebuffer->texture.width; -} - -uint32_t frameBufferGLGetHeight(const framebuffer_t *framebuffer) { - if(framebuffer == NULL) { - return 0; - } - - if(framebuffer == &FRAMEBUFFER_BACKBUFFER) { - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - int32_t windowWidth, windowHeight; - SDL_GetWindowSize(DISPLAY.window, &windowWidth, &windowHeight); - return windowHeight; - #else - return DUSK_DISPLAY_HEIGHT; - #endif - } - - return framebuffer->texture.height; -} - -errorret_t frameBufferGLBind(framebuffer_t *framebuffer) { - assertNotNull(framebuffer, "Framebuffer cannot be NULL"); - - #ifdef DUSK_DISPLAY_SIZE_DYNAMIC - if(framebuffer == &FRAMEBUFFER_BACKBUFFER) { - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); - } else { - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer->id); - } - - glViewport( - 0, 0, - frameBufferGetWidth(framebuffer), frameBufferGetHeight(framebuffer) - ); - #else - glViewport( - 0, 0, - DUSK_DISPLAY_WIDTH, DUSK_DISPLAY_HEIGHT - ); - #endif - - errorChain(errorGLCheck()); - errorOk(); -} - -void frameBufferGLClear(const uint8_t flags, const color_t color) { - GLbitfield glFlags = 0; - - if(flags & FRAMEBUFFER_CLEAR_COLOR) { - glFlags |= GL_COLOR_BUFFER_BIT; - glClearColor( - color.r / 255.0f, - color.g / 255.0f, - color.b / 255.0f, - color.a / 255.0f - ); - assertNoGLError("Failed to set clear color"); - } - - if(flags & FRAMEBUFFER_CLEAR_DEPTH) { - glFlags |= GL_DEPTH_BUFFER_BIT; - } - - glClear(glFlags); - assertNoGLError("Failed to clear framebuffer"); -} - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - errorret_t frameBufferGLInit( - framebuffer_t *fb, - const uint32_t width, - const uint32_t height - ) { - assertNotNull(fb, "Framebuffer cannot be NULL"); - assertTrue(width > 0 && height > 0, "W/H must be greater than 0"); - - memoryZero(fb, sizeof(framebuffer_t)); - textureInit(&fb->texture, width, height, TEXTURE_FORMAT_RGBA, - (texturedata_t){ .rgbaColors = NULL } - ); - errorChain(errorGLCheck()); - - glGenFramebuffersEXT(1, &fb->id); - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb->id); - errorChain(errorGLCheck()); - - glFramebufferTexture2DEXT( - GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, - GL_TEXTURE_2D, fb->texture.id, 0 - ); - errorChain(errorGLCheck()); - - if( - glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) != - GL_FRAMEBUFFER_COMPLETE_EXT - ) { - assertUnreachable("Framebuffer is not complete"); - } - - glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); - errorChain(errorGLCheck()); - errorOk(); - } - - errorret_t frameBufferGLDispose(framebuffer_t *framebuffer) { - assertNotNull(framebuffer, "Framebuffer cannot be NULL"); - - if(framebuffer == &FRAMEBUFFER_BACKBUFFER) { - assertUnreachable("Cannot dispose of backbuffer"); - } - - errorChain(textureDispose(&framebuffer->texture)); - glDeleteFramebuffersEXT(1, &framebuffer->id); - errorChain(errorGLCheck()); - errorOk(); - } -#endif \ No newline at end of file diff --git a/src/duskgl/display/framebuffer/framebuffergl.h b/src/duskgl/display/framebuffer/framebuffergl.h deleted file mode 100644 index 1fa2de28..00000000 --- a/src/duskgl/display/framebuffer/framebuffergl.h +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/texture/texture.h" -#include "error/errorgl.h" - -typedef struct { - GLuint id; - texture_t texture; -} framebuffergl_t; - -/** - * Initializes the backbuffer framebuffer. (OpenGL implementation). - */ -errorret_t frameBufferGLInitBackBuffer(void); - -/** - * Gets the height of the framebuffer. (OpenGL implementation). - * - * @param framebuffer The framebuffer to get the height of. - * @return The height of the framebuffer, or 0 if the framebuffer is NULL. - */ -uint32_t frameBufferGLGetWidth(const framebuffergl_t *framebuffer); - -/** - * Initializes an OpenGL style framebuffer. - * - * @param fb The framebuffer to initialize. - * @param width The width of the framebuffer. - * @param height The height of the framebuffer. - * @return Either error or not. - */ -uint32_t frameBufferGLGetHeight(const framebuffergl_t *framebuffer); - -/** - * Gets the width of the framebuffer. (OpenGL implementation). - * - * @param framebuffer The framebuffer to get the width of. - * @return The width of the framebuffer, or 0 if the framebuffer is NULL. - */ -errorret_t frameBufferGLBind(framebuffergl_t *framebuffer); - -/** - * Clears the framebuffer with the specified flags and color. - * - * @param flags The clear flags. - * @param color The clear color. - */ -void frameBufferGLClear(const uint8_t flags, const color_t color); - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - /** - * Initializes an OpenGL style framebuffer. - * - * @param fb The framebuffer to initialize. - * @param width The width of the framebuffer. - * @param height The height of the framebuffer. - * @return Either error or not. - */ - errorret_t frameBufferGLInit( - framebuffergl_t *fb, - const uint32_t width, - const uint32_t height - ); - - /** - * Disposes of the framebuffer. Will also be used for request disposing of the - * backbuffer. - * - * @param framebuffer The framebuffer to dispose of. - */ - errorret_t frameBufferGLDispose(framebuffergl_t *framebuffer); -#endif \ No newline at end of file diff --git a/src/duskgl/display/framebuffer/framebufferplatform.h b/src/duskgl/display/framebuffer/framebufferplatform.h deleted file mode 100644 index 17d6ee0c..00000000 --- a/src/duskgl/display/framebuffer/framebufferplatform.h +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/framebuffer/framebuffergl.h" -typedef framebuffergl_t framebufferplatform_t; - -#define frameBufferPlatformInitBackBuffer frameBufferGLInitBackBuffer -#define frameBufferPlatformGetWidth frameBufferGLGetWidth -#define frameBufferPlatformGetHeight frameBufferGLGetHeight -#define frameBufferPlatformBind frameBufferGLBind -#define frameBufferPlatformClear frameBufferGLClear - -#ifdef DUSK_DISPLAY_SIZE_DYNAMIC - #define frameBufferPlatformInit frameBufferGLInit - #define frameBufferPlatformDispose frameBufferGLDispose -#endif \ No newline at end of file diff --git a/src/duskgl/display/mesh/CMakeLists.txt b/src/duskgl/display/mesh/CMakeLists.txt deleted file mode 100644 index 5a9b256c..00000000 --- a/src/duskgl/display/mesh/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - meshgl.c -) \ No newline at end of file diff --git a/src/duskgl/display/mesh/meshgl.c b/src/duskgl/display/mesh/meshgl.c deleted file mode 100644 index 136dadfb..00000000 --- a/src/duskgl/display/mesh/meshgl.c +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/mesh/mesh.h" -#include "assert/assertgl.h" -#include "error/errorgl.h" -#include "display/shader/shadergl.h" - -errorret_t meshInitGL( - meshgl_t *mesh, - const meshprimitivetypegl_t primitiveType, - const int32_t vertexCount, - const meshvertex_t *vertices -) { - assertNotNull(mesh, "Mesh cannot be NULL"); - assertNotNull(vertices, "Vertices cannot be NULL"); - assertTrue(vertexCount > 0, "Vertex count must be greater than 0"); - - mesh->primitiveType = primitiveType; - mesh->vertexCount = vertexCount; - mesh->vertices = vertices; - - #ifdef DUSK_OPENGL_LEGACY - // Nothing needed; client state is enabled once globally in - // displayOpenGLInit. - #else - // Generate Vertex Buffer Object - glGenBuffers(1, &mesh->vboId); - errorChain(errorGLCheck()); - glBindBuffer(GL_ARRAY_BUFFER, mesh->vboId); - errorChain(errorGLCheck()); - glBufferData( - GL_ARRAY_BUFFER, - vertexCount * sizeof(meshvertex_t), - vertices, - GL_DYNAMIC_DRAW - ); - errorChain(errorGLCheck()); - - // Generate Vertex Array Object - glGenVertexArrays(1, &mesh->vaoId); - errorChain(errorGLCheck()); - glBindVertexArray(mesh->vaoId); - errorChain(errorGLCheck()); - glBindBuffer(GL_ARRAY_BUFFER, mesh->vboId); - errorChain(errorGLCheck()); - - // Set up vertex attribute pointers - glVertexAttribPointer( - 0, - MESH_VERTEX_POS_SIZE, - GL_FLOAT, - GL_FALSE, - sizeof(meshvertex_t), - (const GLvoid*)offsetof(meshvertex_t, pos) - ); - errorChain(errorGLCheck()); - glEnableVertexAttribArray(0); - errorChain(errorGLCheck()); - - glVertexAttribPointer( - 1, - MESH_VERTEX_UV_SIZE, - GL_FLOAT, - GL_FALSE, - sizeof(meshvertex_t), - (const GLvoid*)offsetof(meshvertex_t, uv) - ); - errorChain(errorGLCheck()); - glEnableVertexAttribArray(1); - errorChain(errorGLCheck()); - - // Unbind VAO and VBO to prevent accidental modification - glBindBuffer(GL_ARRAY_BUFFER, 0); - errorChain(errorGLCheck()); - glBindVertexArray(0); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} - -errorret_t meshFlushGL( - meshgl_t *mesh, - const int32_t vertOffset, - const int32_t vertCount -) { - #ifdef DUSK_OPENGL_LEGACY - // Nothing doing, we use the glClientState stuff. - #else - glBindBuffer(GL_ARRAY_BUFFER, mesh->vboId); - errorChain(errorGLCheck()); - glBufferData( - GL_ARRAY_BUFFER, - mesh->vertexCount * sizeof(meshvertex_t), - mesh->vertices, - // vertCount * sizeof(meshvertex_t), - // &mesh->vertices[vertOffset], - GL_DYNAMIC_DRAW - ); - errorChain(errorGLCheck()); - #endif - errorOk(); -} - -errorret_t meshDrawGL( - const meshgl_t *mesh, - const int32_t offset, - const int32_t count -) { - #ifdef DUSK_OPENGL_LEGACY - // Legacy pointer style rendering - const GLsizei stride = sizeof(meshvertex_t); - - glTexCoordPointer( - MESH_VERTEX_UV_SIZE, - GL_FLOAT, - stride, - (const GLvoid*)&mesh->vertices[offset].uv[0] - ); - - glVertexPointer( - MESH_VERTEX_POS_SIZE, - GL_FLOAT, - stride, - (const GLvoid*)&mesh->vertices[offset].pos[0] - ); - - // Shader may have model matrix here - errorChain(shaderLegacyMatrixUpdate()); - glDrawArrays(mesh->primitiveType, 0, count); - errorChain(errorGLCheck()); - #else - // Modern VAO/VBO rendering - glBindVertexArray(mesh->vaoId); - errorChain(errorGLCheck()); - glDrawArrays(mesh->primitiveType, offset, count); - errorChain(errorGLCheck()); - glBindVertexArray(0); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} - -int32_t meshGetVertexCountGL(const meshgl_t *mesh) { - return mesh->vertexCount; -} - -errorret_t meshDisposeGL(meshgl_t *mesh) { - #ifdef DUSK_OPENGL_LEGACY - // No dynamic resources to free for this mesh implementation - #else - glDeleteBuffers(1, &mesh->vboId); - errorChain(errorGLCheck()); - glDeleteVertexArrays(1, &mesh->vaoId); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} \ No newline at end of file diff --git a/src/duskgl/display/mesh/meshgl.h b/src/duskgl/display/mesh/meshgl.h deleted file mode 100644 index 9b7533c2..00000000 --- a/src/duskgl/display/mesh/meshgl.h +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/mesh/meshvertex.h" - -typedef enum { - MESH_PRIMITIVE_TYPE_TRIANGLES = GL_TRIANGLES, - MESH_PRIMITIVE_TYPE_LINES = GL_LINES, - MESH_PRIMITIVE_TYPE_POINTS = GL_POINTS, -} meshprimitivetypegl_t; - -typedef struct { - int32_t vertexCount; - meshprimitivetypegl_t primitiveType; - const meshvertex_t *vertices; - - #ifdef DUSK_OPENGL_LEGACY - // Nothing needed - #else - GLuint vaoId; - GLuint vboId; - #endif -} meshgl_t; - -/** - * Initializes a mesh for OpenGL. - * - * @param mesh The mesh to initialize. - * @param primitiveType The OpenGL primitive type (e.g., GL_TRIANGLES). - * @param vertexCount The number of vertices in the mesh. - * @param vertices The vertex data for the mesh. - * @return An errorret_t indicating success or failure. - */ -errorret_t meshInitGL( - meshgl_t *mesh, - const meshprimitivetypegl_t primitiveType, - const int32_t vertexCount, - const meshvertex_t *vertices -); - -/** - * Flushes the vertices (stored in memory) to the GPU. - * - * @param mesh Mesh to flush vertices for. - * @param vertOffset First vertice index to flush. - * @param vertCount Count of vertices to flush. - * @return Error state. - */ -errorret_t meshFlushGL( - meshgl_t *mesh, - const int32_t vertOffset, - const int32_t vertCount -); - -/** - * Draws a mesh using OpenGL. - * - * @param mesh The mesh to draw. - * @param vertexOffset The offset in the vertex array to start drawing from. - * @param vertexCount The number of vertices to draw. If -1, draws all vertices. - * @return An errorret_t indicating success or failure. - */ -errorret_t meshDrawGL( - const meshgl_t *mesh, - const int32_t vertexOffset, - const int32_t vertexCount -); - -/** - * Gets the vertex count of a mesh used for OpenGL. - * - * @param mesh The mesh to get the vertex count from. - * @return The vertex count of the mesh. - */ -int32_t meshGetVertexCountGL(const meshgl_t *mesh); - -/** - * Disposes a mesh used for OpenGL. - * - * @param mesh The mesh to dispose. - * @return An errorret_t indicating success or failure. - */ -errorret_t meshDisposeGL(meshgl_t *mesh); \ No newline at end of file diff --git a/src/duskgl/display/mesh/meshplatform.h b/src/duskgl/display/mesh/meshplatform.h deleted file mode 100644 index f05ab503..00000000 --- a/src/duskgl/display/mesh/meshplatform.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "meshgl.h" - -typedef meshprimitivetypegl_t meshprimitivetypeplatform_t; -typedef meshgl_t meshplatform_t; - -#define meshInitPlatform meshInitGL -#define meshFlushPlatform meshFlushGL -#define meshDrawPlatform meshDrawGL -#define meshGetVertexCountPlatform meshGetVertexCountGL -#define meshDisposePlatform meshDisposeGL \ No newline at end of file diff --git a/src/duskgl/display/shader/CMakeLists.txt b/src/duskgl/display/shader/CMakeLists.txt deleted file mode 100644 index 719b437f..00000000 --- a/src/duskgl/display/shader/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - shadergl.c - shaderunlitgl.c -) \ No newline at end of file diff --git a/src/duskgl/display/shader/shadergl.c b/src/duskgl/display/shader/shadergl.c deleted file mode 100644 index f7467f41..00000000 --- a/src/duskgl/display/shader/shadergl.c +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "shadergl.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assertgl.h" -#include "display/shader/shaderunlit.h" - -#ifdef DUSK_OPENGL_LEGACY - shaderlegacygl_t SHADER_LEGACY = { 0 }; -#endif - -errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def) { - assertNotNull(shader, "Shader cannot be null"); - assertNotNull(def, "Shader definition cannot be null"); - memoryZero(shader, sizeof(shadergl_t)); - - shader->definition = def; - - #ifdef DUSK_OPENGL_LEGACY - glm_mat4_identity(shader->view); - glm_mat4_identity(shader->proj); - glm_mat4_identity(shader->model); - - SHADER_LEGACY.boundShader = NULL; - errorOk(); - #else - assertNotNull(def->vert, "Vertex shader source cannot be null"); - assertNotNull(def->frag, "Fragment shader source cannot be null"); - - // Create vertex shader - shader->vertexShaderId = glCreateShader(GL_VERTEX_SHADER); - errorret_t err = errorGLCheck(); - errorChain(err); - - glShaderSource(shader->vertexShaderId, 1, &def->vert, NULL); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteShader(shader->vertexShaderId); - errorChain(err); - } - - glCompileShader(shader->vertexShaderId); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteShader(shader->vertexShaderId); - errorChain(err); - } - - GLint ok = 0; - glGetShaderiv(shader->vertexShaderId, GL_COMPILE_STATUS, &ok); - if(!ok) { - GLchar log[1024]; - glGetShaderInfoLog(shader->vertexShaderId, sizeof(log), NULL, log); - glDeleteShader(shader->vertexShaderId); - errorThrow("Vertex shader compilation failed: %s", log); - } - - // Create fragment shader - shader->fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteShader(shader->vertexShaderId); - errorChain(err); - } - - glShaderSource(shader->fragmentShaderId, 1, &def->frag, NULL); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorChain(err); - } - - glCompileShader(shader->fragmentShaderId); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorChain(err); - } - - glGetShaderiv(shader->fragmentShaderId, GL_COMPILE_STATUS, &ok); - if(!ok) { - GLchar log[1024]; - glGetShaderInfoLog(shader->fragmentShaderId, sizeof(log), NULL, log); - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorThrow("Fragment shader compilation failed: %s", log); - } - - // Create shader program - shader->shaderProgramId = glCreateProgram(); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorChain(err); - } - - glAttachShader(shader->shaderProgramId, shader->vertexShaderId); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteProgram(shader->shaderProgramId); - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorChain(err); - } - - glAttachShader(shader->shaderProgramId, shader->fragmentShaderId); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteProgram(shader->shaderProgramId); - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorChain(err); - } - - glLinkProgram(shader->shaderProgramId); - err = errorGLCheck(); - if(errorIsNotOk(err)) { - glDeleteProgram(shader->shaderProgramId); - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorChain(err); - } - - ok = 0; - glGetProgramiv(shader->shaderProgramId, GL_LINK_STATUS, &ok); - if(!ok) { - GLchar log[1024]; - glGetProgramInfoLog(shader->shaderProgramId, sizeof(log), NULL, log); - glDeleteProgram(shader->shaderProgramId); - glDeleteShader(shader->vertexShaderId); - glDeleteShader(shader->fragmentShaderId); - errorThrow("Shader program linking failed: %s", log); - } - #endif - - errorOk(); -} - -errorret_t shaderParamGetLocationGL( - shadergl_t *shader, - const char_t *name, - GLint *location -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - assertNotNull(location, "Location cannot be null"); - - #ifdef DUSK_OPENGL_LEGACY - assertUnreachable("Cannot get uniform locations on legacy opengl."); - #else - *location = glGetUniformLocation(shader->shaderProgramId, name); - errorChain(errorGLCheck()); - if(*location == -1) { - errorThrow("Uniform '%s' not found in shader.", name); - } - #endif - - errorOk(); -} - -errorret_t shaderSetMatrixGL( - shadergl_t *shader, - const char_t *name, - mat4 mat -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - assertNotNull(mat, "Matrix data cannot be null"); - - #ifdef DUSK_OPENGL_LEGACY - assertTrue( - SHADER_LEGACY.boundShader == shader, - "Shader must be bound to set legacy matrices." - ); - // Use unaligned copy to safely handle possibly unaligned input matrices - if(stringCompare(name, SHADER_UNLIT_PROJECTION) == 0) { - SHADER_LEGACY.dirty |= SHADER_LEGACY_DIRTY_PROJ; - glm_mat4_ucopy(mat, shader->proj); - - } else if(stringCompare(name, SHADER_UNLIT_VIEW) == 0) { - SHADER_LEGACY.dirty |= SHADER_LEGACY_DIRTY_VIEW; - glm_mat4_ucopy(mat, shader->view); - - } else if(stringCompare(name, SHADER_UNLIT_MODEL) == 0) { - SHADER_LEGACY.dirty |= SHADER_LEGACY_DIRTY_MODEL; - glm_mat4_ucopy(mat, shader->model); - - } else { - assertUnreachable("Cannot use a custom matrix on legacy opengl."); - } - #else - GLint location; - errorChain(shaderParamGetLocationGL(shader, name, &location)); - - glUniformMatrix4fv(location, 1, GL_FALSE, (const GLfloat *)mat); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} - -errorret_t shaderSetTextureGL( - shadergl_t *shader, - const char_t *name, - texture_t *texture -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - - #ifdef DUSK_OPENGL_LEGACY - assertStringEqual( - name, - SHADER_UNLIT_TEXTURE, - "Only one texture supported in legacy opengl." - ); - - if(texture == NULL) { - if(SHADER_LEGACY.boundTextureId != 0) { - glDisable(GL_TEXTURE_2D); - errorChain(errorGLCheck()); - SHADER_LEGACY.boundTextureId = 0; - } - errorOk(); - } - - if(texture->id != SHADER_LEGACY.boundTextureId) { - glEnable(GL_TEXTURE_2D); - errorChain(errorGLCheck()); - glBindTexture(GL_TEXTURE_2D, texture->id); - errorChain(errorGLCheck()); - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - errorChain(errorGLCheck()); - SHADER_LEGACY.boundTextureId = texture->id; - } - - // glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PRIMARY_COLOR); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); - // errorChain(errorGLCheck()); - - #else - assertNotNull(shader->definition, "Shader definition cannot be null"); - assertNotNull(shader->definition->setTexture, "Shader cannot do textures."); - errorChain(shader->definition->setTexture(shader, name, texture)); - #endif - - errorOk(); -} - -errorret_t shaderSetColorGL( - shadergl_t *shader, - const char_t *name, - color_t color -) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - - #ifdef DUSK_OPENGL_LEGACY - // if(color.a == 0) { - // glDisable(GL_TEXTURE_2D); - // errorChain(errorGLCheck()); - // errorOk(); - // } - - // glActiveTexture(GL_TEXTURE1); - // errorChain(errorGLCheck()); - - // if(color.r == 255 && color.g == 255 && color.b == 255) { - // glDisable(GL_TEXTURE_2D); - // errorChain(errorGLCheck()); - // errorOk(); - // } - - // glEnable(GL_TEXTURE_2D); - // errorChain(errorGLCheck()); - // glBindTexture(GL_TEXTURE_2D, TEXTURE_WHITE.id); - // errorChain(errorGLCheck()); - - // GLfloat tint[4] = { - // ((float_t)color.r) / 255.0f, - // ((float_t)color.g) / 255.0f, - // ((float_t)color.b) / 255.0f, - // ((float_t)color.a) / 255.0f - // }; - // glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tint); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_CONSTANT); - // errorChain(errorGLCheck()); - // glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); - // errorChain(errorGLCheck()); - - glColor4f( - (float_t)color.r / 255.0f, - (float_t)color.g / 255.0f, - (float_t)color.b / 255.0f, - (float_t)color.a / 255.0f - ); - errorChain(errorGLCheck()); - - #else - GLint location; - errorChain(shaderParamGetLocationGL(shader, name, &location)); - glUniform4f( - location, - color.r / 255.0f, - color.g / 255.0f, - color.b / 255.0f, - color.a / 255.0f - ); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} - -errorret_t shaderBindGL(shadergl_t *shader) { - #ifdef DUSK_OPENGL_LEGACY - assertNotNull(shader, "Cannot bind a null shader."); - SHADER_LEGACY.boundShader = shader; - SHADER_LEGACY.dirty = ( - SHADER_LEGACY_DIRTY_MODEL | - SHADER_LEGACY_DIRTY_PROJ | - SHADER_LEGACY_DIRTY_VIEW - ); - #else - assertNotNull(shader, "Shader cannot be null"); - glUseProgram(shader->shaderProgramId); - errorChain(errorGLCheck()); - #endif - - errorOk(); -} - -errorret_t shaderDisposeGL(shadergl_t *shader) { - assertNotNull(shader, "Shader cannot be null"); - - #ifdef DUSK_OPENGL_LEGACY - SHADER_LEGACY.boundShader = NULL; - #else - if(shader->shaderProgramId != 0) { - glDeleteProgram(shader->shaderProgramId); - } - - if(shader->vertexShaderId != 0) { - glDeleteShader(shader->vertexShaderId); - } - - if(shader->fragmentShaderId != 0) { - glDeleteShader(shader->fragmentShaderId); - } - - assertNoGLError("Failed disposing shader"); - #endif - - memoryZero(shader, sizeof(shadergl_t)); - errorOk(); -} - -#ifdef DUSK_OPENGL_LEGACY - errorret_t shaderLegacyMatrixUpdate() { - assertNotNull(SHADER_LEGACY.boundShader, "No shader is currently bound."); - - if((SHADER_LEGACY.dirty & SHADER_LEGACY_DIRTY_PROJ) != 0) { - glMatrixMode(GL_PROJECTION); - errorChain(errorGLCheck()); - glLoadIdentity(); - errorChain(errorGLCheck()); - glMultMatrixf((const GLfloat *)SHADER_LEGACY.boundShader->proj); - errorChain(errorGLCheck()); - } - - if( - (SHADER_LEGACY.dirty & - (SHADER_LEGACY_DIRTY_VIEW | SHADER_LEGACY_DIRTY_MODEL)) != 0 - ) { - glMatrixMode(GL_MODELVIEW); - errorChain(errorGLCheck()); - mat4 viewModel; - glm_mat4_mul( - SHADER_LEGACY.boundShader->view, - SHADER_LEGACY.boundShader->model, - viewModel - ); - glLoadMatrixf((const GLfloat *)viewModel); - errorChain(errorGLCheck()); - } - - SHADER_LEGACY.dirty = 0; - errorOk(); - } -#endif \ No newline at end of file diff --git a/src/duskgl/display/shader/shadergl.h b/src/duskgl/display/shader/shadergl.h deleted file mode 100644 index 11be1a43..00000000 --- a/src/duskgl/display/shader/shadergl.h +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/errorgl.h" -#include "display/texture/texture.h" - -typedef struct shadergl_s shadergl_t; -typedef union shadermaterial_u shadermaterial_t; - -typedef errorret_t (*shadersettexturefn_t)( - shadergl_t *, - const char_t *, - texture_t * -); - -typedef struct { - errorret_t (*setMaterial)(shadergl_t *, const shadermaterial_t *); - - #ifdef DUSK_OPENGL_LEGACY - #else - errorret_t (*setTexture)(shadergl_t *, const char_t *, texture_t *); - - const char_t *vert; - const char_t *frag; - #endif -} shaderdefinitiongl_t; - -typedef struct shadergl_s { - const shaderdefinitiongl_t *definition; - - #ifdef DUSK_OPENGL_LEGACY - mat4 view; - mat4 proj; - mat4 model; - #else - GLuint shaderProgramId; - GLuint vertexShaderId; - GLuint fragmentShaderId; - #endif -} shadergl_t; - -#if DUSK_OPENGL_LEGACY - typedef struct { - shadergl_t *boundShader; - uint_fast8_t dirty; - GLuint boundTextureId; - } shaderlegacygl_t; - - extern shaderlegacygl_t SHADER_LEGACY; - - #define SHADER_LEGACY_DIRTY_PROJ (1 << 0) - #define SHADER_LEGACY_DIRTY_VIEW (1 << 1) - #define SHADER_LEGACY_DIRTY_MODEL (1 << 2) -#endif - -/** - * Initializes a shader. - * - * @param shader The shader to initialize. - * @param def The definition of the shader to initialize with. - * @return An errorret_t indicating success or failure. - */ -errorret_t shaderInitGL(shadergl_t *shader, const shaderdefinitiongl_t *def); - -/** - * Binds a shader for use in rendering. - * - * @param shader The shader to bind. - * @return An errorret_t indicating success or failure. - */ -errorret_t shaderBindGL(shadergl_t *shader); - -/** - * Retrieves the location of a shader uniform parameter. - * - * @param shader The shader to query. - * @param name The name of the uniform parameter. - * @param location Output parameter to receive the location of the uniform. - * @return An errorret_t indicating success or failure. - */ -errorret_t shaderParamGetLocationGL( - shadergl_t *shader, - const char_t *name, - GLint *location -); - -/** - * Sets a mat4 uniform parameter in the shader. - * - * @param shader The shader to update. - * @param name The name of the uniform parameter. - * @param mat The 4x4 matrix data to set. - * @return An errorret_t indicating success or failure. - */ -errorret_t shaderSetMatrixGL( - shadergl_t *shader, - const char_t *name, - mat4 matrix -); - -/** - * Sets a color uniform parameter in the shader. - * - * @param shader The shader to update. - * @param name The name of the uniform parameter. - * @param color The color data to set. - * @return An errorret_t indicating success or failure. - */ -errorret_t shaderSetTextureGL( - shadergl_t *shader, - const char_t *name, - texture_t *texture -); - -/** - * Sets a color uniform parameter in the shader. - * - * @param shader The shader to update. - * @param name The name of the uniform parameter. - * @param color The color data to set. - * @return An errorret_t indicating success or failure. - */ -errorret_t shaderSetColorGL( - shadergl_t *shader, - const char_t *name, - color_t color -); - -/** - * Disposes of a shader, freeing any associated resources. - * - * @param shader The shader to dispose. - */ -errorret_t shaderDisposeGL(shadergl_t *shader); - -#ifdef DUSK_OPENGL_LEGACY - /** - * During mesh rendering, this is requesting the legacy system to push all - * shaders necessary to render the currently bound shader's matrices. - * - * @return Any error state. - */ - errorret_t shaderLegacyMatrixUpdate(); -#endif \ No newline at end of file diff --git a/src/duskgl/display/shader/shaderplatform.h b/src/duskgl/display/shader/shaderplatform.h deleted file mode 100644 index eb3975b2..00000000 --- a/src/duskgl/display/shader/shaderplatform.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "shadergl.h" - -typedef shadergl_t shaderplatform_t; -typedef shaderdefinitiongl_t shaderdefinitionplatform_t; - -#define shaderInitPlatform shaderInitGL -#define shaderBindPlatform shaderBindGL -#define shaderSetMatrixPlatform shaderSetMatrixGL -#define shaderSetTexturePlatform shaderSetTextureGL -#define shaderSetColorPlatform shaderSetColorGL -#define shaderDisposePlatform shaderDisposeGL \ No newline at end of file diff --git a/src/duskgl/display/shader/shaderunlitgl.c b/src/duskgl/display/shader/shaderunlitgl.c deleted file mode 100644 index f349de83..00000000 --- a/src/duskgl/display/shader/shaderunlitgl.c +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/shader/shaderunlit.h" -#include "assert/assertgl.h" - -#ifdef DUSK_OPENGL_LEGACY - shaderdefinition_t SHADER_UNLIT_DEFINITION = { - .setMaterial = shaderUnlitSetMaterial, - }; -#else - errorret_t shaderUnlitSetTextureGL( - shadergl_t *shader, - const char_t *name, - texture_t *texture - ) { - assertNotNull(shader, "Shader cannot be null"); - assertStrLenMin(name, 1, "Uniform name cannot be empty"); - assertStringEqual( - name, - SHADER_UNLIT_TEXTURE, - "Only one texture supported in unlit shader." - ); - - GLint locTexture, locType, locColorCount, locColors; - - errorChain(shaderParamGetLocationGL(shader, "u_TextureType", &locType)); - - // NULL textures - if(texture == NULL) { - glUniform1i(locType, 0); - errorChain(errorGLCheck()); - errorOk(); - } - - // Set texture. - glActiveTexture(GL_TEXTURE0); - errorChain(errorGLCheck()); - glBindTexture(GL_TEXTURE_2D, texture->id); - errorChain(errorGLCheck()); - - errorChain(shaderParamGetLocationGL(shader, name, &locTexture)); - glUniform1i(locTexture, 0); - errorChain(errorGLCheck()); - - // Set texture type - if(texture->format == TEXTURE_FORMAT_PALETTE) { - glUniform1i(locType, 2); - errorChain(errorGLCheck()); - - shaderParamGetLocationGL(shader, "u_ColorCount", &locColorCount); - glUniform1i(locColorCount, texture->palette->count); - errorChain(errorGLCheck()); - - shaderParamGetLocationGL(shader, "u_Colors", &locColors); - GLuint paletteData[texture->palette->count]; - for(size_t i = 0; i < texture->palette->count; i++) { - color_t color = texture->palette->colors[i]; - paletteData[i] = ( - ((uint32_t)color.r << 24) | - ((uint32_t)color.g << 16) | - ((uint32_t)color.b << 8) | - ((uint32_t)color.a << 0) - ); - } - glUniform1uiv(locColors, texture->palette->count, paletteData); - errorChain(errorGLCheck()); - } else { - glUniform1i(locType, 1); - errorChain(errorGLCheck()); - } - - errorOk(); - } - - - - shaderdefinition_t SHADER_UNLIT_DEFINITION = { - .setMaterial = shaderUnlitSetMaterial, - .setTexture = shaderUnlitSetTextureGL, - - .vert = - #ifdef DUSK_OPENGL_ES - "#version 300 es\n" - "precision mediump float;\n" - "layout(location = 0) in vec3 a_Pos;\n" - "layout(location = 1) in vec2 a_TexCoord;\n" - "uniform mat4 u_Proj;\n" - "uniform mat4 u_View;\n" - "uniform mat4 u_Model;\n" - "out vec4 v_Color;\n" - "out vec2 v_TexCoord;\n" - "void main() {\n" - " gl_Position = u_Proj * u_View * u_Model * vec4(a_Pos, 1.0);\n" - " v_Color = vec4(1.0);\n" - " v_TexCoord = a_TexCoord;\n" - "}\n", - #else - "#version 330 core\n" - "layout(location = 0) in vec3 a_Pos;\n" - "layout(location = 1) in vec2 a_TexCoord;\n" - "uniform mat4 u_Proj;\n" - "uniform mat4 u_View;\n" - "uniform mat4 u_Model;\n" - "out vec4 v_Color;\n" - "out vec2 v_TexCoord;\n" - "void main() {\n" - " gl_Position = u_Proj * u_View * u_Model * vec4(a_Pos, 1.0);\n" - " v_Color = vec4(1.0);\n" - " v_TexCoord = a_TexCoord;\n" - "}\n", - #endif - - - .frag = - #ifdef DUSK_OPENGL_ES - "#version 300 es\n" - "precision mediump float;\n" - // Uniforms - "uniform sampler2D u_Texture;\n" - "uniform int u_TextureType;\n" - "uniform uint u_Colors[256];\n"// For paletted textures. - "uniform int u_ColorCount;\n" - "uniform vec4 u_Color;\n" - // Fragment shader inputs - "in vec4 v_Color;\n" - "in vec2 v_TexCoord;\n" - // Fragment shader output - "out vec4 FragColor;\n" - "void main() {\n" - " if(u_TextureType == 0) {\n"// No texture - " FragColor = v_Color * u_Color;\n" - " return;\n" - " }\n" - " if(u_TextureType == 1) {\n"// Regular texture - " FragColor = texture(u_Texture, v_TexCoord) * v_Color * u_Color;\n" - " return;\n" - " }\n" - " if(u_TextureType == 2) {\n"// Paletted texture - " vec4 texColor = texture(u_Texture, v_TexCoord);\n" - " uint index = uint(floor(texColor.r * 255.0));\n" - " uint palColor = u_Colors[index];\n" - " float r = float((palColor >> 24) & 0xFFu) / 255.0;\n" - " float g = float((palColor >> 16) & 0xFFu) / 255.0;\n" - " float b = float((palColor >> 8) & 0xFFu) / 255.0;\n" - " float a = float((palColor >> 0) & 0xFFu) / 255.0;\n" - " FragColor = vec4(r, g, b, a) * u_Color;\n" - " return;\n" - " }\n" - " FragColor = v_Color * u_Color;\n"// Unknown texture type? - "}\n", - #else - "#version 330 core\n" - // Uniforms - "uniform sampler2D u_Texture;\n" - "uniform int u_TextureType;\n" - "uniform uint u_Colors[256];\n"// For paletted textures. - "uniform int u_ColorCount;\n" - "uniform vec4 u_Color;\n" - // Fragment shader inputs - "in vec4 v_Color;\n" - "in vec2 v_TexCoord;\n" - // Fragment shader output - "out vec4 FragColor;\n" - "void main() {\n" - " if(u_TextureType == 0) {\n"// No texture - " FragColor = v_Color * u_Color;\n" - " return;\n" - " }\n" - " if(u_TextureType == 1) {\n"// Regular texture - " FragColor = texture(u_Texture, v_TexCoord) * v_Color * u_Color;\n" - " return;\n" - " }\n" - " if(u_TextureType == 2) {\n"// Paletted texture - " vec4 texColor = texture(u_Texture, v_TexCoord);\n" - " uint index = uint(floor(texColor.r * 255.0));\n" - " uint palColor = u_Colors[index];\n" - " float r = float((palColor >> 24) & 0xFFu) / 255.0;\n" - " float g = float((palColor >> 16) & 0xFFu) / 255.0;\n" - " float b = float((palColor >> 8) & 0xFFu) / 255.0;\n" - " float a = float((palColor >> 0) & 0xFFu) / 255.0;\n" - " FragColor = vec4(r, g, b, a) * u_Color;\n" - " return;\n" - " }\n" - " FragColor = v_Color * u_Color;\n"// Unknown texture type? - "}\n", - #endif - }; -#endif \ No newline at end of file diff --git a/src/duskgl/display/texture/CMakeLists.txt b/src/duskgl/display/texture/CMakeLists.txt deleted file mode 100644 index b8a5af7c..00000000 --- a/src/duskgl/display/texture/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - texturegl.c -) \ No newline at end of file diff --git a/src/duskgl/display/texture/texturegl.c b/src/duskgl/display/texture/texturegl.c deleted file mode 100644 index c10deef1..00000000 --- a/src/duskgl/display/texture/texturegl.c +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/texture/texture.h" -#include "assert/assert.h" -#include "error/errorgl.h" -#include "util/memory.h" - -errorret_t textureInitGL( - texturegl_t *texture, - const int32_t width, - const int32_t height, - const textureformatgl_t format, - const texturedata_t data -) { - glGenTextures(1, &texture->id); - errorChain(errorGLCheck()); - glBindTexture(GL_TEXTURE_2D, texture->id); - errorChain(errorGLCheck()); - - switch(format) { - case TEXTURE_FORMAT_RGBA: - glTexImage2D( - GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, (void*)data.rgbaColors - ); - errorChain(errorGLCheck()); - break; - - case TEXTURE_FORMAT_PALETTE: - texture->palette = data.paletted.palette; - assertTrue( - texture->palette == &PALETTES[0], - "Only the first palette is supported in legacy opengl." - ); - - #ifdef DUSK_OPENGL_LEGACY - glColorTableEXT( - GL_TEXTURE_2D, GL_RGBA, texture->palette->count, GL_RGBA, - GL_UNSIGNED_BYTE, (const void*)texture->palette->colors - ); - errorChain(errorGLCheck()); - glTexImage2D( - GL_TEXTURE_2D, - 0, GL_COLOR_INDEX8_EXT, - width, height, - 0, GL_COLOR_INDEX8_EXT, - GL_UNSIGNED_BYTE, (void*)data.paletted.indices - ); - errorChain(errorGLCheck()); - #else - // For modern systems we send to only the R channel and the shader does - // the rest. - glTexImage2D( - GL_TEXTURE_2D, 0, GL_RED, width, height, 0, - GL_RED, GL_UNSIGNED_BYTE, (void*)data.paletted.indices - ); - errorChain(errorGLCheck()); - #endif - break; - - default: - assertUnreachable("Unknown texture format"); - break; - } - errorChain(errorGLCheck()); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - errorChain(errorGLCheck()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - errorChain(errorGLCheck()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); - errorChain(errorGLCheck()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - errorChain(errorGLCheck()); - - glBindTexture(GL_TEXTURE_2D, 0); - errorChain(errorGLCheck()); - - errorOk(); -} - -errorret_t textureDisposeGL(texturegl_t *texture) { - assertNotNull(texture, "Texture cannot be NULL"); - assertTrue(texture->id != 0, "Texture ID must be valid"); - - glDeleteTextures(1, &texture->id); - errorChain(errorGLCheck()); - - errorOk(); -} \ No newline at end of file diff --git a/src/duskgl/display/texture/texturegl.h b/src/duskgl/display/texture/texturegl.h deleted file mode 100644 index 21ec2cdc..00000000 --- a/src/duskgl/display/texture/texturegl.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -typedef union texturedata_u texturedata_t; - -typedef enum { - TEXTURE_FORMAT_RGBA = GL_RGBA, - TEXTURE_FORMAT_PALETTE = GL_COLOR_INDEX8_EXT, - // TEXTURE_FORMAT_DXT5 = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT -} textureformatgl_t; - -typedef struct { - GLuint id; - textureformatgl_t format; - int32_t width; - int32_t height; - - union { - palette_t *palette; - }; -} texturegl_t; - -/** - * Initializes a texture. - * - * @param texture The texture to initialize. - * @param width The width of the texture. - * @param height The height of the texture. - * @param format The format of the texture (e.g., GL_RGBA, GL_ALPHA). - * @param data The data for the texture, the format changes per format. - * @return An error if the texture failed to initialize, otherwise success. - */ -errorret_t textureInitGL( - texturegl_t *texture, - const int32_t width, - const int32_t height, - const textureformatgl_t format, - const texturedata_t data -); - -/** - * Disposes a texture. - * - * @param texture The texture to dispose. - * @return An error if the texture failed to dispose, otherwise success. - */ -errorret_t textureDisposeGL(texturegl_t *texture); \ No newline at end of file diff --git a/src/duskgl/display/texture/textureplatform.h b/src/duskgl/display/texture/textureplatform.h deleted file mode 100644 index 922f7556..00000000 --- a/src/duskgl/display/texture/textureplatform.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/texture/texturegl.h" - -typedef textureformatgl_t textureformatplatform_t; -typedef texturegl_t textureplatform_t; - -#define textureInitPlatform textureInitGL -#define textureDisposePlatform textureDisposeGL \ No newline at end of file diff --git a/src/duskgl/duskgl.h b/src/duskgl/duskgl.h deleted file mode 100644 index b677d88b..00000000 --- a/src/duskgl/duskgl.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#ifdef DUSK_OPENGL_ES - #include - #define GL_COLOR_INDEX8_EXT 0x80E5 - #define GL_FRAMEBUFFER_EXT GL_FRAMEBUFFER - #define GL_FRAMEBUFFER_COMPLETE_EXT GL_FRAMEBUFFER_COMPLETE - #define glCheckFramebufferStatusEXT glCheckFramebufferStatus - #define glDeleteFramebuffersEXT glDeleteFramebuffers - #define glGenFramebuffersEXT glGenFramebuffers - #define glBindFramebufferEXT glBindFramebuffer - #define glFramebufferTexture2DEXT glFramebufferTexture2D - #define GL_COLOR_ATTACHMENT0_EXT GL_COLOR_ATTACHMENT0 - #define glClearDepth(depth) glClearDepthf(depth) -#else - // For some platforms (Vita) we do not include GL extensions. - #ifndef GL_GLEXT_PROTOTYPES - #define GL_GLEXT_PROTOTYPES - #include - #include - #else - #include - #endif -#endif \ No newline at end of file diff --git a/src/duskgl/error/CMakeLists.txt b/src/duskgl/error/CMakeLists.txt deleted file mode 100644 index 9a52452e..00000000 --- a/src/duskgl/error/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - errorgl.c -) \ No newline at end of file diff --git a/src/duskgl/error/errorgl.c b/src/duskgl/error/errorgl.c deleted file mode 100644 index af9ceb39..00000000 --- a/src/duskgl/error/errorgl.c +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "errorgl.h" - -errorret_t errorGLCheck(void) { - GLenum err = glGetError(); - if(err != GL_NO_ERROR) { - errorThrow("GL Error: %d", err); - } - - errorOk(); -} \ No newline at end of file diff --git a/src/duskgl/error/errorgl.h b/src/duskgl/error/errorgl.h deleted file mode 100644 index d73d3f80..00000000 --- a/src/duskgl/error/errorgl.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -/** - * Gets the GL error (if any) and throws it as a dusk style error. - * - * @return Error code, if GL error present. - */ -errorret_t errorGLCheck(void); \ No newline at end of file diff --git a/src/dusklinux/CMakeLists.txt b/src/dusklinux/CMakeLists.txt deleted file mode 100644 index 55a11237..00000000 --- a/src/dusklinux/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Subdirs -add_subdirectory(asset) -add_subdirectory(audio) -add_subdirectory(log) -add_subdirectory(input) -if(DUSK_NETWORK) - add_subdirectory(network) -endif() -add_subdirectory(save) -add_subdirectory(system) -add_subdirectory(time) -add_subdirectory(util) \ No newline at end of file diff --git a/src/dusklinux/asset/CMakeLists.txt b/src/dusklinux/asset/CMakeLists.txt deleted file mode 100644 index 3910a3b8..00000000 --- a/src/dusklinux/asset/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - assetlinux.c -) \ No newline at end of file diff --git a/src/dusklinux/asset/assetlinux.c b/src/dusklinux/asset/assetlinux.c deleted file mode 100644 index 36239586..00000000 --- a/src/dusklinux/asset/assetlinux.c +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "asset/asset.h" -#include "asset/assetdsk.h" -#include "engine/engine.h" -#include "util/string.h" -#include "assert/assert.h" - -errorret_t assetInitLinux(void) { - // Engine may have been provided the launch path - if(ENGINE.argc > 0) { - // Get the directory of the executable - char_t buffer[ASSET_SYSTEM_PATH_MAX]; - stringCopy(buffer, ENGINE.argv[0], ASSET_SYSTEM_PATH_MAX); - size_t len = strlen(buffer); - - // Normalize slashes - for(size_t i = 0; i < ASSET_SYSTEM_PATH_MAX; i++) { - if(buffer[i] == '\0') break; - if(buffer[i] == '\\') buffer[i] = '/'; - } - - // Now find the last slash - char_t *end = buffer + len - 1; - do { - end--; - if(*end == '/') { - *end = '\0'; - break; - } - } while(end != buffer); - - - // Did we find a slash? - if(end != buffer) { - // We found the directory, set as system path - stringCopy(ASSET.platform.systemPath, buffer, ASSET_SYSTEM_PATH_MAX); - } - } else { - // Default system path, intended to be overridden by the platform - stringCopy(ASSET.platform.systemPath, ".", ASSET_SYSTEM_PATH_MAX); - } - - // Open dusk.dsk - char_t searchPath[ASSET_SYSTEM_PATH_MAX]; - const char_t **path = ASSET_LINUX_SEARCH_PATHS; - bool_t opened = false; - do { - char_t temp[ASSET_SYSTEM_PATH_MAX]; - snprintf( - temp, - ASSET_SYSTEM_PATH_MAX, - *path, - ASSET_FILE_NAME - ); - - // Ensure combined length does not exceed ASSET_SYSTEM_PATH_MAX - size_t syslen = strlen(ASSET.platform.systemPath); - size_t slashlen = 1; // for '/' - size_t max_temp = ASSET_SYSTEM_PATH_MAX - syslen - slashlen - 1; - if(strlen(temp) > max_temp) { - temp[max_temp] = '\0'; - } - snprintf( - searchPath, - ASSET_SYSTEM_PATH_MAX, - "%s/%s", - ASSET.platform.systemPath, - temp - ); - printf("Try open asset file: %s\n", searchPath); - - // Try open - if(errorIsNotOk(assetDskOpenFromPath(searchPath, &ASSET.zip))) { - printf("Failed to open asset file: %s\n", searchPath); - continue; - } - opened = true; - - // Save devices with a real filesystem path (Linux) reuse this as - // their base directory - see ASSET.baseDirectory's own comment. - stringCopy(ASSET.baseDirectory, searchPath, ASSET_BASE_DIRECTORY_MAX); - char_t *lastSlash = stringFindLastChar(ASSET.baseDirectory, '/'); - if(lastSlash != NULL) *lastSlash = '\0'; - - break;// Found! - } while(*(++path) != NULL); - - // Did we open the asset? - if(!opened) { - errorThrow("Failed to open asset file."); - } - - errorOk(); -} - -errorret_t assetDisposeLinux(void) { - errorOk(); -} \ No newline at end of file diff --git a/src/dusklinux/asset/assetlinux.h b/src/dusklinux/asset/assetlinux.h deleted file mode 100644 index d45457d7..00000000 --- a/src/dusklinux/asset/assetlinux.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "asset/assetfile.h" - -#define ASSET_SYSTEM_PATH_MAX FILENAME_MAX - -static const char_t *ASSET_LINUX_SEARCH_PATHS[] = { - "%s", - "../%s", - "../../%s", - "data/%s", - "../data/%s", - NULL -}; - -typedef struct { - char_t systemPath[ASSET_SYSTEM_PATH_MAX]; -} assetlinux_t; - -/** - * Initializes the asset system on Linux. - * - * @return Error state if failed, otherwise returns success. - */ -errorret_t assetInitLinux(void); - -/** - * Disposes the asset system on Linux. - * - * @return Error state if failed, otherwise returns success. - */ -errorret_t assetDisposeLinux(void); \ No newline at end of file diff --git a/src/dusklinux/asset/assetplatform.h b/src/dusklinux/asset/assetplatform.h deleted file mode 100644 index 669b8701..00000000 --- a/src/dusklinux/asset/assetplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "asset/assetlinux.h" - -typedef assetlinux_t assetplatform_t; -#define assetInitPlatform assetInitLinux -#define assetDisposePlatform assetDisposeLinux \ No newline at end of file diff --git a/src/dusklinux/audio/CMakeLists.txt b/src/dusklinux/audio/CMakeLists.txt deleted file mode 100644 index a2accbc6..00000000 --- a/src/dusklinux/audio/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -# 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 - audiolinux.c - audiostreamlinux.c -) - -# No hardware MP3 decoder on Linux - uses the shared libmad-based software -# backend + decode-ahead ring in src/duskmad instead (also used by -# Dolphin). See audiostreammp3decodersw.h's own comment on why libmad -# (GPL-licensed) replaced this project's original minimp3-based -# implementation. Linking stays here (rather than in src/duskmad itself) -# since Linux and Dolphin genuinely obtain libmad differently - -# find_package() here, the bundled devkitPPC/libogc toolchain copy there. -if(NOT libmad_FOUND) - find_package(libmad REQUIRED) -endif() -target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC libmad::mad) diff --git a/src/dusklinux/audio/audiolinux.c b/src/dusklinux/audio/audiolinux.c deleted file mode 100644 index 078a9524..00000000 --- a/src/dusklinux/audio/audiolinux.c +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiolinux.h" -#include - -errorret_t audioLinuxInit() { - if(SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { - errorThrow("Failed to initialize SDL2 audio subsystem: %s", SDL_GetError()); - } - - errorOk(); -} - -errorret_t audioLinuxUpdate() { - errorOk(); -} - -errorret_t audioLinuxDispose() { - SDL_QuitSubSystem(SDL_INIT_AUDIO); - - errorOk(); -} diff --git a/src/dusklinux/audio/audiolinux.h b/src/dusklinux/audio/audiolinux.h deleted file mode 100644 index a9bb4ce2..00000000 --- a/src/dusklinux/audio/audiolinux.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -/** - * Initializes the Linux-specific audio subsystem state. - * - * @return Error state if any. - */ -errorret_t audioLinuxInit(); - -/** - * Updates the Linux-specific audio subsystem state. Called once per frame. - * - * @return Error state if any. - */ -errorret_t audioLinuxUpdate(); - -/** - * Disposes the Linux-specific audio subsystem state. - * - * @return Error state if any. - */ -errorret_t audioLinuxDispose(); diff --git a/src/dusklinux/audio/audioplatform.h b/src/dusklinux/audio/audioplatform.h deleted file mode 100644 index 51888682..00000000 --- a/src/dusklinux/audio/audioplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audiolinux.h" - -#define audioPlatformInit audioLinuxInit -#define audioPlatformUpdate audioLinuxUpdate -#define audioPlatformDispose audioLinuxDispose diff --git a/src/dusklinux/audio/audiostreamlinux.c b/src/dusklinux/audio/audiostreamlinux.c deleted file mode 100644 index 42bcf123..00000000 --- a/src/dusklinux/audio/audiostreamlinux.c +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreamlinux.h" -#include "audio/stream/audiostream.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include "error/error.h" - -// How many frames of lead time to keep queued ahead of playback. Matches -// SDL_AudioSpec.samples below - the device's own internal buffer size - so -// this is "never let the queue run drier than one SDL-internal buffer." -#define AUDIO_LINUX_LEAD_FRAMES 4096 - -// How many frames audioStreamLinuxFeed() reads and queues per call - a -// few multiples of the lead margin, so one top-up comfortably outlasts -// the time between Update() calls without ever needing to hold more than -// this much decoded audio in memory at once, regardless of how long the -// underlying clip is. -#define AUDIO_LINUX_WINDOW_FRAMES (AUDIO_LINUX_LEAD_FRAMES * 4) - -errorret_t audioStreamLinuxInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - SDL_AudioSpec desired; - memoryZero(&desired, sizeof(SDL_AudioSpec)); - desired.freq = (int) stream->sampleRate; - desired.format = AUDIO_S16SYS; - desired.channels = stream->channels; - desired.samples = AUDIO_LINUX_LEAD_FRAMES; - - stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0); - if(stream->platform.device == 0) { - errorThrow("Failed to open SDL2 audio device: %s", SDL_GetError()); - } - - errorChain(audioStreamMp3RingInitIfNeeded(stream)); - - errorOk(); -} - -errorret_t audioStreamLinuxDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - // Must happen first: audioStreamMp3Dispose() (called right after this - // function returns, from the shared audioStreamDispose()) tears down - // stream->mp3's decoder/file - the decode thread must be fully parked - // before that, not just before this function's own SDL cleanup below. - errorChain(audioStreamMp3RingDisposeIfNeeded(stream)); - - SDL_CloseAudioDevice(stream->platform.device); - - errorOk(); -} - -errorret_t audioStreamLinuxBuffer(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - const size_t frameSize = stream->channels * sizeof(int16_t); - const size_t totalFrames = audioStreamGetTotalFrames(stream); - - // Consumed synchronously (right here, in the same call that decided to - // (re)buffer) rather than left for later - see startFrame's own comment. - const size_t startFrame = mathMin(stream->startFrame, totalFrames); - const bool_t seeking = stream->seeking; - stream->startFrame = 0; - stream->seeking = false; - - // This pass's stop point - the current loop segment's end, or the true - // end of the clip if not looping (or if startFrame already landed past - // the loop end, e.g. a seek into an outro). See - // audioStreamComputeEndFrame()'s own comment. - const size_t endFrame = - audioStreamComputeEndFrame(stream, startFrame, totalFrames); - - // Only an explicit seek discards whatever's still queued and jumps - - // a natural loop restart deliberately leaves the previous pass's tail - // (AUDIO_LINUX_LEAD_FRAMES worth) queued and appends the new pass after - // it, which is what makes looping gapless (see IsFinished()'s comment). - // Clearing here on every pass would destroy that overlap and - // reintroduce the exact gap this was built to avoid. - if(seeking) { - SDL_ClearQueuedAudio(stream->platform.device); - } - - // For MP3 streams, the background decode thread reads via the same - // stream->mp3 decoder/file audioStreamSeek() below is about to mutate - - // must be fully stopped first, every time a new pass begins. Unlike the - // SDL_ClearQueuedAudio() call above, this is unconditional regardless of - // the seeking flag: that flag only controls whether the SDL queue's - // leftover tail is discarded (for gapless natural loop restarts), not - // whether decode needs to re-seek - MP3 has no cheap seek either way, - // see audioStreamSeek()'s own comment. - errorChain(audioStreamMp3RingStopIfNeeded(stream)); - - errorChain(audioStreamSeek(stream, startFrame)); - - stream->platform.position = startFrame; - stream->platform.endFrame = endFrame; - stream->platform.passStartTicks = SDL_GetTicks(); - stream->platform.passStartPosition = startFrame; - - // Launch this pass's decode thread now that position/endFrame are - // finalized - it starts filling the ring immediately in the background, - // ahead of audioStreamLinuxFeed() below even asking for anything. - errorChain(audioStreamMp3RingStartIfNeeded(stream, startFrame, endFrame)); - - errorChain(audioStreamLinuxFeed(stream)); - - SDL_PauseAudioDevice(stream->platform.device, 0); - - errorOk(); -} - -errorret_t audioStreamLinuxFeed(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - const size_t frameSize = stream->channels * sizeof(int16_t); - const size_t framesRemaining = ( - stream->platform.position < stream->platform.endFrame - ? stream->platform.endFrame - stream->platform.position - : 0 - ); - - // IsFinished() only calls this once SDL_GetQueuedAudioSize() reports the - // queue has drained below the lead margin - normally a reliable gate, - // but confirmed unreliable for at least one real device (Bluetooth, - // whose real connection/startup latency means PipeWire can under-report - // how much is actually queued while the link is still establishing). - // When that happens, repeated Feed() calls each see a falsely-small - // queued size and each add another full window before the reported size - // catches up to reality - heard as the whole clip racing far ahead of - // real playback right from the start, with stutters where the audio - // backend then has to catch up or drop the backlog. Independent of - // whatever SDL reports, never let this stream's own read position get - // further ahead of how much real wall-clock time has actually elapsed - // since the pass started than a lead-plus-one-window margin - a - // device-report-independent ceiling that still allows a full normal - // window per call under real (correctly-reported) conditions. - const Uint32 elapsedMs = SDL_GetTicks() - stream->platform.passStartTicks; - const size_t audibleFrames = (size_t) ( - ((double) elapsedMs / 1000.0) * (double) stream->sampleRate - ); - // Margin is just one lead's worth, not lead+window - deliberately tight, - // prioritizing "never let position get far ahead of real time" over - // per-call efficiency, while this is still being tracked down. Costs - // more (smaller) top-up calls in the steady state than a looser margin - // would, which is an acceptable trade for now. - const size_t maxPosition = ( - stream->platform.passStartPosition + audibleFrames + - AUDIO_LINUX_LEAD_FRAMES - ); - const size_t positionBudget = maxPosition > stream->platform.position - ? maxPosition - stream->platform.position - : 0; - - const size_t framesToRead = mathMin( - mathMin(framesRemaining, (size_t) AUDIO_LINUX_WINDOW_FRAMES), positionBudget - ); - - if(framesToRead == 0) { - errorOk(); - } - - int16_t *chunk = memoryAllocate(framesToRead * frameSize); - size_t framesRead = 0; - bool_t notReadyYet = false; - // For MP3 streams, drains already-decoded PCM out of the background ring - // instead of decoding directly - the expensive part (asset I/O + libmad - // decode) already happened on the decode thread, off the main thread. - // See audiostreammp3ring.h's own top-of-file comment for why. For - // everything else, this is just a plain audioStreamRead(). - errorret_t ret = audioStreamReadForPlayback( - stream, chunk, framesToRead, &framesRead, ¬ReadyYet - ); - if(errorIsNotOk(ret)) { - memoryFree(chunk); - errorChain(ret); - } - if(notReadyYet) { - // Decode hasn't caught up to a full window yet - not an error, just - // try again next poll. - memoryFree(chunk); - errorOk(); - } - - const size_t bytesRead = framesRead * frameSize; - int queued; - - if(stream->volume >= 1.0f) { - // Nothing to mix at full volume - queue the window directly instead of - // allocating/zeroing a same-size scratch buffer just to copy it in. - queued = SDL_QueueAudio(stream->platform.device, chunk, (Uint32) bytesRead); - } else { - uint8_t *mixed = memoryAllocate(bytesRead); - memoryZero(mixed, bytesRead); - SDL_MixAudioFormat( - mixed, (uint8_t *) chunk, AUDIO_S16SYS, (Uint32) bytesRead, - (int) (stream->volume * SDL_MIX_MAXVOLUME) - ); - queued = SDL_QueueAudio(stream->platform.device, mixed, (Uint32) bytesRead); - memoryFree(mixed); - } - memoryFree(chunk); - - if(queued != 0) { - errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError()); - } - - stream->platform.position += framesRead; - - errorOk(); -} - -bool_t audioStreamLinuxIsFinished(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - // Reports "finished" (ready to loop/end) with AUDIO_LINUX_LEAD_FRAMES of - // margin still queued, rather than waiting for the queue to actually run - // dry - queuing the next pass this early never causes overlap (SDL's - // queue is a plain FIFO), it just avoids ever going silent while our - // once-per-frame Update() notices and catches up. Waiting for truly - // empty guarantees a gap by definition: silence has already started by - // the time "empty" can be observed. - const Uint32 leadBytes = (Uint32) ( - AUDIO_LINUX_LEAD_FRAMES * stream->channels * sizeof(int16_t) - ); - - if(SDL_GetQueuedAudioSize(stream->platform.device) > leadBytes) { - return false; - } - - // Below the lead margin - if more of the current pass is left to read, - // top up now rather than reporting finished, which would otherwise - // trigger a full loop-restart/end while still mid-pass, just because - // the queue happened to run low. - if(stream->platform.position < stream->platform.endFrame) { - errorret_t ret = audioStreamLinuxFeed(stream); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - return true; // Can't recover - let the shared layer end/loop it. - } - return false; - } - - return true; -} diff --git a/src/dusklinux/audio/audiostreamlinux.h b/src/dusklinux/audio/audiostreamlinux.h deleted file mode 100644 index 6d6dfb39..00000000 --- a/src/dusklinux/audio/audiostreamlinux.h +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "audiostreammp3ring.h" -#include - -typedef struct audiostream_s audiostream_t; - -typedef struct { - SDL_AudioDeviceID device; - - // Frame offset (relative to the start of the PCM data) the next - // audioStreamLinuxFeed() call should read from - advances as each - // window is queued, so a long clip is streamed progressively rather - // than read/queued all at once. - size_t position; - - // Frame offset this pass stops at (the current loop segment's end, or - // the true end of the clip if not looping) - recomputed by - // audioStreamLinuxBuffer() at the start of each pass. - size_t endFrame; - - // SDL_GetTicks() value and starting frame position when the current - // pass started - together, what audioStreamLinuxFeed() paces its own - // feeding rate against (see its own comment on why it doesn't trust - // SDL_GetQueuedAudioSize() alone). - Uint32 passStartTicks; - size_t passStartPosition; - - // Background decode-ahead ring, shared with Dolphin - only - // initialized/used for AUDIO_STREAM_TYPE_MP3 streams (PCM/WAV reads are - // cheap plain copies and don't need this). See audiostreammp3ring.h's - // own comment. - audiostreammp3ring_t mp3Ring; -} audiostreamlinux_t; - -/** - * Initializes the Linux-specific playback state of an audio stream by - * opening an SDL2 audio device matching the stream's PCM format. For MP3 - * streams, also initializes the background decode-ahead ring - * (audioStreamMp3RingInit()) - see audiostreammp3ring.h's own comment. - * - * @param stream The audio stream to initialize. - * @return Error state if any. - */ -errorret_t audioStreamLinuxInit(audiostream_t *stream); - -/** - * Disposes the Linux-specific playback state of an audio stream: for MP3 - * streams, first fully stops and frees the background decode ring - * (audioStreamMp3RingDispose()) - must happen before the shared MP3 layer - * tears down its decoder/file - then closes the SDL2 audio device. - * - * @param stream The audio stream to dispose. - * @return Error state if any. - */ -errorret_t audioStreamLinuxDispose(audiostream_t *stream); - -/** - * Starts a new playback pass: seeks the stream's PCM read cursor to - * stream->startFrame (clearing the SDL queue first if this is an explicit - * seek rather than a natural loop restart - see stream->seeking's own - * comment), determines this pass's loop-segment end. For MP3 streams, also - * stops the previous pass's background decode thread - * (audioStreamMp3RingStop(), which must complete before the seek below - - * see its own comment) and starts a fresh one for this pass - * (audioStreamMp3RingStart()) once position/endFrame are finalized. Finally - * queues the first window via audioStreamLinuxFeed() - for MP3 streams - * this may find the ring still empty and queue nothing yet; the next few - * polls pick it up once decode has caught up. - * - * @param stream The audio stream to output. - * @return Error state if any. - */ -errorret_t audioStreamLinuxBuffer(audiostream_t *stream); - -/** - * Reads and queues one bounded window of PCM data (AUDIO_LINUX_WINDOW_FRAMES) - * from the stream's current platform.position up to platform.endFrame, - * advancing platform.position by however many frames were actually read. - * A no-op once platform.position has reached platform.endFrame. - * - * For MP3 streams, the PCM data comes from draining the background decode - * ring (audioStreamMp3RingDrain()) instead of decoding directly - see - * audiostreammp3ring.h's own comment for why. - * - * Kept as a small, bounded read/queue operation (rather than the whole - * pass at once) specifically so a long clip is never fully resident in - * memory at once - called both by audioStreamLinuxBuffer() (the first - * window of a pass) and by audioStreamLinuxIsFinished() (subsequent - * top-ups as the SDL queue drains). - * - * @param stream The audio stream to feed. - * @return Error state if any. - */ -errorret_t audioStreamLinuxFeed(audiostream_t *stream); - -/** - * Checks whether the stream's SDL2 audio device has finished playing all - * queued data for the current pass. As a side effect, tops up the queue - * (via audioStreamLinuxFeed()) whenever it's running low but more of the - * current pass remains to be read, rather than reporting finished early. - * - * @param stream The audio stream to check. - * @return true if playback has finished, false otherwise. - */ -bool_t audioStreamLinuxIsFinished(audiostream_t *stream); diff --git a/src/dusklinux/audio/audiostreammp3decoder.h b/src/dusklinux/audio/audiostreammp3decoder.h deleted file mode 100644 index 2cb931d8..00000000 --- a/src/dusklinux/audio/audiostreammp3decoder.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once - -// Linux has no hardware MP3 decoder to prefer, so it uses the shared -// libmad-based software backend (also used by Dolphin) - see that -// header's own documentation for the actual struct/interface, including -// why libmad (GPL-licensed) was chosen over the MIT-licensed minimp3 this -// used to be. -#include "audiostreammp3decodersw.h" diff --git a/src/dusklinux/audio/audiostreamplatform.h b/src/dusklinux/audio/audiostreamplatform.h deleted file mode 100644 index 3251c189..00000000 --- a/src/dusklinux/audio/audiostreamplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audiostreamlinux.h" - -typedef audiostreamlinux_t audiostreamplatform_t; - -#define audioStreamPlatformInit audioStreamLinuxInit -#define audioStreamPlatformDispose audioStreamLinuxDispose -#define audioStreamPlatformBuffer audioStreamLinuxBuffer -#define audioStreamPlatformIsFinished audioStreamLinuxIsFinished diff --git a/src/dusklinux/display/displayplatform.h b/src/dusklinux/display/displayplatform.h deleted file mode 100644 index fa23c6d8..00000000 --- a/src/dusklinux/display/displayplatform.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/displaysdl2.h" - -typedef displaysdl2_t displayplatform_t; - -#define displayPlatformInit displaySDL2Init -#define displayPlatformUpdate displaySDL2Update -#define displayPlatformSwap displaySDL2Swap -#define displayPlatformSetState displaySDL2SetState -#define displayPlatformDispose displaySDL2Dispose \ No newline at end of file diff --git a/src/dusklinux/duskplatform.h b/src/dusklinux/duskplatform.h deleted file mode 100644 index 223902d5..00000000 --- a/src/dusklinux/duskplatform.h +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusksdl2.h" \ No newline at end of file diff --git a/src/dusklinux/input/CMakeLists.txt b/src/dusklinux/input/CMakeLists.txt deleted file mode 100644 index aa6698fd..00000000 --- a/src/dusklinux/input/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - inputlinux.c -) \ No newline at end of file diff --git a/src/dusklinux/input/inputlinux.c b/src/dusklinux/input/inputlinux.c deleted file mode 100644 index aaf268d3..00000000 --- a/src/dusklinux/input/inputlinux.c +++ /dev/null @@ -1,550 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "input/input.h" - -inputbuttondata_t INPUT_BUTTON_DATA[] = { - #ifdef DUSK_INPUT_GAMEPAD - { .name = "gamepad_a", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_A } }, - { .name = "gamepad_b", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_B } }, - { .name = "gamepad_x", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_X } }, - { .name = "gamepad_y", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_Y } }, - { .name = "gamepad_start", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_START } }, - { .name = "gamepad_back", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_BACK } }, - { .name = "gamepad_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_UP } }, - { .name = "gamepad_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, - { .name = "gamepad_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, - { .name = "gamepad_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_RIGHT } }, - { .name = "gamepad_l1", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_LEFTSHOULDER } }, - { .name = "gamepad_r1", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER } }, - { .name = "gamepad_l3", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_LEFTSTICK } }, - { .name = "gamepad_r3", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_RIGHTSTICK } }, - { .name = "gamepad_lstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTY, .positive = true } } }, - { .name = "gamepad_lstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTY, .positive = false } } }, - { .name = "gamepad_lstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTX, .positive = true } } }, - { .name = "gamepad_lstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTX, .positive = false } } }, - { .name = "gamepad_rstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_RIGHTY, .positive = true } } }, - { .name = "gamepad_rstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_RIGHTY, .positive = false } } }, - { .name = "gamepad_rstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_RIGHTX, .positive = true } } }, - { .name = "gamepad_rstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_RIGHTX, .positive = false } } }, - { .name = "gamepad_l2", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_TRIGGERLEFT, - .positive = true } } }, - { .name = "gamepad_r2", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_TRIGGERRIGHT, - .positive = true } } }, - #endif - - - #ifdef DUSK_INPUT_POINTER - { .name = "mouse_x", { - .type = INPUT_BUTTON_TYPE_POINTER, - .pointerAxis = INPUT_POINTER_AXIS_X } }, - { .name = "mouse_y", { - .type = INPUT_BUTTON_TYPE_POINTER, - .pointerAxis = INPUT_POINTER_AXIS_Y } }, - { .name = "mouse_wheel_x", { - .type = INPUT_BUTTON_TYPE_POINTER, - .pointerAxis = INPUT_POINTER_AXIS_WHEEL_X } }, - { .name = "mouse_wheel_y", { - .type = INPUT_BUTTON_TYPE_POINTER, - .pointerAxis = INPUT_POINTER_AXIS_WHEEL_Y } }, - #endif - - #ifdef DUSK_INPUT_KEYBOARD - { .name = "a", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_A } }, - { .name = "b", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_B } }, - { .name = "c", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_C } }, - { .name = "d", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_D } }, - { .name = "e", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_E } }, - { .name = "f", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F } }, - { .name = "g", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_G } }, - { .name = "h", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_H } }, - { .name = "i", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_I } }, - { .name = "j", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_J } }, - { .name = "k", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_K } }, - { .name = "l", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_L } }, - { .name = "m", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_M } }, - { .name = "n", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_N } }, - { .name = "o", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_O } }, - { .name = "p", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_P } }, - { .name = "q", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_Q } }, - { .name = "r", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_R } }, - { .name = "s", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_S } }, - { .name = "t", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_T } }, - { .name = "u", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_U } }, - { .name = "v", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_V } }, - { .name = "w", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_W } }, - { .name = "x", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_X } }, - { .name = "y", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_Y } }, - { .name = "z", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_Z } }, - - { .name = "0", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_0 } }, - { .name = "1", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_1 } }, - { .name = "2", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_2 } }, - { .name = "3", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_3 } }, - { .name = "4", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_4 } }, - { .name = "5", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_5 } }, - { .name = "6", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_6 } }, - { .name = "7", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_7 } }, - { .name = "8", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_8 } }, - { .name = "9", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_9 } }, - - { .name = "space", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_SPACE } }, - { .name = "shift", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LSHIFT } }, - { .name = "lshift", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LSHIFT } }, - { .name = "rshift", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RSHIFT } }, - { .name = "lctrl", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LCTRL } }, - { .name = "rctrl", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RCTRL } }, - { .name = "ctrl", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LCTRL } }, - { .name = "lalt", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LALT } }, - { .name = "ralt", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RALT } }, - { .name = "tab", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_TAB } }, - { .name = "enter", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RETURN } }, - { .name = "backspace", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_BACKSPACE } }, - { .name = "escape", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_ESCAPE } }, - { .name = "esc", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_ESCAPE } }, - { .name = "up", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_UP } }, - { .name = "down", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_DOWN } }, - { .name = "left", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LEFT } }, - { .name = "right", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RIGHT } }, - { .name = "pageup", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_PAGEUP } }, - { .name = "pagedown", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_PAGEDOWN } }, - { .name = "home", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_HOME } }, - { .name = "end", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_END } }, - { .name = "insert", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_INSERT } }, - { .name = "delete", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_DELETE } }, - - { .name = "f1", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F1 } }, - { .name = "f2", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F2 } }, - { .name = "f3", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F3 } }, - { .name = "f4", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F4 } }, - { .name = "f5", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F5 } }, - { .name = "f6", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F6 } }, - { .name = "f7", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F7 } }, - { .name = "f8", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F8 } }, - { .name = "f9", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F9 } }, - { .name = "f10", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F10 } }, - { .name = "f11", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F11 } }, - { .name = "f12", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F12 } }, - { .name = "f13", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F13 } }, - { .name = "f14", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F14 } }, - { .name = "f15", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F15 } }, - { .name = "f16", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F16 } }, - { .name = "f17", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F17 } }, - { .name = "f18", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F18 } }, - { .name = "f19", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F19 } }, - { .name = "f20", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F20 } }, - { .name = "f21", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F21 } }, - { .name = "f22", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F22 } }, - { .name = "f23", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F23 } }, - { .name = "f24", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_F24 } }, - - - { .name = "minus", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_MINUS } }, - { .name = "equals", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_EQUALS } }, - { .name = "leftbracket", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LEFTBRACKET } }, - { .name = "rightbracket", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RIGHTBRACKET } }, - { .name = "backslash", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_BACKSLASH } }, - { .name = "semicolon", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_SEMICOLON } }, - { .name = "apostrophe", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_APOSTROPHE } }, - { .name = "grave", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_GRAVE } }, - { .name = "comma", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_COMMA } }, - { .name = "period", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_PERIOD } }, - { .name = "slash", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_SLASH } }, - - { .name = "caps", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_CAPSLOCK } }, - { .name = "capslock", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_CAPSLOCK } }, - { .name = "numlock", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_NUMLOCKCLEAR } }, - { .name = "scrollock", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_SCROLLLOCK } }, - - { .name = "-", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_MINUS } }, - { .name = "=", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_EQUALS } }, - { .name = "[", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_LEFTBRACKET } }, - { .name = "]", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_RIGHTBRACKET } }, - { .name = "\\", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_BACKSLASH } }, - { .name = ";", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_SEMICOLON } }, - { .name = "'", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_APOSTROPHE } }, - { .name = "`", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_GRAVE } }, - { .name = ",", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_COMMA } }, - { .name = ".", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_PERIOD } }, - { .name = "/", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_SLASH } }, - - { .name = "kp_0", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_0 } }, - { .name = "kp_1", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_1 } }, - { .name = "kp_2", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_2 } }, - { .name = "kp_3", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_3 } }, - { .name = "kp_4", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_4 } }, - { .name = "kp_5", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_5 } }, - { .name = "kp_6", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_6 } }, - { .name = "kp_7", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_7 } }, - { .name = "kp_8", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_8 } }, - { .name = "kp_9", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_9 } }, - { .name = "kp_period", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_PERIOD } }, - { .name = "kp_divide", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_DIVIDE } }, - { .name = "kp_multiply", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_MULTIPLY } }, - { .name = "kp_minus", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_MINUS } }, - { .name = "kp_plus", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_PLUS } }, - { .name = "kp_enter", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_ENTER } }, - { .name = "kp_equals", { - .type = INPUT_BUTTON_TYPE_KEYBOARD, - .scancode = SDL_SCANCODE_KP_EQUALS } }, - #endif - - { .name = NULL }, -}; - -errorret_t inputInitLinux(void) { - #define X(buttonName, buttonAction) \ - inputBind(inputButtonGetByName(buttonName), buttonAction); - - #ifdef DUSK_INPUT_KEYBOARD - X("w", INPUT_ACTION_UP); - X("s", INPUT_ACTION_DOWN); - X("a", INPUT_ACTION_LEFT); - X("d", INPUT_ACTION_RIGHT); - X("left", INPUT_ACTION_LEFT); - X("right", INPUT_ACTION_RIGHT); - X("up", INPUT_ACTION_UP); - X("down", INPUT_ACTION_DOWN); - X("enter", INPUT_ACTION_ACCEPT); - X("e", INPUT_ACTION_ACCEPT); - X("space", INPUT_ACTION_ACCEPT); - X("tab", INPUT_ACTION_CANCEL); - X("q", INPUT_ACTION_CANCEL); - X("escape", INPUT_ACTION_RAGEQUIT); - X("`", INPUT_ACTION_CONSOLE); - #endif - - #ifdef DUSK_INPUT_GAMEPAD - X("gamepad_up", INPUT_ACTION_UP); - X("gamepad_down", INPUT_ACTION_DOWN); - X("gamepad_left", INPUT_ACTION_LEFT); - X("gamepad_right", INPUT_ACTION_RIGHT); - X("gamepad_a", INPUT_ACTION_ACCEPT); - X("gamepad_b", INPUT_ACTION_CANCEL); - X("gamepad_back", INPUT_ACTION_RAGEQUIT); - X("gamepad_start", INPUT_ACTION_PAUSE); - X("gamepad_lstick_up", INPUT_ACTION_UP); - X("gamepad_lstick_down", INPUT_ACTION_DOWN); - X("gamepad_lstick_left", INPUT_ACTION_LEFT); - X("gamepad_lstick_right", INPUT_ACTION_RIGHT); - #endif - - #ifdef DUSK_INPUT_POINTER - X("mouse_x", INPUT_ACTION_POINTERX); - X("mouse_y", INPUT_ACTION_POINTERY); - #endif - - #undef X - - errorOk(); -} - -float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return 0.2f; -} \ No newline at end of file diff --git a/src/dusklinux/input/inputlinux.h b/src/dusklinux/input/inputlinux.h deleted file mode 100644 index ba962fd1..00000000 --- a/src/dusklinux/input/inputlinux.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "input/inputsdl2.h" - -/** - * Initializes the input system on Linux. - * - * @return An error code indicating success or failure. - */ -errorret_t inputInitLinux(void); \ No newline at end of file diff --git a/src/dusklinux/input/inputplatform.h b/src/dusklinux/input/inputplatform.h deleted file mode 100644 index b390dcff..00000000 --- a/src/dusklinux/input/inputplatform.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "input/inputlinux.h" - -#define inputInitPlatform inputInitLinux \ No newline at end of file diff --git a/src/dusklinux/log/CMakeLists.txt b/src/dusklinux/log/CMakeLists.txt deleted file mode 100644 index 0d4088ff..00000000 --- a/src/dusklinux/log/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2025 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - log.c -) \ No newline at end of file diff --git a/src/dusklinux/log/log.c b/src/dusklinux/log/log.c deleted file mode 100644 index 951d93bb..00000000 --- a/src/dusklinux/log/log.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "log/log.h" - -void logDebug(const char_t *message, ...) { - va_list args; - va_start(args, message); - vprintf(message, args); - va_end(args); - fflush(stdout); -} - -void logError(const char_t *message, ...) { - va_list args; - va_start(args, message); - vprintf(message, args); - va_end(args); - fflush(stdout); -} \ No newline at end of file diff --git a/src/dusklinux/network/CMakeLists.txt b/src/dusklinux/network/CMakeLists.txt deleted file mode 100644 index c1322dc3..00000000 --- a/src/dusklinux/network/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - networklinux.c -) diff --git a/src/dusklinux/network/networklinux.c b/src/dusklinux/network/networklinux.c deleted file mode 100644 index e3ee4e98..00000000 --- a/src/dusklinux/network/networklinux.c +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "networklinux.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assert.h" - -errorret_t networkLinuxInit() { - errorOk(); -} - -errorret_t networkLinuxUpdate() { - errorOk(); -} - -bool_t networkLinuxIsConnected() { - // Call the OS network stack to check for connectivity. - struct ifaddrs *ifaddr, *ifa; - if(getifaddrs(&ifaddr) == -1) { - return false; - } - - // Check if any non loopback interfaces have running flag set. - bool_t connected = false; - for(ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { - if(!ifa->ifa_name || !ifa->ifa_flags) continue; - - // Skip loopback (localhost) - if(ifa->ifa_flags & IFF_LOOPBACK) continue; - - // Is interface up and running? - if(!(ifa->ifa_flags & IFF_UP)) continue; - if(!(ifa->ifa_flags & IFF_RUNNING)) continue; - - connected = true; - break; - } - - // Free the linked list of interfaces - freeifaddrs(ifaddr); - - return connected; -} - -errorret_t networkLinuxDispose() { - errorOk(); -} - -networkinfo_t networkLinuxGetInfo() { - networkinfo_t info; - memoryZero(&info, sizeof(networkinfo_t)); - - bool_t found = false; - - struct ifaddrs *ifaddr, *ifa; - if(getifaddrs(&ifaddr) == -1) { - assertUnreachable("getifaddrs failed"); - } - - // Find the first non-loopback interface with an IP address - for(ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { - if(!ifa->ifa_name || !ifa->ifa_flags) continue; - - // Skip loopback (localhost) - if(ifa->ifa_flags & IFF_LOOPBACK) continue; - - // Is interface up and running? - if(!(ifa->ifa_flags & IFF_UP)) continue; - if(!(ifa->ifa_flags & IFF_RUNNING)) continue; - if(ifa->ifa_addr == NULL) continue; - - // Check for IPv4 address - if(ifa->ifa_addr->sa_family == AF_INET) { - info.type = NETWORK_TYPE_IPV4; - - struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; - memoryCopy( - info.ipv4.ip, - &sa->sin_addr, - NETWORK_INFO_IPV4_OCTET_COUNT - ); - - found = true; - - // sa = (struct sockaddr_in *)ifa->ifa_netmask; - // memoryCopy( - // info.ipv4.subnet, - // &sa->sin_addr, - // NETWORK_INFO_IPV4_OCTET_COUNT - // ); - break; - } - - // Check for IPv6 address - #ifdef DUSK_NETWORK_IPV6 - if(ifa->ifa_addr->sa_family == AF_INET6) { - info.type = NETWORK_TYPE_IPV6; - - struct sockaddr_in6 *sa = (struct sockaddr_in6 *)ifa->ifa_addr; - memoryCopy( - info.ipv6.ip, - &sa->sin6_addr, - NETWORK_INFO_IPV6_OCTET_COUNT - ); - found = true; - break; - } - #endif - } - - freeifaddrs(ifaddr); - - assertTrue(found, "No active network interface found?"); - return info; -} \ No newline at end of file diff --git a/src/dusklinux/network/networklinux.h b/src/dusklinux/network/networklinux.h deleted file mode 100644 index d81501bd..00000000 --- a/src/dusklinux/network/networklinux.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "network/networkinfo.h" -#include -#include -#include - -typedef struct { - void *nothing; -} networklinux_t; - -/** - * Initializes the network manager. Must be called before any other network - * functions. - * - * @return Any error that occurs. - */ -errorret_t networkLinuxInit(); - -/** - * Updates the network manager, called once per frame to handle completed - * HTTP requests. - * - * @return Any error that occurs. - */ -errorret_t networkLinuxUpdate(); - -/** - * Returns true, Linux uses the OS network stack which is always available. - * - * @return true - */ -bool_t networkLinuxIsConnected(); - -/** - * Disposes the network manager. - * - * @return Any error that occurs. - */ -errorret_t networkLinuxDispose(); - -/** - * Gets the network information for the currently active network connection. - * - * @return Network information for the currently active network connection. - */ -networkinfo_t networkLinuxGetInfo(); \ No newline at end of file diff --git a/src/dusklinux/network/networkplatform.h b/src/dusklinux/network/networkplatform.h deleted file mode 100644 index 0fc02849..00000000 --- a/src/dusklinux/network/networkplatform.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "networklinux.h" - -#define networkPlatformInit networkLinuxInit -#define networkPlatformUpdate networkLinuxUpdate -#define networkPlatformDispose networkLinuxDispose -#define networkPlatformIsConnected networkLinuxIsConnected -#define networkPlatformGetInfo networkLinuxGetInfo - -typedef networklinux_t networkplatform_t; \ No newline at end of file diff --git a/src/dusklinux/save/CMakeLists.txt b/src/dusklinux/save/CMakeLists.txt deleted file mode 100644 index 85be1d9f..00000000 --- a/src/dusklinux/save/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - savedevicelinux.c -) diff --git a/src/dusklinux/save/savedevicelinux.c b/src/dusklinux/save/savedevicelinux.c deleted file mode 100644 index 9f586b0f..00000000 --- a/src/dusklinux/save/savedevicelinux.c +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/savedevice.h" -#include "save/savedevicelinux.h" -#include "save/slot/saveslot.h" -#include "save/settings/savesettings.h" -#include "save/savejson.h" -#include "asset/asset.h" -#include "assert/assert.h" -#include "util/mkdirp.h" -#include "util/string.h" - -errorret_t saveDeviceLinuxInit(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - - device->state = SAVE_DEVICE_STATE_UNKNOWN; - - errorOk(); -} - -errorret_t saveDeviceLinuxUpdate(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - errorOk(); -} - -void saveDeviceLinuxCheckAvailability(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - assertTrue( - device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, - "Device state incorrect?" - ); - - // Resolve the save directory - can't actually fail (just a - // stringFormat), unlike when this derived from $HOME, but this function - // returns void (via saveDeviceFireCallback below) rather than - // errorret_t, so there's no return-early errorChain to use here. - char_t directory[SAVE_DEVICE_LINUX_PATH_MAX]; - errorret_t directoryResult = saveDeviceLinuxGetDirectory( - directory, sizeof(directory) - ); - if(errorIsNotOk(directoryResult)) errorCatch(errorPrint(directoryResult)); - - // Mkdirp the save directory. - int mkdirpResult = mkdirp(directory, 0700); - if(mkdirpResult != 0) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.linux.mkdirp_failed"; - return saveDeviceFireCallback(device); - } - - // The save device is available. - device->state = SAVE_DEVICE_STATE_AVAILABLE; - device->reasonKey = NULL; - saveDeviceFireCallback(device); -} - -errorret_t saveDeviceLinuxGetDirectory(char_t *dest, const size_t destSize) { - assertNotNull(dest, "dest cannot be null"); - - stringFormat( - dest, destSize, "%s/%s", - ASSET.baseDirectory, SAVE_DEVICE_LINUX_SAVES_SUBDIR_NAME - ); - errorOk(); -} - -errorret_t saveDeviceLinuxGetSettingsPath( - char_t *dest, - const size_t destSize -) { - assertNotNull(dest, "dest cannot be null"); - - char_t directory[SAVE_DEVICE_LINUX_PATH_MAX]; - errorChain(saveDeviceLinuxGetDirectory(directory, sizeof(directory))); - - stringFormat( - dest, destSize, "%s/%s", directory, SAVE_DEVICE_LINUX_SETTINGS_FILENAME - ); - errorOk(); -} - -errorret_t saveDeviceLinuxGetSlotPath( - char_t *dest, - const size_t destSize, - const uint8_t slotIndex -) { - assertNotNull(dest, "dest cannot be null"); - - char_t directory[SAVE_DEVICE_LINUX_PATH_MAX]; - errorChain(saveDeviceLinuxGetDirectory(directory, sizeof(directory))); - - char_t filename[SAVE_SLOT_NAME_LENGTH + 16]; - stringFormat( - filename, sizeof(filename) - 1, - SAVE_DEVICE_LINUX_SLOT_FILENAME_FORMAT, (uint32_t)slotIndex - ); - - stringFormat(dest, destSize, "%s/%s", directory, filename); - errorOk(); -} - -errorret_t saveDeviceLinuxSlotWrite( - savedevice_t *device, - saveslot_t *slot, - uint8_t slotIndex -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(slot, "slot cannot be null"); - - char_t path[SAVE_DEVICE_LINUX_PATH_MAX]; - errorChain(saveDeviceLinuxGetSlotPath(path, sizeof(path), slotIndex)); - - writeInit(); - - errorret_t writeResult = saveSlotWriteJSON(slot, doc, object); - if(errorIsNotOk(writeResult)) { - yyjson_mut_doc_free(doc); - errorChain(writeResult); - } - - yyjson_write_err writeErr; - bool_t written = yyjson_mut_write_file(path, doc, 0, NULL, &writeErr); - yyjson_mut_doc_free(doc); - - if(!written) { - errorThrow( - "Failed to write save slot file: %s (%s)", path, writeErr.msg - ); - } - - errorOk(); -} - -errorret_t saveDeviceLinuxSlotRead( - savedevice_t *device, - saveslot_t *slot, - uint8_t slotIndex -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(slot, "slot cannot be null"); - - char_t path[SAVE_DEVICE_LINUX_PATH_MAX]; - errorChain(saveDeviceLinuxGetSlotPath(path, sizeof(path), slotIndex)); - - yyjson_read_err readErr; - yyjson_doc *jsonDoc = yyjson_read_file(path, 0, NULL, &readErr); - if(jsonDoc == NULL) { - // No save file at this slot yet, leave the slot as-is. - if(readErr.code == YYJSON_READ_ERROR_FILE_OPEN) errorOk(); - errorThrow( - "Failed to parse save slot file: %s (%s)", path, readErr.msg - ); - } - - yyjson_val *object = yyjson_doc_get_root(jsonDoc); - if(object == NULL) { - yyjson_doc_free(jsonDoc); - errorThrow("Save slot file missing root object: %s", path); - } - - errorret_t readResult = saveSlotReadJSON(slot, object); - yyjson_doc_free(jsonDoc); - errorChain(readResult); - - errorOk(); -} - -errorret_t saveDeviceLinuxSettingsWrite( - savedevice_t *device, - savesettings_t *settings -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(settings, "settings cannot be null"); - - char_t path[SAVE_DEVICE_LINUX_PATH_MAX]; - errorChain(saveDeviceLinuxGetSettingsPath(path, sizeof(path))); - - writeInit(); - - errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object); - if(errorIsNotOk(writeResult)) { - yyjson_mut_doc_free(doc); - errorChain(writeResult); - } - - yyjson_write_err writeErr; - bool_t written = yyjson_mut_write_file(path, doc, 0, NULL, &writeErr); - yyjson_mut_doc_free(doc); - - if(!written) { - errorThrow( - "Failed to write save settings file: %s (%s)", path, writeErr.msg - ); - } - - errorOk(); -} - -errorret_t saveDeviceLinuxSettingsRead( - savedevice_t *device, - savesettings_t *settings -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(settings, "settings cannot be null"); - - char_t path[SAVE_DEVICE_LINUX_PATH_MAX]; - errorChain(saveDeviceLinuxGetSettingsPath(path, sizeof(path))); - - yyjson_read_err readErr; - yyjson_doc *jsonDoc = yyjson_read_file(path, 0, NULL, &readErr); - if(jsonDoc == NULL) { - // No settings file yet, leave settings as-is. - if(readErr.code == YYJSON_READ_ERROR_FILE_OPEN) errorOk(); - errorThrow( - "Failed to parse save settings file: %s (%s)", path, readErr.msg - ); - } - - yyjson_val *object = yyjson_doc_get_root(jsonDoc); - if(object == NULL) { - yyjson_doc_free(jsonDoc); - errorThrow("Save settings file missing root object: %s", path); - } - - errorret_t readResult = saveSettingsReadJSON(settings, object); - yyjson_doc_free(jsonDoc); - errorChain(readResult); - - errorOk(); -} - -errorret_t saveDeviceLinuxDispose(savedevice_t *device) { - errorOk(); -} diff --git a/src/dusklinux/save/savedevicelinux.h b/src/dusklinux/save/savedevicelinux.h deleted file mode 100644 index 99ccbf89..00000000 --- a/src/dusklinux/save/savedevicelinux.h +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -// Subdirectory of ASSET.baseDirectory (the directory dusk.dsk was found -// in - see its own comment) saves are kept in - a subdirectory, unlike -// the other platforms' single combined-blob file, since Linux keeps -// settings and each save slot as their own separate file. -#define SAVE_DEVICE_LINUX_SAVES_SUBDIR_NAME "saves" -#define SAVE_DEVICE_LINUX_SETTINGS_FILENAME "settings.json" -#define SAVE_DEVICE_LINUX_SLOT_FILENAME_FORMAT "slot%u.json" -#define SAVE_DEVICE_LINUX_PATH_MAX FILENAME_MAX - -typedef struct { - void *nothing; -} savedeviceplatform_t; - -typedef struct savedevice_s savedevice_t; -typedef struct saveslot_s saveslot_t; -typedef struct savesettings_s savesettings_t; - -/** - * Initializes the save device platform. - * - * @param device The save device platform to initialize. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxInit(savedevice_t *device); - -/** - * Updates the save device platform. - * - * @param device The save device platform to update. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxUpdate(savedevice_t *device); - -/** - * Requests the device to check its availability, this will call the callback - * whence completed. - * - * @param device The save device platform to check availability. - */ -void saveDeviceLinuxCheckAvailability(savedevice_t *device); - -/** - * Builds the absolute path to the directory saves are stored in, creating - * intermediate directories is not done by this method, see mkdirp. - * - * @param dest The destination buffer to write the path into. - * @param destSize The size of the destination buffer, exc. null terminator. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxGetDirectory(char_t *dest, const size_t destSize); - -/** - * Builds the absolute path to the settings file. - * - * @param dest The destination buffer to write the path into. - * @param destSize The size of the destination buffer, exc. null terminator. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxGetSettingsPath( - char_t *dest, - const size_t destSize -); - -/** - * Builds the absolute path to the given save slot's file. - * - * @param dest The destination buffer to write the path into. - * @param destSize The size of the destination buffer, exc. null terminator. - * @param slotIndex The slot index to build the path for. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxGetSlotPath( - char_t *dest, - const size_t destSize, - const uint8_t slotIndex -); - -/** - * Writes the given save slot out to its file on disk. - * - * @param device The save device to write to. - * @param slot The save slot to write. - * @param slotIndex The slot index to write to. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxSlotWrite( - savedevice_t *device, - saveslot_t *slot, - uint8_t slotIndex -); - -/** - * Reads a save slot in from its file on disk. If no file exists yet for the - * given slot index, this is not an error - the slot is simply left as-is. - * - * @param device The save device to read from. - * @param slot The save slot to read into. - * @param slotIndex The slot index to read from. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxSlotRead( - savedevice_t *device, - saveslot_t *slot, - uint8_t slotIndex -); - -/** - * Writes the given save settings out to the settings file on disk. - * - * @param device The save device to write to. - * @param settings The save settings to write. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxSettingsWrite( - savedevice_t *device, - savesettings_t *settings -); - -/** - * Reads the save settings in from the settings file on disk. If no settings - * file exists yet, this is not an error - settings is simply left as-is. - * - * @param device The save device to read from. - * @param settings The save settings to read into. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxSettingsRead( - savedevice_t *device, - savesettings_t *settings -); - -/** - * Disposes of the save device platform. - * - * @param device The save device platform to dispose. - * @return Error state if any. - */ -errorret_t saveDeviceLinuxDispose(savedevice_t *device); diff --git a/src/dusklinux/save/savedeviceplatform.h b/src/dusklinux/save/savedeviceplatform.h deleted file mode 100644 index 11bd2c89..00000000 --- a/src/dusklinux/save/savedeviceplatform.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "savedevicelinux.h" - -#define SAVE_DEVICE_COUNT 1 - -#define saveDevicePlatformInit saveDeviceLinuxInit -#define saveDevicePlatformUpdate saveDeviceLinuxUpdate -#define saveDeviceCheckAvailabilityPlatform saveDeviceLinuxCheckAvailability -#define saveDevicePlatformDispose saveDeviceLinuxDispose -#define saveDeviceSlotWritePlatform saveDeviceLinuxSlotWrite -#define saveDeviceSlotReadPlatform saveDeviceLinuxSlotRead -#define saveDeviceSettingsWritePlatform saveDeviceLinuxSettingsWrite -#define saveDeviceSettingsReadPlatform saveDeviceLinuxSettingsRead \ No newline at end of file diff --git a/src/dusklinux/script/module/moduleplatformlinux.h b/src/dusklinux/script/module/moduleplatformlinux.h deleted file mode 100644 index 9b05c65f..00000000 --- a/src/dusklinux/script/module/moduleplatformlinux.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "script/module/modulebase.h" - -static void modulePlatformLinux(void) { - moduleBaseEval("var LINUX = true;\n"); -} diff --git a/src/dusklinux/script/module/moduleplatformplatform.h b/src/dusklinux/script/module/moduleplatformplatform.h deleted file mode 100644 index 4fb3c6eb..00000000 --- a/src/dusklinux/script/module/moduleplatformplatform.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "moduleplatformlinux.h" - -#define modulePlatformPlatform modulePlatformLinux \ No newline at end of file diff --git a/src/dusklinux/system/CMakeLists.txt b/src/dusklinux/system/CMakeLists.txt deleted file mode 100644 index 6c2b20a2..00000000 --- a/src/dusklinux/system/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - systemlinux.c -) diff --git a/src/dusklinux/system/systemlinux.c b/src/dusklinux/system/systemlinux.c deleted file mode 100644 index 4fd328e3..00000000 --- a/src/dusklinux/system/systemlinux.c +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "systemlinux.h" - -errorret_t systemInitLinux() { - errorOk(); -} - -systemdialogtype_t systemGetActiveDialogTypeLinux() { - return SYSTEM_DIALOG_TYPE_NONE; -} - -const localeinfo_t * systemGetLocaleLinux() { - return &LOCALE_DEFAULT; -} \ No newline at end of file diff --git a/src/dusklinux/system/systemlinux.h b/src/dusklinux/system/systemlinux.h deleted file mode 100644 index 5059b4d5..00000000 --- a/src/dusklinux/system/systemlinux.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "system/system.h" - -/** - * Initializes the Linux system module. - */ -errorret_t systemInitLinux(void); - -/** - * Currently just returns SYSTEM_DIALOG_TYPE_NONE. - * - * @return Currently open system dialog type. - */ -systemdialogtype_t systemGetActiveDialogTypeLinux(); - -/** - * Returns the current locale of the system. Linux has no system language - * query wired up yet, so this always returns LOCALE_DEFAULT. - * - * @return The current locale. - */ -const localeinfo_t * systemGetLocaleLinux(); \ No newline at end of file diff --git a/src/dusklinux/system/systemplatform.h b/src/dusklinux/system/systemplatform.h deleted file mode 100644 index 0d9f5218..00000000 --- a/src/dusklinux/system/systemplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "system/systemlinux.h" - -#define systemInitPlatform systemInitLinux -#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypeLinux -#define systemGetLocalePlatform systemGetLocaleLinux \ No newline at end of file diff --git a/src/dusklinux/time/CMakeLists.txt b/src/dusklinux/time/CMakeLists.txt deleted file mode 100644 index ccd11c31..00000000 --- a/src/dusklinux/time/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - timelinux.c -) diff --git a/src/dusklinux/time/timelinux.c b/src/dusklinux/time/timelinux.c deleted file mode 100644 index b7a17918..00000000 --- a/src/dusklinux/time/timelinux.c +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "time/timelinux.h" -#include - -double_t timeGetRealLinux(void) { - struct timespec ts; - - if(clock_gettime(CLOCK_REALTIME, &ts) != 0) { - return 0.0; - } - - return ( - ((double)ts.tv_sec) + - ((double)ts.tv_nsec / 1000000000.0) - ); -} - -double_t timeGetRealTimeZoneLinux(void) { - time_t now = time(NULL); - struct tm local_tm; - localtime_r(&now, &local_tm); - return (double_t)(local_tm.tm_gmtoff); -} \ No newline at end of file diff --git a/src/dusklinux/time/timelinux.h b/src/dusklinux/time/timelinux.h deleted file mode 100644 index 19c5fc3c..00000000 --- a/src/dusklinux/time/timelinux.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Returns the real current time, in seconds since January 1, 1970. - * - * @return The real current time, in seconds since January 1, 1970. - */ -double_t timeGetRealLinux(void); - -/** - * Returns the real time zone offset in seconds from UTC. - * - * @return The real time zone offset in seconds from UTC. - */ -double_t timeGetRealTimeZoneLinux(void); \ No newline at end of file diff --git a/src/dusklinux/time/timeplatform.h b/src/dusklinux/time/timeplatform.h deleted file mode 100644 index 29d44593..00000000 --- a/src/dusklinux/time/timeplatform.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "time/timesdl2.h" -#include "time/timelinux.h" - -#define timeTickPlatform timeTickSDL2 -#define timeGetDeltaPlatform timeGetDeltaSDL2 -#define timeGetRealPlatform timeGetRealLinux -#define timeGetRealTimeZonePlatform timeGetRealTimeZoneLinux \ No newline at end of file diff --git a/src/dusklinux/util/CMakeLists.txt b/src/dusklinux/util/CMakeLists.txt deleted file mode 100644 index b60e7416..00000000 --- a/src/dusklinux/util/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - mkdirp.c -) diff --git a/src/dusklinux/util/mkdirp.c b/src/dusklinux/util/mkdirp.c deleted file mode 100644 index 2c086ea8..00000000 --- a/src/dusklinux/util/mkdirp.c +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "util/mkdirp.h" -#include "util/string.h" -#include "assert/assert.h" -#include -#include - -int_t mkdirp(const char_t *path, const int_t mode) { - assertNotNull(path, "path cannot be null"); - assertStrLenMax(path, PATH_MAX, "path is too long"); - - char_t buffer[PATH_MAX]; - stringCopy(buffer, path, sizeof(buffer) - 1); - - size_t len = strlen(buffer); - if(len == 0) return -1; - - // Strip a trailing slash so the loop below doesn't try to create the same - // directory twice. - if(buffer[len - 1] == '/') buffer[len - 1] = '\0'; - - for(char_t *p = buffer + 1; *p != '\0'; p++) { - if(*p != '/') continue; - - *p = '\0'; - if(mkdir(buffer, mode) != 0 && errno != EEXIST) return -1; - *p = '/'; - } - - if(mkdir(buffer, mode) != 0 && errno != EEXIST) return -1; - return 0; -} diff --git a/src/dusklinux/util/mkdirp.h b/src/dusklinux/util/mkdirp.h deleted file mode 100644 index c752ef28..00000000 --- a/src/dusklinux/util/mkdirp.h +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Recursively creates a directory and any missing parent directories, similar - * to the shell command `mkdir -p`. If the directory already exists, this is - * considered a success. - * - * @param path The path of the directory to create. - * @param mode The permissions to create the directories with. - * @return 0 on success, -1 on failure (errno will be set). - */ -int_t mkdirp(const char_t *path, const int_t mode); diff --git a/src/duskmad/CMakeLists.txt b/src/duskmad/CMakeLists.txt deleted file mode 100644 index 94fb2d7a..00000000 --- a/src/duskmad/CMakeLists.txt +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# libmad-based MP3 decode, shared by every platform that doesn't have (or -# use) a hardware MP3 decoder - currently Linux/knulli and Dolphin -# (Wii/GameCube), both via the same software decoder and background -# decode-ahead ring (audiostreammp3decodersw.c, audiostreammp3ring.c). See -# src/CMakeLists.txt for which DUSK_TARGET_SYSTEM values pull this -# directory in at all - PSP uses its own hardware sceMp3 decoder instead -# (src/duskpsp/audio/audiostreammp3decoder.c) and never references -# anything here. -# -# Note: libmad is GPL-licensed, unlike the rest of this project - see -# audiostreammp3decodersw.h's own comment. Linking/finding libmad itself -# stays with each platform's own audio CMakeLists.txt (src/dusklinux/audio, -# cmake/targets/dolphin.cmake) rather than living here, since Linux and -# Dolphin genuinely obtain it differently (find_package() vs the bundled -# devkitPPC/libogc toolchain copy) - this file only owns the shared source. - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Sources -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - audiostreammp3decodersw.c - audiostreammp3ring.c -) diff --git a/src/duskmad/audiostreammp3decodersw.c b/src/duskmad/audiostreammp3decodersw.c deleted file mode 100644 index bc8a6857..00000000 --- a/src/duskmad/audiostreammp3decodersw.c +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreammp3decodersw.h" -#include "audio/stream/audiostream.h" -#include "audio/stream/audiostreammp3.h" -#include "assert/assert.h" -#include "util/memory.h" - -errorret_t audioStreamMp3DecoderInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - mad_stream_init(&decoder->stream); - mad_frame_init(&decoder->frame); - mad_synth_init(&decoder->synth); - decoder->bufferFilled = 0; - decoder->endOfFile = false; - - errorOk(); -} - -errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - mad_frame_finish(&decoder->frame); - mad_stream_finish(&decoder->stream); - - errorOk(); -} - -errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - const assetmp3file_t *mp3 = &stream->asset->data.mp3; - - errorChain(assetFileRewind(&stream->mp3.file)); - errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset)); - - errorChain(audioStreamMp3DecoderDispose(stream)); - errorChain(audioStreamMp3DecoderInit(stream)); - - errorOk(); -} - -errorret_t audioStreamMp3DecoderRefill(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - - // Shift whatever libmad hasn't consumed yet (it tracks this itself via - // next_frame, since it never copies our buffer - just holds pointers - // into it) to the front before appending more. - size_t leftover = 0; - if(decoder->stream.next_frame != NULL) { - leftover = (size_t) (decoder->stream.bufend - decoder->stream.next_frame); - if(leftover > 0) { - memoryMove(decoder->buffer, decoder->stream.next_frame, leftover); - } - } - decoder->bufferFilled = leftover; - - if(!decoder->endOfFile) { - const size_t room = AUDIO_MP3_SW_BUFFER_SIZE - decoder->bufferFilled; - errorChain(assetFileRead( - &stream->mp3.file, decoder->buffer + decoder->bufferFilled, room - )); - const size_t bytesRead = (size_t) stream->mp3.file.lastRead; - decoder->bufferFilled += bytesRead; - if(bytesRead < room) decoder->endOfFile = true; - } - - if(decoder->bufferFilled == 0 && decoder->endOfFile) { - // Genuinely nothing left, not even a leftover partial frame - don't - // bother re-buffering an empty (plus guard) window. - errorOk(); - } - - size_t bufferLength = decoder->bufferFilled; - if(decoder->endOfFile) { - // libmad may read up to MAD_BUFFER_GUARD bytes past the end of valid - // data while decoding the last real frame(s) - pad with zeros so that - // never reads past this array. - memoryZero(decoder->buffer + decoder->bufferFilled, MAD_BUFFER_GUARD); - bufferLength += MAD_BUFFER_GUARD; - } - - mad_stream_buffer(&decoder->stream, decoder->buffer, bufferLength); - - errorOk(); -} - -int16_t audioStreamMp3DecoderScale(mad_fixed_t sample) { - // Round to nearest, then clamp to +/-1.0 rather than letting an - // out-of-range sample wrap - the standard scale() conversion from - // libmad's own reference examples (minimad.c et al.). - sample += (1L << (MAD_F_FRACBITS - 16)); - if(sample >= MAD_F_ONE) { - sample = MAD_F_ONE - 1; - } else if(sample < -MAD_F_ONE) { - sample = -MAD_F_ONE; - } - return (int16_t) (sample >> (MAD_F_FRACBITS + 1 - 16)); -} - -errorret_t audioStreamMp3DecoderDecodeFrame( - audiostream_t *stream, - int16_t *out, - size_t *outFrames -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(out, "Out buffer cannot be NULL."); - assertNotNull(outFrames, "outFrames cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - - // Prime the window on this stream's very first-ever call - mad_frame_ - // decode() assumes mad_stream_buffer() has already been called at least - // once. Every subsequent refill is triggered reactively below, only when - // a decode attempt itself reports it ran out of buffered data - // (MAD_ERROR_BUFLEN) - checking decoder->stream.error as a general loop - // guard instead isn't reliable, since nothing guarantees a successful - // decode leaves it holding any particular value. - if(decoder->stream.buffer == NULL) { - errorChain(audioStreamMp3DecoderRefill(stream)); - if(decoder->bufferFilled == 0 && decoder->endOfFile) { - *outFrames = 0; - errorOk(); - } - } - - for(;;) { - if(mad_frame_decode(&decoder->frame, &decoder->stream) == -1) { - if(decoder->stream.error == MAD_ERROR_BUFLEN) { - // Needs more data than is currently buffered - if the file's also - // exhausted, there will never be more, so this is a genuine end of - // stream; otherwise refill and retry. - if(decoder->endOfFile) { - *outFrames = 0; - errorOk(); - } - errorChain(audioStreamMp3DecoderRefill(stream)); - if(decoder->bufferFilled == 0 && decoder->endOfFile) { - *outFrames = 0; - errorOk(); - } - continue; - } - if(!MAD_RECOVERABLE(decoder->stream.error)) { - errorThrow("mad_frame_decode failed: 0x%04X", decoder->stream.error); - } - // Recoverable (e.g. MAD_ERROR_LOSTSYNC, a genuinely corrupt/non-frame - // byte run) - libmad already advanced stream.next_frame past the - // offending bytes internally; retry decoding from there. - continue; - } - - mad_synth_frame(&decoder->synth, &decoder->frame); - - const size_t frames = (size_t) decoder->synth.pcm.length; - const size_t channels = (size_t) MAD_NCHANNELS(&decoder->frame.header); - // `out` (stream->mp3.pending) is sized for - // AUDIO_MP3_MAX_SAMPLES_PER_FRAME samples - one MPEG frame is at most - // 1152 samples/channel, well within it. - assertTrue( - frames * channels <= AUDIO_MP3_MAX_SAMPLES_PER_FRAME, - "mad_synth_frame produced more PCM than the destination buffer can hold." - ); - for(size_t i = 0; i < frames; i++) { - for(size_t c = 0; c < channels; c++) { - out[i * channels + c] = audioStreamMp3DecoderScale( - decoder->synth.pcm.samples[c][i] - ); - } - } - - *outFrames = frames; - errorOk(); - } -} diff --git a/src/duskmad/audiostreammp3decodersw.h b/src/duskmad/audiostreammp3decodersw.h deleted file mode 100644 index 22d9cb4c..00000000 --- a/src/duskmad/audiostreammp3decodersw.h +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include - -typedef struct audiostream_s audiostream_t; - -// Size of the sliding compressed-byte window libmad decodes from - just -// needs to comfortably exceed one MPEG frame's worth of bytes plus -// whatever bit-reservoir carryover libmad itself buffers internally -// (MAD_BUFFER_MDLEN), with room to refill in reasonably-sized chunks -// rather than one frame at a time. Unlike this file's previous minimp3- -// based implementation, libmad reports "not enough data" (MAD_ERROR_BUFLEN) -// and "genuinely bad data" (every other error code) as distinct, explicit -// signals rather than one overloaded return value - so, verified against a -// real ~236s VBR file, this window doesn't need minimp3's 256KB (itself -// only ~99.7% accurate) to decode every frame correctly. -#define AUDIO_MP3_SW_BUFFER_SIZE (32 * 1024) - -/** - * Software MP3 decoder state, shared by every platform that doesn't have - * (or doesn't use) a hardware MP3 decoder - currently Linux and Dolphin, - * both via this same libmad-based implementation - * (audiostreammp3decodersw.c). See audiostreammp3.h for how this plugs - * into the shared MP3 stream layer, and audiostreammp3.h's own - * documentation of audioStreamMp3DecoderInit()/Dispose()/Rewind()/ - * DecodeFrame() for the interface this and the PSP hardware backend - * (src/duskpsp/audio/audiostreammp3decoder.c) both implement. - * - * Note this is libmad (https://www.underbit.com/products/mad/), used here - * for its bitstream-accurate frame sync (see AUDIO_MP3_SW_BUFFER_SIZE's own - * comment) - unlike the rest of this project, libmad is GPL-licensed, not - * MIT. Any binary linking this file's compiled output must comply with the - * GPL; a deliberate tradeoff made after minimp3 proved unable to decode a - * real-world VBR file without audible, unresolvable data loss. - */ -typedef struct { - struct mad_stream stream; - struct mad_frame frame; - struct mad_synth synth; - - // libmad reads up to MAD_BUFFER_GUARD bytes past whatever it's given as - // the end of valid data while decoding the final real frame(s) in a - // buffer - the trailing MAD_BUFFER_GUARD bytes here are that padding - // (zeroed once endOfFile is set), never counted in bufferFilled. - uint8_t buffer[AUDIO_MP3_SW_BUFFER_SIZE + MAD_BUFFER_GUARD]; - size_t bufferFilled; - - // Set once assetFileRead() returns fewer bytes than requested - there's - // no more compressed data to refill `buffer` with, though whatever's - // still in it may still decode into one or more final frames. - bool_t endOfFile; -} audiostreammp3decoder_t; - -/** - * Initializes the software MP3 decoder for the given stream - resets - * libmad's stream/frame/synth state and the compressed-byte window (empty, - * not yet filled from the asset). - * - * @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderInit(audiostream_t *stream); - -/** - * Disposes the software MP3 decoder for the given stream: tears down - * libmad's stream/frame state (mad_stream_finish()/mad_frame_finish()) - - * mad_synth carries no allocated state of its own, so there's nothing to - * release for it. - * - * @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream); - -/** - * Resets the decoder to the very start of the compressed stream: rewinds - * stream->mp3.file back to the asset's parsed data offset, resets libmad's - * stream/frame/synth state and the compressed-byte window, so the next - * DecodeFrame() call starts decoding from the first MPEG frame again. See - * audioStreamMp3Seek()'s own comment on why every seek goes through here. - * - * @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream); - -/** - * Tops up the compressed-byte window from stream->mp3.file: shifts - * whatever libmad hasn't consumed yet (from its own stream.next_frame - * cursor) to the front of the window, reads as much more as there's room - * for, and re-buffers libmad's stream (mad_stream_buffer()) over the - * result - padding with MAD_BUFFER_GUARD zero bytes once the file's - * genuinely exhausted, since libmad may read slightly past the end of - * valid data while decoding a final frame. - * - * @param stream The audio stream to refill. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderRefill(audiostream_t *stream); - -/** - * Converts one libmad fixed-point PCM sample to a clamped 16-bit sample - - * the standard scale() conversion from libmad's own reference examples - * (minimad.c et al.), rounding to nearest and clamping to +/-1.0 rather - * than wrapping on overflow. - * - * @param sample The fixed-point sample to convert (mad_fixed_t, - * Q1.MAD_F_FRACBITS). - * @return The converted 16-bit signed sample. - */ -int16_t audioStreamMp3DecoderScale(mad_fixed_t sample); - -/** - * Decodes the next MPEG frame's worth of PCM samples, topping up the - * compressed-byte window from stream->mp3.file as needed - * (audioStreamMp3DecoderRefill()). Writes decoded samples to `out` (sized - * for at least AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets - * *outFrames to how many frames (not samples) were produced - 0 once the - * compressed stream is genuinely exhausted, never negative (a corrupt/ - * truncated stream is treated the same as a clean end, not an error, - * matching audioStreamPcmRead()'s own tolerance for a short/truncated - * asset). - * - * @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3. - * @param out Destination buffer for decoded samples. - * @param outFrames Set to the number of frames actually decoded. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderDecodeFrame( - audiostream_t *stream, - int16_t *out, - size_t *outFrames -); diff --git a/src/duskmad/audiostreammp3ring.c b/src/duskmad/audiostreammp3ring.c deleted file mode 100644 index 64893d40..00000000 --- a/src/duskmad/audiostreammp3ring.c +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreammp3ring.h" -#include "audio/stream/audiostream.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include - -errorret_t audioStreamMp3RingInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3ring_t *ring = &stream->platform.mp3Ring; - const size_t frameSize = stream->channels * sizeof(int16_t); - - ring->data = (int16_t *) memoryAllocate(AUDIO_MP3_RING_FRAMES * frameSize); - ring->scratch = - (int16_t *) memoryAllocate(AUDIO_MP3_RING_STEP_FRAMES * frameSize); - ring->readPos = 0; - ring->writePos = 0; - ring->filled = 0; - ring->reachedEnd = false; - ring->failed = false; - ring->startFrame = 0; - ring->endFrame = 0; - threadMutexInit(&ring->lock); - threadInit(&ring->thread, audioStreamMp3RingThreadFunc); - - errorOk(); -} - -errorret_t audioStreamMp3RingDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3ring_t *ring = &stream->platform.mp3Ring; - - // Must fully park the decode thread before freeing anything it might - // still be touching, or before the caller lets the shared MP3 layer - // tear down stream->mp3's decoder/file out from under it. - threadStop(&ring->thread); - threadMutexDispose(&ring->lock); - memoryFree(ring->data); - memoryFree(ring->scratch); - ring->data = NULL; - ring->scratch = NULL; - - errorOk(); -} - -errorret_t audioStreamMp3RingStop(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3ring_t *ring = &stream->platform.mp3Ring; - - // Blocking - guarantees the decode thread is fully parked (not mid-read) - // before the caller runs audioStreamSeek(), which mutates the same - // stream->mp3 decoder/file this thread reads via audioStreamRead(). A - // safe no-op if the thread was never started or already finished this - // pass on its own (reached endFrame). - threadStop(&ring->thread); - - threadMutexLock(&ring->lock); - ring->readPos = 0; - ring->writePos = 0; - ring->filled = 0; - ring->reachedEnd = false; - ring->failed = false; - threadMutexUnlock(&ring->lock); - - errorOk(); -} - -errorret_t audioStreamMp3RingStart( - audiostream_t *stream, size_t startFrame, size_t endFrame -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3ring_t *ring = &stream->platform.mp3Ring; - - // Finalized here, before the thread is launched - thread creation itself - // is the happens-before edge that makes these writes visible to the new - // thread, so no lock is needed around this handoff. - ring->startFrame = startFrame; - ring->endFrame = endFrame; - ring->thread.data = stream; - threadStartRequest(&ring->thread); - - errorOk(); -} - -size_t audioStreamMp3RingDrain( - audiostream_t *stream, - int16_t *out, - size_t maxFrames, - bool_t *outDecodeDone -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(out, "Out buffer cannot be NULL."); - assertNotNull(outDecodeDone, "outDecodeDone cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3ring_t *ring = &stream->platform.mp3Ring; - const size_t channels = stream->channels; - - threadMutexLock(&ring->lock); - const size_t available = ring->filled; - const bool_t decodeDone = ring->reachedEnd || ring->failed; - - // Wait for a full window rather than handing back a partial one, unless - // decode has genuinely nothing more to give this pass - keeps a - // platform's own output handoff cadence matching its previous - // whole-window-per-call behavior instead of fragmenting into many tiny - // calls just because decode happened to be a little behind at this exact - // poll. - if(available < maxFrames && !decodeDone) { - threadMutexUnlock(&ring->lock); - *outDecodeDone = false; - return 0; - } - - const size_t toCopy = mathMin(available, maxFrames); - const size_t firstPart = - mathMin(toCopy, AUDIO_MP3_RING_FRAMES - ring->readPos); - memoryCopy( - out, ring->data + ring->readPos * channels, - firstPart * channels * sizeof(int16_t) - ); - if(toCopy > firstPart) { - memoryCopy( - out + firstPart * channels, ring->data, - (toCopy - firstPart) * channels * sizeof(int16_t) - ); - } - ring->readPos = (ring->readPos + toCopy) % AUDIO_MP3_RING_FRAMES; - ring->filled -= toCopy; - threadMutexUnlock(&ring->lock); - - *outDecodeDone = decodeDone; - return toCopy; -} - -bool_t audioStreamMp3RingFailed(const audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - return stream->platform.mp3Ring.failed; -} - -void audioStreamMp3RingThreadFunc(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - - audiostream_t *stream = (audiostream_t *) thread->data; - audiostreammp3ring_t *ring = &stream->platform.mp3Ring; - const size_t channels = stream->channels; - - // Captured once - only ever written by audioStreamMp3RingStart() before - // this thread was launched (a platform's Feed() advances its own - // position field as it drains the ring, never these). Tracks its own - // progress in a thread-local counter rather than re-reading endFrame - // repeatedly, though it wouldn't change mid-pass either way. - const size_t startFrame = ring->startFrame; - const size_t endFrame = ring->endFrame; - size_t decoded = 0; - - while(!threadShouldStop(thread)) { - threadMutexLock(&ring->lock); - const size_t room = AUDIO_MP3_RING_FRAMES - ring->filled; - threadMutexUnlock(&ring->lock); - - if(room == 0) { - // Never sleeps while holding the lock - re-checks threadShouldStop() - // immediately after waking, bounding a stop request's worst-case - // wait here to one poll interval, not a whole ring's worth of idle. - usleep(AUDIO_MP3_RING_POLL_MICROS); - continue; - } - - if(startFrame + decoded >= endFrame) break; - const size_t framesRemaining = endFrame - (startFrame + decoded); - const size_t step = mathMin( - mathMin(room, framesRemaining), (size_t) AUDIO_MP3_RING_STEP_FRAMES - ); - - size_t framesRead = 0; - // The expensive call (asset I/O + libmad decode) - deliberately - // outside any lock, so it never blocks the main thread's own - // audioStreamMp3RingDrain() calls while it runs. - errorret_t ret = audioStreamRead(stream, ring->scratch, step, &framesRead); - if(errorIsNotOk(ret)) { - errorCatch(errorPrint(ret)); - threadMutexLock(&ring->lock); - ring->failed = true; - threadMutexUnlock(&ring->lock); - return; - } - if(framesRead == 0) break; // Genuine early end of underlying data. - - threadMutexLock(&ring->lock); - const size_t firstPart = mathMin( - framesRead, AUDIO_MP3_RING_FRAMES - ring->writePos - ); - memoryCopy( - ring->data + ring->writePos * channels, ring->scratch, - firstPart * channels * sizeof(int16_t) - ); - if(framesRead > firstPart) { - memoryCopy( - ring->data, ring->scratch + firstPart * channels, - (framesRead - firstPart) * channels * sizeof(int16_t) - ); - } - ring->writePos = (ring->writePos + framesRead) % AUDIO_MP3_RING_FRAMES; - ring->filled += framesRead; - threadMutexUnlock(&ring->lock); - - decoded += framesRead; - // Loops back to the top - threadShouldStop() is rechecked before the - // NEXT audioStreamRead(), not just once at the top of an outer loop, - // so a stop request doesn't have to wait through a whole pass. - } - - threadMutexLock(&ring->lock); - ring->reachedEnd = true; - threadMutexUnlock(&ring->lock); -} - -errorret_t audioStreamMp3RingInitIfNeeded(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->type == AUDIO_STREAM_TYPE_MP3) { - errorChain(audioStreamMp3RingInit(stream)); - } - - errorOk(); -} - -errorret_t audioStreamMp3RingDisposeIfNeeded(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->type == AUDIO_STREAM_TYPE_MP3) { - errorChain(audioStreamMp3RingDispose(stream)); - } - - errorOk(); -} - -errorret_t audioStreamMp3RingStopIfNeeded(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->type == AUDIO_STREAM_TYPE_MP3) { - errorChain(audioStreamMp3RingStop(stream)); - } - - errorOk(); -} - -errorret_t audioStreamMp3RingStartIfNeeded( - audiostream_t *stream, size_t startFrame, size_t endFrame -) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->type == AUDIO_STREAM_TYPE_MP3) { - errorChain(audioStreamMp3RingStart(stream, startFrame, endFrame)); - } - - errorOk(); -} - -errorret_t audioStreamReadForPlayback( - audiostream_t *stream, - int16_t *out, - size_t maxFrames, - size_t *outFramesRead, - bool_t *outNotReadyYet -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(out, "Out buffer cannot be NULL."); - assertNotNull(outFramesRead, "outFramesRead cannot be NULL."); - assertNotNull(outNotReadyYet, "outNotReadyYet cannot be NULL."); - - *outNotReadyYet = false; - - if(stream->type == AUDIO_STREAM_TYPE_MP3) { - bool_t decodeDone = false; - *outFramesRead = - audioStreamMp3RingDrain(stream, out, maxFrames, &decodeDone); - if(*outFramesRead == 0 && !decodeDone) { - *outNotReadyYet = true; - errorOk(); - } - if(*outFramesRead == 0 && audioStreamMp3RingFailed(stream)) { - errorThrow("Background MP3 decode failed."); - } - errorOk(); - } - - errorChain(audioStreamRead(stream, out, maxFrames, outFramesRead)); - - errorOk(); -} diff --git a/src/duskmad/audiostreammp3ring.h b/src/duskmad/audiostreammp3ring.h deleted file mode 100644 index bf160433..00000000 --- a/src/duskmad/audiostreammp3ring.h +++ /dev/null @@ -1,289 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "thread/thread.h" -#include "thread/threadmutex.h" - -typedef struct audiostream_s audiostream_t; - -// How many frames of decoded PCM the ring holds - deliberately generous -// (~1.5s at 44100Hz stereo, 256KB) so the background decode thread can run -// well ahead of what a platform's Feed() actually needs per poll, without -// being a meaningful fraction of even GameCube's 24MB (~1.1%) total RAM. -#define AUDIO_MP3_RING_FRAMES (64 * 1024) - -// How many frames the decode thread reads per audioStreamRead() call - -// bounds both normal decode-thread iteration granularity and, more -// importantly, audioStreamMp3RingStop()'s worst-case blocking wait (roughly -// one of these reads' worth of asset I/O + libmad decode, since the thread -// only rechecks threadShouldStop() between reads, not mid-read). -#define AUDIO_MP3_RING_STEP_FRAMES 4096 - -// How long the decode thread sleeps between polls when the ring is already -// full - matches the shared asset-load thread's own idle-poll interval -// (src/dusk/asset/asset.c) rather than inventing a new convention. -#define AUDIO_MP3_RING_POLL_MICROS 1000 - -/** - * Background MP3 decode-ahead ring, shared by every platform whose MP3 - * decode is expensive enough to hitch the main thread if done synchronously - * (currently Linux and Dolphin, both via the libmad-based software decoder - * - audiostreammp3decodersw.c; PSP doesn't use this, see below). Moves - * audioStreamRead()'s real cost (asset I/O + libmad decode) off the main - * thread onto a background thread that decodes ahead into this ring - a - * platform's own Feed() then drains already-decoded PCM out of it (a - * cheap, lock-protected copy) instead of decoding directly. Sits upstream - * of whatever a platform does with the PCM afterward (SDL_QueueAudio on - * Linux, ASND_SetVoice()/ASND_AddVoice() on Dolphin) - this struct knows - * nothing about either. - * - * Only used for AUDIO_STREAM_TYPE_MP3 streams - PCM/WAV reads are already - * cheap plain memory copies and don't need this. Not used by PSP: its - * hardware sceMp3 decoder is a completely different API (see - * src/duskpsp/audio/audiostreammp3decoder.c), and PSP's own audio backend - * already has its own background thread + ring - for *output* (draining - * to hardware), not decode - layering this under it would just be a - * second, redundant ring. - * - * The background thread is one-shot per playback pass, not a persistent - * worker (unlike PSP's own output thread, which idles cheaply between - * passes) - a fresh thread is started for every new pass (initial play, - * explicit seek, or loop restart), since those are rare relative to - * per-frame polls and a brief synchronous stop-then-restart at pass - * boundaries is a fine trade for not needing a mid-pass "abandon and - * reset" handshake. - */ -typedef struct { - int16_t *data; // AUDIO_MP3_RING_FRAMES * channels * sizeof(int16_t) - int16_t *scratch; // AUDIO_MP3_RING_STEP_FRAMES * channels * sizeof(int16_t), - // the decode thread's own read target - never touched - // by the main thread, so it needs no lock protection. - - // Guards exactly filled/readPos/writePos/reachedEnd/failed below, - // grouped together for simplicity rather than split per-field (same - // rationale as the PSP backend's own ring lock). Never held across - // audioStreamRead() or usleep() (decode thread) or a platform's own - // output call (main thread). - threadmutex_t lock; - - // Next frame audioStreamMp3RingDrain() (main thread) reads. - size_t readPos; - // Next frame the decode thread writes. - size_t writePos; - // How many valid frames are currently buffered. - size_t filled; - - // Set by the decode thread once it has produced every frame this pass - // will ever have (reached endFrame, or the underlying read returned 0 - // frames early) - tells Drain() there's nothing more coming, so it - // should hand back whatever's left rather than waiting for a full - // window that will never arrive. - bool_t reachedEnd; - // Set by the decode thread if audioStreamRead() itself errored - Drain() - // treats this the same as reachedEnd for draining purposes, but a - // platform's Feed() should surface it as a real thrown error once the - // ring is empty. - bool_t failed; - - // This pass's frame bounds, set by audioStreamMp3RingStart() before the - // thread is launched and read-only after (thread creation is the - // happens-before edge) - stored here rather than read from a platform's - // own platform_t fields, since this struct is shared across platforms - // whose own position/endFrame-equivalent fields aren't guaranteed to - // exist or be named the same way. - size_t startFrame; - size_t endFrame; - - // One-shot per pass - see this struct's own top-of-file comment. - thread_t thread; -} audiostreammp3ring_t; - -/** - * Allocates this stream's ring/scratch buffers (sized from its channel - * count) and initializes its mutex and thread_t - does not start decoding - * anything yet, that's audioStreamMp3RingStart()'s job. Call once, from a - * platform's own Init(), only for AUDIO_STREAM_TYPE_MP3 streams. - * - * @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingInit(audiostream_t *stream); - -/** - * Blocking-stops the decode thread (a no-op if it was never started or has - * already finished this pass) and frees the ring/scratch buffers. Call - * from a platform's own Dispose() to guarantee the thread is fully parked - * before the rest of this stream's (and the shared MP3 layer's) teardown - * runs. - * - * @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingDispose(audiostream_t *stream); - -/** - * Blocking-stops the decode thread (safe no-op if not running) and resets - * the ring to empty, ready for a fresh pass. Must be called (and must - * return) before audioStreamSeek() runs for a new pass, since the decode - * thread reads via the same stream->mp3 decoder/file audioStreamSeek() - * mutates - and before audioStreamMp3RingStart() launches the next one. - * - * @param stream The audio stream to stop. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingStop(audiostream_t *stream); - -/** - * Starts a fresh one-shot decode thread for the pass a platform's own - * Buffer() just set up (stream->platform.position/endFrame - or - * equivalent, whatever the platform's own struct calls them - already - * finalized) - decodes forward via audioStreamRead() into the ring until - * it reaches endFrame, filling back up whenever audioStreamMp3RingDrain() - * makes room. - * - * @param stream The audio stream to start decoding. Must be - * AUDIO_STREAM_TYPE_MP3. - * @param startFrame This pass's starting frame offset. - * @param endFrame This pass's stop frame offset. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingStart( - audiostream_t *stream, size_t startFrame, size_t endFrame -); - -/** - * Copies up to maxFrames already-decoded frames out of the ring into `out`, - * advancing the ring's read cursor. Only returns fewer than maxFrames (down - * to 0) once *outDecodeDone is also true (the decode thread has produced - * everything it ever will for this pass, or hit an error) - otherwise it - * waits for a full window rather than handing back a partial one, so a - * platform's own output handoff doesn't fragment into many tiny calls just - * because decode happened to be a little behind at this exact poll. - * - * @param stream The audio stream to drain from. Must be AUDIO_STREAM_TYPE_MP3. - * @param out Destination buffer, sized for at least maxFrames frames. - * @param maxFrames Maximum number of frames to copy out. - * @param outDecodeDone Set to true if the decode thread has nothing more to - * give this pass (cleanly finished or failed). - * @return The number of frames actually copied out. - */ -size_t audioStreamMp3RingDrain( - audiostream_t *stream, - int16_t *out, - size_t maxFrames, - bool_t *outDecodeDone -); - -/** - * True if the decode thread hit a real read error this pass (see the - * `failed` field's own comment) - a platform's Feed() checks this once - * Drain() has returned 0 frames with decode reported done, to decide - * whether that's a genuine end of stream or something to surface as an - * error. - * - * @param stream The audio stream to check. Must be AUDIO_STREAM_TYPE_MP3. - * @return true if the decode thread failed this pass. - */ -bool_t audioStreamMp3RingFailed(const audiostream_t *stream); - -/** - * The decode thread's own entry point (passed to threadInit()). Reads - * forward via audioStreamRead() in AUDIO_MP3_RING_STEP_FRAMES chunks, - * writing into the ring whenever there's room, until it reaches this - * pass's endFrame, the underlying read returns 0 frames, or a real read - * error occurs - never touches stream->onLoop/onEnd or any other - * main-thread-owned stream state, only the ring and its own captured - * startFrame/endFrame. - * - * @param thread This ring's own thread_t (thread->data is the audiostream_t). - */ -void audioStreamMp3RingThreadFunc(thread_t *thread); - -// The functions below are thin, type-checking wrappers around the ones -// above, meant to be called unconditionally from a platform's own -// Init()/Dispose()/Buffer()/Feed() - each is a no-op (or, for -// audioStreamReadForPlayback(), falls through to a plain audioStreamRead()) -// for anything other than an AUDIO_STREAM_TYPE_MP3 stream, so callers don't -// need their own `if(stream->type == AUDIO_STREAM_TYPE_MP3)` check at every -// call site. This is what actually keeps Linux's and Dolphin's own -// Init/Dispose/Buffer/Feed implementations close to each other - see -// audiostreamlinux.c/audiostreamdolphin.c for how they're used. - -/** - * Calls audioStreamMp3RingInit() if stream is an MP3 stream, otherwise a - * no-op. - * - * @param stream The audio stream to initialize. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingInitIfNeeded(audiostream_t *stream); - -/** - * Calls audioStreamMp3RingDispose() if stream is an MP3 stream, otherwise a - * no-op. - * - * @param stream The audio stream to dispose. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingDisposeIfNeeded(audiostream_t *stream); - -/** - * Calls audioStreamMp3RingStop() if stream is an MP3 stream, otherwise a - * no-op. Safe (and cheap) to call unconditionally at the start of every - * platform Buffer() pass, before audioStreamSeek(). - * - * @param stream The audio stream to stop. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingStopIfNeeded(audiostream_t *stream); - -/** - * Calls audioStreamMp3RingStart() if stream is an MP3 stream, otherwise a - * no-op. Safe to call unconditionally once a platform Buffer() pass has - * finalized its startFrame/endFrame, after audioStreamSeek(). - * - * @param stream The audio stream to start decoding. - * @param startFrame This pass's starting frame offset. - * @param endFrame This pass's stop frame offset. - * @return Error state if any. - */ -errorret_t audioStreamMp3RingStartIfNeeded( - audiostream_t *stream, size_t startFrame, size_t endFrame -); - -/** - * Reads up to maxFrames frames of decoded PCM for playback into `out`: for - * MP3 streams, drains the background decode ring - * (audioStreamMp3RingDrain()); for anything else, reads directly via - * audioStreamRead(). The single call a platform's own Feed() needs to get - * its next window's worth of PCM, regardless of stream type. - * - * *outNotReadyYet distinguishes the two ways this can return 0 frames - * without erroring: true means an MP3 stream's decode thread simply hasn't - * produced a full window yet (retry next poll, don't treat as end of - * stream); false with 0 frames means genuinely nothing left to read this - * pass. A background MP3 decode failure surfaces as a real thrown error - * (via errorThrow) rather than through either of these. - * - * @param stream The audio stream to read from. - * @param out Destination buffer, sized for at least maxFrames frames. - * @param maxFrames Maximum number of frames to read. - * @param outFramesRead Set to the number of frames actually read. - * @param outNotReadyYet Set to true if this returned 0 frames only because - * decode hasn't caught up yet, not because playback - * has actually reached the end. - * @return Error state - a background MP3 decode failure surfaces here. - */ -errorret_t audioStreamReadForPlayback( - audiostream_t *stream, - int16_t *out, - size_t maxFrames, - size_t *outFramesRead, - bool_t *outNotReadyYet -); diff --git a/src/dusknetwork/CMakeLists.txt b/src/dusknetwork/CMakeLists.txt deleted file mode 100644 index 4dfe35e2..00000000 --- a/src/dusknetwork/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Subdirs -add_subdirectory(network) diff --git a/src/dusknetwork/network/CMakeLists.txt b/src/dusknetwork/network/CMakeLists.txt deleted file mode 100644 index 6926e0b1..00000000 --- a/src/dusknetwork/network/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - network.c - networkinfo.c -) diff --git a/src/dusknetwork/network/network.c b/src/dusknetwork/network/network.c deleted file mode 100644 index 9ebd57fd..00000000 --- a/src/dusknetwork/network/network.c +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "network.h" -#include "util/memory.h" -#include "assert/assert.h" -#include "log/log.h" - -network_t NETWORK; - -errorret_t networkInit() { - memoryZero(&NETWORK, sizeof(network_t)); - - NETWORK.errorState.code = ERROR_OK; - NETWORK.onDisconnect = NULL; - - return networkPlatformInit(); -} - -errorret_t networkUpdate() { - errorChain(networkPlatformUpdate()); - - if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) { - NETWORK.state = NETWORK_STATE_DISCONNECTED; - - if(NETWORK.onDisconnect) { - errorret_t ret; - if(NETWORK.errorState.code == ERROR_OK) { - ret = errorThrowImpl( - &NETWORK.errorState, - ERROR_NOT_OK, - __FILE__, __func__, __LINE__, - "Network connection lost" - ); - } else { - ret.code = NETWORK.errorState.code; - ret.state = &NETWORK.errorState; - } - NETWORK.onDisconnect(ret, NETWORK.disconnectUser); - } - } - - errorOk(); -} - -bool_t networkIsConnected() { - return networkPlatformIsConnected(); -} - -void networkRequestConnection( - void (*onConnected)(void *user), - void (*onFailed)(errorret_t error, void *user), - void (*onDisconnect)(errorret_t error, void *user), - void *user -) { - assertNotNull(onConnected, "onConnected callback must not be null"); - assertNotNull(onFailed, "onFailed callback must not be null"); - assertNotNull(onDisconnect, "onDisconnect callback must not be null"); - - NETWORK.state = NETWORK_STATE_CONNECTING; - NETWORK.onDisconnect = onDisconnect; - NETWORK.disconnectUser = user; - - #ifndef networkPlatformRequestConnection - // This is a platform cannot be requested to go online, this would basically - // be for platforms like Linux or Windows where the OS is responsible for - // maintaining the network connection. - - if(networkIsConnected()) { - NETWORK.state = NETWORK_STATE_CONNECTED; - onConnected(user); - } else { - errorret_t ret = errorThrowImpl( - &NETWORK.errorState, - ERROR_NOT_OK, - __FILE__, __func__, __LINE__, - "No network connection available" - ); - onFailed(ret, user); - } - #else - networkPlatformRequestConnection(onConnected, onFailed, onDisconnect, user); - #endif -} - -void networkRequestDisconnection( - void (*onComplete)(void *user), - void *user -) { - assertNotNull(onComplete, "onComplete callback must not be null"); - NETWORK.state = NETWORK_STATE_DISCONNECTING; - - #ifndef networkPlatformRequestDisconnection - NETWORK.state = NETWORK_STATE_DISCONNECTED; - onComplete(user); - #else - networkPlatformRequestDisconnection(onComplete, user); - #endif -} - -void networkDisconnectedDuringDispose(void *u) { - logDebug("Network disconnected during dispose\n"); -} - -errorret_t networkDispose() { - if(NETWORK.state == NETWORK_STATE_CONNECTED) { - networkRequestDisconnection(networkDisconnectedDuringDispose, NULL); - } - - errorChain(networkPlatformDispose()); - errorOk(); -} diff --git a/src/dusknetwork/network/network.h b/src/dusknetwork/network/network.h deleted file mode 100644 index 122739af..00000000 --- a/src/dusknetwork/network/network.h +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "network/networkplatform.h" -#ifndef networkPlatformInit - #error "networkPlatformInit must be defined" -#endif -#ifndef networkPlatformUpdate - #error "networkPlatformUpdate must be defined" -#endif -#ifndef networkPlatformDispose - #error "networkPlatformDispose must be defined" -#endif -#ifndef networkPlatformIsConnected - #error "networkPlatformIsConnected must be defined" -#endif - -typedef enum { - NETWORK_STATE_DISCONNECTED, - NETWORK_STATE_CONNECTING, - NETWORK_STATE_CONNECTED, - NETWORK_STATE_DISCONNECTING, -} networkstate_t; - -typedef struct { - networkplatform_t platform; - errorstate_t errorState; - networkstate_t state; - - void (*onDisconnect)(errorret_t error, void *user); - void *disconnectUser; -} network_t; - -extern network_t NETWORK; - -/** - * Initializes the network system. This will NOT connect to the network. - * - * @return An error code indicating success or failure. - */ -errorret_t networkInit(); - -/** - * Updates the network manager, dispatching any completed async request - * callbacks on the main thread. - * - * @return An error code indicating success or failure. - */ -errorret_t networkUpdate(); - -/** - * Disposes of the network manager. This will NOT disconnect from the network. - * - * @return An error code indicating success or failure. - */ -errorret_t networkDispose(); - -/** - * Returns true if the system is connected to AN network, this doesn't mean that - * requests will succeed, doesn't mean there's internet, just that we could - * possibly make network requests. If this is false, this usually means - * something like; - * - A network cable is not connnected - * - No Wi-Fi Connection has been established - * - No IP Address has been assigned - * - * That kinda stuff. - * - * In future I will probably have REASONS for why it's not connected, for - * example; - * - On PSP you need to "request" network access - * - On GameCube, network settings need to be defined, including DHCP, etc. - * - On Windows, this may require additional permissions - * - * @return True if some network connection is detected. - */ -bool_t networkIsConnected(); - -/** - * See networkIsConnected for a bit more info, but this is for some - * platforms (mainly PSP) to request the system to start doing networking. - * - * You should only call this once and assume that it is "pending" until either - * onComplete or onFailed is invoked. If you call this twice it is undefined - * behavior. - * - * onDisconnect must be provided, and is called whenever the network is lost - * after a successful connection. This will NOT be called if disconnect is - * manually triggered, but WILL if an error occurs, or a network stack bug, etc. - * - * @param onConnected Callback to invoke when the network is connected. - * @param onFailed Callback to invoke if the network connection fails. - * @param onDisconnect Called after a successful connection, when disconnected. - * @param user User data to pass to the callbacks. - */ -void networkRequestConnection( - void (*onConnected)(void *user), - void (*onFailed)(errorret_t error, void *user), - void (*onDisconnect)(errorret_t error, void *user), - void *user -); - -/** - * Requests the system to disconnect from the network. This is basically just - * for PSP, but I guess it could be used on other platforms in future if they - * have some kind of "network connection mode" that needs to be exited. - * - * You should only call this once and assume that it is "pending" until - * onComplete is invoked. If you call this twice it is undefined behavior. - * - * If it fails, you still get onComplete called, but may fail if you try to - * reconnect later unfortunately. - * - * @param onComplete Callback to invoke when the network is disconnected. - * @param user User data to pass to the callback. - */ -void networkRequestDisconnection( - void (*onComplete)(void *user), - void *user -); \ No newline at end of file diff --git a/src/dusknetwork/network/networkinfo.c b/src/dusknetwork/network/networkinfo.c deleted file mode 100644 index d60980be..00000000 --- a/src/dusknetwork/network/networkinfo.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "network.h" -#include "networkinfo.h" -#include "network/networkplatform.h" -#include "assert/assert.h" - -#ifndef networkPlatformGetInfo - #error "networkPlatformGetInfo must be defined" -#endif - -networkinfo_t networkGetInfo() { - assertTrue( - NETWORK.state == NETWORK_STATE_CONNECTED, - "networkGetInfo called when not connected" - ); - - return networkPlatformGetInfo(); -} \ No newline at end of file diff --git a/src/dusknetwork/network/networkinfo.h b/src/dusknetwork/network/networkinfo.h deleted file mode 100644 index 8558c234..00000000 --- a/src/dusknetwork/network/networkinfo.h +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -#define NETWORK_INFO_IPV4_DNS_COUNT_MAX 2 -#define NETWORK_INFO_IPV4_OCTET_COUNT 4 -#define NETWORK_INFO_IPV6_OCTET_COUNT 16 -#define NETWORK_INFO_IPV6_DNS_COUNT_MAX 2 -#define NETWORK_INFO_FORMAT_IPV4 "%u.%u.%u.%u" -#define NETWORK_INFO_FORMAT_IPV6 \ - "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x" - -typedef enum { - NETWORK_TYPE_IPV4, - #ifdef DUSK_NETWORK_IPV6 - NETWORK_TYPE_IPV6, - #endif -} networktype_t; - -typedef struct { - uint8_t ip[NETWORK_INFO_IPV4_OCTET_COUNT]; -} networkinfoipv4_t; - -#ifdef DUSK_NETWORK_IPV6 - typedef struct { - uint8_t ip[NETWORK_INFO_IPV6_OCTET_COUNT]; - } networkinfoipv6_t; -#endif - -typedef struct { - networktype_t type; - union { - networkinfoipv4_t ipv4; - #ifdef DUSK_NETWORK_IPV6 - networkinfoipv6_t ipv6; - #endif - }; -} networkinfo_t; - -/** - * Returns the network information for the currently active network connection. - * - * @return Network information for the currently active network connection. - */ -networkinfo_t networkGetInfo(); \ No newline at end of file diff --git a/src/duskpsp/CMakeLists.txt b/src/duskpsp/CMakeLists.txt deleted file mode 100644 index 97448068..00000000 --- a/src/duskpsp/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Sources -target_sources(${DUSK_BINARY_TARGET_NAME} - PUBLIC -) - -# Subdirs -add_subdirectory(asset) -add_subdirectory(audio) -add_subdirectory(input) -add_subdirectory(log) -if(DUSK_NETWORK) - add_subdirectory(network) -endif() -add_subdirectory(save) -add_subdirectory(system) -add_subdirectory(time) \ No newline at end of file diff --git a/src/duskpsp/asset/CMakeLists.txt b/src/duskpsp/asset/CMakeLists.txt deleted file mode 100644 index 412f7a82..00000000 --- a/src/duskpsp/asset/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - assetpbp.c - assetpsp.c -) \ No newline at end of file diff --git a/src/duskpsp/asset/assetpbp.c b/src/duskpsp/asset/assetpbp.c deleted file mode 100644 index 2877d6e1..00000000 --- a/src/duskpsp/asset/assetpbp.c +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "asset/asset.h" -#include "asset/assetdsk.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" - -errorret_t assetInitPBP(const char_t *pbpPath) { - assertNotNull(pbpPath, "PBP path cannot be null."); - assertStrLenMin(pbpPath, 1, "PBP path cannot be empty."); - assertStrLenMax(pbpPath, ASSET_PBP_PATH_MAX, "PBP path is too long."); - - // Save devices with a real filesystem path (PSP) reuse this as their - // base directory - see ASSET.baseDirectory's own comment. dusk.dsk - // itself is embedded inside this PBP rather than a loose file, so this - // is the directory the PBP (and thus the game) actually lives in. - stringCopy(ASSET.baseDirectory, pbpPath, ASSET_BASE_DIRECTORY_MAX); - char_t *lastSlash = stringFindLastChar(ASSET.baseDirectory, '/'); - if(lastSlash != NULL) *lastSlash = '\0'; - - FILE *pbpFile = fopen(pbpPath, "rb"); - if(pbpFile == NULL) { - errorThrow("Failed to open PBP file: %s", pbpPath); - } - - // Get size of PBP file. - if(fseek(pbpFile, 0, SEEK_END) != 0) { - fclose(pbpFile); - errorThrow("Failed to seek to end of PBP file : %s", pbpPath); - } - - size_t pbpSize = ftell(pbpFile); - if(pbpSize == -1L) { - fclose(pbpFile); - errorThrow("Failed to get size of PBP file : %s", pbpPath); - } - - if(pbpSize < sizeof(assetpbpheader_t)) { - fclose(pbpFile); - errorThrow("PBP file is too small to be valid: %s", pbpPath); - } - - // Rewind to start - if(fseek(pbpFile, 0, SEEK_SET) != 0) { - fclose(pbpFile); - errorThrow("Failed to seek to start of PBP file : %s", pbpPath); - } - - // Read the PBP header - size_t read = fread( - &ASSET.platform.pbpHeader, 1, sizeof(assetpbpheader_t), pbpFile - ); - fclose(pbpFile); - if(read != sizeof(assetpbpheader_t)) { - errorThrow("Failed to read PBP header", pbpPath); - } - - if(memoryCompare( - ASSET.platform.pbpHeader.signature, - ASSET_PBP_SIGNATURE, - sizeof(ASSET_PBP_SIGNATURE) - ) != 0) { - errorThrow("Invalid PBP signature in file: %s", pbpPath); - } - - // The PSAR region holds the embedded dusk.dsk (DSK2) archive - I'm not - // sure what PSAR was intended for, but it holds any user data we want, - // so I shoved the entire dusk.dsk there. Opened lazily/on-demand - // straight from the PBP file by path+offset (see - // assetDskOpenFromPathRange), the same way every other platform opens - // dusk.dsk, rather than reading the whole PSAR into memory up front. - size_t psarSize = pbpSize - ASSET.platform.pbpHeader.psarOffset; - errorChain(assetDskOpenFromPathRange( - pbpPath, - ASSET.platform.pbpHeader.psarOffset, - psarSize, - &ASSET.zip - )); - - errorOk(); -} - -errorret_t assetDisposePBP(void) { - errorOk(); -} \ No newline at end of file diff --git a/src/duskpsp/asset/assetpbp.h b/src/duskpsp/asset/assetpbp.h deleted file mode 100644 index dbbcc02c..00000000 --- a/src/duskpsp/asset/assetpbp.h +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "asset/assetfile.h" - -#define ASSET_PBP_SIGNATURE_SIZE 4 -#define ASSET_PBP_SIGNATURE "\0PBP" - -#define ASSET_PBP_PATH_MAX FILENAME_MAX - -typedef struct { - char_t signature[ASSET_PBP_SIGNATURE_SIZE]; - uint32_t version; - uint32_t sfoOffset; - uint32_t icon0Offset; - uint32_t icon1Offset; - uint32_t pic0Offset; - uint32_t pic1Offset; - uint32_t snd0Offset; - uint32_t pspOffset; - uint32_t psarOffset; -} assetpbpheader_t; - -typedef struct { - assetpbpheader_t pbpHeader; -} assetpbp_t; - -/** - * Initializes the PBP style asset system. - * - * @param pbpPath The file path to the PBP file to read. - * @returns An errorret_t indicating success or failure of the operation. - */ -errorret_t assetInitPBP(const char_t *pbpPath); - -/** - * Disposes the PBP style asset system. - * - * @returns An errorret_t indicating success or failure of the operation. - */ -errorret_t assetDisposePBP(void); \ No newline at end of file diff --git a/src/duskpsp/asset/assetplatform.h b/src/duskpsp/asset/assetplatform.h deleted file mode 100644 index b7440a8c..00000000 --- a/src/duskpsp/asset/assetplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "assetpsp.h" - -typedef assetpsp_t assetplatform_t; -#define assetInitPlatform assetInitPSP -#define assetDisposePlatform assetDisposePSP \ No newline at end of file diff --git a/src/duskpsp/asset/assetpsp.c b/src/duskpsp/asset/assetpsp.c deleted file mode 100644 index 0d983bfb..00000000 --- a/src/duskpsp/asset/assetpsp.c +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "asset/asset.h" -#include "engine/engine.h" -#include "assert/assert.h" -#include "util/string.h" - -errorret_t assetInitPSP(void) { - assertTrue(ENGINE.argc >= 1, "PSP requires launch argument."); - - // PSP is given either the prx OR the PBP file. - // In the format of "ms0:/PSP/GAME/DUSK/EBOOT.PBP" or "host0:/Dusk.prx" - // IF the file is the PBP file, we are loading directly on the PSP itself. - // IF the file is the .prx then we are debugging and fopen will return - // relative filepaths correctly, e.g. host0:/dusk.dsk will be on host. - - // Originally I was going to allow host to read the .dsk directly but then - // I'd have to maintain two file implementations. - - const char_t *pbpPath; - if(stringEndsWithCaseInsensitive(ENGINE.argv[0], ASSET_PSP_EXTENSION_PBP)) { - pbpPath = ENGINE.argv[0]; - } else { - // In Debugging this would be next to host0:/Dusk.prx - pbpPath = "./EBOOT.PBP"; - } - - errorChain(assetInitPBP(pbpPath)); - errorOk(); -} - -errorret_t assetDisposePSP(void) { - errorChain(assetDisposePBP()); - errorOk(); -} \ No newline at end of file diff --git a/src/duskpsp/asset/assetpsp.h b/src/duskpsp/asset/assetpsp.h deleted file mode 100644 index badcd314..00000000 --- a/src/duskpsp/asset/assetpsp.h +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "assetpbp.h" - -#define ASSET_PSP_EXTENSION_PBP ".pbp" - -typedef assetpbp_t assetpsp_t; - -/** - * Initializes the PSP style asset system. - * - * @returns An errorret_t indicating success or failure of the operation. - */ -errorret_t assetInitPSP(void); - -/** - * Disposes the PSP style asset system. - * - * @returns An errorret_t indicating success or failure of the operation. - */ -errorret_t assetDisposePSP(void); \ No newline at end of file diff --git a/src/duskpsp/audio/CMakeLists.txt b/src/duskpsp/audio/CMakeLists.txt deleted file mode 100644 index 3ad1c569..00000000 --- a/src/duskpsp/audio/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - audiopsp.c - audiostreampsp.c - audiostreammp3decoder.c -) diff --git a/src/duskpsp/audio/audioplatform.h b/src/duskpsp/audio/audioplatform.h deleted file mode 100644 index 46595536..00000000 --- a/src/duskpsp/audio/audioplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audiopsp.h" - -#define audioPlatformInit audioPSPInit -#define audioPlatformUpdate audioPSPUpdate -#define audioPlatformDispose audioPSPDispose diff --git a/src/duskpsp/audio/audiopsp.c b/src/duskpsp/audio/audiopsp.c deleted file mode 100644 index 9815d5f1..00000000 --- a/src/duskpsp/audio/audiopsp.c +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiopsp.h" -#include "audiostreammp3decoder.h" - -errorret_t audioPSPInit() { - errorOk(); -} - -errorret_t audioPSPUpdate() { - errorOk(); -} - -errorret_t audioPSPDispose() { - errorChain(audioStreamMp3DecoderGlobalDispose()); - - errorOk(); -} diff --git a/src/duskpsp/audio/audiopsp.h b/src/duskpsp/audio/audiopsp.h deleted file mode 100644 index b5657274..00000000 --- a/src/duskpsp/audio/audiopsp.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -/** - * Initializes the PSP-specific audio subsystem state. - * - * @return Error state if any. - */ -errorret_t audioPSPInit(); - -/** - * Updates the PSP-specific audio subsystem state. Called once per frame. - * - * @return Error state if any. - */ -errorret_t audioPSPUpdate(); - -/** - * Disposes the PSP-specific audio subsystem state. - * - * @return Error state if any. - */ -errorret_t audioPSPDispose(); diff --git a/src/duskpsp/audio/audiostreammp3decoder.c b/src/duskpsp/audio/audiostreammp3decoder.c deleted file mode 100644 index 18ed83c1..00000000 --- a/src/duskpsp/audio/audiostreammp3decoder.c +++ /dev/null @@ -1,264 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreammp3decoder.h" -#include "audio/stream/audiostream.h" -#include "assert/assert.h" -#include "util/memory.h" -#include -#include - -// Per the PSP SDK's mp3 sample and pspmp3.h's own documentation - these -// are hard minimums sceMp3ReserveMp3Handle() requires, not tunable. -#define AUDIO_MP3_PSP_STREAM_BUF_SIZE (16 * 1024) -#define AUDIO_MP3_PSP_PCM_BUF_SIZE (16 * (1152 / 2)) - -// sceMp3 manages its own internal ring buffer of compressed bytes rather -// than being handed a whole file/buffer up front (unlike, say, -// zip_source_buffer_create elsewhere in this codebase) - it tells the -// caller exactly where in the source stream it wants more data from -// (sceMp3GetInfoToAddStreamData()'s srcpos) and how much room is free to -// receive it into, and the caller is expected to fetch exactly that and -// call sceMp3NotifyAddStreamData(). This decoder only ever feeds it -// forward, sequentially, from stream->mp3.file (never anything sceMp3 -// itself wouldn't naturally request next), so despite the API being built -// for arbitrary positioning, this driver never needs to seek that file -// out of sequence - see audioStreamMp3DecoderFillStream()'s own assertion. - -// End-of-stream sentinel returned by sceMp3Decode() once nothing more can -// be decoded - not documented in pspmp3.h, but confirmed by the PSP SDK's -// own mp3 sample (main.c), which explicitly excludes it from its generic -// "sceMp3Decode failed" error path. -#define AUDIO_MP3_PSP_DECODE_END_OF_STREAM 0x80671402 - -// Set once audioStreamMp3DecoderInit() has loaded sceMp3's firmware -// modules and initialized its resource pool - lazily, on the first MP3 -// stream ever created, rather than unconditionally at engine startup, so -// a game that never plays an MP3 never pays for it. Guards -// audioStreamMp3DecoderGlobalDispose() so it only tears down what was -// actually set up. -static bool_t AUDIO_MP3_PSP_RESOURCES_READY = false; - -errorret_t audioStreamMp3DecoderFillStream(audiostream_t *stream) { - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - - SceUChar8 *dst = NULL; - SceInt32 toWrite = 0; - SceInt32 srcPos = 0; - SceInt32 status = sceMp3GetInfoToAddStreamData( - decoder->handle, &dst, &toWrite, &srcPos - ); - if(status < 0) { - errorThrow("sceMp3GetInfoToAddStreamData failed: 0x%08X", status); - } - - if(toWrite <= 0) { - errorOk(); - } - - assertTrue( - (size_t) srcPos == decoder->streamPosition, - "sceMp3 requested stream data out of the sequence this decoder feeds it in." - ); - - errorChain(assetFileRead(&stream->mp3.file, dst, (size_t) toWrite)); - const size_t bytesRead = (size_t) stream->mp3.file.lastRead; - - status = sceMp3NotifyAddStreamData(decoder->handle, (SceInt32) bytesRead); - if(status < 0) { - errorThrow("sceMp3NotifyAddStreamData failed: 0x%08X", status); - } - - decoder->streamPosition += bytesRead; - if(bytesRead < (size_t) toWrite) decoder->endOfFile = true; - - errorOk(); -} - -errorret_t audioStreamMp3DecoderInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - if(!AUDIO_MP3_PSP_RESOURCES_READY) { - int status = sceUtilityLoadModule(PSP_MODULE_AV_AVCODEC); - if(status < 0) { - errorThrow("Failed to load PSP_MODULE_AV_AVCODEC: 0x%08X", status); - } - status = sceUtilityLoadModule(PSP_MODULE_AV_MP3); - if(status < 0) { - errorThrow("Failed to load PSP_MODULE_AV_MP3: 0x%08X", status); - } - status = sceMp3InitResource(); - if(status < 0) { - errorThrow("sceMp3InitResource failed: 0x%08X", status); - } - AUDIO_MP3_PSP_RESOURCES_READY = true; - } - - const assetmp3file_t *mp3 = &stream->asset->data.mp3; - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - - decoder->mp3Buf = memoryAlign(64, AUDIO_MP3_PSP_STREAM_BUF_SIZE); - decoder->pcmBuf = memoryAlign(64, AUDIO_MP3_PSP_PCM_BUF_SIZE); - decoder->streamPosition = mp3->dataOffset; - decoder->endOfFile = false; - - SceMp3InitArg initArg; - memoryZero(&initArg, sizeof(initArg)); - initArg.mp3StreamStart = (SceOff) mp3->dataOffset; - initArg.mp3StreamEnd = (SceOff) (mp3->dataOffset + mp3->dataSize); - initArg.mp3Buf = decoder->mp3Buf; - initArg.mp3BufSize = AUDIO_MP3_PSP_STREAM_BUF_SIZE; - initArg.pcmBuf = decoder->pcmBuf; - initArg.pcmBufSize = AUDIO_MP3_PSP_PCM_BUF_SIZE; - - const SceInt32 handle = sceMp3ReserveMp3Handle(&initArg); - if(handle < 0) { - memoryFree(decoder->mp3Buf); - memoryFree(decoder->pcmBuf); - errorThrow("sceMp3ReserveMp3Handle failed: 0x%08X", handle); - } - decoder->handle = handle; - - // sceMp3Init() needs some stream data already fed before it can - // determine the stream's format - see the PSP SDK's own mp3 sample. - errorChain(audioStreamMp3DecoderFillStream(stream)); - - const SceInt32 initStatus = sceMp3Init(decoder->handle); - if(initStatus < 0) { - sceMp3ReleaseMp3Handle(decoder->handle); - memoryFree(decoder->mp3Buf); - memoryFree(decoder->pcmBuf); - errorThrow("sceMp3Init failed: 0x%08X", initStatus); - } - - errorOk(); -} - -errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - - sceMp3ReleaseMp3Handle(decoder->handle); - memoryFree(decoder->mp3Buf); - decoder->mp3Buf = NULL; - memoryFree(decoder->pcmBuf); - decoder->pcmBuf = NULL; - - errorOk(); -} - -errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - const assetmp3file_t *mp3 = &stream->asset->data.mp3; - - errorChain(assetFileRewind(&stream->mp3.file)); - errorChain(assetFileRead(&stream->mp3.file, NULL, mp3->dataOffset)); - - // Deliberately not sceMp3ResetPlayPosition(): the SDK doesn't document - // precisely enough whether it fully clears sceMp3's own internal - // compressed-data ring buffer or just resets a playback cursor, leaving - // room for it to disagree with the fresh stream position we just fed it - // from - a mismatch that would only be audible as an occasional click - // right at the loop point, depending on how much of sceMp3's internal - // buffer happened to still hold stale data at reset time. Fully tearing - // down and recreating the decoder instead guarantees a clean state by - // construction, via the exact same path already proven correct for the - // stream's very first Init - one extra handle release/reserve cycle per - // loop wrap is negligible next to how rarely loops actually happen. - errorChain(audioStreamMp3DecoderDispose(stream)); - errorChain(audioStreamMp3DecoderInit(stream)); - - errorOk(); -} - -errorret_t audioStreamMp3DecoderDecodeFrame( - audiostream_t *stream, - int16_t *out, - size_t *outFrames -) { - assertNotNull(stream, "Stream cannot be NULL."); - assertNotNull(out, "Out buffer cannot be NULL."); - assertNotNull(outFrames, "outFrames cannot be NULL."); - assertTrue(stream->type == AUDIO_STREAM_TYPE_MP3, "Stream is not MP3."); - - audiostreammp3decoder_t *decoder = &stream->mp3.decoder; - const size_t frameSize = stream->channels * sizeof(int16_t); - - for(;;) { - if( - sceMp3CheckStreamDataNeeded(decoder->handle) > 0 && !decoder->endOfFile - ) { - errorChain(audioStreamMp3DecoderFillStream(stream)); - } - - SceShort16 *pcm = NULL; - const SceInt32 bytesDecoded = sceMp3Decode(decoder->handle, &pcm); - - if(bytesDecoded > 0) { - // `out` (stream->mp3.pending) is sized for - // AUDIO_MP3_MAX_SAMPLES_PER_FRAME samples - sized specifically to - // cover sceMp3's pcmBuf provisioning (see this decoder's own - // AUDIO_MP3_PSP_PCM_BUF_SIZE comment). Asserted rather than trusted: - // a real hardware overflow here was previously silent, corrupting - // whatever followed stream->mp3.pending in memory, and only showed - // up as very intermittent audio clicking. - assertTrue( - (size_t) bytesDecoded <= - AUDIO_MP3_MAX_SAMPLES_PER_FRAME * sizeof(int16_t), - "sceMp3Decode returned more PCM than the destination buffer can hold." - ); - const size_t frames = (size_t) bytesDecoded / frameSize; - memoryCopy(out, pcm, (size_t) bytesDecoded); - *outFrames = frames; - errorOk(); - } - - if( - bytesDecoded < 0 && - (uint32_t) bytesDecoded != AUDIO_MP3_PSP_DECODE_END_OF_STREAM - ) { - errorThrow("sceMp3Decode failed: 0x%08X", bytesDecoded); - } - - // Decoded nothing this call - if sceMp3 doesn't need more data either, - // it's genuinely out of frames to produce. - if(sceMp3CheckStreamDataNeeded(decoder->handle) <= 0) { - *outFrames = 0; - errorOk(); - } - - if(decoder->endOfFile) { - // sceMp3 wants more data but the file has none left - one last - // decode attempt already happened above with whatever was fed; - // nothing further will ever arrive. - *outFrames = 0; - errorOk(); - } - } -} - -errorret_t audioStreamMp3DecoderGlobalDispose(void) { - if(!AUDIO_MP3_PSP_RESOURCES_READY) { - errorOk(); - } - - const SceInt32 status = sceMp3TermResource(); - if(status < 0) { - errorThrow("sceMp3TermResource failed: 0x%08X", status); - } - - sceUtilityUnloadModule(PSP_MODULE_AV_MP3); - sceUtilityUnloadModule(PSP_MODULE_AV_AVCODEC); - AUDIO_MP3_PSP_RESOURCES_READY = false; - - errorOk(); -} diff --git a/src/duskpsp/audio/audiostreammp3decoder.h b/src/duskpsp/audio/audiostreammp3decoder.h deleted file mode 100644 index 71bc0bfe..00000000 --- a/src/duskpsp/audio/audiostreammp3decoder.h +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include - -typedef struct audiostream_s audiostream_t; - -/** - * Hardware MP3 decoder state (sceMp3) for a single stream. Firmware-side - * decode, off the CPU entirely - see audiostreammp3decoder.c's top-of-file - * comment for the streaming model this drives (sceMp3 manages its own - * internal compressed-byte ring buffer, pulling more from - * stream->mp3.file on demand via audioStreamMp3DecoderFillStream()). - */ -typedef struct { - // Handle returned by sceMp3ReserveMp3Handle(), valid for this stream's - // whole lifetime. - SceInt32 handle; - - // Buffers owned by this decoder and registered with sceMp3 via - // SceMp3InitArg - sized to the SDK's documented minimums (mp3Buf must be - // >= 8192 bytes, pcmBuf >= 9216 bytes). Allocated once in - // audioStreamMp3DecoderInit(), freed in audioStreamMp3DecoderDispose(). - uint8_t *mp3Buf; - uint8_t *pcmBuf; - - // This decoder's own record of how many compressed bytes have been fed - // to sceMp3 so far (relative to the start of the whole file, matching - // stream->mp3.file's own position) - sceMp3GetInfoToAddStreamData() - // reports back the absolute source position it wants read from next, - // and since this decoder only ever feeds it forward, sequentially, that - // position should always match this - asserted rather than silently - // trusted, in audioStreamMp3DecoderFillStream(). - size_t streamPosition; - - // Set once assetFileRead() returns fewer bytes than requested while - // filling sceMp3's stream buffer - there's no more compressed data to - // add, though sceMp3 may still have enough already-fed data to decode - // one or more final frames. - bool_t endOfFile; -} audiostreammp3decoder_t; - -/** - * Feeds sceMp3 more compressed bytes if (and only as much as) it currently - * wants: asks it where in the stream and how much - * (sceMp3GetInfoToAddStreamData()), reads exactly that from stream->mp3.file, - * and hands it back - * (sceMp3NotifyAddStreamData()). A no-op if sceMp3 doesn't currently want - * more. See this file's own top-of-file comment on why the source - * position sceMp3 requests is asserted rather than actually used to seek. - * - * @param stream The audio stream to feed. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderFillStream(audiostream_t *stream); - -/** - * Initializes the hardware MP3 decoder for the given stream: lazily loads - * the sceMp3/AVCODEC firmware modules and initializes sceMp3's resource - * pool the first time any stream ever needs it (see - * audioStreamMp3DecoderGlobalDispose()'s own comment), reserves a handle - * sized from the stream's asset metadata, and feeds it enough initial - * compressed data for sceMp3Init() to succeed. - * - * @param stream The audio stream to initialize. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderInit(audiostream_t *stream); - -/** - * Disposes the hardware MP3 decoder for the given stream: releases its - * sceMp3 handle and frees its buffers. Does not tear down sceMp3's global - * resource pool - see audioStreamMp3DecoderGlobalDispose(). - * - * @param stream The audio stream to dispose. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderDispose(audiostream_t *stream); - -/** - * Resets the decoder to the very start of the compressed stream: rewinds - * stream->mp3.file back to the asset's parsed data offset, resets this - * decoder's own streamPosition to match, and calls - * sceMp3ResetPlayPosition() so sceMp3 requests stream data from the start - * again. See audioStreamMp3Seek()'s own comment on why every seek goes - * through here. - * - * @param stream The audio stream to rewind. Must be AUDIO_STREAM_TYPE_MP3. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderRewind(audiostream_t *stream); - -/** - * Decodes the next MPEG frame's worth of PCM samples, feeding sceMp3's - * internal stream buffer from stream->mp3.file as it requests more. - * Writes decoded samples to `out` (sized for at least - * AUDIO_MP3_MAX_SAMPLES_PER_FRAME int16_t values) and sets *outFrames to - * how many frames (not samples) were produced - 0 once sceMp3 reports it - * both has nothing left to decode and needs no more stream data (a - * genuine end of stream). - * - * @param stream The audio stream to decode. Must be AUDIO_STREAM_TYPE_MP3. - * @param out Destination buffer for decoded samples. - * @param outFrames Set to the number of frames actually decoded. - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderDecodeFrame( - audiostream_t *stream, - int16_t *out, - size_t *outFrames -); - -/** - * Tears down sceMp3's global resource pool and unloads its firmware - * modules, if audioStreamMp3DecoderInit() ever actually initialized them - - * a no-op otherwise. Called once from audioPSPDispose(), regardless of - * whether any MP3 stream was ever created. - * - * @return Error indicating success or failure. - */ -errorret_t audioStreamMp3DecoderGlobalDispose(void); diff --git a/src/duskpsp/audio/audiostreamplatform.h b/src/duskpsp/audio/audiostreamplatform.h deleted file mode 100644 index 0ea4c93c..00000000 --- a/src/duskpsp/audio/audiostreamplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "audiostreampsp.h" - -typedef audiostreampsp_t audiostreamplatform_t; - -#define audioStreamPlatformInit audioStreamPSPInit -#define audioStreamPlatformDispose audioStreamPSPDispose -#define audioStreamPlatformBuffer audioStreamPSPBuffer -#define audioStreamPlatformIsFinished audioStreamPSPIsFinished diff --git a/src/duskpsp/audio/audiostreampsp.c b/src/duskpsp/audio/audiostreampsp.c deleted file mode 100644 index a33ee1ff..00000000 --- a/src/duskpsp/audio/audiostreampsp.c +++ /dev/null @@ -1,470 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "audiostreampsp.h" -#include "audio/stream/audiostream.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/math.h" -#include -#include - -// Matches pspaudiolib's own PSP_NUM_AUDIO_SAMPLES convention - PSP audio -// hardware is meant to be fed small chunks continuously, not one large -// buffer per call. This is purely an output granularity now - see -// audioStreamPSPTopUp() for how much it reads per call. -#define AUDIO_PSP_CHUNK_FRAMES 1024 - -// PSP priorities are inverted (lower = higher priority). This project's -// main thread runs at the PSPSDK default of 32 (PSP_MAIN_THREAD_PRIORITY -// is never overridden). A pthread created with default attributes runs at -// 60 - LOWER priority than the main/render thread - so under load the -// output thread would starve and the hardware channel underruns, heard as -// jitter/crackle that gets worse the busier (lower-fps) a frame is. Raise -// it above main so audio feeding always wins scheduling contention. -#define AUDIO_PSP_THREAD_PRIORITY 18 - -// How many frames at the very end of a stream get linearly faded to zero, -// avoiding the audible click of the waveform stopping at a non-zero -// amplitude (either into padding on a partial final chunk, or the DAC -// just stopping outright on an exact-multiple-length one). -#define AUDIO_PSP_FADE_FRAMES 32 - -// How long the persistent output thread sleeps between checks - both for -// a new play request while idle, and for the ring buffer to have a full -// chunk ready while active. -#define AUDIO_PSP_IDLE_POLL_MICROS 500 - -errorret_t audioStreamPSPInit(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->channels != 1 && stream->channels != 2) { - errorThrow( - "PSP audio only supports mono or stereo PCM, got %d channels.", - stream->channels - ); - } - - // sceAudioChReserve's hardware channels always run at the PSP's native - // 44100Hz; there is no per-channel sample rate. Arbitrary rates would need - // sceAudioSRCChReserve's single exclusive channel instead. - if(stream->sampleRate != 44100) { - errorThrow( - "PSP audio channels are fixed at 44100Hz, got %uHz.", - stream->sampleRate - ); - } - - const int format = stream->channels == 1 - ? PSP_AUDIO_FORMAT_MONO - : PSP_AUDIO_FORMAT_STEREO; - - const int channel = sceAudioChReserve( - PSP_AUDIO_NEXT_CHANNEL, AUDIO_PSP_CHUNK_FRAMES, format - ); - if(channel < 0) { - errorThrow("Failed to reserve PSP audio channel: 0x%08X", channel); - } - - stream->platform.channel = channel; - stream->platform.finished = false; - stream->platform.playRequested = false; - stream->platform.hasLastVolume = false; - - const size_t ringSize = - AUDIO_PSP_RING_FRAMES * stream->channels * sizeof(int16_t); - stream->platform.ring = memoryAllocate(ringSize); - stream->platform.scratch = memoryAllocate(ringSize); - threadMutexInit(&stream->platform.ringLock); - - threadInit(&stream->platform.thread, audioStreamPSPThreadFeed); - stream->platform.thread.data = stream; - threadStartRequest(&stream->platform.thread); - - errorOk(); -} - -errorret_t audioStreamPSPDispose(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - threadStop(&stream->platform.thread); - sceAudioChRelease(stream->platform.channel); - - memoryFree(stream->platform.ring); - stream->platform.ring = NULL; - memoryFree(stream->platform.scratch); - stream->platform.scratch = NULL; - threadMutexDispose(&stream->platform.ringLock); - - errorOk(); -} - -errorret_t audioStreamPSPBuffer(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - stream->platform.finished = false; - stream->platform.readReachedEnd = false; - stream->platform.readFailed = false; - - stream->platform.totalFrames = audioStreamGetTotalFrames(stream); - - // loopEndFrame/loopToFrame define the loop segment [loopToFrame, - // loopEndFrame) that a looping pass wraps within, once it's reached - - // defaulting to the whole clip (loopStart == -1, loopTo == 0) so - // behaviour is unchanged when no explicit loop points are configured. - stream->platform.loopEndFrame = stream->loopStart >= 0 - ? mathMin( - (size_t) (stream->loopStart * stream->sampleRate), - stream->platform.totalFrames - ) - : stream->platform.totalFrames; - stream->platform.loopToFrame = mathMin( - (size_t) (stream->loopTo * stream->sampleRate), - stream->platform.loopEndFrame - ); - - const size_t startFrame = - mathMin(stream->startFrame, stream->platform.totalFrames); - stream->startFrame = 0; - stream->seeking = false; - - errorChain(audioStreamSeek(stream, startFrame)); - stream->platform.readPosition = startFrame; - - threadMutexLock(&stream->platform.ringLock); - stream->platform.ringReadPos = 0; - stream->platform.ringWritePos = 0; - stream->platform.ringFilled = 0; - stream->platform.framesEnqueued = 0; - stream->platform.loopMarkerHead = 0; - stream->platform.loopMarkerCount = 0; - threadMutexUnlock(&stream->platform.ringLock); - - stream->platform.framesOutput = 0; - stream->platform.playRequested = true; - - errorOk(); -} - -void audioStreamPSPTopUp(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - if(stream->platform.readReachedEnd || stream->platform.readFailed) return; - - threadMutexLock(&stream->platform.ringLock); - const size_t filled = stream->platform.ringFilled; - threadMutexUnlock(&stream->platform.ringLock); - if(filled >= AUDIO_PSP_LEAD_FRAMES) return; - - const size_t channels = stream->channels; - const size_t frameSize = channels * sizeof(int16_t); - - // Fill however much room the ring has, up to one bounded step, not just - // whatever's needed to reach LEAD in a single shot - AUDIO_PSP_RING_FRAMES - // is now sized to absorb a slow MP3 loop-wrap (see its own comment), and - // greedily trying to fill all of it in one call right after a fresh - // Buffer() (when the ring starts empty) would turn the very first - // top-up into one long blocking decode burst on the main thread instead - // of many small ones - worse for startup smoothness, not better. Still - // uncapped per se: reaching LEAD just takes a few calls (a few engine - // frames) instead of one, which comfortably keeps up with real-time - // consumption the same way an unbounded fill would, since a temporary - // frame-rate dip only slows how fast the ring tops up, never how much - // room is left to fill on the next call. Bounded by the ring's own - // physical capacity, and by loopEndFrame (the loop segment's end) - - // except a seek can legitimately land past it (e.g. into an outro after - // the loop point), in which case read out to the true end of the clip - // once instead of underflowing framesRemainingInSegment. - const size_t room = mathMin( - AUDIO_PSP_RING_FRAMES - filled, (size_t) AUDIO_PSP_TOPUP_STEP_FRAMES - ); - const size_t currentEndFrame = - stream->platform.readPosition < stream->platform.loopEndFrame - ? stream->platform.loopEndFrame - : stream->platform.totalFrames; - const size_t framesRemainingInSegment = - currentEndFrame - stream->platform.readPosition; - const size_t framesToRead = mathMin(room, framesRemainingInSegment); - - if(framesToRead > 0) { - int16_t *scratch = stream->platform.scratch; - - size_t framesRead = 0; - if(errorIsNotOk( - audioStreamRead(stream, scratch, framesToRead, &framesRead) - )) { - stream->platform.readFailed = true; - return; - } - - if(framesRead > 0) { - threadMutexLock(&stream->platform.ringLock); - for(size_t i = 0; i < framesRead; i++) { - const size_t writeIndex = - (stream->platform.ringWritePos + i) % AUDIO_PSP_RING_FRAMES; - memoryCopy( - stream->platform.ring + (writeIndex * channels), - scratch + (i * channels), - frameSize - ); - } - stream->platform.ringWritePos = - (stream->platform.ringWritePos + framesRead) % AUDIO_PSP_RING_FRAMES; - stream->platform.ringFilled += framesRead; - stream->platform.framesEnqueued += framesRead; - threadMutexUnlock(&stream->platform.ringLock); - - stream->platform.readPosition += framesRead; - } - } - - // Reached (or ran past, which can't actually happen since framesToRead - // is itself bounded by framesRemainingInSegment) the segment end only if - // readPosition genuinely got there - never inferred from how much was - // merely requested. A short read (framesRead < framesToRead, including - // 0) does NOT by itself mean the segment is over: unlike PCM (where a - // short read only ever happens at a truly corrupt/truncated file's real - // end), MP3 decode can plausibly produce fewer frames than asked without - // that meaning "no more content" (e.g. a hardware decoder backend - // reporting it has nothing ready *this instant*, not that there's - // nothing left in the file) - trusting the request size instead of - // actual progress here silently let readPosition race ahead of real - // decode progress, confirmed as the cause of premature loop-segment-end - // detection ("racing through content"). A short read that isn't - // genuinely the end just means less got queued this call; the next - // TopUp() call (next engine frame) naturally retries. - - // A limit of 0 means unlimited - see loopRestartCount's own comment for - // why counting decided (not yet necessarily audible) restarts here is - // still the right thing to compare against loopLimit. - const bool_t loopLimitReached = stream->loopLimit > 0 && - stream->loopRestartCount >= stream->loopLimit; - const bool_t reachesSegmentEnd = - stream->platform.readPosition >= currentEndFrame; - const bool_t willLoop = reachesSegmentEnd && - (stream->state & AUDIO_STREAM_STATE_LOOPING) && !loopLimitReached; - - if(reachesSegmentEnd) { - if(willLoop) { - if(errorIsNotOk(audioStreamSeek(stream, stream->platform.loopToFrame))) { - stream->platform.readFailed = true; - return; - } - stream->platform.readPosition = stream->platform.loopToFrame; - stream->loopRestartCount++; - - threadMutexLock(&stream->platform.ringLock); - if(stream->platform.loopMarkerCount < AUDIO_PSP_LOOP_MARKER_MAX) { - const size_t index = ( - stream->platform.loopMarkerHead + stream->platform.loopMarkerCount - ) % AUDIO_PSP_LOOP_MARKER_MAX; - stream->platform.loopMarkerFrames[index] = - stream->platform.framesEnqueued; - stream->platform.loopMarkerCount++; - } - threadMutexUnlock(&stream->platform.ringLock); - } else { - stream->platform.readReachedEnd = true; - } - } -} - -bool_t audioStreamPSPIsFinished(audiostream_t *stream) { - assertNotNull(stream, "Stream cannot be NULL."); - - // Only ever called while this stream is actively playing (see - // audioStreamUpdate()) - piggybacking the per-frame top-up here mirrors - // dusklinux's own audioStreamLinuxIsFinished()/audioStreamLinuxFeed() - // pattern, rather than needing a dedicated thread to do PCM I/O. - audioStreamPSPTopUp(stream); - - return stream->platform.finished; -} - -void audioStreamPSPThreadFeed(thread_t *thread) { - assertNotNull(thread, "Thread cannot be NULL."); - - sceKernelChangeThreadPriority( - sceKernelGetThreadId(), AUDIO_PSP_THREAD_PRIORITY - ); - - audiostream_t *stream = (audiostream_t *) thread->data; - const size_t channels = stream->channels; - const size_t frameSize = channels * sizeof(int16_t); - int16_t *chunk = memoryAllocate(AUDIO_PSP_CHUNK_FRAMES * frameSize); - - // Runs for the stream's whole lifetime - idles here between plays rather - // than exiting, so a loop restart (or any replay) is just a flag flip - // audioStreamPSPBuffer() sets, not a whole new thread being spawned. - while(!threadShouldStop(thread)) { - if(!stream->platform.playRequested) { - sceKernelDelayThread(AUDIO_PSP_IDLE_POLL_MICROS); - continue; - } - stream->platform.playRequested = false; - - bool_t reachedEnd = false; - - // Every call always sends a full, constant-size AUDIO_PSP_CHUNK_FRAMES - // buffer - the channel is never re-declared to a different length, to - // avoid relying on sceAudioSetChannelDataLen's undocumented behavior - // mid-stream. Loops internally (rather than going idle and waiting for - // the engine's once-per-frame Update() to notice finished and - // re-trigger Buffer()) so a looping stream never has a - // detection-latency gap. - while(!threadShouldStop(thread) && !reachedEnd) { - // Wait until either a full hardware chunk is ready in the ring, or - // the main thread has stopped producing (a genuine end or a read - // failure) and whatever's left (0..one chunk) is all there'll ever - // be - the pass's final, possibly-partial chunk. - size_t available = 0; - bool_t stopRequested = false; - for(;;) { - threadMutexLock(&stream->platform.ringLock); - available = stream->platform.ringFilled; - threadMutexUnlock(&stream->platform.ringLock); - - if(available >= AUDIO_PSP_CHUNK_FRAMES) break; - if( - stream->platform.readReachedEnd || stream->platform.readFailed - ) break; - if(threadShouldStop(thread)) { - stopRequested = true; - break; - } - sceKernelDelayThread(AUDIO_PSP_IDLE_POLL_MICROS); - } - if(stopRequested) break; - - // Paused: skip consuming from the ring and skip - // sceAudioOutputPannedBlocking() entirely - that call is the only - // thing keeping the PSP's own tiny hardware buffer fed, so simply - // not calling it silences output within about one hardware chunk - // (~23ms) instead of waiting for this thread's own much larger - // software ring (AUDIO_PSP_RING_FRAMES, ~1.1s) to drain naturally. - // The ring is left completely untouched, so resuming - // (audioStreamPlay()) continues exactly where playback left off, - // with no skip or gap. See audiostream.h's own comment on why - // reading stream->state from this thread is safe. - if(!(stream->state & AUDIO_STREAM_STATE_PLAYING)) { - sceKernelDelayThread(AUDIO_PSP_IDLE_POLL_MICROS); - continue; - } - - // Production has stopped and what's left fits in one chunk (0 up to - // AUDIO_PSP_CHUNK_FRAMES - it can never be more, since production - // never adds more once readReachedEnd/readFailed is set) - this is - // the pass's last chunk. - const bool_t isFinalChunk = - (stream->platform.readReachedEnd || stream->platform.readFailed) && - available <= AUDIO_PSP_CHUNK_FRAMES; - const size_t framesThisChunk = isFinalChunk - ? available - : AUDIO_PSP_CHUNK_FRAMES; - - threadMutexLock(&stream->platform.ringLock); - for(size_t i = 0; i < framesThisChunk; i++) { - const size_t readIndex = - (stream->platform.ringReadPos + i) % AUDIO_PSP_RING_FRAMES; - memoryCopy( - chunk + (i * channels), - stream->platform.ring + (readIndex * channels), - frameSize - ); - } - stream->platform.ringReadPos = - (stream->platform.ringReadPos + framesThisChunk) % - AUDIO_PSP_RING_FRAMES; - stream->platform.ringFilled -= framesThisChunk; - threadMutexUnlock(&stream->platform.ringLock); - - if(framesThisChunk < AUDIO_PSP_CHUNK_FRAMES) { - // True final chunk is shorter than a full hardware chunk - pad the - // rest with silence rather than play whatever was left in `chunk` - // from a previous pass. - memoryZero( - chunk + (framesThisChunk * channels), - (AUDIO_PSP_CHUNK_FRAMES - framesThisChunk) * frameSize - ); - } - - if(isFinalChunk) { - // Fade the real tail down to zero so the waveform never stops (or - // meets padding) at a non-zero amplitude, which is what was heard - // as a click at the end of playback. - const size_t fadeFrames = framesThisChunk < AUDIO_PSP_FADE_FRAMES - ? framesThisChunk - : AUDIO_PSP_FADE_FRAMES; - for(size_t i = 0; i < fadeFrames; i++) { - const size_t frame = framesThisChunk - fadeFrames + i; - const float_t factor = - 1.0f - ((float_t) (i + 1) / (float_t) fadeFrames); - for(size_t c = 0; c < channels; c++) { - int16_t *sample = &chunk[frame * channels + c]; - *sample = (int16_t) ((float_t) *sample * factor); - } - } - } - - // Checked every chunk (~23ms at 44100Hz) so SetVolume/SetDirectionality - // take effect mid-playback, unlike the platform's other one-shot - // calls - but only actually recomputed when one of them has changed - // since the last chunk (see lastVolume's own comment); otherwise the - // previous chunk's already-computed leftVolume/rightVolume are reused - // as-is. - if( - !stream->platform.hasLastVolume || - stream->platform.lastVolume != stream->volume || - stream->platform.lastDirectionality != stream->directionality - ) { - float_t leftFactor, rightFactor; - audioStreamGetPanFactors(stream, &leftFactor, &rightFactor); - - const int baseVolume = (int) (stream->volume * PSP_AUDIO_VOLUME_MAX); - stream->platform.lastLeftVolume = (int) (baseVolume * leftFactor); - stream->platform.lastRightVolume = (int) (baseVolume * rightFactor); - stream->platform.lastVolume = stream->volume; - stream->platform.lastDirectionality = stream->directionality; - stream->platform.hasLastVolume = true; - } - - sceAudioOutputPannedBlocking( - stream->platform.channel, - stream->platform.lastLeftVolume, - stream->platform.lastRightVolume, - chunk - ); - - stream->platform.framesOutput += framesThisChunk; - - // Fire any loop markers this chunk just played past - never call - // stream->onLoop directly from this thread, since it may do - // arbitrary, possibly-slow work (this is exactly what caused a loud - // crackle in the past: onLoop console-printing took long enough to - // starve the next chunk). audioStreamUpdate() picks up loopCount - // changes and fires onLoop safely from the main thread instead. - threadMutexLock(&stream->platform.ringLock); - while( - stream->platform.loopMarkerCount > 0 && - stream->platform.loopMarkerFrames[stream->platform.loopMarkerHead] <= - stream->platform.framesOutput - ) { - stream->platform.loopMarkerHead = - (stream->platform.loopMarkerHead + 1) % AUDIO_PSP_LOOP_MARKER_MAX; - stream->platform.loopMarkerCount--; - stream->loopCount++; - } - threadMutexUnlock(&stream->platform.ringLock); - - if(isFinalChunk) reachedEnd = true; - } - - stream->platform.finished = true; - } - - memoryFree(chunk); -} diff --git a/src/duskpsp/audio/audiostreampsp.h b/src/duskpsp/audio/audiostreampsp.h deleted file mode 100644 index 80f4d730..00000000 --- a/src/duskpsp/audio/audiostreampsp.h +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "thread/thread.h" - -typedef struct audiostream_s audiostream_t; - -// How far ahead of hardware playback the ring buffer is allowed to hold -// already-decoded PCM - mirrors dusklinux's AUDIO_LINUX_WINDOW_FRAMES. -// Physical capacity of the ring; must comfortably exceed -// AUDIO_PSP_LEAD_FRAMES plus one top-up step so a single top-up call can -// never overwrite data the output thread hasn't consumed yet. -// -// Sized well above what a PCM stream would ever need on its own, because -// an MP3 stream's loop-wrap (audioStreamMp3DecoderRewind() on PSP) fully -// tears down and recreates the hardware decoder - a real handle release/ -// reserve round-trip plus a fresh ~16KB Memory Stick read to re-prime -// sceMp3's stream buffer - all synchronously, inside the same TopUp() call -// that's supposed to be keeping this ring fed. That's slow enough, -// occasionally, to eat into a tighter lead margin and click right at the -// loop point (confirmed on real hardware) - this gives it generous room to -// do so without the output thread ever catching up to empty. -#define AUDIO_PSP_RING_FRAMES 49152 - -// Top-up threshold - audioStreamPSPTopUp() (called once per engine -// Update(), see audioStreamPSPIsFinished()) only reads more data once the -// ring drops below this, same trigger dusklinux uses for its own lead -// margin. See AUDIO_PSP_RING_FRAMES's own comment for why this is sized -// well beyond dusklinux's equivalent. -#define AUDIO_PSP_LEAD_FRAMES 12288 - -// Max frames audioStreamPSPTopUp() will read in one call, regardless of -// how much ring room is actually free - keeps each call's worst-case -// blocking time on the main thread bounded and roughly constant even -// though AUDIO_PSP_RING_FRAMES/AUDIO_PSP_LEAD_FRAMES are large; reaching -// LEAD from an empty ring just takes a few calls (a few engine frames) -// instead of one long one. See AUDIO_PSP_RING_FRAMES's own comment. -#define AUDIO_PSP_TOPUP_STEP_FRAMES 4096 - -// Max number of pending loop-wrap events (see audiostreampsploopmarker_t -// below) the ring can remember at once. Sized generously relative to how -// many loop wraps could conceivably be produced ahead of playback within -// one ring's worth of lookahead; a loop shorter than -// AUDIO_PSP_RING_FRAMES / AUDIO_PSP_LOOP_MARKER_MAX frames could in theory -// overflow this, in which case onLoop simply won't fire for the excess -// wraps until the queue drains - not hit by anything in this codebase -// today (the shared test tone's loop is the whole 1-second clip). -#define AUDIO_PSP_LOOP_MARKER_MAX 8 - -typedef struct { - // Reserved hardware output channel, from sceAudioChReserve. Reserved with - // a small fixed chunk size (AUDIO_PSP_CHUNK_FRAMES) - PSP audio hardware - // expects continuous small-chunk feeding, not one large buffer per call. - int channel; - - // Sole persistent thread, created once in Init and alive for the - // stream's whole lifetime - the only thing that ever calls - // sceAudioOutputPannedBlocking(). Never touches the asset/PCM layer - // itself; only ever drains the ring buffer below in fixed - // AUDIO_PSP_CHUNK_FRAMES chunks, so hardware output timing is never at - // the mercy of a Memory Stick/zip read. Idles (polling playRequested) - // between plays instead of exiting, same reasoning as before. - thread_t thread; - - // Set by audioStreamPSPBuffer() to wake the idling thread into a new - // pass; cleared by the thread once it picks it up. - volatile bool_t playRequested; - - // Set by the thread once it has output the last chunk of a pass. - volatile bool_t finished; - - // Ring buffer of already-decoded, contiguous PCM. Filled by - // audioStreamPSPTopUp() (runs on the MAIN thread, called once per engine - // Update() while playing - see audioStreamPSPIsFinished(), same pattern - // dusklinux uses for audioStreamLinuxFeed()) and drained by the output - // thread above. A loop wrap is just a seek back to loopToFrame partway - // through filling - the ring itself holds one seamless, contiguous - // stream of samples with no seams to splice, unlike the old per-hardware - // -chunk design; the output thread doesn't need to know where a loop - // boundary falls, only when it has *played past* one (see the loop - // marker fields below). - // - // ringReadPos/ringWritePos/ringFilled are the only fields touched by - // both the main thread (producer) and the output thread (consumer); - // both must hold ringLock to touch any of them. - int16_t *ring; // AUDIO_PSP_RING_FRAMES * channels * sizeof(int16_t) bytes - size_t ringReadPos; // next frame index the output thread reads (wraps) - size_t ringWritePos; // next frame index the main thread writes (wraps) - size_t ringFilled; // valid frames currently in the ring - threadmutex_t ringLock; - - // Scratch buffer audioStreamPcmRead() decodes into before it's copied - // into the ring - sized to the ring's full capacity since a single - // top-up call can need to fill the entire thing at once (e.g. right at - // pass start, or after a frame-rate dip let the ring run dry). Main- - // thread-only, like the rest of the top-up state below. - int16_t *scratch; - - // Total frames ever written into the ring this pass (monotonic, unlike - // ringWritePos which wraps) - what loopMarkerFrames[] values are - // expressed in, so they stay comparable to framesOutput below regardless - // of how many times the physical ring has wrapped around. - size_t framesEnqueued; - - // FIFO of pending loop-wrap events: audioStreamPSPTopUp() records - // framesEnqueued's value at the moment it seeks back to loopToFrame: - // once the output thread's own running framesOutput reaches that value, - // it has just played the last sample before the wrap and bumps - // loopCount (never calls onLoop directly - see the loop over these in - // audiostreampsp.c for why). Guarded by ringLock, same as the ring - // itself. - size_t loopMarkerFrames[AUDIO_PSP_LOOP_MARKER_MAX]; - size_t loopMarkerHead; - size_t loopMarkerCount; - - // Running count of frames the output thread has sent to hardware since - // the current pass started - what loopMarkerFrames[] positions are - // compared against. - size_t framesOutput; - - // Main-thread-only sequencing state for the current pass (loop points, - // read position) - only ever touched by audioStreamPSPBuffer()/ - // audioStreamPSPTopUp(), both of which only ever run on the main thread, - // so none of this needs locking. - size_t totalFrames; - size_t loopEndFrame; - size_t loopToFrame; - size_t readPosition; - - // Set by audioStreamPSPTopUp() once it has written the pass's true final - // frame (not a loop wrap - a genuine, non-looping end or a read/seek - // failure) - it stops reading further once either is set. The output - // thread treats "readReachedEnd (or readFailed) and the ring holds at - // most one hardware chunk" as its cue that whatever's left is the - // pass's last, possibly-partial chunk. - bool_t readReachedEnd; - bool_t readFailed; - - // Cache of the last stream->volume/directionality the output thread - // computed lastLeftVolume/lastRightVolume from - recomputing - // audioStreamGetPanFactors() and both multiplications is wasted work on - // every single chunk (~23ms) whenever neither has actually changed since - // the previous one, which is the common case outside of an active fade/ - // pan. PSP itself has no persistent per-channel volume to cache this - // for us (see sceAudioOutputPannedBlocking's own call site) - hence - // caching it ourselves. Output-thread-only, like the rest of this - // section - see sceAudioOutputPannedBlocking's own call site. - float_t lastVolume; - float_t lastDirectionality; - int lastLeftVolume; - int lastRightVolume; - bool_t hasLastVolume; -} audiostreampsp_t; - -/** - * Initializes the PSP-specific playback state of an audio stream (e.g. - * reserving a hardware output channel via sceAudioChReserve). - * - * @param stream The audio stream to initialize. - * @return Error state if any. - */ -errorret_t audioStreamPSPInit(audiostream_t *stream); - -/** - * Disposes the PSP-specific playback state of an audio stream, stopping its - * output thread and releasing its hardware output channel. - * - * @param stream The audio stream to dispose. - * @return Error state if any. - */ -errorret_t audioStreamPSPDispose(audiostream_t *stream); - -/** - * Starts a new playback pass: resets the ring buffer and loop-point - * bookkeeping, seeks the asset to the pass's start frame, and wakes the - * persistent output thread. Actual PCM reading happens afterward, driven - * by audioStreamPSPTopUp() (see audioStreamPSPIsFinished()) rather than - * here. - * - * @param stream The audio stream to output. - * @return Error state if any. - */ -errorret_t audioStreamPSPBuffer(audiostream_t *stream); - -/** - * Tops up the stream's ring buffer from the main thread if it has fallen - * below AUDIO_PSP_LEAD_FRAMES, reading enough of the asset in one call to - * fill whatever room the ring currently has (bounded by the current loop - * segment/clip end) - not just one small fixed step, so a temporary - * engine frame-rate dip can't let production fall permanently behind - * playback's consumption rate. Seeks back to loopToFrame and records a - * loop marker if this read crosses a looping pass's loop point, or marks - * readReachedEnd on a genuine end/failure. Called once per engine Update() - * via audioStreamPSPIsFinished(), same as dusklinux's own feed-on-poll - * pattern. A no-op once readReachedEnd or readFailed is set, since there's - * nothing left to add. - * - * @param stream The audio stream to top up. - */ -void audioStreamPSPTopUp(audiostream_t *stream); - -/** - * Checks whether the stream's output thread has finished outputting its - * currently buffered data - also drives audioStreamPSPTopUp() each call, - * since this is invoked exactly once per engine Update() while the stream - * is playing (see audioStreamUpdate()). - * - * @param stream The audio stream to check. - * @return true if playback has finished, false otherwise. - */ -bool_t audioStreamPSPIsFinished(audiostream_t *stream); - -/** - * Output thread entry point, run once for the stream's whole lifetime. - * Idles (polling playRequested) until woken by audioStreamPSPBuffer(), - * then repeatedly waits for a full hardware chunk to become available in - * the ring buffer (topped up by the main thread - see - * audioStreamPSPTopUp()) and outputs it, blocking naturally on each - * sceAudioOutputPannedBlocking() call, until the pass's final - * (possibly-partial, faded) chunk has been sent - then goes back to - * idling, ready for the next play request, until the thread is asked to - * stop. Never touches the asset/PCM layer directly. - * - * @param thread The running thread_t, with data set to the audiostream_t. - */ -void audioStreamPSPThreadFeed(thread_t *thread); diff --git a/src/duskpsp/display/displayplatform.h b/src/duskpsp/display/displayplatform.h deleted file mode 100644 index 57dbf17e..00000000 --- a/src/duskpsp/display/displayplatform.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/displaysdl2.h" - -typedef displaysdl2_t displayplatform_t; - -#define displayPlatformInit displaySDL2Init -#define displayPlatformUpdate displaySDL2Update -#define displayPlatformSwap displaySDL2Swap -#define displayPlatformDispose displaySDL2Dispose -#define displayPlatformSetState displaySDL2SetState \ No newline at end of file diff --git a/src/duskpsp/duskplatform.h b/src/duskpsp/duskplatform.h deleted file mode 100644 index 96ea9ac2..00000000 --- a/src/duskpsp/duskplatform.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include -#include -#include -#include -#include -#include "dusksdl2.h" \ No newline at end of file diff --git a/src/duskpsp/input/CMakeLists.txt b/src/duskpsp/input/CMakeLists.txt deleted file mode 100644 index 6fdee7fd..00000000 --- a/src/duskpsp/input/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - inputpsp.c -) \ No newline at end of file diff --git a/src/duskpsp/input/inputplatform.h b/src/duskpsp/input/inputplatform.h deleted file mode 100644 index 8de3d7fa..00000000 --- a/src/duskpsp/input/inputplatform.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "input/inputpsp.h" - -#define inputInitPlatform inputInitPSP \ No newline at end of file diff --git a/src/duskpsp/input/inputpsp.c b/src/duskpsp/input/inputpsp.c deleted file mode 100644 index c3dd4c18..00000000 --- a/src/duskpsp/input/inputpsp.c +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "input/input.h" - -// #define INPUT_PSP_GAMEPAD_BUTTON_ACCEPT INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM -// #define INPUT_PSP_GAMEPAD_BUTTON_CANCEL INPUT_SDL2_GAMEPAD_BUTTON_CUSTOM - -inputbuttondata_t INPUT_BUTTON_DATA[] = { - { .name = "triangle", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_Y } }, - { .name = "cross", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_A } }, - { .name = "circle", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_B } }, - { .name = "square", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_X } }, - { .name = "start", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_START } }, - { .name = "select", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_BACK } }, - { .name = "up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_UP } }, - { .name = "down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, - { .name = "left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, - { .name = "right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_RIGHT } }, - { .name = "l", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_LEFTSHOULDER } }, - { .name = "r", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER } }, - - // Refer to systempsp.c for some extra info. - { .name = "accept", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_A } }, - { .name = "cancel", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_B } }, - - { .name = "lstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTY, .positive = true } } }, - { .name = "lstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTY, .positive = false } } }, - { .name = "lstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTX, .positive = true } } }, - { .name = "lstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { .axis = SDL_CONTROLLER_AXIS_LEFTX, .positive = false } } }, - - { .name = NULL } -}; - -errorret_t inputInitPSP(void) { - #define X(buttonName, buttonAction) \ - inputBind(inputButtonGetByName(buttonName), buttonAction); - X("up", INPUT_ACTION_UP); - X("down", INPUT_ACTION_DOWN); - X("left", INPUT_ACTION_LEFT); - X("right", INPUT_ACTION_RIGHT); - X("accept", INPUT_ACTION_ACCEPT); - X("cancel", INPUT_ACTION_CANCEL); - X("triangle", INPUT_ACTION_CONSOLE); - X("select", INPUT_ACTION_RAGEQUIT); - X("start", INPUT_ACTION_PAUSE); - X("lstick_up", INPUT_ACTION_UP); - X("lstick_down", INPUT_ACTION_DOWN); - X("lstick_left", INPUT_ACTION_LEFT); - X("lstick_right", INPUT_ACTION_RIGHT); - #undef X - - errorOk(); -} - -float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return 0.25f; -} \ No newline at end of file diff --git a/src/duskpsp/input/inputpsp.h b/src/duskpsp/input/inputpsp.h deleted file mode 100644 index b51fa36a..00000000 --- a/src/duskpsp/input/inputpsp.h +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "input/inputsdl2.h" - -/** - * Initializes the input system on PSP. - * - * @return An error code indicating success or failure. - */ -errorret_t inputInitPSP(void); \ No newline at end of file diff --git a/src/duskpsp/log/CMakeLists.txt b/src/duskpsp/log/CMakeLists.txt deleted file mode 100644 index e6535a76..00000000 --- a/src/duskpsp/log/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# 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 - log.c -) \ No newline at end of file diff --git a/src/duskpsp/log/log.c b/src/duskpsp/log/log.c deleted file mode 100644 index 3d8b0d1c..00000000 --- a/src/duskpsp/log/log.c +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "log/log.h" - -void logDebug(const char_t *message, ...) { - va_list args; - va_start(args, message); - - // print to stdout - va_list copy; - va_copy(copy, args); - vprintf(message, copy); - va_end(copy); - - #ifdef DUSK_PSP_LOG_FILE - // print to file - fopen+fclose every call is real, measured Memory - // Stick I/O cost (this is why the flag exists - see psp.cmake). - FILE *file = fopen("ms0:/PSP/GAME/Dusk/debug.log", "a"); - if(file) { - va_copy(copy, args); - vfprintf(file, message, copy); - va_end(copy); - fclose(file); - } - #endif - - va_end(args); -} - -void logError(const char_t *message, ...) { - va_list args; - va_start(args, message); - - // print to stderr - va_list copy; - va_copy(copy, args); - vfprintf(stderr, message, copy); - va_end(copy); - - #ifdef DUSK_PSP_LOG_FILE - // print to file - fopen+fclose every call is real, measured Memory - // Stick I/O cost (this is why the flag exists - see psp.cmake). - FILE *file = fopen("ms0:/PSP/GAME/Dusk/error.log", "a"); - if(file) { - va_copy(copy, args); - vfprintf(file, message, copy); - va_end(copy); - fclose(file); - } - #endif - - va_end(args); -} \ No newline at end of file diff --git a/src/duskpsp/network/CMakeLists.txt b/src/duskpsp/network/CMakeLists.txt deleted file mode 100644 index c302baf6..00000000 --- a/src/duskpsp/network/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - networkpsp.c -) diff --git a/src/duskpsp/network/networkplatform.h b/src/duskpsp/network/networkplatform.h deleted file mode 100644 index 1645d141..00000000 --- a/src/duskpsp/network/networkplatform.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "networkpsp.h" - -#define networkPlatformInit networkPSPInit -#define networkPlatformUpdate networkPSPUpdate -#define networkPlatformDispose networkPSPDispose -#define networkPlatformIsConnected networkPSPIsConnected -#define networkPlatformRequestConnection networkPSPRequestConnection -#define networkPlatformRequestDisconnection networkPSPRequestDisconnection -#define networkPlatformGetInfo networkPSPGetInfo - -typedef networkpsp_t networkplatform_t; \ No newline at end of file diff --git a/src/duskpsp/network/networkpsp.c b/src/duskpsp/network/networkpsp.c deleted file mode 100644 index 14290ed7..00000000 --- a/src/duskpsp/network/networkpsp.c +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "network/network.h" -#include "util/memory.h" -#include "util/string.h" -#include "assert/assert.h" -#include "display/displaysdl2.h" - -errorret_t networkPSPInit() { - // Requests the PSP to load the network modules. - int ret; - - ret = sceUtilityLoadNetModule(PSP_NET_MODULE_COMMON); - if(ret < 0) errorThrow("Failed to init NET COMMON: 0x%08X", ret); - - ret = sceUtilityLoadNetModule(PSP_NET_MODULE_INET); - if(ret < 0) errorThrow("Failed to init NET INET: 0x%08X", ret); - - // ret = sceUtilityLoadNetModule(PSP_NET_MODULE_PARSEURI); - // if(ret < 0) errorThrow("Failed to init NET PARSEURI: 0x%08X", ret); - - // ret = sceUtilityLoadNetModule(PSP_NET_MODULE_PARSEHTTP); - // if(ret < 0) errorThrow("Failed to init NET PARSEHTTP: 0x%08X", ret); - - // ret = sceUtilityLoadNetModule(PSP_NET_MODULE_SSL); - // if(ret < 0) errorThrow("Failed to init NET SSL: 0x%08X", ret); - - // ret = sceUtilityLoadNetModule(PSP_NET_MODULE_HTTP); - // if(ret < 0) errorThrow("Failed to init NET HTTP: 0x%08X", ret); - - // ret = sceSslInit(0x28000); - // if(ret < 0) errorThrow("sceSslInit failed: 0x%08X", ret); - - // ret = sceHttpInit(0x25800); - // if(ret < 0) errorThrow("sceHttpInit failed: 0x%08X", ret); - - // ret = sceHttpsInit(0, 0, 0, 0); - // if(ret < 0) errorThrow("sceHttpsInit failed: 0x%08X", ret); - - // ret = sceHttpsLoadDefaultCert(0, 0); - // if(ret < 0) errorThrow("sceHttpsLoadDefaultCert failed: 0x%08X", ret); - - // ret = sceHttpLoadSystemCookie(); - // if(ret < 0) errorThrow("sceHttpLoadSystemCookie failed: 0x%08X", ret); - - // All good. - errorOk(); -} - -errorret_t networkPSPUpdate() { - int ret; - if(NETWORK.state == NETWORK_STATE_CONNECTING) { - switch(sceUtilityNetconfGetStatus()) { - case PSP_UTILITY_DIALOG_INIT: - break; - - case PSP_UTILITY_DIALOG_NONE: - NETWORK.state = NETWORK_STATE_DISCONNECTED; - errorThrow("PSP Netconf dialog disappeared without result"); - break; - - case PSP_UTILITY_DIALOG_VISIBLE: - // 1 is mandatory? - ret = sceUtilityNetconfUpdate(1); - if(ret != 0) { - errorThrow("sceUtilityNetconfUpdate failed: 0x%08X", ret); - } - break; - - case PSP_UTILITY_DIALOG_QUIT: - ret = sceUtilityNetconfShutdownStart(); - if(ret != 0) { - errorThrow("sceUtilityNetconfShutdownStart failed: 0x%08X", ret); - } - break; - - case PSP_UTILITY_DIALOG_FINISHED: - // Did we connect? - int apState = 0; - sceNetApctlGetState(&apState); - - if(apState == PSP_NET_APCTL_STATE_GOT_IP) { - NETWORK.state = NETWORK_STATE_CONNECTED; - assertNotNull( - NETWORK.platform.onConnected, - "Network platform onConnected callback should be set." - ); - NETWORK.platform.onConnected(NETWORK.platform.onConnectedUser); - } else { - NETWORK.state = NETWORK_STATE_DISCONNECTED; - - assertNotNull( - NETWORK.platform.onFailed, - "Network platform onFailed callback should be set." - ); - - // Kill the PSP network stack. - errorret_t err = networkPSPTerm(); - if(errorIsNotOk(err)) { - errorCatch(errorPrint(err)); - } - - errorret_t error = errorThrowImpl( - &NETWORK.errorState, - ERROR_NOT_OK, - __FILE__, __func__, __LINE__, - "Failed to connect to network" - ); - NETWORK.platform.onFailed(error, NETWORK.platform.onConnectedUser); - } - break; - - default: - errorThrow( - "Unknown PSP Netconf dialog status: %d", sceUtilityNetconfGetStatus() - ); - } - } - - errorOk(); -} - -errorret_t networkPSPDispose() { - sceUtilityNetconfGetStatus(); - errorCatch(errorPrint(networkPSPTerm())); - - sceUtilityUnloadNetModule(PSP_NET_MODULE_HTTP); - sceUtilityUnloadNetModule(PSP_NET_MODULE_INET); - sceUtilityUnloadNetModule(PSP_NET_MODULE_COMMON); - - errorOk(); -} - -bool_t networkPSPIsConnected() { - return NETWORK.state == NETWORK_STATE_CONNECTED; -} - -void networkPSPRequestConnection( - void (*onConnected)(void *user), - void (*onFailed)(errorret_t error, void *user), - void (*onDisconnect)(errorret_t error, void *user), - void *user -) { - assertTrue( - NETWORK.state == NETWORK_STATE_CONNECTING, - "Network host should be in a connecting state." - ); - - NETWORK.platform.onConnected = onConnected; - NETWORK.platform.onFailed = onFailed; - NETWORK.platform.onConnectedUser = user; - - int ret; - - // Init the PSP network stack. - ret = sceNetInit(0x20000, 0x20, 4096, 0x20, 4096); - assertTrue(ret >= 0, "Failed to init net: 0x%08X"); - - ret = sceNetInetInit(); - assertTrue(ret >= 0, "Failed to init net inet: 0x%08X"); - - ret = sceNetResolverInit(); - assertTrue(ret >= 0, "Failed to init net resolver: 0x%08X"); - - ret = sceNetApctlInit(0x1800, 0x30); - assertTrue(ret >= 0, "Failed to init net apctl: 0x%08X"); - - - // This is all related to getting the PSP online, refer to; - // https://github.com/joel16/CMFileManager-PSP/blob/ - // 00dab16c64cd48bf6452fc274a3b898d77c39a8d/app/source/net.cpp#L97 - // since I follow this implementation closely. - memoryZero(&NETWORK.platform.dialogData, sizeof(NETWORK.platform.dialogData)); - memoryZero( - &NETWORK.platform.dialogAdhoc, sizeof(NETWORK.platform.dialogAdhoc) - ); - - NETWORK.platform.dialogData.base.size = sizeof(pspUtilityNetconfData); - NETWORK.platform.dialogData.base.language = systemPSPGetLanguage(); - NETWORK.platform.dialogData.base.buttonSwap = - systemPSPGetCrossButtonSetting(); - NETWORK.platform.dialogData.base.graphicsThread = 17; - NETWORK.platform.dialogData.base.accessThread = 19; - NETWORK.platform.dialogData.base.fontThread = 18; - NETWORK.platform.dialogData.base.soundThread = 16; - NETWORK.platform.dialogData.action = PSP_NETCONF_ACTION_CONNECTAP; - NETWORK.platform.dialogData.adhocparam = ( - &NETWORK.platform.dialogAdhoc - ); - - ret = sceUtilityNetconfInitStart(&NETWORK.platform.dialogData); - assertTrue(ret >= 0, "Failed to init netconf"); -} - -void networkPSPRequestDisconnection( - void (*onComplete)(void *user), - void *user -) { - assertTrue( - NETWORK.state == NETWORK_STATE_DISCONNECTING, - "Network host should be in a disconnecting state." - ); - - errorret_t err = networkPSPTerm(); - if(errorIsNotOk(err)) { - errorCatch(errorPrint(err)); - } - - NETWORK.state = NETWORK_STATE_DISCONNECTED; - onComplete(user); -} - -errorret_t networkPSPTerm() { - int_t ret; - ret = sceNetApctlTerm(); - if(ret < 0) { - errorThrow("Failed to terminate netctl: 0x%08X", ret); - } - - ret = sceNetResolverTerm(); - if(ret < 0) { - errorThrow("Failed to terminate net resolver: 0x%08X", ret); - } - - ret = sceNetInetTerm(); - if(ret < 0) { - errorThrow("Failed to terminate net inet: 0x%08X", ret); - } - - ret = sceNetTerm(); - if(ret < 0) { - errorThrow("Failed to terminate net: 0x%08X", ret); - } - - errorOk(); -} - -networkinfo_t networkPSPGetInfo() { - networkinfo_t netInfo; - memoryZero(&netInfo, sizeof(networkinfo_t)); - - // Get the IP address of the current connection. - union SceNetApctlInfo info; - int ret = sceNetApctlGetInfo(PSP_NET_APCTL_INFO_IP, &info); - assertTrue(ret >= 0, "Failed to get IP address"); - - // Parse the IP address string into octets. - netInfo.type = NETWORK_TYPE_IPV4; - ret = sscanf( - info.ip, - "%hhu.%hhu.%hhu.%hhu", - &netInfo.ipv4.ip[0], - &netInfo.ipv4.ip[1], - &netInfo.ipv4.ip[2], - &netInfo.ipv4.ip[3] - ); - assertTrue(ret == 4, "Failed to parse IP address"); - - return netInfo; -} \ No newline at end of file diff --git a/src/duskpsp/network/networkpsp.h b/src/duskpsp/network/networkpsp.h deleted file mode 100644 index e8b50889..00000000 --- a/src/duskpsp/network/networkpsp.h +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "network/networkinfo.h" -#include "system/systempsp.h" -#include -#include -#include -#include -#include -#include -#include - -// #define NETWORK_HTTP_PENDING_MAX 4 -// #define NETWORK_HTTP_URL_MAX 512 -// #define NETWORK_HTTP_BODY_MAX 2048 -// #define NETWORK_HTTP_RESPONSE_MAX 16384 -// #define NETWORK_HTTP_HEADER_MAX 8 -// #define NETWORK_HTTP_HEADER_KEY_MAX 64 -// #define NETWORK_HTTP_HEADER_VAL_MAX 256 -// #define NETWORK_ERROR_MESSAGE_MAX 256 -// #define NETWORK_PSP_AGENT "DuskEngine/1.0" - -typedef struct { - pspUtilityNetconfData dialogData; - struct pspUtilityNetconfAdhoc dialogAdhoc; - - // Used during establishing connection - void *onConnectedUser; - void (*onConnected)(void *user); - void (*onFailed)(errorret_t error, void *user); -} networkpsp_t; - -/** - * Initializes the PSP Network manager. This will NOT do network connecting, - * only prep it for being able to connect in future. - * - * @return Error state (if any). - */ -errorret_t networkPSPInit(); - -/** - * Called each frame for handling PSP requests, basically this is where all - * communication between the HTTP thread and the main thread happens. - * - * @return Error state (if any). - */ -errorret_t networkPSPUpdate(); - -/** - * Disposes the PSP Network manager, this will clean all resources and, if the - * network is connected, it will disconnect it safely. - * - * @return Error state (if any). - */ -errorret_t networkPSPDispose(); - -/** - * Checks if the PSP network is connected. - * - * @return True if the PSP is connected to a network, false otherwise. - */ -bool_t networkPSPIsConnected(); - -/** - * Requests the PSP to connect to a network (Shows the Wi-Fi connected). - * - * @param onConnected Callback connected successfully. - * @param onFailed Callback if the connection failed. - * @param onDisconnect Callback when connection is lost. - * @param user User data to pass to the callbacks. - */ -void networkPSPRequestConnection( - void (*onConnected)(void *user), - void (*onFailed)(errorret_t error, void *user), - void (*onDisconnect)(errorret_t error, void *user), - void *user -); - -/** - * Requests the PSP to disconnect from the network. - * - * @param onComplete Callback when disconnection is complete. - * @param user User data to pass to the callback. - */ -void networkPSPRequestDisconnection( - void (*onComplete)(void *user), - void *user -); - -/** - * Disposes the PSP sce net libraries, doesn't unload the modules and won't - * term the dialog if it's active. - * - * @return Error state (if any). - */ -errorret_t networkPSPTerm(); - -/** - * Gets the network information for the currently active network connection. - * - * @return Network information for the currently active network connection. - */ -networkinfo_t networkPSPGetInfo(); \ No newline at end of file diff --git a/src/duskpsp/save/CMakeLists.txt b/src/duskpsp/save/CMakeLists.txt deleted file mode 100644 index d0fd4fe5..00000000 --- a/src/duskpsp/save/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - savedevicepsp.c -) diff --git a/src/duskpsp/save/savedeviceplatform.h b/src/duskpsp/save/savedeviceplatform.h deleted file mode 100644 index a992c259..00000000 --- a/src/duskpsp/save/savedeviceplatform.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "savedevicepsp.h" - -#define SAVE_DEVICE_COUNT 1 - -#define saveDevicePlatformInit saveDevicePSPInit -#define saveDevicePlatformUpdate saveDevicePSPUpdate -#define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability -#define saveDevicePlatformDispose saveDevicePSPDispose - -// The memory stick has no directory-listing UI convention to preserve like -// GameCube/Wii memory cards do, so settings + all save slots are combined -// into one compressed blob rather than kept as separate files - see -// SAVE_DEVICE_DATA_RAW in save/savedevice.h. -#define SAVE_DEVICE_DATA_RAW -#define saveDeviceDataWritePlatform saveDevicePSPDataWrite -#define saveDeviceDataReadPlatform saveDevicePSPDataRead - -// Data file lives directly in ASSET.baseDirectory (the directory EBOOT.PBP -// was launched from - see its own comment) rather than a separate fixed -// directory, so it doesn't need creating: that directory is already known -// to exist, since the running EBOOT.PBP lives there. -#ifndef SAVE_DEVICE_PSP_DATA_FILENAME - #define SAVE_DEVICE_PSP_DATA_FILENAME "save.dat" -#endif diff --git a/src/duskpsp/save/savedevicepsp.c b/src/duskpsp/save/savedevicepsp.c deleted file mode 100644 index 8eadee92..00000000 --- a/src/duskpsp/save/savedevicepsp.c +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "save/savedevice.h" -#include "save/savedevicepsp.h" -#include "asset/asset.h" -#include "assert/assert.h" -#include "util/memory.h" -#include "util/string.h" -#include - -#define SAVE_DEVICE_PSP_PATH_MAX 64 - -errorret_t saveDevicePSPInit(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - - device->state = SAVE_DEVICE_STATE_UNKNOWN; - - errorOk(); -} - -errorret_t saveDevicePSPUpdate(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - errorOk(); -} - -void saveDevicePSPCheckAvailability(savedevice_t *device) { - assertNotNull(device, "device cannot be null"); - assertIsMainThread("Invalid thread"); - assertTrue( - device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, - "Device state incorrect?" - ); - - // Confirm the memory stick is actually reachable - the real save/load - // (via sceUtilitySavedata) only reports failure once a save/load is - // attempted, so this is checked separately up front. sceIoGetstat on a - // bare device root (e.g. "ms0:/") is unreliable on real hardware - - // confirmed returning EINVAL there even with a memory stick present - - // so open/close the root directory instead, which is the SDK's own - // documented way to probe a device (see sceIoDopen's doc comment). - SceUID dir = sceIoDopen(SAVE_DEVICE_PSP_ROOT); - if(dir < 0) { - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.psp.no_memory_stick"; - return saveDeviceFireCallback(device); - } - sceIoDclose(dir); - - device->state = SAVE_DEVICE_STATE_AVAILABLE; - device->reasonKey = NULL; - saveDeviceFireCallback(device); -} - -errorret_t saveDevicePSPGetDataPath( - char_t *dest, - const size_t destSize -) { - assertNotNull(dest, "dest cannot be null"); - - stringFormat( - dest, destSize, "%s/%s", - ASSET.baseDirectory, SAVE_DEVICE_PSP_DATA_FILENAME - ); - - errorOk(); -} - -errorret_t saveDevicePSPDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(buffer, "buffer cannot be null"); - - char_t finalPath[SAVE_DEVICE_PSP_PATH_MAX]; - errorChain(saveDevicePSPGetDataPath(finalPath, sizeof(finalPath))); - - char_t tempPath[SAVE_DEVICE_PSP_PATH_MAX]; - stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); - - SceUID fd = sceIoOpen( - tempPath, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777 - ); - if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath); - - int written = sceIoWrite(fd, buffer, (SceSize)size); - sceIoClose(fd); - if(written < 0 || (size_t)written != size) { - errorThrow("Failed to write save data: %s", tempPath); - } - - // Atomic replace: the temp file is fully written before it ever takes the - // final name, so a crash/power-loss here can never leave a half-written - // save behind - at worst the previous save survives untouched. - sceIoRemove(finalPath);// Not an error if it doesn't exist yet. - if(sceIoRename(tempPath, finalPath) < 0) { - errorThrow("Failed to finalize save data: %s", finalPath); - } - - errorOk(); -} - -errorret_t saveDevicePSPDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -) { - assertNotNull(device, "device cannot be null"); - assertNotNull(outBuffer, "outBuffer cannot be null"); - assertNotNull(outSize, "outSize cannot be null"); - - char_t path[SAVE_DEVICE_PSP_PATH_MAX]; - errorChain(saveDevicePSPGetDataPath(path, sizeof(path))); - - SceIoStat stat; - if(sceIoGetstat(path, &stat) < 0) { - // No save yet - not an error. - *outBuffer = NULL; - *outSize = 0; - errorOk(); - } - - size_t size = (size_t)stat.st_size; - uint8_t *buffer = memoryAllocate(size); - - SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0); - if(fd < 0) { - memoryFree(buffer); - errorThrow("Failed to open save data for reading: %s", path); - } - - int readBytes = sceIoRead(fd, buffer, (SceSize)size); - sceIoClose(fd); - if(readBytes < 0 || (size_t)readBytes != size) { - memoryFree(buffer); - errorThrow("Failed to read save data: %s", path); - } - - *outBuffer = buffer; - *outSize = size; - errorOk(); -} - -errorret_t saveDevicePSPDispose(savedevice_t *device) { - errorOk(); -} diff --git a/src/duskpsp/save/savedevicepsp.h b/src/duskpsp/save/savedevicepsp.h deleted file mode 100644 index 43717d5d..00000000 --- a/src/duskpsp/save/savedevicepsp.h +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -// PSP only ever exposes one save device - the memory stick. -#ifndef SAVE_DEVICE_PSP_ROOT - #define SAVE_DEVICE_PSP_ROOT "ms0:/" -#endif - -typedef struct { - void *nothing; -} savedeviceplatform_t; - -typedef struct savedevice_s savedevice_t; - -/** - * Initializes the save device platform. - * - * @param device The save device platform to initialize. - * @return Error state if any. - */ -errorret_t saveDevicePSPInit(savedevice_t *device); - -/** - * Builds the absolute path to the combined save data file. - * - * @param dest The destination buffer to write the path into. - * @param destSize The size of the destination buffer, exc. null terminator. - * @return Error state if any. - */ -errorret_t saveDevicePSPGetDataPath(char_t *dest, const size_t destSize); - -/** - * Writes the combined save data blob out to the memory stick, replacing it - * atomically (write to a temp file, then swap it in) so a crash mid-write - * can never leave a half-written file behind. - * - * @param device The save device to write to. - * @param buffer The raw bytes to write. - * @param size The number of bytes to write. - * @return Error state if any. - */ -errorret_t saveDevicePSPDataWrite( - savedevice_t *device, - const uint8_t *buffer, - size_t size -); - -/** - * Reads the combined save data blob in from the memory stick. If no save - * data exists yet, this is not an error - *outBuffer is set to NULL and - * *outSize to 0. - * - * @param device The save device to read from. - * @param outBuffer Receives a memoryAllocate'd buffer the caller must free - * with memoryFree, or NULL if nothing has been saved yet. - * @param outSize Receives the number of bytes in *outBuffer. - * @return Error state if any. - */ -errorret_t saveDevicePSPDataRead( - savedevice_t *device, - uint8_t **outBuffer, - size_t *outSize -); - -/** - * Updates the save device platform. - * - * @param device The save device platform to update. - * @return Error state if any. - */ -errorret_t saveDevicePSPUpdate(savedevice_t *device); - -/** - * Requests the device to check its availability, this will call the callback - * whence completed. - * - * @param device The save device platform to check availability. - */ -void saveDevicePSPCheckAvailability(savedevice_t *device); - -/** - * Disposes of the save device platform. - * - * @param device The save device platform to dispose. - * @return Error state if any. - */ -errorret_t saveDevicePSPDispose(savedevice_t *device); diff --git a/src/duskpsp/script/module/moduleplatformplatform.h b/src/duskpsp/script/module/moduleplatformplatform.h deleted file mode 100644 index d718a08b..00000000 --- a/src/duskpsp/script/module/moduleplatformplatform.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "moduleplatformpsp.h" - -#define modulePlatformPlatform modulePlatformPSP \ No newline at end of file diff --git a/src/duskpsp/script/module/moduleplatformpsp.h b/src/duskpsp/script/module/moduleplatformpsp.h deleted file mode 100644 index d733251e..00000000 --- a/src/duskpsp/script/module/moduleplatformpsp.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "script/module/modulebase.h" - -static void modulePlatformPSP(void) { - moduleBaseEval("var PSP = true;\n"); -} diff --git a/src/duskpsp/system/CMakeLists.txt b/src/duskpsp/system/CMakeLists.txt deleted file mode 100644 index b83bd32f..00000000 --- a/src/duskpsp/system/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - systempsp.c -) diff --git a/src/duskpsp/system/systemplatform.h b/src/duskpsp/system/systemplatform.h deleted file mode 100644 index 3afc4779..00000000 --- a/src/duskpsp/system/systemplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "system/systempsp.h" - -#define systemInitPlatform systemInitPSP -#define systemGetActiveDialogTypePlatform systemGetActiveDialogTypePSP -#define systemGetLocalePlatform systemGetLocalePSP \ No newline at end of file diff --git a/src/duskpsp/system/systempsp.c b/src/duskpsp/system/systempsp.c deleted file mode 100644 index 603e098b..00000000 --- a/src/duskpsp/system/systempsp.c +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "systempsp.h" -#include "input/input.h" -#include "util/string.h" -#include "assert/assert.h" - -errorret_t systemInitPSP() { - // Bind ACCEPT and CANCEL binds. - inputbuttondata_t *buttonCross, *buttonCircle, *buttonAccept, *buttonCancel; - - inputbuttondata_t *i = INPUT_BUTTON_DATA; - while(i->name) { - if(stringCompare(i->name, "cross") == 0) { - buttonCross = i; - } else if(stringCompare(i->name, "circle") == 0) { - buttonCircle = i; - } else if(stringCompare(i->name, "accept") == 0) { - buttonAccept = i; - } else if(stringCompare(i->name, "cancel") == 0) { - buttonCancel = i; - } - i++; - } - - assertNotNull(buttonCross, "Cross button not found!"); - assertNotNull(buttonCircle, "Circle button not found!"); - assertNotNull(buttonAccept, "Accept button not found!"); - assertNotNull(buttonCancel, "Cancel button not found!"); - - if(systemPSPGetCrossButtonSetting() == PSP_UTILITY_ACCEPT_CROSS) { - buttonAccept->button.gpButton = buttonCross->button.gpButton; - buttonCancel->button.gpButton = buttonCircle->button.gpButton; - } else { - buttonAccept->button.gpButton = buttonCircle->button.gpButton; - buttonCancel->button.gpButton = buttonCross->button.gpButton; - } - - errorOk(); -} - -systemdialogtype_t systemGetActiveDialogTypePSP() { - return SYSTEM_DIALOG_TYPE_NONE; -} - -int_t systemPSPGetLanguage() { - int_t ret; - sceUtilityGetSystemParamInt(PSP_SYSTEMPARAM_ID_INT_LANGUAGE, &ret); - return ret; -} - -int_t systemPSPGetCrossButtonSetting() { - int_t ret; - // See: https://pspdev.github.io/pspsdk/psputility__sysparam_8h.html - // #ab588fd5a14adc025f065e09325ffe729 - sceUtilityGetSystemParamInt(PSP_SYSTEMPARAM_ID_INT_UNKNOWN, &ret); - return ( - ret == 1 ? PSP_UTILITY_ACCEPT_CROSS : PSP_UTILITY_ACCEPT_CIRCLE - ); -} - -const localeinfo_t * systemGetLocalePSP(void) { - switch(systemPSPGetLanguage()) { - case PSP_SYSTEMPARAM_LANGUAGE_JAPANESE: return &LOCALE_INFO_JP_JP; - case PSP_SYSTEMPARAM_LANGUAGE_SPANISH: return &LOCALE_INFO_ES_MX; - default: return &LOCALE_DEFAULT; - } -} \ No newline at end of file diff --git a/src/duskpsp/system/systempsp.h b/src/duskpsp/system/systempsp.h deleted file mode 100644 index 6dc3b58c..00000000 --- a/src/duskpsp/system/systempsp.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "system/system.h" -#include - -/** - * Initializes the PSP system module. - * - * @return Error code indicating success or failure. - */ -errorret_t systemInitPSP(void); - -/** - * Returns which PSP system dialog is currently open (if any). - * - * @return Currently open system dialog type. - */ -systemdialogtype_t systemGetActiveDialogTypePSP(); - -/** - * Returns the PSP_SYSTEMPARAM language for the current system. - * - * @return PSP_SYSTEMPARAM language value. - */ -int_t systemPSPGetLanguage(); - -/** - * Returns the user's setting for the PSP "Cross Button" configuration, which - * determines whether the "Cross" or "Circle" button is used for "Accept" - * actions in system dialogs. - * - * @return PSP_UTILITY_ACCEPT_CROSS or PSP_UTILITY_ACCEPT_CIRCLE. - */ -int_t systemPSPGetCrossButtonSetting(); - -/** - * Returns the locale matching the system's configured language, falling - * back to LOCALE_DEFAULT if the system language has no matching locale. - * - * @return The current locale. - */ -const localeinfo_t * systemGetLocalePSP(void); \ No newline at end of file diff --git a/src/duskpsp/time/CMakeLists.txt b/src/duskpsp/time/CMakeLists.txt deleted file mode 100644 index b450020d..00000000 --- a/src/duskpsp/time/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -target_sources(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - timepsp.c -) diff --git a/src/duskpsp/time/timeplatform.h b/src/duskpsp/time/timeplatform.h deleted file mode 100644 index cecd40e3..00000000 --- a/src/duskpsp/time/timeplatform.h +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "time/timesdl2.h" -#include "time/timepsp.h" - -#define timeTickPlatform timeTickSDL2 -#define timeGetDeltaPlatform timeGetDeltaSDL2 -#define timeGetRealPlatform timeGetRealPSP -#define timeGetRealTimeZonePlatform timeGetRealTimeZonePSP \ No newline at end of file diff --git a/src/duskpsp/time/timepsp.c b/src/duskpsp/time/timepsp.c deleted file mode 100644 index 04045fbc..00000000 --- a/src/duskpsp/time/timepsp.c +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "time/timepsp.h" -#include -#include -#include - -double_t timeGetRealPSP(void) { - u64 utc_ticks = 0; - - if(sceRtcGetCurrentTick(&utc_ticks) < 0) return 0.0; - - /* - PSP RTC ticks are microseconds. - Return seconds since 1970-01-01 UTC. - */ - return (double_t)utc_ticks / 1000000.0; -} - -double_t timeGetRealTimeZonePSP(void) { - u64 utc_ticks = 0; - u64 local_ticks = 0; - - if(sceRtcGetCurrentTick(&utc_ticks) < 0) return 0.0; - if(sceRtcConvertUtcToLocalTime(&utc_ticks, &local_ticks) < 0) return 0.0; - - /* - Return timezone offset in hours. - Example: - UTC-6 => -6.0 - UTC+2 => 2.0 - */ - int64_t offset_us = (int64_t)local_ticks - (int64_t)utc_ticks; - return (double_t)offset_us / (1000000.0 * 60.0 * 60.0); -} \ No newline at end of file diff --git a/src/duskpsp/time/timepsp.h b/src/duskpsp/time/timepsp.h deleted file mode 100644 index a0f02316..00000000 --- a/src/duskpsp/time/timepsp.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -/** - * Returns the real current time, in seconds since January 1, 1970. - * - * @return The real current time, in seconds since January 1, 1970. - */ -double_t timeGetRealPSP(void); - -/** - * Returns the real time zone offset in seconds from UTC. - * - * @return The real time zone offset in seconds from UTC. - */ -double_t timeGetRealTimeZonePSP(void); \ No newline at end of file diff --git a/src/dusksdl2/CMakeLists.txt b/src/dusksdl2/CMakeLists.txt deleted file mode 100644 index 69f5232a..00000000 --- a/src/dusksdl2/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Subdirs -add_subdirectory(display) -add_subdirectory(input) -add_subdirectory(time) \ No newline at end of file diff --git a/src/dusksdl2/display/CMakeLists.txt b/src/dusksdl2/display/CMakeLists.txt deleted file mode 100644 index 8db4dd71..00000000 --- a/src/dusksdl2/display/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - displaysdl2.c -) - -# Subdirs \ No newline at end of file diff --git a/src/dusksdl2/display/displaysdl2.c b/src/dusksdl2/display/displaysdl2.c deleted file mode 100644 index 74125db9..00000000 --- a/src/dusksdl2/display/displaysdl2.c +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "display/display.h" -#include "engine/engine.h" -#include "display/displaygl.h" -#include "error/errorgl.h" - -errorret_t displaySDL2Init(void) { - uint32_t flags = SDL_INIT_VIDEO; - #ifdef DUSK_INPUT_GAMEPAD - flags |= SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK; - #endif - if(SDL_Init(flags) != 0) { - errorThrow("SDL Failed to Initialize: %s", SDL_GetError()); - } - - // Set OpenGL attributes (Needs to be done now or later?) - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - - #ifdef DUSK_OPENGL_LEGACY - #else - SDL_GL_SetAttribute( - SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE - ); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - #endif - - // Create window with OpenGL flag. - DISPLAY.window = SDL_CreateWindow( - "Dusk", - SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, - DUSK_DISPLAY_WIDTH_DEFAULT, - DUSK_DISPLAY_HEIGHT_DEFAULT, - SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | - SDL_WINDOW_OPENGL - ); - if(!DISPLAY.window) { - errorThrow("SDL_CreateWindow failed: %s", SDL_GetError()); - } - - // Create OpenGL context - DISPLAY.glContext = SDL_GL_CreateContext(DISPLAY.window); - if(!DISPLAY.glContext) { - errorThrow("SDL_GL_CreateContext failed: %s", SDL_GetError()); - } - errorChain(errorGLCheck()); - - SDL_GL_SetSwapInterval(1); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - errorChain(errorGLCheck()); - - errorChain(displayOpenGLInit()); - - errorChain(errorGLCheck()); - errorOk(); -} - -errorret_t displaySDL2Update(void) { - SDL_Event event; - while(SDL_PollEvent(&event)) { - switch(event.type) { - case SDL_QUIT: { - ENGINE.running = false; - break; - } - - case SDL_WINDOWEVENT: { - switch(event.window.event) { - case SDL_WINDOWEVENT_CLOSE: { - ENGINE.running = false; - break; - } - - default: { - break; - } - } - } - - default: { - break; - } - } - } - - SDL_GL_MakeCurrent(DISPLAY.window, DISPLAY.glContext); - errorChain(errorGLCheck()); - // errorChain(shaderPaletteTextureBindGL(&testShader)); - errorOk(); -} - -errorret_t displaySDL2Swap(void) { - SDL_GL_SwapWindow(DISPLAY.window); - errorChain(errorGLCheck()); - errorOk(); -} - -errorret_t displaySDL2SetState(displaystate_t state) { - if(state.flags & DISPLAY_STATE_FLAG_CULL) { - glEnable(GL_CULL_FACE); - errorChain(errorGLCheck()); - glCullFace(GL_BACK); - errorChain(errorGLCheck()); - } else { - glDisable(GL_CULL_FACE); - errorChain(errorGLCheck()); - } - - if(state.flags & DISPLAY_STATE_FLAG_DEPTH_TEST) { - glEnable(GL_DEPTH_TEST); - errorChain(errorGLCheck()); - glDepthFunc(GL_LEQUAL); - errorChain(errorGLCheck()); - glClearDepth(1.0f); - errorChain(errorGLCheck()); - } else { - glDisable(GL_DEPTH_TEST); - errorChain(errorGLCheck()); - } - - if(state.flags & DISPLAY_STATE_FLAG_BLEND) { - glEnable(GL_BLEND); - errorChain(errorGLCheck()); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - errorChain(errorGLCheck()); - } else { - glDisable(GL_BLEND); - errorChain(errorGLCheck()); - } - - errorOk(); -} - -void displaySDL2Dispose(void) { - if(DISPLAY.glContext) { - SDL_GL_DeleteContext(DISPLAY.glContext); - DISPLAY.glContext = NULL; - } - if(DISPLAY.window) { - SDL_DestroyWindow(DISPLAY.window); - DISPLAY.window = NULL; - } - SDL_Quit(); -} \ No newline at end of file diff --git a/src/dusksdl2/display/displaysdl2.h b/src/dusksdl2/display/displaysdl2.h deleted file mode 100644 index bb02d478..00000000 --- a/src/dusksdl2/display/displaysdl2.h +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "display/displaystate.h" - -typedef struct { - SDL_Window *window; - SDL_GLContext glContext; - bool_t usingShaderedPalettes; -} displaysdl2_t; - -/** - * Initializes the SDL2 display system. - */ -errorret_t displaySDL2Init(void); - -/** - * Handles SDL2 Updating (pre-render context initialization) - */ -errorret_t displaySDL2Update(void); - -/** - * Handles SDL2 Display Swapping (to switch from frontbuffer to backbuffer). - */ -errorret_t displaySDL2Swap(void); - -/** - * Sets the SDL2 display state. - */ -errorret_t displaySDL2SetState(displaystate_t state); - -/** - * Disposes/Cleans up the SDL2 Render system. - */ -void displaySDL2Dispose(void); \ No newline at end of file diff --git a/src/dusksdl2/dusksdl2.h b/src/dusksdl2/dusksdl2.h deleted file mode 100644 index 44a23c2c..00000000 --- a/src/dusksdl2/dusksdl2.h +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include -#include "duskgl.h" \ No newline at end of file diff --git a/src/dusksdl2/input/CMakeLists.txt b/src/dusksdl2/input/CMakeLists.txt deleted file mode 100644 index d64d14cc..00000000 --- a/src/dusksdl2/input/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - inputsdl2.c -) - -# Subdirs \ No newline at end of file diff --git a/src/dusksdl2/input/inputsdl2.c b/src/dusksdl2/input/inputsdl2.c deleted file mode 100644 index e51b8ee6..00000000 --- a/src/dusksdl2/input/inputsdl2.c +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "input/input.h" -#include "assert/assert.h" -#include "display/display.h" - -void inputUpdateSDL2(void) { - #ifdef DUSK_INPUT_GAMEPAD - INPUT.platform.controller = NULL; - - for(int32_t i = 0; i < SDL_NumJoysticks(); i++) { - if(!SDL_IsGameController(i)) continue; - INPUT.platform.controller = SDL_GameControllerOpen(i); - if(INPUT.platform.controller) break; - } - #endif - - #ifdef DUSK_INPUT_KEYBOARD - INPUT.platform.keyboardState = SDL_GetKeyboardState(NULL); - #endif - - #ifdef DUSK_INPUT_POINTER - int_t pointerX, pointerY; - SDL_GetMouseState(&pointerX, &pointerY); - - int_t windowWidth, windowHeight; - SDL_GetWindowSize(DISPLAY.window, &windowWidth, &windowHeight); - - INPUT.platform.mouseX = (float_t)pointerX / (float_t)windowWidth; - INPUT.platform.mouseY = (float_t)pointerY / (float_t)windowHeight; - INPUT.platform.scrollX = 0.0f; - INPUT.platform.scrollY = 0.0f; - #endif -} - -float_t inputButtonGetValueSDL2(const inputbutton_t button) { - switch(button.type) { - #ifdef DUSK_INPUT_KEYBOARD - case INPUT_BUTTON_TYPE_KEYBOARD: { - return INPUT.platform.keyboardState[button.scancode] ? 1.0f : 0.0f; - } - #endif - - #ifdef DUSK_INPUT_POINTER - case INPUT_BUTTON_TYPE_POINTER: { - switch(button.pointerAxis) { - case INPUT_POINTER_AXIS_X: - return INPUT.platform.mouseX; - - case INPUT_POINTER_AXIS_Y: - return INPUT.platform.mouseY; - - case INPUT_POINTER_AXIS_Z: - return 0.0f; // Not supported in SDL2 - - case INPUT_POINTER_AXIS_WHEEL_X: - return INPUT.platform.scrollX; - - case INPUT_POINTER_AXIS_WHEEL_Y: - return INPUT.platform.scrollY; - - default: - assertUnreachable("Unknown pointer axis"); - return 0.0f; - } - } - #endif - - #ifdef DUSK_INPUT_GAMEPAD - case INPUT_BUTTON_TYPE_GAMEPAD: { - assertTrue( - button.gpButton < SDL_CONTROLLER_BUTTON_MAX, - "Gamepad button out of range" - ); - - if(SDL_GameControllerGetButton( - INPUT.platform.controller, button.gpButton - )) { - return 1.0f; - } - return 0.0f; - } - - case INPUT_BUTTON_TYPE_GAMEPAD_AXIS: { - float_t value = 0.0f; - - Sint16 axis = SDL_GameControllerGetAxis( - INPUT.platform.controller, button.gpAxis.axis - ); - value = (float_t)axis / 32767.0f; - if(!button.gpAxis.positive) value = -value; - value = inputDeadzone(value, inputGetDeadzoneSDL2(button)); - return value; - } - #endif - - default: { - assertUnreachable("Unknown input button type"); - return 0.0f; - } - } -} \ No newline at end of file diff --git a/src/dusksdl2/input/inputsdl2.h b/src/dusksdl2/input/inputsdl2.h deleted file mode 100644 index 03ba9ebc..00000000 --- a/src/dusksdl2/input/inputsdl2.h +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 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 inputbutton_s inputbutton_t; - -#ifdef DUSK_INPUT_KEYBOARD - typedef SDL_Scancode inputscancodesdl2_t; -#endif - -#ifdef DUSK_INPUT_GAMEPAD - typedef SDL_GameControllerButton inputgamepadbuttonsdl2_t; - typedef SDL_GameControllerAxis inputgamepadaxissdl2_t; -#endif - -#ifdef DUSK_INPUT_POINTER - typedef enum { - INPUT_POINTER_AXIS_X, - INPUT_POINTER_AXIS_Y, - INPUT_POINTER_AXIS_Z, - INPUT_POINTER_AXIS_WHEEL_X, - INPUT_POINTER_AXIS_WHEEL_Y, - } inputpointeraxissdl2_t; -#endif - -typedef struct { - #ifdef DUSK_INPUT_GAMEPAD - SDL_GameController *controller; - #endif - - #ifdef DUSK_INPUT_KEYBOARD - const uint8_t *keyboardState; - #endif - - #ifdef DUSK_INPUT_POINTER - float_t mouseX, mouseY; - float_t scrollX, scrollY; - #endif -} inputsdl2_t; - -// Setup platform variables for input. -#ifdef DUSK_INPUT_KEYBOARD - typedef inputscancodesdl2_t inputscancodeplatform_t; -#endif - -#ifdef DUSK_INPUT_GAMEPAD - typedef inputgamepadbuttonsdl2_t inputgamepadbuttonplatform_t; - typedef inputgamepadaxissdl2_t inputgamepadaxissplatform_t; -#endif - -#ifdef DUSK_INPUT_POINTER - typedef inputpointeraxissdl2_t inputpointeraxisplatform_t; -#endif - -#define inputUpdatePlatform inputUpdateSDL2 -#define inputButtonGetValuePlatform inputButtonGetValueSDL2 - -typedef inputsdl2_t inputplatform_t; - -/** - * Updates the input state for SDL2. - */ -void inputUpdateSDL2(void); - -/** - * Returns the deadzone for the given gamepad axis. Requires implementation by - * the host platform. - * - * @param button The button to get the deadzone of. - * @return The deadzone for the given gamepad axis. - */ -float_t inputGetDeadzoneSDL2(const inputbutton_t button); - -/** - * Returns the input value (between 0 and 1) of the given button. - * - * @param button The button to get the value of. - * @return The value of the button, between 0 and 1. - */ -float_t inputButtonGetValueSDL2(const inputbutton_t button); \ No newline at end of file diff --git a/src/dusksdl2/time/CMakeLists.txt b/src/dusksdl2/time/CMakeLists.txt deleted file mode 100644 index 79fb7459..00000000 --- a/src/dusksdl2/time/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# 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 - timesdl2.c -) - -# Subdirs \ No newline at end of file diff --git a/src/dusksdl2/time/timeplatform.h b/src/dusksdl2/time/timeplatform.h deleted file mode 100644 index 316e96a3..00000000 --- a/src/dusksdl2/time/timeplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "timesdl2.h" - -#define timeTickPlatform timeTickSDL2 -#define timeGetDeltaPlatform timeGetDeltaSDL2 -#define timeGetRealPlatform timeGetRealSDL2 \ No newline at end of file diff --git a/src/dusksdl2/time/timesdl2.c b/src/dusksdl2/time/timesdl2.c deleted file mode 100644 index c52c3f2f..00000000 --- a/src/dusksdl2/time/timesdl2.c +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "timesdl2.h" -#include "assert/assert.h" -#include - -void timeTickSDL2(void) { - TIME_TICKS_LAST_SDL2 = TIME_TICKS_SDL2; - TIME_TICKS_SDL2 = SDL_GetTicks64(); -} - -float_t timeGetDeltaSDL2(void) { - assertTrue(TIME_TICKS_SDL2 >= TIME_TICKS_LAST_SDL2, "Ticks went backwards?"); - return (float_t)(TIME_TICKS_SDL2 - TIME_TICKS_LAST_SDL2) / 1000.0f; -} \ No newline at end of file diff --git a/src/dusksdl2/time/timesdl2.h b/src/dusksdl2/time/timesdl2.h deleted file mode 100644 index 42a93c4b..00000000 --- a/src/dusksdl2/time/timesdl2.h +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" - -static uint64_t TIME_TICKS_SDL2 = 0; -static uint64_t TIME_TICKS_LAST_SDL2 = 0; - -/** - * Requests SDL2 to update its internal game ticks. - */ -void timeTickSDL2(void); - -/** - * Gets the time delta for this tick in seconds. - * - * @return The time delta for this tick in seconds. - */ -float_t timeGetDeltaSDL2(void); \ No newline at end of file diff --git a/src/duskvita/CMakeLists.txt b/src/duskvita/CMakeLists.txt deleted file mode 100644 index 8e31820d..00000000 --- a/src/duskvita/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -# Includes -target_include_directories(${DUSK_LIBRARY_TARGET_NAME} - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) - -# Sources -target_sources(${DUSK_BINARY_TARGET_NAME} - PUBLIC -) - -# Subdirs -add_subdirectory(asset) -add_subdirectory(input) -add_subdirectory(log) -add_subdirectory(save) diff --git a/src/duskvita/asset/CMakeLists.txt b/src/duskvita/asset/CMakeLists.txt deleted file mode 100644 index d3531331..00000000 --- a/src/duskvita/asset/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - assetvita.c -) diff --git a/src/duskvita/asset/assetplatform.h b/src/duskvita/asset/assetplatform.h deleted file mode 100644 index 725e5122..00000000 --- a/src/duskvita/asset/assetplatform.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "assetvita.h" - -typedef assetvita_t assetplatform_t; -#define assetInitPlatform assetInitVita -#define assetDisposePlatform assetDisposeVita diff --git a/src/duskvita/asset/assetvita.c b/src/duskvita/asset/assetvita.c deleted file mode 100644 index 76197b80..00000000 --- a/src/duskvita/asset/assetvita.c +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "asset/asset.h" -#include "asset/assetdsk.h" -#include "assert/assert.h" - -errorret_t assetInitVita(void) { - errorChain(assetDskOpenFromPath(ASSET_VITA_DSK_PATH, &ASSET.zip)); - errorOk(); -} - -errorret_t assetDisposeVita(void) { - errorOk(); -} diff --git a/src/duskvita/asset/assetvita.h b/src/duskvita/asset/assetvita.h deleted file mode 100644 index 82d2bbbf..00000000 --- a/src/duskvita/asset/assetvita.h +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -// dusk.dsk is packaged at the root of the VPK and accessible via app0:/ -#define ASSET_VITA_DSK_PATH "app0:/" ASSET_FILE_NAME - -typedef struct { - uint8_t _unused; -} assetvita_t; - -/** - * Initializes the Vita asset system, opening dusk.dsk from the VPK mount. - * - * @returns An errorret_t indicating success or failure. - */ -errorret_t assetInitVita(void); - -/** - * Disposes the Vita asset system. - * - * @returns An errorret_t indicating success or failure. - */ -errorret_t assetDisposeVita(void); diff --git a/src/duskvita/display/displayplatform.h b/src/duskvita/display/displayplatform.h deleted file mode 100644 index 94ecc21f..00000000 --- a/src/duskvita/display/displayplatform.h +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "display/displaysdl2.h" - -typedef displaysdl2_t displayplatform_t; - -#define displayPlatformInit displaySDL2Init -#define displayPlatformUpdate displaySDL2Update -#define displayPlatformSwap displaySDL2Swap -#define displayPlatformDispose displaySDL2Dispose diff --git a/src/duskvita/duskplatform.h b/src/duskvita/duskplatform.h deleted file mode 100644 index be1634ef..00000000 --- a/src/duskvita/duskplatform.h +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include - -#define GL_COLOR_INDEX8_EXT 0x80E5 -#define GL_NO_ERROR 0 -#define glDrawArrays(type, first, count) ((void)0) -#define glColorTableEXT( \ - target, internalformat, count, format, type, pixels) ((void)0) -#define glDepthFunc(func) ((void)0) -#define glBlendFunc(sfactor, dfactor) ((void)0) -#define glGetError() GL_NO_ERROR -#define GL_GLEXT_PROTOTYPES - -typedef uint32_t GLbitfield; - -#include "dusksdl2.h" \ No newline at end of file diff --git a/src/duskvita/input/CMakeLists.txt b/src/duskvita/input/CMakeLists.txt deleted file mode 100644 index 8c00669a..00000000 --- a/src/duskvita/input/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - inputvita.c -) diff --git a/src/duskvita/input/inputvita.c b/src/duskvita/input/inputvita.c deleted file mode 100644 index 7194ceaf..00000000 --- a/src/duskvita/input/inputvita.c +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "input/input.h" - -inputbuttondata_t INPUT_BUTTON_DATA[] = { - { .name = "triangle", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_Y } }, - { .name = "cross", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_A } }, - { .name = "circle", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_B } }, - { .name = "square", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_X } }, - { .name = "start", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_START } }, - { .name = "select", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_BACK } }, - { .name = "up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_UP } }, - { .name = "down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_DOWN } }, - { .name = "left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_LEFT } }, - { .name = "right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_DPAD_RIGHT } }, - { .name = "l", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_LEFTSHOULDER } }, - { .name = "r", { - .type = INPUT_BUTTON_TYPE_GAMEPAD, - .gpButton = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER } }, - - { .name = "lstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_LEFTY, .positive = true } } }, - { .name = "lstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_LEFTY, .positive = false } } }, - { .name = "lstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_LEFTX, .positive = true } } }, - { .name = "lstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_LEFTX, .positive = false } } }, - - { .name = "rstick_down", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_RIGHTY, .positive = true } } }, - { .name = "rstick_up", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_RIGHTY, .positive = false } } }, - { .name = "rstick_right", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_RIGHTX, .positive = true } } }, - { .name = "rstick_left", { - .type = INPUT_BUTTON_TYPE_GAMEPAD_AXIS, - .gpAxis = { - .axis = SDL_CONTROLLER_AXIS_RIGHTX, .positive = false } } }, - - { .name = NULL } -}; - -float_t inputGetDeadzoneSDL2(const inputbutton_t button) { - return 0.2f; -} diff --git a/src/duskvita/log/CMakeLists.txt b/src/duskvita/log/CMakeLists.txt deleted file mode 100644 index 9c72f882..00000000 --- a/src/duskvita/log/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -# 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 - log.c -) diff --git a/src/duskvita/log/log.c b/src/duskvita/log/log.c deleted file mode 100644 index 90d888d0..00000000 --- a/src/duskvita/log/log.c +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "log/log.h" - -void logDebug(const char_t *message, ...) { - va_list args; - va_start(args, message); - - // print to stdout - va_list copy; - va_copy(copy, args); - vprintf(message, copy); - va_end(copy); - - // print to file - FILE *file = fopen("ux0:data/Dusk/debug.log", "a"); - if(file) { - va_copy(copy, args); - vfprintf(file, message, copy); - va_end(copy); - fclose(file); - } - - va_end(args); -} - -void logError(const char_t *message, ...) { - va_list args; - va_start(args, message); - - // print to stderr - va_list copy; - va_copy(copy, args); - vfprintf(stderr, message, copy); - va_end(copy); - - // print to file - FILE *file = fopen("ux0:data/Dusk/error.log", "a"); - if(file) { - va_copy(copy, args); - vfprintf(file, message, copy); - va_end(copy); - fclose(file); - } - - va_end(args); -} diff --git a/src/duskvita/script/module/moduleplatformplatform.h b/src/duskvita/script/module/moduleplatformplatform.h deleted file mode 100644 index b4c40956..00000000 --- a/src/duskvita/script/module/moduleplatformplatform.h +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "moduleplatformvita.h" - -#define modulePlatformPlatform modulePlatformVita diff --git a/src/duskvita/script/module/moduleplatformvita.h b/src/duskvita/script/module/moduleplatformvita.h deleted file mode 100644 index 4d58a148..00000000 --- a/src/duskvita/script/module/moduleplatformvita.h +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "script/scriptmanager.h" - -void modulePlatformVita(void) { - scriptManagerExec("VITA = true\n", NULL); -} diff --git a/story.md b/story.md deleted file mode 100644 index 21fd3b8d..00000000 --- a/story.md +++ /dev/null @@ -1,608 +0,0 @@ -# Story - -This document tracks the game's narrative - premise, world, characters, and -lore - as it's developed. It will grow incrementally; sections marked -**TBD** are placeholders for details not yet locked in. - -## Premise - -At its core, this is a story about a prophecy someone tries to prevent, -only to become the one who causes it. - -A king is warned by a fortune teller that a mage wielding -**Culinary/Hospitality Magic** *(name not final)* - a magic of creating and -enchanting food, drink, and other everyday comforts, the kind of thing a -normal person would dismiss as "nice to have" rather than a weapon - will -one day be the one to bring him down. - -Rather than risk it, the king moves to wipe out every caster of that magic -in his kingdom. The culling nearly succeeds: only a handful of casters -survive, among them our protagonist - who, at the story's start, doesn't -even know she has the power herself. It is this act of trying to prevent -the prophecy that sets in motion the very events that will fulfill it. - -## Story Structure - -The story is told across a run of chapters, grouped into eight broad -movements. Individual chapter-to-movement mapping is **TBD** - these are -the beats each stretch of the story needs to hit, not a fixed chapter -count. - -1. **The Intro** - Establishes the protagonist. Plants the first, vague - seeds that she's powerful in a way that's unfamiliar and a little - unsettling - a kind of magic people haven't seen in a long time, if - ever. -2. **The Buildup** - She starts learning about a wider world she didn't - know existed. Something is subtly steering her toward conflict - not - overtly, but persistently - and nobody around her can say why, or even - quite agree that it's happening. -3. **Early Realization** - Threads start connecting back to the king. - Former enemies let information slip, whether by accident or design, - and the shape of something larger starts to form. -4. **The Realization** - The big reveal: who she thought she was isn't - true. She is a culinary mage. This is who she is. The only questions - left are why, and how. -5. **Full Circle** - The king's intentions and motives come out in full: - why she's a threat to him, and how he orchestrated events from the - very start. -6. **The Buildup (Endgame)** - Goals are set and motivations are clear on - both sides. She has to stop the king, while he's simultaneously moving - to stop her. -7. **The Finale** - The prophecy is fulfilled. -8. **What's Next** - This world has more stories left to tell - this is - only the surface. Freedom has been won, but now it has to be - maintained. Sets up potential future games in the same world. - -## Chapters - -Detailed, scene-level breakdowns of individual chapters as they're -developed. Currently covers the opening of Chapter 1 only; more to come. - -### Chapter 1: The Intro - -The story doesn't open on the protagonist - it opens on a small party of -bounty hunters, though they'd bristle at the word; they consider their -work more noble than that. They've been secretly hired by the king on a -straightforward job: rumor has it a culinary magic user is living far up -in a freezing mountain range. Go get her. Bring her back alive. - -We follow this chapter through the party leader's perspective - the -story's second main character. He's honor-bound to a fault, treating the -king's word as gospel without question. Even so, the job needles at him -from the start: why this person specifically, why this remote location - -questions he swallows, because it isn't his place to question the king. - -**Opening scene.** The story begins in a village square - a small, quiet -settlement with one modestly bustling street, the last real point of -civilization before the mountain range swallows everything beyond it. The -bubbly ranger enters first, delighted just to be somewhere new, already -itching to go sample everything the village has to offer. The rest of the -party follows a beat behind: the party leader, steadfast as ever, keeping -everyone's eyes on the mission first and foremost; the fratboy mage, big -and boisterous; and the rogue, shy and silent, saying nothing at all. The -party: - -- **The Party Leader** - the group's tank/paladin. -- **The Bubbly Ranger** - ranged attacker, longbow. -- **The Fratboy Mage** - support and primary magic attacker. -- **The Rogue** - shy and silent but strong, the party's main melee - frontliner. - -They head straight for the village's guild hall to find a teleporter up -the mountain - there's no time to rest, and that urgency is exactly what -surfaces the party's first real friction: funds are already thin, and -every extra night spent in town is a night they can't really afford. A -minor bicker over money breaks out - nothing serious yet, just the first -crack. - -Eventually they find a teleporter willing to take the job: rugged, -plainspoken, a cowgirl through and through. She offers to take them up the -mountain for a third of what the guild hall was asking - a deal too good -to question closely. The catch, mentioned almost as an aside, is that she -can't bring them back down; she's got a contract of her own to honor once -she's up there. She waves it off as nothing much - they can figure that -out later, can't they? - and the party, just relieved to have a way up at -all, doesn't push it. - -None of them understand yet how ordinary an arrangement this actually is. -A teleporter at the mountain doesn't care what she's paid to carry -upward, only that it gives her an excuse to make the climb, since all -that actually matters to her - to every teleporter working that route - -is filling her mana hauling ore back down (see World Notes). Getting a -teleport back *down* the mountain, the party will learn, is another -matter entirely. - -They arrive to find an isolated, blizzard-bound mining town built around -that rare ore. The culinary mage they've come for is well-liked and -respected here, and a group of outsiders showing up to take her away, on -the word of a king who has never done anything for this region, does not -go over well - the town turns on them. - -Then comes the first real complication. The party's funds - a stipend -from the king that was never generous to begin with - come nowhere close -to what it costs to buy a teleport back down: ore shipments and -deep-pocketed royalty take priority over everyone else, always. With no -way down, and nowhere welcome to wait it out, the party fractures. Some -see easy, guaranteed pay in the town's mines and no reason to stay loyal -to a king who clearly doesn't value them; others still see the king's -request - however small and uncertain the eventual payout - as the job -they signed up to finish. The bubbly ranger is the first to go, no longer -willing to freeze and starve for a job that was never straight with her. -The party splits, and what's left sets out to trek down the mountain the -old-fashioned way. -*(Whether the fratboy mage and/or the rogue also leave here, or stay -through to the castle: TBD.)* - -What remains is a smaller group, supplies dwindling, no clear sense of why -any of this is still worth it, hauling what feels like dead weight: a -prisoner who, as far as anyone can tell, wouldn't hurt a fly. - -Around the chapter's midpoint, the party leader gets the sense they're -being followed. Nothing confirmed, no confrontation - just a feeling, a -shape at the edge of the blizzard, tracks that shouldn't be there. He -says nothing to the rest of the party. The audience is left aware someone -or something is watching, well before anyone in-story can do anything -about it. - -As the journey back wears on, she shows her true character - even as a -captive, she uses her magic to help the party survive. It's this, more -than anything, that starts cracking the party leader's certainty. He -keeps carrying out his orders. He just isn't sure of them anymore. - -The chapter's climax comes at the castle. The party presents her to the -king, and she's dragged off to the dungeon - implicitly to be executed, or -simply to disappear. In open defiance, the party leader confronts the -king: who is she, and why did his party go through hell to drag her here? -The king, unwilling to admit to anything about hunting culinary mages -specifically, dismisses him as a traitor for questioning the crown - -denies him payment, and all but tells him to be grateful he's keeping his -head. - -*(The presence felt at the chapter's midpoint is, unknown to anyone in the -party at the time, The Detective - see Characters.)* - -### Chapter 2 (early notes) - -Partial beats only for now - not a full breakdown yet. - -The protagonist, imprisoned and awaiting execution, is freed when a key -is thrown through the bars of her cell - by the Detective. He tells her, -briefly, that he's the one who's been following the party this whole -time, and that their paths will cross again, before leaving her to make -her own escape. - -Separately, the Detective seeks out the party leader, now dismissed and -adrift - having gone through hell to complete the king's request with his -honor intact, only to be discarded for asking the wrong question. Already -in the middle of his own crisis of faith about honor, authority, and -everything he's built his life on, he's approached by the Detective, who -reveals - sparingly - that he's a detective, and that he'd been following -the party since before they ever reached the mountains. He won't say much -more than that; he can't show his hand. But he does suggest, deliberately, -that the protagonist might be worth looking into further - and floats -that she may have some connection to the Great Explosion, given that she -practices the forbidden magic herself. - -Between these two encounters, it's becoming clear the king has *some* -concrete reason to distrust the protagonist specifically - though no one, -including the audience, knows what it is yet. Doubt about the king has -been planted, in both the party leader and the player, but nothing is -confirmed. As quickly as the Detective appears, he's gone again, already -moving on to other threads of his own investigation - this, as far as -he's concerned, is a dead end not worth lingering on. - -## The Great Explosion - -The event is so significant to this kingdom that it reset their calendar -around it. Where the rest of the world reckons time by the old calendar -(currently **Year 4192**), this kingdom counts years as **AGE** - "After -the Great Explosion." The story opens in **Year 25 AGE** - which puts the -protagonist, born the night it happened, at roughly 25 years old. - -Within the kingdom, the event itself is barely spoken of - referenced only -obliquely, in hushed tones, as "the Great Explosion." What actually -happened is not public knowledge, is actively lied about by the crown -(see "The Official Story" below), and is revealed to the player gradually -over the course of the story, rather than up front. What follows is the -truth of it, for reference - not something the player, or the protagonist, -knows this early. - -### What Really Happened - -Having traced the culinary mages to their hometown - a whole town built -around them, led by their most powerful practitioner, the **Great Mage** -*(name TBD)*, who is also the town's mayor - the king moves to have them -all executed in a single, coordinated purge. He doesn't delegate this one: -he's there in person, with a large force of soldiers, to make sure it -happens exactly as planned. - -The town is rounded up into the streets - every citizen, women and -children included - and the king demands the Great Mage surrender himself. -The Great Mage has done nothing wrong; there is no plot, no uprising, only -a prophecy and a king who has convinced himself, completely, that this man -is the one fated to end him. The soldiers aren't there to prevent some -future act - as far as anyone in the crowd can tell, they're just there to -make sure nothing stops the execution. - -To force the issue, the king takes the Great Mage's wife hostage, along -with their newborn daughter - the protagonist - and threatens to kill them -both on the spot if he doesn't come forward. The Great Mage relents and -agrees to surrender himself into captivity. - -It isn't enough. Having gotten what he demanded, the king - by this point -well past reason - orders the entire village executed anyway. Soldiers -begin killing citizens one by one in the street. It is, plainly, a -massacre. - -With no options left, the Great Mage begins casting: a spell built on an -old, well-known melody, one every villager grew up knowing as a soothing -folk song. As he chants it, others in the crowd who can cast start singing -and casting along with him - even now, even like this. What starts as one -desperate act becomes the whole town casting together, and the combined -spell detonates as the Great Explosion. - -The king escapes only barely, teleported clear at the last second by his -own personal, and extremely rare, teleporter. Everyone else in the town is -killed - along with nearly the entire army the king brought to enforce the -purge. It's a catastrophic loss on every side: the king loses the bulk of -his military in an instant, and every known culinary mage but a scattered -handful dies with the town. In the aftermath, even the king isn't sure -what to make of it - whether this was the prophecy finally arriving, or -whether wiping out every culinary mage in existence means he's just -avoided it for good. - -He's wrong either way. In the same instant, the Great Mage's wife casts a -protective spell of her own on their newborn daughter - small in scope, -powerful enough to matter, enough to keep only one person alive through -what's coming. It saves the protagonist, and nothing else. She ends up one -of a literal handful of survivors, alongside a few surviving soldiers and -a handful of villager stragglers. Not all of the soldiers who survive stand -down - some try to finish what they were ordered to do, hunting down and -killing a few more of the survivors even after the explosion. Those who -can still run do, and they take the newly orphaned protagonist with them. - -### The Official Story - -History gets written by whoever's left standing. The king and what -remains of his army return home with a different account entirely: the -culinary mages were insurgents, plotting to overthrow the crown, and the -Great Explosion was their own doing - a last, failed act of rebellion, not -a massacre. It sticks. It becomes the official record, and it's why the -truth of what happened stays buried, spoken of only in fragments and -hushed tones, for the twenty-five years since. - -## World Notes - -- **Two calendars**: the wider world uses a conventional year count - (Year 4192 and counting); this kingdom alone uses Year *N* AGE, counting - from the Great Explosion. The disparity itself is a hint to outsiders - - and to the player - that something happened here. -- **Culinary/Hospitality Magic**: magic themed around food, drink, and - comfort - underestimated by most as domestic or decorative, not martial. - Exact mechanics/rules TBD. Following the Official Story branding its - practitioners as insurgents, it's now known within the kingdom as "the - forbidden magic" - officially outlawed, which is part of why the - protagonist has never been trained in it and doesn't know she has it. -- **Teleportation Magic**: the rarest and single most valuable magical - skill in the world - fewer than 1% of magic users can do it at all. - Teleporters are indispensable for logistics (messages, cargo, urgent - travel) and can effectively name their own price; they're held in - correspondingly high regard almost everywhere. - - This reaches its extreme in the mining mountains. A rare ore is mined - there *(name TBD)*, valuable enough and difficult enough to move any - other way that the teleporters working that route are among the - highest-paid individuals anywhere in the world. - - A teleporter at the mountain cares about exactly one thing: getting - ore *down*. They'll spend their entire reserve of mana hauling as much - of it downward as they can carry, and nothing takes priority over that - - short of a royal client with genuinely deep pockets. - - Because of that singular focus, teleporters will take almost any - paying job to get back *up* the mountain first - they don't care what - (or who) they're carrying, only that it justifies the trip so they can - turn around and come back down loaded with ore. Supply runs upward are - therefore easy and cheap to arrange; there's never a shortage of - teleporters looking for an excuse to make the climb. - - Getting a teleport *down* the mountain is the opposite problem - entirely - one outsiders unfamiliar with the trade routinely - underestimate. See Chapter 1. - - The king keeps a personal teleporter on retainer - rare enough to - afford only for a monarch. It's this teleporter who gets him clear of - the Great Explosion in the instant before it kills nearly everyone - else present. - -## Characters - -### The Protagonist -One of the last surviving casters of Culinary/Hospitality Magic. Does not -yet know she has this power. Born the night of the Great Explosion to the -Great Mage and his wife, and saved from it only by her mother's protective -spell cast in its final instant - a fact she has no memory or knowledge of. -Raised after by whichever survivors and stragglers fled the scene with -her. Her arc is the prophecy's fulfillment - the king's attempt to prevent -her existence is, narratively, what creates her. -*(Name, background, and how she discovers her power: TBD.)* - -### The King -Ordered the culling of all Culinary/Hospitality mages after being warned -of the prophecy, and personally oversaw the resulting massacre at the -Great Mage's town. Tunnel-visioned to the point of ordering the entire -village executed even after getting the surrender he demanded - a -decision that costs him nearly his whole army in the Great Explosion he -provoked. Escaped only via his own personal teleporter. Still doesn't know -whether he caused the prophecy or avoided it, and doesn't know the -protagonist survived at all. Motivations beyond self-preservation, and his -current awareness of how the prophecy is unfolding: TBD. - -### The Fortune Teller -Delivered the original prophecy to the king. Role beyond the inciting -incident: TBD. - -### The Great Mage -The protagonist's father, the most powerful known culinary mage of his -generation, and the mayor of the town the king wipes out in the Great -Explosion. Innocent of the plot the king believes in - there was no -plot. Surrenders himself to try to save his wife and newborn daughter, -only for the king to order the massacre anyway. Leads the town's final, -collective casting - built on a spell that doubles as a folk song every -villager already knew - that becomes the Great Explosion. Dies in it. -*(Name: TBD.)* - -### The Protagonist's Mother -The Great Mage's wife. Held hostage alongside her newborn daughter to -force her husband's surrender. In the Great Explosion's final instant, -casts a small but powerful protective spell that saves the protagonist -and no one else, at the cost of her own life. The protagonist has no -memory of her. -*(Name: TBD.)* - -### The Bubbly Ranger -One of the party leader's original bounty-hunting party - the group's -ranged attacker, fighting with a longbow. Enters the story first, ahead -of the rest of the party, delighted at the chance to explore the small -village at the mountain's foot. Her enthusiasm doesn't survive the -mountain: she's the first to abandon the mission once the party's stranded -and broke, unwilling to keep freezing and starving for a job that was -never honest with her from the start. -*(Name: TBD.)* - -### The Fratboy Mage -One of the party leader's original bounty-hunting party - support and -primary magic attacker. Big, boisterous, overconfident energy. Whether he -stays with the party through to the castle, or leaves alongside the -ranger: TBD. -*(Name: TBD.)* - -### The Rogue -One of the party leader's original bounty-hunting party - the group's -main melee frontliner. Shy and silent, says almost nothing, but -physically strong and imposing - a quiet contrast to the ranger's -bubbliness. Whether she stays with the party through to the castle, or -leaves alongside the ranger: TBD. -*(Name: TBD.)* - -### The Mountain Teleporter -A rugged, plainspoken, cowgirl-type teleporter met at the village guild -hall at the foot of the mountain. Offers to take the party up for a third -of the guild's asking price - a deal too good to question - and mentions, -almost as an aside, that she can't bring them back down; she has her own -ore-hauling contract to fulfill once she's up there. Framed as an -innocent, easily-dismissed detail in the moment, it's actually the seed -of the chapter's central complication (see World Notes on Teleportation -Magic). Likely a one-scene character, not expected to reappear. -*(Name: TBD.)* - -### The Party Leader -The story's second main character, and whose perspective opens Chapter 1. -Leads the band of bounty hunters secretly hired by the king to retrieve -the protagonist from the mountains. Honor-bound to a fault, and treats the -king's word as absolute - but the ordeal of the retrieval, and the -protagonist's own conduct as a captive, starts opening cracks in that -certainty. Still carries out his orders even as he stops trusting them. -Publicly denounced as a traitor by the king at the end of Chapter 1 for -questioning why she was really taken. - -**Backstory.** As a very young soldier, he served in an army squadron -under a mentor he idolized - a strictly honor-bound officer who was, to -him, something like a superhero. On the night the army was dispatched to -the Great Mage's town, that mentor broke orders outright: he told the -entire squadron to stay behind and went in alone. With their mentor gone, -the squadron had no one to hold it together - it disbanded, and everyone -in it eventually left the army to find other work. The mentor never -returned; he died in the Great Explosion. - -The party leader never learned the king was actually behind the massacre, -so he never had reason to connect his mentor's last order to anything but -a personal choice. Instead, he built his entire sense of honor around -faithfully executing orders and serving "the greater good," now as a -mercenary rather than a soldier - honor as obedience, just no longer to a -chain of command, but to his own idea of duty (which is why the king's -word still carries the weight of gospel for him in Chapter 1). - -It's through the protagonist that this starts to crack: he comes to -understand that real honor has to come from one's own judgment, not -blind adherence to orders - his mentor's, the king's, or otherwise. By -the story's end, he realizes the truth: his mentor almost certainly went -that night already knowing the king intended a massacre, and chose to -disobey orders specifically so his squadron would never have to take part -in - or witness - that side of what "honor" can mean. Because the mentor -sent them home instead of into the town, none of them died in the Great -Explosion. The mentor's final act of disobedience *was* the honorable one -- the party leader just spends most of the story unable to see it that -way. - -Further chapters will follow up with other surviving members of that same -squadron, now scattered across the world, each having drawn their own, -different lesson from their mentor's example of what honor means. Of the -five other survivors, three are met over the course of the story: Strider, -Downes, and Egor (see below). -*(Name and background: TBD.)* - -### The Mentor -The party leader's former commanding officer, and the closest thing he -had to a hero growing up. Rigidly honor-bound. Ordered his entire -squadron to stand down and went alone into the Great Mage's town on the -night of the purge - almost certainly already aware of what the king -intended, and unwilling to let his soldiers take part in it. Dies in the -Great Explosion. His true reasoning is never stated outright; the party -leader only pieces it together well after the fact. -*(Name: TBD.)* - -### Strider -Former squadron-mate of the party leader, now a merc who's found his own, -settled definition of honor. Runs a protection business for merchants who -can't afford a teleporter's price - riding along their carriages, -guarding their goods, and fending off bandits the whole way. Has found -genuine peace in it: protecting people who have no one else looking out -for them and no way to afford real protection. He's welcomed, loved, and -respected in that niche precisely because his care for the people he -protects is real, not transactional. The clearest example of the -squadron's shared "honor" being taken somewhere quietly good. - -### Downes -Once one of the strongest fighters the squadron had. Now fully retired -from soldiering, living as a farmer in a quiet village, and won't so much -as pick up a weapon. His honor is his family - protecting them, and -building them a life away from all of it, is the whole of it for him now. -Tends his crops and animals, and wants nothing to do with his past. -Represents the opposite lesson from the rest of the squadron: that there -are things in life more important than fighting. That said, the story -brings him to a moment where he has no choice but to fight again - proof -that even he can't outrun what he used to be forever. -*(Circumstances of that fight: TBD.)* - -### Egor -Once part of the same squadron, now a highly respected member of the -Royal Guard, decorated for his power and his devotion. He's the story's -clearest villain among the three, though he'd never see himself that way: -he genuinely believes honor *is* following orders, full stop. He considers -his former squadron-mates - the party leader especially - to have gone -rogue, and is completely blind to what the king actually is, misreading -their old mentor's final act as some kind of failure of nerve rather than -its own form of honor. - -Egor ultimately faces the party and the protagonist directly, and is -defeated. He dies still certain his honor was sound to the very end - -deluded about it right up until his last moments, when something finally -breaks through: a flash of understanding that his blind devotion earned -him nothing but pain and misery. Whether he actually regrets it, in the -end, is left unknown - even to him, maybe. - -### The Detective -Deliberately kept thin by design - this entry should stay short even as -the rest of the document grows. The less the audience (and we) nail down -about him early, the better he works later. - -Working for the king, he's been secretly tailing the party since before -Chapter 1 even reaches the mountains - he's the presence felt at that -chapter's midpoint. Neither the party nor the protagonist know he exists -until Chapter 2, when he throws a cell key through the bars to free the -protagonist ahead of her execution, tells her only that he's been -following them and that they'll meet again, and leaves her to escape on -her own. He separately approaches the newly-dismissed party leader, -confirms he's a detective and that he's been on the party's trail from -the start, and - without showing any more of his hand than that - -suggests the protagonist is worth investigating further, floating that -her connection to the Great Explosion may run deeper than anyone -realizes, given she practices the forbidden magic herself. Then he's -gone, back to his own investigation, treating this as a dead end not -worth his time. - -Physically described as looking oddly caught between two trades - "half -banker, half farmer" - the kind of appearance that resists being placed -or remembered clearly. - -Works for the king, but his actual loyalties, goals, and how much he -personally knows about the Great Explosion or the prophecy are all -intentionally undefined for now. -*(Name and everything else: TBD, on purpose.)* - -## Themes - -- Self-fulfilling prophecy - the act of prevention *is* the cause. -- Power in what's dismissed as harmless or domestic. -- Institutional violence and its cover-up - a kingdom that has to - reorganize its own sense of time to live with what it did. -- Blind loyalty vs. conscience - orders followed even as faith in them - erodes. -- Honor redefined as judgment, not obedience - real honor sometimes looks - like disobeying orders, and can go unrecognized as such for years. The - same mentor and the same night produced six different soldiers who each - landed somewhere completely different: quiet self-doubt (the party - leader), care for the unrepresented (Strider), retreat into family and - peace (Downes), and honor curdled into blind, fatal devotion (Egor). -- You can't outrun who you were - Downes builds a life as far from - soldiering as he can get, and the story still forces one more fight out - of him. -- The tragedy of unshakeable certainty - Egor never once doubts himself, - and it kills him without ever teaching him anything, except perhaps at - the very last second. -- The crown's disregard for the people who prop it up - a distant king - extracting from a region (soldiers, subjects, this mage) that owes him - nothing and gets nothing back. -- Humanity mistaken for weakness - the "prisoner" who helps her captors - anyway. -- History written by the victors - an entire massacre rewritten into an - insurgency, and believed, because there was no one left to say - otherwise. -- Collective action under duress - an entire town choosing, in its final - moments, to act together rather than die separately. -- Duality of the same magic as comfort and catastrophe - a lullaby every - villager grew up with becomes the spell that ends them. -- A mother's sacrifice narrowed to the smallest possible act - saving one - life, not the town, because that's all that could be done. -- Everyone acting on partial information - the king, the Detective, the - party leader, the protagonist, and the player all hold different, often - incomplete pieces of the same truth. - -## Open Questions / TBD - -- Final name for Culinary/Hospitality Magic. -- Protagonist's name and how her power surfaces. -- How much of the Great Explosion's truth the player learns, and when. -- What the "couple of casters" who survived alongside the protagonist are - doing now - allies, threats, or unaware of each other entirely. -- Why the king specifically wanted *this* mage, from *this* location - - deliberately left unanswered in Chapter 1, to be paid off later. -- The party leader's name and background. -- Names for the bubbly ranger, the fratboy mage, the rogue, and the - mountain teleporter. -- What becomes of the bubbly ranger (and possibly the mage/rogue) after - they split off from the party at the foot of the mountain. -- The name of the small village at the mountain's foot, and the mining - town's name and its rare ore. -- Whether/how the party leader and protagonist's paths cross again after - Chapter 1. -- Whether teleportation magic (and its rarity) becomes relevant again - beyond Chapter 1's logistics trap - e.g. a named teleporter character, - or its scarcity mattering to the wider plot/prophecy. -- Names for the Great Mage, the protagonist's mother, and their hometown. -- The name/nature of the folk song that doubles as the spell behind the - Great Explosion. -- Who exactly fled with the infant protagonist (soldiers vs. villager - stragglers), who ultimately raised her, and whether any of them - reappear later in the story. -- Whether the king ever learns the protagonist survived, and if so, when. -- Whether/how the protagonist eventually learns the truth about her - parents and the Official Story's lie. -- Names for the party leader and his mentor. -- The two remaining, still-unnamed survivors of the mentor's squadron - - whether they're ever introduced, and if so how. -- Which chapters/movements Strider, Downes, and Egor each actually appear - in, and in what order the party leader reconnects with them. -- The circumstances that force Downes back into a fight. -- How Egor and the party actually cross paths, and what leads directly to - their confrontation. -- How and when the party leader pieces together that his mentor already - knew what the king intended that night. -- The Detective's name, true loyalties, and how much he actually knows - about the Great Explosion, the prophecy, and the protagonist's - parentage - kept deliberately unresolved for as long as possible. -- What the Detective's other "threads of investigation" are, and whether - they connect back to this story later. -- Where and when the Detective and the protagonist "cross paths again," as - he promises in Chapter 2. -- What the party leader does with the small amount the Detective tells - him - whether he acts on it immediately or it simmers. -- Exactly what concrete reason (if any) the king has to distrust the - protagonist specifically, beyond her simply being a surviving forbidden - mage - left unconfirmed at this point in the story. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index c09ae827..00000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -add_subdirectory(animation) -add_subdirectory(assert) -add_subdirectory(asset) -add_subdirectory(console) -add_subdirectory(error) -add_subdirectory(thread) -add_subdirectory(display) -add_subdirectory(rpg) -add_subdirectory(item) -add_subdirectory(save) -add_subdirectory(time) -add_subdirectory(util) \ No newline at end of file diff --git a/test/animation/CMakeLists.txt b/test/animation/CMakeLists.txt deleted file mode 100644 index aabb6fdf..00000000 --- a/test/animation/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_easing.c) -dusktest(test_keyframe.c) -dusktest(test_animation.c) diff --git a/test/animation/test_animation.c b/test/animation/test_animation.c deleted file mode 100644 index 39eb6b12..00000000 --- a/test/animation/test_animation.c +++ /dev/null @@ -1,443 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "animation/animation.h" -#include "util/memory.h" - -typedef struct { - uint16_t updateCount; - uint16_t loopCount; - uint16_t completeCount; - float_t lastValues[8]; -} animationtestcallbacks_t; - -static void testOnUpdate( - const uint16_t layer, const float_t value, void *user -) { - animationtestcallbacks_t *cb = (animationtestcallbacks_t *)user; - cb->updateCount++; - cb->lastValues[layer] = value; -} - -static void testOnLoop(void *user) { - ((animationtestcallbacks_t *)user)->loopCount++; -} - -static void testOnComplete(void *user) { - ((animationtestcallbacks_t *)user)->completeCount++; -} - -static void animationTestInit( - animation_t *anim, - animationtestcallbacks_t *cb, - keyframe_t *keyframes, - uint16_t *keyframeCounts, - const uint16_t layerCount -) { - memoryZero(cb, sizeof(animationtestcallbacks_t)); - animationInit(anim, keyframes, keyframeCounts, layerCount); - anim->user = cb; - anim->onUpdate = testOnUpdate; - anim->onLoop = testOnLoop; - anim->onComplete = testOnComplete; -} - -static void test_animationInitSingleLayerDuration(void **state) { - keyframe_t keyframes[3] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 3 }; - animation_t anim; - - animationInit(&anim, keyframes, keyframeCounts, 1); - - assert_float_equal(anim.duration, 2.0f, 0.0001f); - assert_int_equal(anim.layerCount, 1); - assert_float_equal(anim.time, 0.0f, 0.0001f); - assert_int_equal(anim.flags, 0); -} - -static void test_animationInitMultiLayerDurationIsMax(void **state) { - // Both layers must share the same keyframe count - animationInit/ - // animationGetLayerValue flatten the array as layer * keyframeCounts[layer], - // which only produces the correct offset when every layer's count matches. - keyframe_t keyframes[4] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, - { .time = 3.0f, .value = 300.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[2] = { 2, 2 }; - animation_t anim; - - animationInit(&anim, keyframes, keyframeCounts, 2); - - // Layer 1's last keyframe (time=3) is later than layer 0's (time=1). - assert_float_equal(anim.duration, 3.0f, 0.0001f); -} - -static void test_animationInitNullAsserts(void **state) { - keyframe_t keyframes[1] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 1 }; - animation_t anim; - - expect_assert_failure(animationInit(NULL, keyframes, keyframeCounts, 1)); - expect_assert_failure(animationInit(&anim, NULL, keyframeCounts, 1)); - expect_assert_failure(animationInit(&anim, keyframes, NULL, 1)); -} - -static void test_animationInitZeroLayerCountAsserts(void **state) { - keyframe_t keyframes[1] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 1 }; - animation_t anim; - - expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 0)); -} - -static void test_animationInitUnsortedKeyframesAsserts(void **state) { - keyframe_t keyframes[2] = { - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - - expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1)); -} - -static void test_animationGetLayerValueSamplesEachLayerIndependently( - void **state -) { - keyframe_t keyframes[4] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[2] = { 2, 2 }; - animation_t anim; - - animationInit(&anim, keyframes, keyframeCounts, 2); - anim.time = 1.0f; - - assert_float_equal(animationGetLayerValue(&anim, 0), 10.0f, 0.0001f); - assert_float_equal(animationGetLayerValue(&anim, 1), 50.0f, 0.0001f); -} - -static void test_animationGetLayerValueOutOfBoundsAsserts(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - - animationInit(&anim, keyframes, keyframeCounts, 1); - expect_assert_failure(animationGetLayerValue(&anim, 1)); -} - -static void test_animationUpdateAdvancesTimeAndCallsOnUpdate(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - animationUpdate(&anim, 0.5f); - - assert_float_equal(anim.time, 0.5f, 0.0001f); - assert_int_equal(cb.updateCount, 1); - assert_float_equal(cb.lastValues[0], 5.0f, 0.0001f); - assert_int_equal(cb.completeCount, 0); -} - -static void test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly( - void **state -) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - - // Overshoot the duration in a single update. - animationUpdate(&anim, 5.0f); - assert_float_equal(anim.time, 2.0f, 0.0001f); - assert_float_equal(cb.lastValues[0], 20.0f, 0.0001f); - assert_int_equal(cb.completeCount, 1); - - // Further updates must not clamp past the end or refire onComplete. - animationUpdate(&anim, 1.0f); - assert_float_equal(anim.time, 2.0f, 0.0001f); - assert_int_equal(cb.completeCount, 1); -} - -static void test_animationUpdateExactLandingOnEndFiresOnComplete(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - - // deltaTime exactly matches the remaining distance to the boundary. - animationUpdate(&anim, 2.0f); - assert_float_equal(anim.time, 2.0f, 0.0001f); - assert_int_equal(cb.completeCount, 1); -} - -static void test_animationUpdateLoopWrapsAndFiresOnLoop(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_LOOP; - - // 2.5 durations worth of time: wraps twice, lands at 1.0 into the third. - animationUpdate(&anim, 5.0f); - - assert_float_equal(anim.time, 1.0f, 0.0001f); - assert_int_equal(cb.loopCount, 2); - assert_int_equal(cb.completeCount, 0); -} - -static void test_animationUpdateLoopStopEndCompletesInsteadOfWrapping( - void **state -) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_STOP_END; - - animationUpdate(&anim, 5.0f); - - assert_float_equal(anim.time, 2.0f, 0.0001f); - assert_int_equal(cb.loopCount, 0); - assert_int_equal(cb.completeCount, 1); -} - -static void test_animationUpdateReversePlaysBackward(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_REVERSE; - anim.time = anim.duration; - - animationUpdate(&anim, 0.5f); - - assert_float_equal(anim.time, 1.5f, 0.0001f); - assert_float_equal(cb.lastValues[0], 15.0f, 0.0001f); -} - -static void test_animationUpdateReverseStopsAtBeginning(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_REVERSE; - anim.time = anim.duration; - - animationUpdate(&anim, 10.0f); - - assert_float_equal(anim.time, 0.0f, 0.0001f); - assert_float_equal(cb.lastValues[0], 0.0f, 0.0001f); - assert_int_equal(cb.completeCount, 1); -} - -static void test_animationUpdatePingpongBouncesForeverWithoutStopFlags( - void **state -) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_PINGPONG; - - // 0 -> 2 (2s consumed), reflect, backward 1s more -> time=1, going backward. - animationUpdate(&anim, 3.0f); - assert_float_equal(anim.time, 1.0f, 0.0001f); - assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0); - assert_int_equal(cb.completeCount, 0); - - // Continue backward to 0 (1s), reflect, forward 1s more -> time=1, forward. - animationUpdate(&anim, 2.0f); - assert_float_equal(anim.time, 1.0f, 0.0001f); - assert_true((anim.flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) == 0); - assert_int_equal(cb.completeCount, 0); -} - -static void test_animationUpdatePingpongStopEndCompletesAtEnd(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_END; - - animationUpdate(&anim, 10.0f); - - assert_float_equal(anim.time, 2.0f, 0.0001f); - assert_int_equal(cb.completeCount, 1); -} - -static void test_animationUpdatePingpongStopBeginningCompletesAtStart( - void **state -) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_PINGPONG | ANIMATION_FLAG_STOP_BEGINNING; - anim.time = anim.duration; - anim.flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD; - - animationUpdate(&anim, 10.0f); - - assert_float_equal(anim.time, 0.0f, 0.0001f); - assert_int_equal(cb.completeCount, 1); -} - -static void test_animationUpdateMultiLayerCallsOnUpdatePerLayer(void **state) { - keyframe_t keyframes[4] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[2] = { 2, 2 }; - animation_t anim; - animationtestcallbacks_t cb; - - animationTestInit(&anim, &cb, keyframes, keyframeCounts, 2); - animationUpdate(&anim, 1.0f); - - assert_int_equal(cb.updateCount, 2); - assert_float_equal(cb.lastValues[0], 10.0f, 0.0001f); - assert_float_equal(cb.lastValues[1], 50.0f, 0.0001f); -} - -static void test_animationUpdateNullAsserts(void **state) { - expect_assert_failure(animationUpdate(NULL, 1.0f)); -} - -static void test_animationUpdateNegativeDeltaTimeAsserts(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - - animationInit(&anim, keyframes, keyframeCounts, 1); - expect_assert_failure(animationUpdate(&anim, -1.0f)); -} - -static void test_animationInitZeroDurationAsserts(void **state) { - keyframe_t keyframes[1] = { - { .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 1 }; - animation_t anim; - - expect_assert_failure(animationInit(&anim, keyframes, keyframeCounts, 1)); -} - -static void test_animationUpdateLoopAndPingpongTogetherAsserts(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - uint16_t keyframeCounts[1] = { 2 }; - animation_t anim; - - animationInit(&anim, keyframes, keyframeCounts, 1); - anim.flags = ANIMATION_FLAG_LOOP | ANIMATION_FLAG_PINGPONG; - expect_assert_failure(animationUpdate(&anim, 0.1f)); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_animationInitSingleLayerDuration), - cmocka_unit_test(test_animationInitMultiLayerDurationIsMax), - cmocka_unit_test(test_animationInitNullAsserts), - cmocka_unit_test(test_animationInitZeroLayerCountAsserts), - cmocka_unit_test(test_animationInitUnsortedKeyframesAsserts), - cmocka_unit_test(test_animationGetLayerValueSamplesEachLayerIndependently), - cmocka_unit_test(test_animationGetLayerValueOutOfBoundsAsserts), - cmocka_unit_test(test_animationUpdateAdvancesTimeAndCallsOnUpdate), - cmocka_unit_test(test_animationUpdatePlainPlayStopsAtEndAndFiresOnceOnly), - cmocka_unit_test(test_animationUpdateExactLandingOnEndFiresOnComplete), - cmocka_unit_test(test_animationUpdateLoopWrapsAndFiresOnLoop), - cmocka_unit_test(test_animationUpdateLoopStopEndCompletesInsteadOfWrapping), - cmocka_unit_test(test_animationUpdateReversePlaysBackward), - cmocka_unit_test(test_animationUpdateReverseStopsAtBeginning), - cmocka_unit_test( - test_animationUpdatePingpongBouncesForeverWithoutStopFlags - ), - cmocka_unit_test(test_animationUpdatePingpongStopEndCompletesAtEnd), - cmocka_unit_test(test_animationUpdatePingpongStopBeginningCompletesAtStart), - cmocka_unit_test(test_animationUpdateMultiLayerCallsOnUpdatePerLayer), - cmocka_unit_test(test_animationUpdateNullAsserts), - cmocka_unit_test(test_animationUpdateNegativeDeltaTimeAsserts), - cmocka_unit_test(test_animationInitZeroDurationAsserts), - cmocka_unit_test(test_animationUpdateLoopAndPingpongTogetherAsserts), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/animation/test_easing.c b/test/animation/test_easing.c deleted file mode 100644 index 55e843f9..00000000 --- a/test/animation/test_easing.c +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "animation/easing.h" - -#define EASING_TOLERANCE 0.0001f - -static void test_easingLinear(void **state) { - assert_float_equal(easingLinear(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingLinear(0.25f), 0.25f, EASING_TOLERANCE); - assert_float_equal(easingLinear(0.5f), 0.5f, EASING_TOLERANCE); - assert_float_equal(easingLinear(0.75f), 0.75f, EASING_TOLERANCE); - assert_float_equal(easingLinear(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInSine(void **state) { - assert_float_equal(easingInSine(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInSine(0.25f), 0.076120f, EASING_TOLERANCE); - assert_float_equal(easingInSine(0.5f), 0.292893f, EASING_TOLERANCE); - assert_float_equal(easingInSine(0.75f), 0.617317f, EASING_TOLERANCE); - assert_float_equal(easingInSine(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingOutSine(void **state) { - assert_float_equal(easingOutSine(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingOutSine(0.25f), 0.382683f, EASING_TOLERANCE); - assert_float_equal(easingOutSine(0.5f), 0.707107f, EASING_TOLERANCE); - assert_float_equal(easingOutSine(0.75f), 0.923880f, EASING_TOLERANCE); - assert_float_equal(easingOutSine(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInOutSine(void **state) { - assert_float_equal(easingInOutSine(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInOutSine(0.25f), 0.146447f, EASING_TOLERANCE); - assert_float_equal(easingInOutSine(0.5f), 0.5f, EASING_TOLERANCE); - assert_float_equal(easingInOutSine(0.75f), 0.853553f, EASING_TOLERANCE); - assert_float_equal(easingInOutSine(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInQuad(void **state) { - assert_float_equal(easingInQuad(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInQuad(0.25f), 0.0625f, EASING_TOLERANCE); - assert_float_equal(easingInQuad(0.5f), 0.25f, EASING_TOLERANCE); - assert_float_equal(easingInQuad(0.75f), 0.5625f, EASING_TOLERANCE); - assert_float_equal(easingInQuad(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingOutQuad(void **state) { - assert_float_equal(easingOutQuad(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingOutQuad(0.25f), 0.4375f, EASING_TOLERANCE); - assert_float_equal(easingOutQuad(0.5f), 0.75f, EASING_TOLERANCE); - assert_float_equal(easingOutQuad(0.75f), 0.9375f, EASING_TOLERANCE); - assert_float_equal(easingOutQuad(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInOutQuad(void **state) { - assert_float_equal(easingInOutQuad(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuad(0.25f), 0.125f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuad(0.5f), 0.5f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuad(0.75f), 0.875f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuad(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInCubic(void **state) { - assert_float_equal(easingInCubic(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInCubic(0.25f), 0.015625f, EASING_TOLERANCE); - assert_float_equal(easingInCubic(0.5f), 0.125f, EASING_TOLERANCE); - assert_float_equal(easingInCubic(0.75f), 0.421875f, EASING_TOLERANCE); - assert_float_equal(easingInCubic(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingOutCubic(void **state) { - assert_float_equal(easingOutCubic(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingOutCubic(0.25f), 0.578125f, EASING_TOLERANCE); - assert_float_equal(easingOutCubic(0.5f), 0.875f, EASING_TOLERANCE); - assert_float_equal(easingOutCubic(0.75f), 0.984375f, EASING_TOLERANCE); - assert_float_equal(easingOutCubic(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInOutCubic(void **state) { - assert_float_equal(easingInOutCubic(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInOutCubic(0.25f), 0.0625f, EASING_TOLERANCE); - assert_float_equal(easingInOutCubic(0.5f), 0.5f, EASING_TOLERANCE); - assert_float_equal(easingInOutCubic(0.75f), 0.9375f, EASING_TOLERANCE); - assert_float_equal(easingInOutCubic(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInQuart(void **state) { - assert_float_equal(easingInQuart(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInQuart(0.25f), 0.003906f, EASING_TOLERANCE); - assert_float_equal(easingInQuart(0.5f), 0.0625f, EASING_TOLERANCE); - assert_float_equal(easingInQuart(0.75f), 0.316406f, EASING_TOLERANCE); - assert_float_equal(easingInQuart(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingOutQuart(void **state) { - assert_float_equal(easingOutQuart(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingOutQuart(0.25f), 0.683594f, EASING_TOLERANCE); - assert_float_equal(easingOutQuart(0.5f), 0.9375f, EASING_TOLERANCE); - assert_float_equal(easingOutQuart(0.75f), 0.996094f, EASING_TOLERANCE); - assert_float_equal(easingOutQuart(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInOutQuart(void **state) { - assert_float_equal(easingInOutQuart(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuart(0.25f), 0.03125f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuart(0.5f), 0.5f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuart(0.75f), 0.96875f, EASING_TOLERANCE); - assert_float_equal(easingInOutQuart(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInBack(void **state) { - // "In back" overshoots below 0 before rising to 1 - that's the point of it. - assert_float_equal(easingInBack(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInBack(0.25f), -0.064137f, EASING_TOLERANCE); - assert_float_equal(easingInBack(0.5f), -0.087698f, EASING_TOLERANCE); - assert_float_equal(easingInBack(0.75f), 0.182590f, EASING_TOLERANCE); - assert_float_equal(easingInBack(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingOutBack(void **state) { - // "Out back" overshoots above 1 before settling at 1. - assert_float_equal(easingOutBack(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingOutBack(0.25f), 0.817410f, EASING_TOLERANCE); - assert_float_equal(easingOutBack(0.5f), 1.087697f, EASING_TOLERANCE); - assert_float_equal(easingOutBack(0.75f), 1.064137f, EASING_TOLERANCE); - assert_float_equal(easingOutBack(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingInOutBack(void **state) { - assert_float_equal(easingInOutBack(0.0f), 0.0f, EASING_TOLERANCE); - assert_float_equal(easingInOutBack(0.25f), -0.099682f, EASING_TOLERANCE); - assert_float_equal(easingInOutBack(0.5f), 0.5f, EASING_TOLERANCE); - assert_float_equal(easingInOutBack(0.75f), 1.099682f, EASING_TOLERANCE); - assert_float_equal(easingInOutBack(1.0f), 1.0f, EASING_TOLERANCE); -} - -static void test_easingApplyDispatchesToEachType(void **state) { - assert_float_equal( - easingApply(EASING_LINEAR, 0.5f), easingLinear(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_SINE, 0.5f), easingInSine(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_OUT_SINE, 0.5f), easingOutSine(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_OUT_SINE, 0.5f), easingInOutSine(0.5f), - EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_QUAD, 0.5f), easingInQuad(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_OUT_QUAD, 0.5f), easingOutQuad(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_OUT_QUAD, 0.5f), easingInOutQuad(0.5f), - EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_CUBIC, 0.5f), easingInCubic(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_OUT_CUBIC, 0.5f), easingOutCubic(0.5f), - EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_OUT_CUBIC, 0.5f), easingInOutCubic(0.5f), - EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_QUART, 0.5f), easingInQuart(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_OUT_QUART, 0.5f), easingOutQuart(0.5f), - EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_OUT_QUART, 0.5f), easingInOutQuart(0.5f), - EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_BACK, 0.5f), easingInBack(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_OUT_BACK, 0.5f), easingOutBack(0.5f), EASING_TOLERANCE - ); - assert_float_equal( - easingApply(EASING_IN_OUT_BACK, 0.5f), easingInOutBack(0.5f), - EASING_TOLERANCE - ); -} - -static void test_easingApplyInvalidTypeAsserts(void **state) { - expect_assert_failure(easingApply(EASING_COUNT, 0.5f)); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_easingLinear), - cmocka_unit_test(test_easingInSine), - cmocka_unit_test(test_easingOutSine), - cmocka_unit_test(test_easingInOutSine), - cmocka_unit_test(test_easingInQuad), - cmocka_unit_test(test_easingOutQuad), - cmocka_unit_test(test_easingInOutQuad), - cmocka_unit_test(test_easingInCubic), - cmocka_unit_test(test_easingOutCubic), - cmocka_unit_test(test_easingInOutCubic), - cmocka_unit_test(test_easingInQuart), - cmocka_unit_test(test_easingOutQuart), - cmocka_unit_test(test_easingInOutQuart), - cmocka_unit_test(test_easingInBack), - cmocka_unit_test(test_easingOutBack), - cmocka_unit_test(test_easingInOutBack), - cmocka_unit_test(test_easingApplyDispatchesToEachType), - cmocka_unit_test(test_easingApplyInvalidTypeAsserts), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/animation/test_keyframe.c b/test/animation/test_keyframe.c deleted file mode 100644 index 5d6bf60d..00000000 --- a/test/animation/test_keyframe.c +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "animation/keyframe.h" - -static void test_keyframeGetValueSingleSegmentLinear(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - - assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 0.0f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 2, 0.25f), 2.5f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 5.0f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 2, 0.75f), 7.5f, 0.0001f); -} - -static void test_keyframeGetValueMultiSegment(void **state) { - keyframe_t keyframes[3] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 2.0f, .value = 20.0f, .easing = EASING_LINEAR }, - }; - - // Within the first segment. - assert_float_equal(keyframeGetValue(keyframes, 3, 0.5f), 5.0f, 0.0001f); - - // Exactly on the interior keyframe, resolved as the end of segment one. - assert_float_equal(keyframeGetValue(keyframes, 3, 1.0f), 10.0f, 0.0001f); - - // Within the second segment. - assert_float_equal(keyframeGetValue(keyframes, 3, 1.5f), 15.0f, 0.0001f); -} - -static void test_keyframeGetValueDescendingValues(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 100.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - - assert_float_equal(keyframeGetValue(keyframes, 2, 0.0f), 100.0f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 50.0f, 0.0001f); -} - -static void test_keyframeGetValueAppliesEasing(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_IN_QUAD }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - - // EASING_IN_QUAD is t * t, so halfway through time should be a quarter of - // the way through the value range, not half. - assert_float_equal(keyframeGetValue(keyframes, 2, 0.5f), 2.5f, 0.0001f); -} - -static void test_keyframeGetValueNonZeroStartTime(void **state) { - keyframe_t keyframes[2] = { - { .time = 5.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 10.0f, .value = 100.0f, .easing = EASING_LINEAR }, - }; - - assert_float_equal(keyframeGetValue(keyframes, 2, 5.0f), 0.0f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 2, 7.5f), 50.0f, 0.0001f); -} - -// NOTE: keyframeGetValue does not clamp to the first keyframe's value when -// queried before the first keyframe's time - the start and end pointers -// collapse onto the same keyframe, so the interpolation divides by a zero -// time delta. This test documents that current behavior rather than -// asserting it is desirable; flag to the maintainer if this should instead -// clamp like the last-keyframe case does. -static void test_keyframeGetValueBeforeFirstKeyframeIsNaN(void **state) { - keyframe_t keyframes[2] = { - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 3.0f, .value = 30.0f, .easing = EASING_LINEAR }, - }; - - assert_true(isnan(keyframeGetValue(keyframes, 2, 0.0f))); -} - -static void test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue( - void **state -) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - - assert_float_equal(keyframeGetValue(keyframes, 2, 1.0f), 10.0f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 2, 2.0f), 10.0f, 0.0001f); -} - -static void test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue( - void **state -) { - keyframe_t keyframes[1] = { - { .time = 5.0f, .value = 42.0f, .easing = EASING_LINEAR }, - }; - - assert_float_equal(keyframeGetValue(keyframes, 1, 5.0f), 42.0f, 0.0001f); - assert_float_equal(keyframeGetValue(keyframes, 1, 10.0f), 42.0f, 0.0001f); - - // Before the single keyframe's time is still the documented NaN case. - assert_true(isnan(keyframeGetValue(keyframes, 1, 0.0f))); -} - -static void test_keyframeGetValueNullKeyframesAsserts(void **state) { - expect_assert_failure(keyframeGetValue(NULL, 1, 0.0f)); -} - -static void test_keyframeGetValueZeroCountAsserts(void **state) { - keyframe_t keyframes[1] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - - expect_assert_failure(keyframeGetValue(keyframes, 0, 0.0f)); -} - -static void test_keyframeGetValueNegativeTimeAsserts(void **state) { - keyframe_t keyframes[2] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - - expect_assert_failure(keyframeGetValue(keyframes, 2, -1.0f)); -} - -static void test_keyframeGetValueUnsortedKeyframesAsserts(void **state) { - keyframe_t keyframes[2] = { - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - }; - - expect_assert_failure(keyframeGetValue(keyframes, 2, 0.0f)); -} - -static void test_keyframeGetValueLaterKeyframeOutOfOrderAsserts(void **state) { - // The first pair is sorted, but the third keyframe is out of order - the - // check must walk the whole array, not just the first pair. - keyframe_t keyframes[3] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - { .time = 0.5f, .value = 5.0f, .easing = EASING_LINEAR }, - }; - - expect_assert_failure(keyframeGetValue(keyframes, 3, 0.0f)); -} - -static void test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert( - void **state -) { - // Equal (non-decreasing) times are allowed - only strictly decreasing - // times should trigger the sorted check. - keyframe_t keyframes[3] = { - { .time = 0.0f, .value = 0.0f, .easing = EASING_LINEAR }, - { .time = 0.0f, .value = 5.0f, .easing = EASING_LINEAR }, - { .time = 1.0f, .value = 10.0f, .easing = EASING_LINEAR }, - }; - - assert_float_equal(keyframeGetValue(keyframes, 3, 0.0f), 5.0f, 0.0001f); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_keyframeGetValueSingleSegmentLinear), - cmocka_unit_test(test_keyframeGetValueMultiSegment), - cmocka_unit_test(test_keyframeGetValueDescendingValues), - cmocka_unit_test(test_keyframeGetValueAppliesEasing), - cmocka_unit_test(test_keyframeGetValueNonZeroStartTime), - cmocka_unit_test(test_keyframeGetValueBeforeFirstKeyframeIsNaN), - cmocka_unit_test( - test_keyframeGetValueAtOrAfterLastKeyframeClampsToLastValue - ), - cmocka_unit_test(test_keyframeGetValueSingleKeyframeAtOrAfterClampsToValue), - cmocka_unit_test(test_keyframeGetValueNullKeyframesAsserts), - cmocka_unit_test(test_keyframeGetValueZeroCountAsserts), - cmocka_unit_test(test_keyframeGetValueNegativeTimeAsserts), - cmocka_unit_test(test_keyframeGetValueUnsortedKeyframesAsserts), - cmocka_unit_test(test_keyframeGetValueLaterKeyframeOutOfOrderAsserts), - cmocka_unit_test(test_keyframeGetValueEqualConsecutiveTimesDoesNotAssert), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/assert/CMakeLists.txt b/test/assert/CMakeLists.txt deleted file mode 100644 index 1afa80e5..00000000 --- a/test/assert/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_assert.c) \ No newline at end of file diff --git a/test/assert/test_assert.c b/test/assert/test_assert.c deleted file mode 100644 index e99d366c..00000000 --- a/test/assert/test_assert.c +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "assert/assert.h" - -static void test_assertTrueImpl(void **state) { - // Should not assert when true false statement - assertTrueImpl(__FILE__, __LINE__, true, "This should not assert"); - - // Should assert when passing false statement - expect_assert_failure(assertTrueImpl(__FILE__, __LINE__, false, "asserts")); -} - -static void test_assertFalseImpl(void **state) { - // Should not assert when passing false statement - assertFalseImpl(__FILE__, __LINE__, false, "This should not assert"); - - // Should assert when passing true statement - expect_assert_failure(assertFalseImpl(__FILE__, __LINE__, true, "asserts")); -} - - -static void test_assertUnreachableImpl(void **state) { - // Should always assert false - expect_assert_failure(assertUnreachableImpl(__FILE__, __LINE__, "asserts")); -} - -static void test_assertNotNullImpl(void **state) { - int someValue = 42; - - // Should not assert with a valid pointer - assertNotNullImpl(__FILE__, __LINE__, &someValue, "not assert"); - - // Should assert with a NULL pointer - expect_assert_failure(assertNotNullImpl(__FILE__, __LINE__, NULL, "asserts")); -} - -static void test_assertDeprecatedImpl(void **state) { - // Always asserts when called - expect_assert_failure(assertDeprecatedImpl(__FILE__, __LINE__, "deprecated")); -} - -static void test_assertTrue(void **state) { - // Should not assert when passing true statement - assertTrue(true, "This should not assert"); - - // Should assert when passing false statement - expect_assert_failure(assertTrue(false, "asserts")); -} - -static void test_assertFalse(void **state) { - // Should not assert when passing true statement - assertFalse(false, "This should not assert"); - - // Should assert when passing false statement - expect_assert_failure(assertFalse(true, "asserts")); -} - -static void test_assertUnreachable(void **state) { - // Should always assert false - expect_assert_failure(assertUnreachable("asserts")); -} - -static void test_assertNotNull(void **state) { - int someValue = 42; - - // Should not assert with a valid pointer - assertNotNull(&someValue, "not assert"); - - // Should assert with a NULL pointer - expect_assert_failure(assertNotNull(NULL, "asserts")); -} - -static void test_assertNull(void **state) { - int someValue = 42; - - // Should assert with a valid pointer - expect_assert_failure(assertNull(&someValue, "asserts")); - - // Should not assert with a NULL pointer - assertNull(NULL, "not assert"); -} - -static void test_assertDeprecated(void **state) { - // Always asserts when called - expect_assert_failure(assertDeprecated("deprecated")); -} - -static void test_assertStrLenMax(void **state) { - // Should not assert when string length is within limit - assertStrLenMax("test", 10, "This should not assert"); - - // Should assert when string length exceeds limit - expect_assert_failure(assertStrLenMax("exceeding", 5, "asserts")); -} - -static void test_assertStrLenMin(void **state) { - // Should not assert when string length meets minimum - assertStrLenMin("sufficient", 5, "This should not assert"); - - // Should assert when string length is below minimum - expect_assert_failure(assertStrLenMin("tiny", 10, "asserts")); -} - -static void test_assertIsMainThread(void **state) { - // The group setup recorded this thread as main - assertIsMainThread - // must pass. - assertIsMainThread("test thread is main, should not assert"); - - // assertNotMainThread must fail when called from the main thread. - expect_assert_failure(assertNotMainThread("asserts")); -} - -static void test_assertNotMainThread(void **state) { - // assertNotMainThread must fail because we are still on the main thread. - expect_assert_failure(assertNotMainThread("asserts")); - - // assertIsMainThread must pass. - assertIsMainThread("test thread is main, should not assert"); -} - -static int groupSetup(void **state) { - assertInit(); - return 0; -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_assertTrueImpl), - cmocka_unit_test(test_assertFalseImpl), - cmocka_unit_test(test_assertUnreachableImpl), - cmocka_unit_test(test_assertNotNullImpl), - cmocka_unit_test(test_assertDeprecatedImpl), - cmocka_unit_test(test_assertTrue), - cmocka_unit_test(test_assertFalse), - cmocka_unit_test(test_assertUnreachable), - cmocka_unit_test(test_assertNotNull), - cmocka_unit_test(test_assertNull), - cmocka_unit_test(test_assertDeprecated), - cmocka_unit_test(test_assertStrLenMax), - cmocka_unit_test(test_assertStrLenMin), - cmocka_unit_test(test_assertIsMainThread), - cmocka_unit_test(test_assertNotMainThread), - }; - - return cmocka_run_group_tests(tests, groupSetup, NULL); -} \ No newline at end of file diff --git a/test/asset/CMakeLists.txt b/test/asset/CMakeLists.txt deleted file mode 100644 index f0f5c424..00000000 --- a/test/asset/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -dusktest(test_assetlocale.c) -dusktest(test_asset.c) -dusktest(test_assetjsonloader.c) -dusktest(test_assettilesetloader.c) diff --git a/test/asset/test_asset.c b/test/asset/test_asset.c deleted file mode 100644 index 9120ba9b..00000000 --- a/test/asset/test_asset.c +++ /dev/null @@ -1,692 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "asset/loader/assetentry.h" -#include "util/memory.h" -#include "util/string.h" - -// ============================================================ -// Stub loader callbacks -// ============================================================ - -static errorret_t stub_load_success(assetloading_t *loading) { - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -static errorret_t stub_load_fail(assetloading_t *loading) { - loading->entry->state = ASSET_ENTRY_STATE_ERROR; - errorThrow("Stub loader failed"); -} - -static errorret_t stub_dispose(assetentry_t *entry) { - errorOk(); -} - -// ============================================================ -// Per-test setup / teardown -// ============================================================ - -static assetloadercallbacks_t saved_callbacks[ASSET_LOADER_TYPE_COUNT]; - -static int asset_setup(void **state) { - // Save real callbacks so we can restore them in teardown. - memoryCopy(saved_callbacks, ASSET_LOADER_CALLBACKS, sizeof(saved_callbacks)); - - // Manually init ASSET - no thread, no ZIP. - memoryZero(&ASSET, sizeof(ASSET)); - for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - threadMutexInit(&ASSET.loading[i].mutex); - } - - // Replace all loader callbacks with stubs. - for(int i = 0; i < ASSET_LOADER_TYPE_COUNT; i++) { - ASSET_LOADER_CALLBACKS[i].loadSync = stub_load_success; - ASSET_LOADER_CALLBACKS[i].dispose = stub_dispose; - } - - return 0; -} - -static int asset_teardown(void **state) { - // Dispose any entries that tests left behind. - for(int i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { - if(ASSET.entries[i].type != ASSET_LOADER_TYPE_NULL) { - errorret_t ret = assetEntryDispose(&ASSET.entries[i]); - if(errorIsNotOk(ret)) errorCatch(ret); - } - } - - for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - threadMutexDispose(&ASSET.loading[i].mutex); - } - - // Restore real callbacks before zeroing state. - memoryCopy(ASSET_LOADER_CALLBACKS, saved_callbacks, sizeof(saved_callbacks)); - memoryZero(&ASSET, sizeof(ASSET)); - return 0; -} - -// ============================================================ -// Helper: find which loading slot owns a given entry -// ============================================================ - -static bool_t loading_slot_has_entry(const assetentry_t *entry) { - for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - if(ASSET.loading[i].entry == entry) return true; - } - return false; -} - -// ============================================================ -// assetGetEntry tests -// ============================================================ - -static void test_getEntry_creates_new(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assert_non_null(entry); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED); - assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE); - assert_true(stringEquals(entry->name, "test.locale")); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_getEntry_dedup(void **state) { - assetentry_t *a = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assetentry_t *b = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assert_ptr_equal(a, b); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_getEntry_distinct_names(void **state) { - assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assetentry_t *b = assetGetEntry("b.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assert_ptr_not_equal(a, b); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetUpdate - state machine tests -// ============================================================ - -static void test_update_entry_reaches_loaded(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED); - - assetEntryLock(entry); - errorret_t ret = assetUpdate(); - assert_true(errorIsOk(ret)); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_slot_cleared_after_first_update(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - - // A synchronous load clears its own loading slot in the same update call - // that completes it, so no second update is needed to free it. - assert_false(loading_slot_has_entry(entry)); - assetEntryUnlock(entry); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_slot_stays_cleared_on_further_updates(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); - - assert_false(loading_slot_has_entry(entry)); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_four_slots_fill_independently(void **state) { - // ASSET_LOADING_COUNT_MAX concurrent entries should all load in one pass. - assetentry_t *entries[ASSET_LOADING_COUNT_MAX]; - for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - char_t name[ASSET_FILE_NAME_MAX]; - snprintf(name, sizeof(name), "asset%d.locale", i); - entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL); - assetEntryLock(entries[i]); - } - - errorret_t ret = assetUpdate(); - assert_true(errorIsOk(ret)); - - for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - assert_int_equal(entries[i]->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entries[i]); - } - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_error_state(void **state) { - ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail; - - assetentry_t *entry = - assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - // First update: dispatches and calls the failing stub. - // assetUpdate itself returns OK here; the error from loadSync is - // caught internally. - errorret_t ret = assetUpdate(); - assert_true(errorIsOk(ret)); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR); - - // Second update: sees ERROR state and propagates it. - ret = assetUpdate(); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_noop_on_empty_table(void **state) { - errorret_t ret = assetUpdate(); - assert_true(errorIsOk(ret)); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_loaded_entry_not_redispatched(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); // slot freed - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - - // Further updates must not re-dispatch or modify a LOADED entry. - assetUpdate(); - assetUpdate(); - assetUpdate(); - - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assert_false(loading_slot_has_entry(entry)); - assetEntryUnlock(entry); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_overflow_queues_entries(void **state) { - // Create one more entry than there are loading slots. - const int TOTAL = ASSET_LOADING_COUNT_MAX + 1; - assetentry_t *entries[ASSET_LOADING_COUNT_MAX + 1]; - - for(int i = 0; i < TOTAL; i++) { - char_t name[ASSET_FILE_NAME_MAX]; - snprintf(name, sizeof(name), "asset%d.locale", i); - entries[i] = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL); - } - - // Lock every entry, including the overflow one, so the reaper won't - // collect any of them - a synchronous load can reach LOADED and free its - // slot within the same update call the reaper runs in. - for(int i = 0; i < TOTAL; i++) { - assetEntryLock(entries[i]); - } - - // Update 1: fills all slots, first ASSET_LOADING_COUNT_MAX entries reach - // LOADED and immediately free their slots. The overflow entry has no slot - // yet because dispatch ran before this update's turn began, so it stays - // NOT_STARTED. - errorret_t ret = assetUpdate(); - assert_true(errorIsOk(ret)); - for(int i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - assert_int_equal(entries[i]->state, ASSET_ENTRY_STATE_LOADED); - } - assert_int_equal( - entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_NOT_STARTED - ); - - // Update 2: the slots freed during update 1 are now available, so the - // overflow entry is dispatched and loaded in this pass. - ret = assetUpdate(); - assert_true(errorIsOk(ret)); - assert_int_equal( - entries[ASSET_LOADING_COUNT_MAX]->state, ASSET_ENTRY_STATE_LOADED - ); - - for(int i = 0; i < TOTAL; i++) { - assetEntryUnlock(entries[i]); - } - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_update_error_slot_stays_occupied(void **state) { - ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail; - - assetentry_t *entry = - assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetUpdate(); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR); - - // Unlike LOADED, the ERROR case does NOT clear the slot - it throws - // instead. - assert_true(loading_slot_has_entry(entry)); - - errorret_t ret = assetUpdate(); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetEntryInit - input copy -// ============================================================ - -static void test_getEntry_null_input_stays_null(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assert_null(entry->input); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_getEntry_input_copied_into_entry(void **state) { - assetloaderinput_t input; - memoryZero(&input, sizeof(input)); - input.texture = (textureformat_t)42; - - assetentry_t *entry = - assetGetEntry("test.texture", ASSET_LOADER_TYPE_TEXTURE, &input); - - // input must have been copied - entry->input must point inside the entry. - assert_non_null(entry->input); - assert_ptr_equal(entry->input, &entry->inputData); - assert_int_equal((int)entry->input->texture, 42); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetUpdate - re-entrant sync loader -// ============================================================ - -static assetentry_t *reentrant_inner_entry = NULL; -static bool_t reentrant_inner_loaded = false; - -static errorret_t reentrant_stub_load(assetloading_t *loading) { - if(reentrant_inner_entry == NULL) { - // Simulate a script loading another asset from within its own sync step. - reentrant_inner_entry = assetGetEntry( - "inner.json", ASSET_LOADER_TYPE_JSON, NULL - ); - errorret_t inner_ret = assetRequireLoaded(reentrant_inner_entry); - reentrant_inner_loaded = errorIsOk(inner_ret); - if(errorIsNotOk(inner_ret)) errorChain(inner_ret); - } - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -static void test_update_reentrant_sync_loader(void **state) { - reentrant_inner_entry = NULL; - reentrant_inner_loaded = false; - // LOCALE uses the re-entrant loader; JSON keeps the default - // stub_load_success. - ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = - reentrant_stub_load; - - assetentry_t *outer = - assetGetEntry("outer.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - errorret_t ret = assetRequireLoaded(outer); - assert_true(errorIsOk(ret)); - assert_int_equal(outer->state, ASSET_ENTRY_STATE_LOADED); - // Verify the re-entrant load ran and completed successfully. - // (The inner entry may have been reaped by the time we check here, - // so we capture the result inside the callback rather than reading state.) - assert_non_null(reentrant_inner_entry); - assert_true(reentrant_inner_loaded); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetGetEntry - dedup against non-NOT_STARTED entries -// ============================================================ - -static void test_getEntry_returns_loaded_entry(void **state) { - assetentry_t *a = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(a); - assetUpdate(); - assert_int_equal(a->state, ASSET_ENTRY_STATE_LOADED); - - // A second request for the same name must return the same entry even though - // it is already LOADED rather than NOT_STARTED. - assetentry_t *b = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assert_ptr_equal(a, b); - assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(a); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetEntryDispose tests -// ============================================================ - -static void test_entry_dispose_clears_entry(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); // ensure loading slot is freed before disposing - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - - errorret_t ret = assetEntryDispose(entry); - assert_true(errorIsOk(ret)); - assert_int_equal(entry->type, ASSET_LOADER_TYPE_NULL); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_entry_dispose_slot_reusable(void **state) { - assetentry_t *a = assetGetEntry("a.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(a); - assetUpdate(); - assetUpdate(); - assetEntryUnlock(a); - assert_false(loading_slot_has_entry(a)); - - assetEntryDispose(a); - assert_int_equal(a->type, ASSET_LOADER_TYPE_NULL); - - // The freed slot should now accept a new entry. - assetentry_t *b = assetGetEntry("b.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assert_non_null(b); - assert_int_equal(b->state, ASSET_ENTRY_STATE_NOT_STARTED); - - assetEntryLock(b); - errorret_t ret = assetUpdate(); - assert_true(errorIsOk(ret)); - assert_int_equal(b->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(b); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetRequireLoaded tests -// ============================================================ - -static void test_requireLoaded_already_loaded(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - - // Should return immediately without calling assetUpdate again. - errorret_t ret = assetRequireLoaded(entry); - assert_true(errorIsOk(ret)); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_requireLoaded_spins_to_loaded(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_NOT_STARTED); - - // requireLoaded calls assetUpdate internally until LOADED. - errorret_t ret = assetRequireLoaded(entry); - assert_true(errorIsOk(ret)); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_requireLoaded_propagates_error(void **state) { - ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_LOCALE].loadSync = stub_load_fail; - - assetentry_t *entry = - assetGetEntry("fail.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - // requireLoaded spins assetUpdate until LOADED - but the loader - // always fails, so the second assetUpdate sees ERROR and throws, which - // errorChain propagates. - errorret_t ret = assetRequireLoaded(entry); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - assert_int_equal(entry->state, ASSET_ENTRY_STATE_ERROR); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetReapUnused tests -// ============================================================ - -static void test_update_does_not_reap_automatically(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); // slot freed - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - - // Unlike the old behavior, assetUpdate no longer reaps zero-ref entries on - // its own - a LOADED entry must survive further updates untouched. - assetUpdate(); - assetUpdate(); - assetUpdate(); - - assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - - errorret_t ret = assetEntryDispose(entry); - assert_true(errorIsOk(ret)); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_reapUnused_disposes_zero_ref_loaded(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - - errorret_t ret = assetReapUnused(); - assert_true(errorIsOk(ret)); - assert_int_equal(entry->type, ASSET_LOADER_TYPE_NULL); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_reapUnused_ignores_referenced_entries(void **state) { - assetentry_t *entry = - assetGetEntry("test.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - - // Still locked - a reap must leave it alone. - errorret_t ret = assetReapUnused(); - assert_true(errorIsOk(ret)); - assert_int_equal(entry->type, ASSET_LOADER_TYPE_LOCALE); - - assetEntryUnlock(entry); - errorret_t disposeRet = assetEntryDispose(entry); - assert_true(errorIsOk(disposeRet)); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_getEntry_reaps_when_pool_full(void **state) { - // Fill every entry slot with a zero-ref LOADED entry. - for(int i = 0; i < ASSET_ENTRY_COUNT_MAX; i++) { - char_t name[ASSET_FILE_NAME_MAX]; - snprintf(name, sizeof(name), "full%d.locale", i); - assetentry_t *entry = assetGetEntry(name, ASSET_LOADER_TYPE_LOCALE, NULL); - assetEntryLock(entry); - assetUpdate(); - assetUpdate(); - assert_int_equal(entry->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(entry); - } - - // The pool is now completely full of zero-ref LOADED entries with no - // ASSET_LOADER_TYPE_NULL slots left. Requesting one more must trigger an - // implicit reap instead of asserting. - assetentry_t *fresh = - assetGetEntry("fresh.locale", ASSET_LOADER_TYPE_LOCALE, NULL); - assert_non_null(fresh); - assert_int_equal(fresh->state, ASSET_ENTRY_STATE_NOT_STARTED); - - assetEntryLock(fresh); - assetUpdate(); - assetUpdate(); - assert_int_equal(fresh->state, ASSET_ENTRY_STATE_LOADED); - assetEntryUnlock(fresh); - - errorret_t ret = assetEntryDispose(fresh); - assert_true(errorIsOk(ret)); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// main -// ============================================================ - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - // getEntry - cmocka_unit_test_setup_teardown( - test_getEntry_creates_new, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_getEntry_dedup, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_getEntry_distinct_names, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_getEntry_returns_loaded_entry, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_getEntry_null_input_stays_null, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_getEntry_input_copied_into_entry, asset_setup, asset_teardown - ), - - // assetUpdate - state machine - cmocka_unit_test_setup_teardown( - test_update_noop_on_empty_table, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_entry_reaches_loaded, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_slot_cleared_after_first_update, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_slot_stays_cleared_on_further_updates, - asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_four_slots_fill_independently, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_loaded_entry_not_redispatched, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_overflow_queues_entries, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_error_state, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_error_slot_stays_occupied, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_update_reentrant_sync_loader, asset_setup, asset_teardown - ), - - // assetEntryDispose - cmocka_unit_test_setup_teardown( - test_entry_dispose_clears_entry, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_entry_dispose_slot_reusable, asset_setup, asset_teardown - ), - - // assetRequireLoaded - cmocka_unit_test_setup_teardown( - test_requireLoaded_already_loaded, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_requireLoaded_spins_to_loaded, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_requireLoaded_propagates_error, asset_setup, asset_teardown - ), - - // assetReapUnused - cmocka_unit_test_setup_teardown( - test_update_does_not_reap_automatically, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_reapUnused_disposes_zero_ref_loaded, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_reapUnused_ignores_referenced_entries, asset_setup, asset_teardown - ), - cmocka_unit_test_setup_teardown( - test_getEntry_reaps_when_pool_full, asset_setup, asset_teardown - ), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/asset/test_assetjsonloader.c b/test/asset/test_assetjsonloader.c deleted file mode 100644 index d8439aab..00000000 --- a/test/asset/test_assetjsonloader.c +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "asset/loader/assetentry.h" -#include "asset/loader/json/assetjsonloader.h" -#include "thread/thread.h" -#include "util/memory.h" -#include - -// ============================================================ -// Fixtures -// ============================================================ - -static const char_t *JSON_VALID = "{\"hello\":\"world\",\"count\":42}"; -static const char_t *JSON_INVALID = "{ this is definitely not valid json !!!"; - -// ============================================================ -// Async thread helper -// ============================================================ - -typedef struct { - assetloading_t *loading; - bool_t ok; -} json_async_run_t; - -static void json_async_thread_cb(thread_t *thread) { - json_async_run_t *run = (json_async_run_t *)thread->data; - errorret_t ret = assetJsonLoaderAsync(run->loading); - run->ok = errorIsOk(ret); - if(errorIsNotOk(ret)) errorCatch(ret); -} - -static bool_t run_json_async(assetloading_t *loading) { - json_async_run_t run = { .loading = loading, .ok = false }; - thread_t thread; - threadInit(&thread, json_async_thread_cb); - thread.data = &run; - threadStart(&thread); - threadStop(&thread); - return run.ok; -} - -// ============================================================ -// In-memory ZIP -// ============================================================ - -static zip_t *g_zip = NULL; - -static int zip_setup(void **state) { - zip_error_t err; - zip_error_init(&err); - - zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err); - if(!write_src) return -1; - - zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err); - if(!za) { zip_source_free(write_src); return -1; } - - // valid.json - zip_source_t *s; - s = zip_source_buffer(za, JSON_VALID, strlen(JSON_VALID), 0); - if(zip_file_add(za, "valid.json", s, ZIP_FL_OVERWRITE) < 0) { - zip_close(za); return -1; - } - - // invalid.json - s = zip_source_buffer(za, JSON_INVALID, strlen(JSON_INVALID), 0); - if(zip_file_add(za, "invalid.json", s, ZIP_FL_OVERWRITE) < 0) { - zip_close(za); return -1; - } - - zip_source_keep(write_src); - if(zip_close(za) != 0) { zip_source_free(write_src); return -1; } - - zip_stat_t zs; - memset(&zs, 0, sizeof(zs)); - if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) { - zip_source_free(write_src); return -1; - } - - void *zipbuf = malloc((size_t)zs.size); - if(!zipbuf) { zip_source_free(write_src); return -1; } - - if(zip_source_open(write_src) != 0) { - free(zipbuf); zip_source_free(write_src); return -1; - } - zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size); - zip_source_close(write_src); - zip_source_free(write_src); - - zip_error_init(&err); - zip_source_t *read_src = zip_source_buffer_create( - zipbuf, (zip_uint64_t)zs.size, 1, &err - ); - if(!read_src) { free(zipbuf); return -1; } - - g_zip = zip_open_from_source(read_src, 0, &err); - if(!g_zip) { zip_source_free(read_src); return -1; } - - ASSET.zip = g_zip; - return 0; -} - -static int zip_teardown(void **state) { - if(g_zip) { zip_close(g_zip); g_zip = NULL; } - ASSET.zip = NULL; - return 0; -} - -// ============================================================ -// Loader pipeline helper -// ============================================================ - -typedef struct { - assetentry_t entry; - assetloading_t loading; -} loader_ctx_t; - -static void loader_ctx_init(loader_ctx_t *ctx, const char_t *name) { - assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_JSON, NULL); - threadMutexInit(&ctx->loading.mutex); - memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading)); - ctx->loading.type = ASSET_LOADER_TYPE_JSON; - ctx->loading.entry = &ctx->entry; - ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC; -} - -// Drives sync(INITIAL) -> async(READ) -> sync(PARSE). -static errorret_t loader_ctx_run(loader_ctx_t *ctx) { - errorret_t ret = assetJsonLoaderSync(&ctx->loading); - if(errorIsNotOk(ret)) return ret; - - if(!run_json_async(&ctx->loading)) { - ctx->entry.state = ASSET_ENTRY_STATE_ERROR; - errorThrow("Async JSON load failed"); - } - - return assetJsonLoaderSync(&ctx->loading); -} - -static void loader_ctx_dispose(loader_ctx_t *ctx) { - if(ctx->entry.type != ASSET_LOADER_TYPE_NULL) { - errorret_t ret = assetEntryDispose(&ctx->entry); - if(errorIsNotOk(ret)) errorCatch(ret); - } - threadMutexDispose(&ctx->loading.mutex); -} - -// ============================================================ -// Tests -// ============================================================ - -static void test_json_valid_loads(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "valid.json"); - - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsOk(ret)); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_LOADED); - assert_non_null(ctx.entry.data.json); - - // Verify content is accessible - yyjson_val *root = yyjson_doc_get_root(ctx.entry.data.json); - assert_non_null(root); - assert_true(yyjson_is_obj(root)); - - yyjson_val *hello = yyjson_obj_get(root, "hello"); - assert_non_null(hello); - assert_string_equal(yyjson_get_str(hello), "world"); - - yyjson_val *count = yyjson_obj_get(root, "count"); - assert_non_null(count); - assert_int_equal((int)yyjson_get_int(count), 42); - - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_json_parse_error(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "invalid.json"); - - // Async read succeeds; sync parse fails because yyjson rejects the content. - errorret_t ret = assetJsonLoaderSync(&ctx.loading); - assert_true(errorIsOk(ret)); - - assert_true(run_json_async(&ctx.loading)); - - ret = assetJsonLoaderSync(&ctx.loading); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - assert_null(ctx.entry.data.json); - - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_json_missing_file(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "nonexistent.json"); - - errorret_t ret = assetJsonLoaderSync(&ctx.loading); - assert_true(errorIsOk(ret)); - - // Async phase stat-fails because the file isn't in the ZIP. - assert_false(run_json_async(&ctx.loading)); - - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_json_buffer_cleared_after_load(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "valid.json"); - - loader_ctx_run(&ctx); - - // The scratch buffer must be freed by the time sync returns LOADED. - assert_null(ctx.loading.loading.json.buffer); - - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// main -// ============================================================ - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test_setup_teardown( - test_json_valid_loads, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_json_parse_error, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_json_missing_file, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_json_buffer_cleared_after_load, zip_setup, zip_teardown - ), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/asset/test_assetlocale.c b/test/asset/test_assetlocale.c deleted file mode 100644 index 8bf2f237..00000000 --- a/test/asset/test_assetlocale.c +++ /dev/null @@ -1,739 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "asset/loader/locale/assetlocaleloader.h" -#include "asset/asset.h" -#include "util/memory.h" -#include "util/string.h" -#include - -// ============================================================ -// Test locale file (gettext PO format) -// ============================================================ - -static const char_t *LOCALE_EN = - "msgid \"\"\n" - "msgstr \"\"\n" - "\"Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\\n\"\n" - "\n" - "msgid \"greeting\"\n" - "msgstr \"Hello, World!\"\n" - "\n" - "msgid \"item\"\n" - "msgid_plural \"items\"\n" - "msgstr[0] \"one item\"\n" - "msgstr[1] \"many items\"\n" - "\n" - "msgid \"score\"\n" - "msgstr \"Score: %d\"\n" - "\n" - "msgid \"player\"\n" - "msgstr \"Player: %s\"\n"; - -// ============================================================ -// In-memory ZIP fixture (shared across all ZIP-based tests) -// ============================================================ - -static zip_t *g_zip = NULL; -static assetlocalefile_t g_locale; - -static int locale_setup(void **state) { - zip_error_t err; - zip_error_init(&err); - - // Phase 1: write the zip to a growable buffer source - zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err); - if(!write_src) return -1; - - zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err); - if(!za) { zip_source_free(write_src); return -1; } - - size_t flen = strlen(LOCALE_EN); - zip_source_t *fs = zip_source_buffer(za, LOCALE_EN, flen, 0); - if(zip_file_add(za, "en.locale", fs, ZIP_FL_OVERWRITE) < 0) { - zip_close(za); return -1; - } - - // Keep write_src alive after zip_close so we can read the bytes back out - zip_source_keep(write_src); - if(zip_close(za) != 0) { zip_source_free(write_src); return -1; } - - // Phase 2: extract the raw zip bytes from the write buffer. - // zip_source_stat must be called before zip_source_open on a written source. - zip_stat_t zs; - memset(&zs, 0, sizeof(zs)); - if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) { - zip_source_free(write_src); return -1; - } - - void *zipbuf = malloc((size_t)zs.size); - if(!zipbuf) { zip_source_free(write_src); return -1; } - - if(zip_source_open(write_src) != 0) { - free(zipbuf); zip_source_free(write_src); return -1; - } - zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size); - zip_source_close(write_src); - zip_source_free(write_src); - - // Phase 3: open a fresh read-only archive from the extracted bytes. - // The archive takes ownership of the source (and thus zipbuf via freep=1). - zip_error_init(&err); - zip_source_t *read_src = zip_source_buffer_create( - zipbuf, (zip_uint64_t)zs.size, 1, &err - ); - if(!read_src) { free(zipbuf); return -1; } - - g_zip = zip_open_from_source(read_src, 0, &err); - if(!g_zip) { zip_source_free(read_src); return -1; } - - ASSET.zip = g_zip; - - // Init locale file and parse the header - memoryZero(&g_locale, sizeof(g_locale)); - errorret_t ret = assetFileInit(&g_locale.file, "en.locale", NULL, NULL); - if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; } - - ret = assetFileOpen(&g_locale.file); - if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; } - - char_t header[512]; - ret = assetLocaleGetString(&g_locale, "", 0, header, sizeof(header)); - if(errorIsNotOk(ret)) { - errorCatch(ret); assetFileClose(&g_locale.file); goto fail; - } - - ret = assetLocaleParseHeader(&g_locale, header, sizeof(header)); - if(errorIsNotOk(ret)) { - errorCatch(ret); assetFileClose(&g_locale.file); goto fail; - } - - return 0; - -fail: - zip_close(g_zip); g_zip = NULL; - ASSET.zip = NULL; - return -1; -} - -static int locale_teardown(void **state) { - if(g_locale.file.zipFile != NULL) { - errorret_t ret = assetFileClose(&g_locale.file); - if(errorIsNotOk(ret)) errorCatch(ret); - } - - if(g_zip != NULL) { - zip_close(g_zip); // also frees the read_src and zipbuf - g_zip = NULL; - } - - ASSET.zip = NULL; - if(g_locale.cache != NULL) memoryFree(g_locale.cache); - memoryZero(&g_locale, sizeof(g_locale)); - return 0; -} - -// ============================================================ -// assetLocaleParseHeader - pure tests (no ZIP required) -// ============================================================ - -static void test_parseHeader_english(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsOk(ret)); - assert_int_equal(locale.pluralStateCount, 2); - assert_int_equal(locale.pluralDefaultIndex, 0); - assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_NOT_EQUAL); - assert_int_equal(locale.pluralValues[0], 1); - assert_int_equal(locale.pluralIndices[0], 1); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_singular(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsOk(ret)); - assert_int_equal(locale.pluralStateCount, 1); - assert_int_equal(locale.pluralDefaultIndex, 0); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_less_than(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsOk(ret)); - assert_int_equal(locale.pluralStateCount, 2); - assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_LESS); - assert_int_equal(locale.pluralValues[0], 2); - assert_int_equal(locale.pluralIndices[0], 0); - assert_int_equal(locale.pluralDefaultIndex, 1); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_greater_equal(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n >= 2 ? 1 : 0);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsOk(ret)); - assert_int_equal(locale.pluralStateCount, 2); - assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL); - assert_int_equal(locale.pluralValues[0], 2); - assert_int_equal(locale.pluralIndices[0], 1); - assert_int_equal(locale.pluralDefaultIndex, 0); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_no_plural_forms(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Content-Type: text/plain; charset=UTF-8\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsOk(ret)); - assert_int_equal(locale.pluralStateCount, 0); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_error_nplurals_zero(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: nplurals=0; plural=(0);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_error_nplurals_too_large(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: nplurals=7; plural=(0);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_parseHeader_error_missing_nplurals(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t hdr[] = "Plural-Forms: plural=(0);\n"; - errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetLocaleEvaluatePlural - pure tests -// ============================================================ - -static void test_evaluatePlural_english_singular(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n"; - assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_evaluatePlural_english_plural(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n"; - assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 1); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 100), 1); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_evaluatePlural_singular_only(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n"; - assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 99), 0); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_evaluatePlural_less_than_boundary(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n"; - assetLocaleParseHeader(&locale, hdr, sizeof(hdr)); - - assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1); - assert_int_equal(assetLocaleEvaluatePlural(&locale, 10), 1); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// assetLocaleGetString - ZIP-based tests -// ============================================================ - -static void test_getString_simple(void **state) { - char_t result[256]; - errorret_t ret = - assetLocaleGetString(&g_locale, "greeting", 0, result, sizeof(result)); - - assert_true(errorIsOk(ret)); - assert_string_equal(result, "Hello, World!"); - // 1, not 0: the locale-string cache is lazily allocated on first use (the - // header lookup in locale_setup already triggered it) and lives for the - // rest of this file's lifetime - it's a bounded one-time cost, not a leak. - assert_int_equal(memoryGetAllocatedCount(), 1); -} - -static void test_getString_plural_singular(void **state) { - char_t result[256]; - errorret_t ret = - assetLocaleGetString(&g_locale, "item", 1, result, sizeof(result)); - - assert_true(errorIsOk(ret)); - assert_string_equal(result, "one item"); - assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple -} - -static void test_getString_plural_many(void **state) { - char_t result[256]; - errorret_t ret = - assetLocaleGetString(&g_locale, "item", 5, result, sizeof(result)); - - assert_true(errorIsOk(ret)); - assert_string_equal(result, "many items"); - assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple -} - -static void test_getString_multiple_calls(void **state) { - char_t a[256], b[256]; - errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, a, sizeof(a)); - assert_true(errorIsOk(ret)); - - // Second call is now served straight from the cache the first call - // populated, rather than rewinding the file and re-scanning from scratch. - ret = assetLocaleGetString(&g_locale, "greeting", 0, b, sizeof(b)); - assert_true(errorIsOk(ret)); - - assert_string_equal(a, b); - assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple -} - -static void test_getString_missing_id(void **state) { - char_t result[256]; - errorret_t ret = - assetLocaleGetString(&g_locale, "nonexistent", 0, result, sizeof(result)); - - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple -} - -// ============================================================ -// assetLocaleCacheFind / assetLocaleCacheInsert - pure tests -// ============================================================ -// These call the cache directly rather than through assetLocaleGetString - -// no open file is ever needed since the cache never touches file->file. - -static void test_cache_findMiss_onEmptyCache(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t result[64]; - bool_t hit = true; - errorret_t ret = - assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), &hit); - - assert_true(errorIsOk(ret)); - assert_false(hit); - assert_null(locale.cache);// a pure miss must not allocate anything - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_cache_insertThenFind_hits(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - assetLocaleCacheInsert(&locale, "greeting", 0, "Hello, World!"); - assert_non_null(locale.cache); - - char_t result[64]; - bool_t hit = false; - errorret_t ret = - assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), &hit); - - assert_true(errorIsOk(ret)); - assert_true(hit); - assert_string_equal(result, "Hello, World!"); - - memoryFree(locale.cache); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_cache_distinguishesByPluralCount(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - assetLocaleCacheInsert(&locale, "item", 1, "one item"); - assetLocaleCacheInsert(&locale, "item", 5, "many items"); - - char_t result[64]; - bool_t hit = false; - - errorret_t ret = - assetLocaleCacheFind(&locale, "item", 1, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_true(hit); - assert_string_equal(result, "one item"); - - ret = assetLocaleCacheFind(&locale, "item", 5, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_true(hit); - assert_string_equal(result, "many items"); - - memoryFree(locale.cache); -} - -static void test_cache_find_missOnDifferentMessageId(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - assetLocaleCacheInsert(&locale, "greeting", 0, "Hello, World!"); - - char_t result[64]; - bool_t hit = true; - errorret_t ret = - assetLocaleCacheFind(&locale, "farewell", 0, result, sizeof(result), &hit); - - assert_true(errorIsOk(ret)); - assert_false(hit); - - memoryFree(locale.cache); -} - -static void test_cache_find_bufferTooSmall_errors(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - assetLocaleCacheInsert(&locale, "greeting", 0, "Hello, World!"); - - char_t result[4];// too small for "Hello, World!" - bool_t hit = false; - errorret_t ret = - assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), &hit); - - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - memoryFree(locale.cache); -} - -static void test_cache_insert_tooLongMessageId_notCached(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t longId[ASSET_LOCALE_CACHE_KEY_MAX + 1]; - memorySet(longId, 'a', sizeof(longId) - 1); - longId[sizeof(longId) - 1] = '\0'; - - assetLocaleCacheInsert(&locale, longId, 0, "value"); - - // Nothing should have been cached - the cache itself is never even - // allocated, since this was the only insert attempted. - assert_null(locale.cache); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_cache_insert_tooLongValue_notCached(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - char_t longValue[ASSET_LOCALE_CACHE_VALUE_MAX + 1]; - memorySet(longValue, 'a', sizeof(longValue) - 1); - longValue[sizeof(longValue) - 1] = '\0'; - - assetLocaleCacheInsert(&locale, "greeting", 0, longValue); - - assert_null(locale.cache); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_cache_insert_evictsLeastRecentlyUsed(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - // Fill the cache completely, "id0" first (so it's the first to become - // least-recently-used) through "id127" last (most-recently-used). - char_t key[32], value[32]; - for(int32_t i = 0; i < ASSET_LOCALE_CACHE_COUNT; i++) { - stringFormat(key, sizeof(key), "id%d", i); - stringFormat(value, sizeof(value), "value%d", i); - assetLocaleCacheInsert(&locale, key, 0, value); - } - - // One more insert should evict "id0", the least-recently-used entry. - assetLocaleCacheInsert(&locale, "newcomer", 0, "new value"); - - char_t result[32]; - bool_t hit = true; - errorret_t ret = - assetLocaleCacheFind(&locale, "id0", 0, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_false(hit); - - // But "id1" (and everything after it) should have survived. - ret = assetLocaleCacheFind(&locale, "id1", 0, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_true(hit); - assert_string_equal(result, "value1"); - - memoryFree(locale.cache); -} - -static void test_cache_find_movesHitToFront(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - - // Fill the cache, then immediately re-touch "id0" so it becomes - // most-recently-used instead of least-recently-used. - char_t key[32], value[32]; - for(int32_t i = 0; i < ASSET_LOCALE_CACHE_COUNT; i++) { - stringFormat(key, sizeof(key), "id%d", i); - stringFormat(value, sizeof(value), "value%d", i); - assetLocaleCacheInsert(&locale, key, 0, value); - } - - char_t result[32]; - bool_t hit = false; - errorret_t ret = - assetLocaleCacheFind(&locale, "id0", 0, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_true(hit); - - // Without the touch above, this insert would have evicted "id0" (the - // original least-recently-used entry) - it should now evict "id1" instead. - assetLocaleCacheInsert(&locale, "newcomer", 0, "new value"); - - ret = assetLocaleCacheFind(&locale, "id0", 0, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_true(hit); - - ret = assetLocaleCacheFind(&locale, "id1", 0, result, sizeof(result), &hit); - assert_true(errorIsOk(ret)); - assert_false(hit); - - memoryFree(locale.cache); -} - -static void test_cache_nullAsserts(void **state) { - assetlocalefile_t locale; - memoryZero(&locale, sizeof(locale)); - char_t result[64]; - bool_t hit; - - expect_assert_failure( - assetLocaleCacheFind(NULL, "greeting", 0, result, sizeof(result), &hit) - ); - expect_assert_failure( - assetLocaleCacheFind(&locale, NULL, 0, result, sizeof(result), &hit) - ); - expect_assert_failure( - assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), NULL) - ); - - expect_assert_failure(assetLocaleCacheInsert(NULL, "greeting", 0, "value")); - expect_assert_failure(assetLocaleCacheInsert(&locale, NULL, 0, "value")); - expect_assert_failure(assetLocaleCacheInsert(&locale, "greeting", 0, NULL)); -} - -static void test_localeDispose_freesCache(void **state) { - zip_error_t err; - zip_error_init(&err); - zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err); - assert_non_null(write_src); - zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err); - assert_non_null(za); - size_t flen = strlen(LOCALE_EN); - zip_source_t *fs = zip_source_buffer(za, LOCALE_EN, flen, 0); - assert_true(zip_file_add(za, "en.locale", fs, ZIP_FL_OVERWRITE) >= 0); - zip_source_keep(write_src); - assert_int_equal(zip_close(za), 0); - - zip_stat_t zs; - memset(&zs, 0, sizeof(zs)); - assert_int_equal(zip_source_stat(write_src, &zs), 0); - void *zipbuf = malloc((size_t)zs.size); - assert_non_null(zipbuf); - assert_int_equal(zip_source_open(write_src), 0); - zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size); - zip_source_close(write_src); - zip_source_free(write_src); - - zip_error_init(&err); - zip_source_t *read_src = - zip_source_buffer_create(zipbuf, (zip_uint64_t)zs.size, 1, &err); - assert_non_null(read_src); - zip_t *zip = zip_open_from_source(read_src, 0, &err); - assert_non_null(zip); - ASSET.zip = zip; - - assetentry_t entry; - memoryZero(&entry, sizeof(entry)); - entry.type = ASSET_LOADER_TYPE_LOCALE; - - errorret_t ret = - assetFileInit(&entry.data.locale.file, "en.locale", NULL, NULL); - assert_true(errorIsOk(ret)); - ret = assetFileOpen(&entry.data.locale.file); - assert_true(errorIsOk(ret)); - - char_t header[512]; - ret = assetLocaleGetString(&entry.data.locale, "", 0, header, sizeof(header)); - assert_true(errorIsOk(ret)); - assert_non_null(entry.data.locale.cache);// the header lookup populated it - - ret = assetLocaleDispose(&entry); - assert_true(errorIsOk(ret)); - assert_null(entry.data.locale.cache); - assert_int_equal(memoryGetAllocatedCount(), 0); - - zip_close(zip); - ASSET.zip = NULL; -} - -// ============================================================ -// assetLocaleGetStringWithArgs - ZIP-based tests -// ============================================================ - -static void test_getStringWithArgs_int(void **state) { - assetlocalearg_t args[] = { - { .type = ASSET_LOCALE_ARG_INT, .intValue = 42 } - }; - char_t result[256]; - errorret_t ret = assetLocaleGetStringWithArgs( - &g_locale, "score", 0, result, sizeof(result), args, 1 - ); - - assert_true(errorIsOk(ret)); - assert_string_equal(result, "Score: 42"); - assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple -} - -static void test_getStringWithArgs_string(void **state) { - assetlocalearg_t args[] = { - { .type = ASSET_LOCALE_ARG_STRING, .stringValue = "Alice" } - }; - char_t result[256]; - errorret_t ret = assetLocaleGetStringWithArgs( - &g_locale, "player", 0, result, sizeof(result), args, 1 - ); - - assert_true(errorIsOk(ret)); - assert_string_equal(result, "Player: Alice"); - assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple -} - -// ============================================================ -// main -// ============================================================ - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - // parseHeader - pure - cmocka_unit_test(test_parseHeader_english), - cmocka_unit_test(test_parseHeader_singular), - cmocka_unit_test(test_parseHeader_less_than), - cmocka_unit_test(test_parseHeader_greater_equal), - cmocka_unit_test(test_parseHeader_no_plural_forms), - cmocka_unit_test(test_parseHeader_error_nplurals_zero), - cmocka_unit_test(test_parseHeader_error_nplurals_too_large), - cmocka_unit_test(test_parseHeader_error_missing_nplurals), - - // evaluatePlural - pure - cmocka_unit_test(test_evaluatePlural_english_singular), - cmocka_unit_test(test_evaluatePlural_english_plural), - cmocka_unit_test(test_evaluatePlural_singular_only), - cmocka_unit_test(test_evaluatePlural_less_than_boundary), - - // getString - in-memory ZIP - cmocka_unit_test_setup_teardown( - test_getString_simple, locale_setup, locale_teardown - ), - cmocka_unit_test_setup_teardown( - test_getString_plural_singular, locale_setup, locale_teardown - ), - cmocka_unit_test_setup_teardown( - test_getString_plural_many, locale_setup, locale_teardown - ), - cmocka_unit_test_setup_teardown( - test_getString_multiple_calls, locale_setup, locale_teardown - ), - cmocka_unit_test_setup_teardown( - test_getString_missing_id, locale_setup, locale_teardown - ), - - // getStringWithArgs - in-memory ZIP - cmocka_unit_test_setup_teardown( - test_getStringWithArgs_int, locale_setup, locale_teardown - ), - cmocka_unit_test_setup_teardown( - test_getStringWithArgs_string, locale_setup, locale_teardown - ), - - // assetLocaleCacheFind / assetLocaleCacheInsert - pure - cmocka_unit_test(test_cache_findMiss_onEmptyCache), - cmocka_unit_test(test_cache_insertThenFind_hits), - cmocka_unit_test(test_cache_distinguishesByPluralCount), - cmocka_unit_test(test_cache_find_missOnDifferentMessageId), - cmocka_unit_test(test_cache_find_bufferTooSmall_errors), - cmocka_unit_test(test_cache_insert_tooLongMessageId_notCached), - cmocka_unit_test(test_cache_insert_tooLongValue_notCached), - cmocka_unit_test(test_cache_insert_evictsLeastRecentlyUsed), - cmocka_unit_test(test_cache_find_movesHitToFront), - cmocka_unit_test(test_cache_nullAsserts), - - // assetLocaleDispose - in-memory ZIP - cmocka_unit_test(test_localeDispose_freesCache), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/asset/test_assettilesetloader.c b/test/asset/test_assettilesetloader.c deleted file mode 100644 index eb21f85e..00000000 --- a/test/asset/test_assettilesetloader.c +++ /dev/null @@ -1,422 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "asset/loader/assetentry.h" -#include "asset/loader/display/assettilesetloader.h" -#include "thread/thread.h" -#include "util/memory.h" -#include - -// ============================================================ -// DTF binary fixtures -// DTF layout: -// [0-2] magic "DTF" -// [3] version 0x00 -// [4-5] tileWidth (uint16 LE) -// [6-7] tileHeight (uint16 LE) -// [8-9] columns (uint16 LE) -// [10-11] rows (uint16 LE) -// [12-15] reserved -// [16-19] uv[0] (float LE) -// [20-23] uv[1] (float LE, must be 0.0–1.0) -// ============================================================ - -// uv[1] = 0.5f = 0x3F000000 LE = {0x00,0x00,0x00,0x3F} -// uv[1] = 2.0f = 0x40000000 LE = {0x00,0x00,0x00,0x40} (out-of-range) - -static const uint8_t DTF_VALID[] = { - 'D','T','F', 0x00, - 16, 0, - 16, 0, - 4, 0, - 4, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 0x00, 0x00, 0x00, 0x3F -}; - -static const uint8_t DTF_BAD_MAGIC[] = { - 'X','Y','Z', 0x00, - 16, 0, 16, 0, 4, 0, 4, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F -}; - -static const uint8_t DTF_BAD_VERSION[] = { - 'D','T','F', 0xFF, - 16, 0, 16, 0, 4, 0, 4, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F -}; - -static const uint8_t DTF_ZERO_WIDTH[] = { - 'D','T','F', 0x00, - 0, 0, - 16, 0, 4, 0, 4, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F -}; - -static const uint8_t DTF_ZERO_HEIGHT[] = { - 'D','T','F', 0x00, - 16, 0, - 0, 0, - 4, 0, 4, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F -}; - -static const uint8_t DTF_ZERO_COLUMNS[] = { - 'D','T','F', 0x00, - 16, 0, 16, 0, - 0, 0, - 4, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F -}; - -static const uint8_t DTF_ZERO_ROWS[] = { - 'D','T','F', 0x00, - 16, 0, 16, 0, 4, 0, - 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0x3F -}; - -// uv[1] = 2.0f, which fails the 0.0–1.0 range check -static const uint8_t DTF_INVALID_UV[] = { - 'D','T','F', 0x00, - 16, 0, 16, 0, 4, 0, 4, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0x00, 0x00, 0x00, 0x40 -}; - -// ============================================================ -// Async thread helper -// ============================================================ - -typedef struct { - assetloading_t *loading; - bool_t ok; -} tileset_async_run_t; - -static void tileset_async_thread_cb(thread_t *thread) { - tileset_async_run_t *run = (tileset_async_run_t *)thread->data; - errorret_t ret = assetTilesetLoaderAsync(run->loading); - run->ok = errorIsOk(ret); - if(errorIsNotOk(ret)) errorCatch(ret); -} - -static bool_t run_tileset_async(assetloading_t *loading) { - tileset_async_run_t run = { .loading = loading, .ok = false }; - thread_t thread; - threadInit(&thread, tileset_async_thread_cb); - thread.data = &run; - threadStart(&thread); - threadStop(&thread); - return run.ok; -} - -// ============================================================ -// In-memory ZIP -// ============================================================ - -static zip_t *g_zip = NULL; - -static int tileset_zip_add( - zip_t *za, const char_t *name, const void *data, size_t len -) { - zip_source_t *s = zip_source_buffer(za, data, len, 0); - return (int)zip_file_add(za, name, s, ZIP_FL_OVERWRITE); -} - -static int zip_setup(void **state) { - zip_error_t err; - zip_error_init(&err); - - zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err); - if(!write_src) return -1; - - zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err); - if(!za) { zip_source_free(write_src); return -1; } - - if( - tileset_zip_add(za, "valid.tileset", DTF_VALID, sizeof(DTF_VALID)) < 0 || - tileset_zip_add( - za, "badmagic.tileset", DTF_BAD_MAGIC, sizeof(DTF_BAD_MAGIC) - ) < 0 || - tileset_zip_add( - za, "badversion.tileset", DTF_BAD_VERSION, sizeof(DTF_BAD_VERSION) - ) < 0 || - tileset_zip_add( - za, "zerowidth.tileset", DTF_ZERO_WIDTH, sizeof(DTF_ZERO_WIDTH) - ) < 0 || - tileset_zip_add( - za, "zeroheight.tileset", DTF_ZERO_HEIGHT, sizeof(DTF_ZERO_HEIGHT) - ) < 0 || - tileset_zip_add( - za, "zerocolumns.tileset", DTF_ZERO_COLUMNS, sizeof(DTF_ZERO_COLUMNS) - ) < 0 || - tileset_zip_add( - za, "zerorows.tileset", DTF_ZERO_ROWS, sizeof(DTF_ZERO_ROWS) - ) < 0 || - tileset_zip_add( - za, "invaliduv.tileset", DTF_INVALID_UV, sizeof(DTF_INVALID_UV) - ) < 0 - ) { - zip_close(za); return -1; - } - - zip_source_keep(write_src); - if(zip_close(za) != 0) { zip_source_free(write_src); return -1; } - - zip_stat_t zs; - memset(&zs, 0, sizeof(zs)); - if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) { - zip_source_free(write_src); return -1; - } - - void *zipbuf = malloc((size_t)zs.size); - if(!zipbuf) { zip_source_free(write_src); return -1; } - - if(zip_source_open(write_src) != 0) { - free(zipbuf); zip_source_free(write_src); return -1; - } - zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size); - zip_source_close(write_src); - zip_source_free(write_src); - - zip_error_init(&err); - zip_source_t *read_src = zip_source_buffer_create( - zipbuf, (zip_uint64_t)zs.size, 1, &err - ); - if(!read_src) { free(zipbuf); return -1; } - - g_zip = zip_open_from_source(read_src, 0, &err); - if(!g_zip) { zip_source_free(read_src); return -1; } - - ASSET.zip = g_zip; - return 0; -} - -static int zip_teardown(void **state) { - if(g_zip) { zip_close(g_zip); g_zip = NULL; } - ASSET.zip = NULL; - return 0; -} - -// ============================================================ -// Loader pipeline helper -// ============================================================ - -typedef struct { - assetentry_t entry; - assetloading_t loading; -} loader_ctx_t; - -static void loader_ctx_init(loader_ctx_t *ctx, const char_t *name) { - assetEntryInit(&ctx->entry, name, ASSET_LOADER_TYPE_TILESET, NULL); - threadMutexInit(&ctx->loading.mutex); - memoryZero(&ctx->loading.loading, sizeof(ctx->loading.loading)); - ctx->loading.type = ASSET_LOADER_TYPE_TILESET; - ctx->loading.entry = &ctx->entry; - ctx->entry.state = ASSET_ENTRY_STATE_PENDING_SYNC; -} - -static errorret_t loader_ctx_run(loader_ctx_t *ctx) { - errorret_t ret = assetTilesetLoaderSync(&ctx->loading); - if(errorIsNotOk(ret)) return ret; - - if(!run_tileset_async(&ctx->loading)) { - ctx->entry.state = ASSET_ENTRY_STATE_ERROR; - errorThrow("Async tileset load failed"); - } - - return assetTilesetLoaderSync(&ctx->loading); -} - -static void loader_ctx_dispose(loader_ctx_t *ctx) { - if(ctx->entry.type != ASSET_LOADER_TYPE_NULL) { - errorret_t ret = assetEntryDispose(&ctx->entry); - if(errorIsNotOk(ret)) errorCatch(ret); - } - threadMutexDispose(&ctx->loading.mutex); -} - -// ============================================================ -// Tests -// ============================================================ - -static void test_tileset_valid_loads(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "valid.tileset"); - - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsOk(ret)); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_LOADED); - - tileset_t *ts = &ctx.entry.data.tileset; - assert_int_equal(ts->tileWidth, 16); - assert_int_equal(ts->tileHeight, 16); - assert_int_equal(ts->columns, 4); - assert_int_equal(ts->rows, 4); - assert_int_equal(ts->tileCount, 16); // columns * rows - - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_data_cleared_after_load(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "valid.tileset"); - loader_ctx_run(&ctx); - - // The async scratch buffer must be freed by the time sync returns LOADED. - assert_null(ctx.loading.loading.tileset.data); - - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_bad_magic(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "badmagic.tileset"); - - errorret_t ret = assetTilesetLoaderSync(&ctx.loading); - assert_true(errorIsOk(ret)); - assert_true(run_tileset_async(&ctx.loading)); - ret = assetTilesetLoaderSync(&ctx.loading); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_bad_version(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "badversion.tileset"); - - errorret_t ret = assetTilesetLoaderSync(&ctx.loading); - assert_true(errorIsOk(ret)); - assert_true(run_tileset_async(&ctx.loading)); - ret = assetTilesetLoaderSync(&ctx.loading); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_zero_tile_width(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "zerowidth.tileset"); - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_zero_tile_height(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "zeroheight.tileset"); - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_zero_columns(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "zerocolumns.tileset"); - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_zero_rows(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "zerorows.tileset"); - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_invalid_uv(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "invaliduv.tileset"); - errorret_t ret = loader_ctx_run(&ctx); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_tileset_missing_file(void **state) { - loader_ctx_t ctx; - loader_ctx_init(&ctx, "nonexistent.tileset"); - - errorret_t ret = assetTilesetLoaderSync(&ctx.loading); - assert_true(errorIsOk(ret)); - - assert_false(run_tileset_async(&ctx.loading)); - - assert_int_equal(ctx.entry.state, ASSET_ENTRY_STATE_ERROR); - loader_ctx_dispose(&ctx); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// ============================================================ -// main -// ============================================================ - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test_setup_teardown( - test_tileset_valid_loads, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_data_cleared_after_load, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_bad_magic, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_bad_version, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_zero_tile_width, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_zero_tile_height, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_zero_columns, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_zero_rows, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_invalid_uv, zip_setup, zip_teardown - ), - cmocka_unit_test_setup_teardown( - test_tileset_missing_file, zip_setup, zip_teardown - ), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/console/CMakeLists.txt b/test/console/CMakeLists.txt deleted file mode 100644 index a0a98903..00000000 --- a/test/console/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_console.c) diff --git a/test/console/test_console.c b/test/console/test_console.c deleted file mode 100644 index 6d90f081..00000000 --- a/test/console/test_console.c +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "console/console.h" -#include "thread/thread.h" -#include "util/string.h" - -static void test_consoleInitDefaults(void **state) { - consoleInit(); - - assert_false(CONSOLE.visible); - for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - assert_string_equal(CONSOLE.line[i], ""); - } - - consoleDispose(); -} - -static void test_consolePrintAddsNewestAtEnd(void **state) { - consoleInit(); - - consolePrint("hello %d", 5); - - assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "hello 5"); - - consoleDispose(); -} - -static void test_consolePrintShiftsHistory(void **state) { - consoleInit(); - - consolePrint("one"); - consolePrint("two"); - consolePrint("three"); - - assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], "three"); - assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 2], "two"); - assert_string_equal(CONSOLE.line[CONSOLE_HISTORY_MAX - 3], "one"); - - consoleDispose(); -} - -static void test_consolePrintOverflowDropsOldest(void **state) { - consoleInit(); - - const int32_t total = CONSOLE_HISTORY_MAX + 2; - for(int32_t i = 0; i < total; i++) { - consolePrint("line-%d", i); - } - - // The oldest two ("line-0", "line-1") must have been dropped. - for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - char_t expected[CONSOLE_LINE_MAX]; - stringFormat(expected, CONSOLE_LINE_MAX, "line-%d", i + 2); - assert_string_equal(CONSOLE.line[i], expected); - } - - consoleDispose(); -} - -// --- Thread-safety of consolePrint's shared history buffer --- - -typedef struct { - int32_t threadIndex; - int32_t iterations; -} consoleprinter_data_t; - -static void helper_consolePrinter(thread_t *thread) { - consoleprinter_data_t *data = (consoleprinter_data_t *)thread->data; - for(int32_t i = 0; i < data->iterations; i++) { - consolePrint("T%d-%03d", data->threadIndex, i); - } -} - -// Parses a "T-" line, requiring the whole line to match. -// Returns false for an empty or malformed line. -static bool_t parsePrintedLine( - const char_t *line, int32_t *threadIndex, int32_t *seq -) { - int32_t consumed = 0; - int32_t matched = sscanf(line, "T%d-%d%n", threadIndex, seq, &consumed); - return matched == 2 && consumed == (int32_t)strlen(line); -} - -#define CONSOLE_THREAD_COUNT 4 - -static void test_consolePrintConcurrentExactlyFillsHistory(void **state) { - // 4 threads x 4 prints = exactly CONSOLE_HISTORY_MAX, so every slot must - // end up holding one well-formed, uncorrupted message - none left empty, - // none torn/interleaved by a missing lock. - consoleInit(); - - const int32_t iterations = CONSOLE_HISTORY_MAX / CONSOLE_THREAD_COUNT; - consoleprinter_data_t data[CONSOLE_THREAD_COUNT]; - thread_t threads[CONSOLE_THREAD_COUNT]; - - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) { - data[i].threadIndex = i; - data[i].iterations = iterations; - threadInit(&threads[i], helper_consolePrinter); - threads[i].data = &data[i]; - } - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStart(&threads[i]); - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStop(&threads[i]); - - int32_t seenPerThread[CONSOLE_THREAD_COUNT] = { 0 }; - for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - int32_t threadIndex, seq; - assert_true(parsePrintedLine(CONSOLE.line[i], &threadIndex, &seq)); - assert_true(threadIndex >= 0 && threadIndex < CONSOLE_THREAD_COUNT); - assert_true(seq >= 0 && seq < iterations); - seenPerThread[threadIndex]++; - } - - // Every message from every thread survived, since total prints == capacity. - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) { - assert_int_equal(seenPerThread[i], iterations); - } - - consoleDispose(); -} - -static void test_consolePrintConcurrentStressNoCorruption(void **state) { - // Far more prints than history capacity, from multiple threads at once. - // If printMutex didn't actually serialize the shift+copy in consolePrint, - // concurrent writers would tear each other's memoryMove/memoryCopy calls - // and this would surface as lines failing to parse as "T-". - consoleInit(); - - const int32_t iterations = 500; - consoleprinter_data_t data[CONSOLE_THREAD_COUNT]; - thread_t threads[CONSOLE_THREAD_COUNT]; - - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) { - data[i].threadIndex = i; - data[i].iterations = iterations; - threadInit(&threads[i], helper_consolePrinter); - threads[i].data = &data[i]; - } - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStart(&threads[i]); - for(int32_t i = 0; i < CONSOLE_THREAD_COUNT; i++) threadStop(&threads[i]); - - for(int32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - int32_t threadIndex, seq; - assert_true(parsePrintedLine(CONSOLE.line[i], &threadIndex, &seq)); - assert_true(threadIndex >= 0 && threadIndex < CONSOLE_THREAD_COUNT); - assert_true(seq >= 0 && seq < iterations); - } - - consoleDispose(); -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_consoleInitDefaults), - cmocka_unit_test(test_consolePrintAddsNewestAtEnd), - cmocka_unit_test(test_consolePrintShiftsHistory), - cmocka_unit_test(test_consolePrintOverflowDropsOldest), - cmocka_unit_test(test_consolePrintConcurrentExactlyFillsHistory), - cmocka_unit_test(test_consolePrintConcurrentStressNoCorruption), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/display/CMakeLists.txt b/test/display/CMakeLists.txt deleted file mode 100644 index 47bf3628..00000000 --- a/test/display/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_color.c) \ No newline at end of file diff --git a/test/display/test_color.c b/test/display/test_color.c deleted file mode 100644 index 9037e302..00000000 --- a/test/display/test_color.c +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "display/color.h" - -static void test_color3f_create(void **state) { - color3f_t color = color3f(0.1f, 0.2f, 0.3f); - assert_float_equal(color.r, 0.1f, 0.0001f); - assert_float_equal(color.g, 0.2f, 0.0001f); - assert_float_equal(color.b, 0.3f, 0.0001f); -} - -static void test_color4f_create(void **state) { - color4f_t color = color4f(0.1f, 0.2f, 0.3f, 0.4f); - assert_float_equal(color.r, 0.1f, 0.0001f); - assert_float_equal(color.g, 0.2f, 0.0001f); - assert_float_equal(color.b, 0.3f, 0.0001f); - assert_float_equal(color.a, 0.4f, 0.0001f); -} - -static void test_color3b_create(void **state) { - color3b_t color = color3b(10, 20, 30); - assert_int_equal(color.r, 10); - assert_int_equal(color.g, 20); - assert_int_equal(color.b, 30); -} - -static void test_color4b_create(void **state) { - - color4b_t color = color4b(10, 20, 30, 40); - assert_int_equal(color.r, 10); - assert_int_equal(color.g, 20); - assert_int_equal(color.b, 30); - assert_int_equal(color.a, 40); -} - -static void test_color_create(void **state) { - - color_t color = color(10, 20, 30, 40); - assert_int_equal(color.r, 10); - assert_int_equal(color.g, 20); - assert_int_equal(color.b, 30); - assert_int_equal(color.a, 40); -} - -static void test_colorHex_create(void **state) { - - color_t color = colorHex(0x11223344); - assert_int_equal(color.r, 0x11); - assert_int_equal(color.g, 0x22); - assert_int_equal(color.b, 0x33); - assert_int_equal(color.a, 0x44); - - color = colorHex(0xFF00FF00); - assert_int_equal(color.r, 0xFF); - assert_int_equal(color.g, 0x00); - assert_int_equal(color.b, 0xFF); - assert_int_equal(color.a, 0x00); - - color_t comp = color(255, 0, 255, 0); - assert_int_equal(color.r, comp.r); - assert_int_equal(color.g, comp.g); - assert_int_equal(color.b, comp.b); - assert_int_equal(color.a, comp.a); - - color = colorHex(0xFFFFFFFF); - assert_int_equal(color.r, COLOR_WHITE.r); - assert_int_equal(color.g, COLOR_WHITE.g); - assert_int_equal(color.b, COLOR_WHITE.b); - assert_int_equal(color.a, COLOR_WHITE.a); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_color3f_create), - cmocka_unit_test(test_color4f_create), - cmocka_unit_test(test_color3b_create), - cmocka_unit_test(test_color4b_create), - cmocka_unit_test(test_color_create), - cmocka_unit_test(test_colorHex_create), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/dusktest.h b/test/dusktest.h deleted file mode 100644 index d1d8cb8a..00000000 --- a/test/dusktest.h +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "dusk.h" -#include "assert/assert.h"// Includes cmocka when DUSK_TEST_ASSERT is set \ No newline at end of file diff --git a/test/error/CMakeLists.txt b/test/error/CMakeLists.txt deleted file mode 100644 index c233a159..00000000 --- a/test/error/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_error.c) diff --git a/test/error/test_error.c b/test/error/test_error.c deleted file mode 100644 index 04ffa96f..00000000 --- a/test/error/test_error.c +++ /dev/null @@ -1,371 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "error/error.h" -#include "thread/thread.h" -#include "util/memory.h" - -// Helper that throws an error. -static errorret_t helper_throw(void) { - errorThrow("Test error %d", 42); -} - -// Helper that returns ok. -static errorret_t helper_ok(void) { - errorOk(); -} - -// Helper that chains to helper_throw. -static errorret_t helper_chain(void) { - errorChain(helper_throw()); - errorOk(); -} - -// Helper that throws with no format arguments (exercises the zero-arg -// ##__VA_ARGS__ branch of the errorThrow macro). -static errorret_t helper_throwNoArgs(void) { - errorThrow("Simple message with no arguments"); -} - -static const errorcode_t CUSTOM_ERROR_CODE = 7; - -// Helper that throws with an explicit, non-default error code. -static errorret_t helper_throwWithCode(void) { - errorThrowWithCode(CUSTOM_ERROR_CODE, "Custom error %d", 99); -} - -static void test_errorThrow(void **state) { - errorret_t ret = helper_throw(); - - assert_int_not_equal(ret.code, ERROR_OK); - assert_non_null(ret.state); - assert_non_null(ret.state->message); - assert_non_null(ret.state->lines); - - // Message should contain our format argument - assert_non_null(strstr(ret.state->message, "42")); - - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorOk(void **state) { - errorret_t ret = helper_ok(); - - assert_int_equal(ret.code, ERROR_OK); - assert_null(ret.state); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorIsOk(void **state) { - errorret_t ok = helper_ok(); - assert_true(errorIsOk(ok)); - assert_false(errorIsNotOk(ok)); - - errorret_t err = helper_throw(); - assert_false(errorIsOk(err)); - assert_true(errorIsNotOk(err)); - - errorCatch(err); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorChain(void **state) { - errorret_t ret = helper_chain(); - - // Error propagated up - assert_int_not_equal(ret.code, ERROR_OK); - assert_non_null(ret.state); - assert_non_null(ret.state->lines); - - // Lines should contain at least two stack entries - int32_t count = 0; - const char_t *p = ret.state->lines; - while((p = strstr(p, " at ")) != NULL) { - count++; - p++; - } - assert_true(count >= 2); - - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorThrowNoFormatArguments(void **state) { - errorret_t ret = helper_throwNoArgs(); - - assert_int_not_equal(ret.code, ERROR_OK); - assert_string_equal(ret.state->message, "Simple message with no arguments"); - - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorThrowWithCode(void **state) { - errorret_t ret = helper_throwWithCode(); - - // The explicit code must round-trip, not just "not OK". - assert_int_equal(ret.code, CUSTOM_ERROR_CODE); - assert_non_null(strstr(ret.state->message, "99")); - - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorChainOkRetvalPassesThroughUnchanged(void **state) { - // errorChainImpl's own early-return branch, called directly since the - // errorChain macro pre-filters on code != ERROR_OK before ever reaching it. - errorret_t ok = helper_ok(); - errorret_t chained = errorChainImpl(ok, __FILE__, __func__, __LINE__); - - assert_int_equal(chained.code, ERROR_OK); - assert_null(chained.state); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorPrintOkRetvalPassesThroughUnchanged(void **state) { - errorret_t ok = helper_ok(); - errorret_t printed = errorPrint(ok); - - assert_int_equal(printed.code, ERROR_OK); - assert_null(printed.state); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorPrintErrorRetvalPassesThroughUnchanged(void **state) { - errorret_t ret = helper_throw(); - errorret_t printed = errorPrint(ret); - - // errorPrint logs, but must not mutate or consume the error state. - assert_int_equal(printed.code, ret.code); - assert_ptr_equal(printed.state, ret.state); - assert_non_null(printed.state->message); - - errorCatch(printed); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// --- Negative / misuse paths --- -// -// These bypass the errorThrow/errorChain/errorOk macros to call the *Impl -// functions directly with invalid arguments, since correct macro usage can -// never produce these states (they always supply a valid &ERROR_STATE, -// __FILE__, __func__, __LINE__). - -static void test_errorThrowImplNullStateAsserts(void **state) { - expect_assert_failure( - errorThrowImpl(NULL, ERROR_NOT_OK, __FILE__, __func__, __LINE__, "msg") - ); -} - -static void test_errorThrowImplOkCodeAsserts(void **state) { - errorstate_t local = { 0 }; - expect_assert_failure( - errorThrowImpl(&local, ERROR_OK, __FILE__, __func__, __LINE__, "msg") - ); -} - -static void test_errorThrowImplNullFileAsserts(void **state) { - errorstate_t local = { 0 }; - expect_assert_failure( - errorThrowImpl(&local, ERROR_NOT_OK, NULL, __func__, __LINE__, "msg") - ); -} - -static void test_errorThrowImplNullFunctionAsserts(void **state) { - errorstate_t local = { 0 }; - expect_assert_failure( - errorThrowImpl(&local, ERROR_NOT_OK, __FILE__, NULL, __LINE__, "msg") - ); -} - -static void test_errorThrowImplNegativeLineAsserts(void **state) { - errorstate_t local = { 0 }; - expect_assert_failure( - errorThrowImpl(&local, ERROR_NOT_OK, __FILE__, __func__, -1, "msg") - ); -} - -static void test_errorThrowImplNullMessageAsserts(void **state) { - errorstate_t local = { 0 }; - expect_assert_failure( - errorThrowImpl(&local, ERROR_NOT_OK, __FILE__, __func__, __LINE__, NULL) - ); -} - -static void test_errorOkAssertsIfPreviousErrorNotCaught(void **state) { - // Simulate a caller forgetting to errorCatch a previous throw: leave - // ERROR_STATE.code non-OK, then errorOk() must assert rather than silently - // returning an OK that papers over the uncaught error. - assert_int_equal(ERROR_STATE.code, ERROR_OK); - ERROR_STATE.code = ERROR_NOT_OK; - - expect_assert_failure(helper_ok()); - - // Nothing was actually allocated for this fake corruption - just restore - // the global so later tests see a clean state. - ERROR_STATE.code = ERROR_OK; -} - -static void test_errorChainImplNullStateAsserts(void **state) { - errorret_t bad = { .code = ERROR_NOT_OK, .state = NULL }; - expect_assert_failure( - errorChainImpl(bad, __FILE__, __func__, __LINE__) - ); -} - -static void test_errorChainImplNullMessageAsserts(void **state) { - errorstate_t local = { .code = ERROR_NOT_OK, .message = NULL, .lines = NULL }; - errorret_t bad = { .code = ERROR_NOT_OK, .state = &local }; - expect_assert_failure( - errorChainImpl(bad, __FILE__, __func__, __LINE__) - ); -} - -static void test_errorCatchNullStateAsserts(void **state) { - errorret_t bad = { .code = ERROR_NOT_OK, .state = NULL }; - expect_assert_failure(errorCatch(bad)); -} - -static void test_errorCatchNullMessageAsserts(void **state) { - errorstate_t local = { .code = ERROR_NOT_OK, .message = NULL, .lines = NULL }; - errorret_t bad = { .code = ERROR_NOT_OK, .state = &local }; - expect_assert_failure(errorCatch(bad)); -} - -static void test_errorPrintNullStateAsserts(void **state) { - errorret_t bad = { .code = ERROR_NOT_OK, .state = NULL }; - expect_assert_failure(errorPrint(bad)); -} - -static void test_errorPrintNullMessageAsserts(void **state) { - errorstate_t local = { .code = ERROR_NOT_OK, .message = NULL, .lines = NULL }; - errorret_t bad = { .code = ERROR_NOT_OK, .state = &local }; - expect_assert_failure(errorPrint(bad)); -} - -static void test_errorCatch_ok(void **state) { - // Catching an ok ret should be a no-op - errorret_t ret = helper_ok(); - errorCatch(ret); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_errorCatch_error(void **state) { - errorret_t ret = helper_throw(); - assert_int_not_equal(ret.code, ERROR_OK); - - errorCatch(ret); - - // After catch the global state should be cleared - assert_int_equal(ERROR_STATE.code, ERROR_OK); - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -// --- Threaded tests --- - -typedef struct { - errorcode_t capturedCode; - bool_t messageHas42; -} thread_test_data_t; - -static void helper_thread_throw(thread_t *thread) { - errorret_t ret = helper_throw(); - thread_test_data_t *data = (thread_test_data_t *)thread->data; - data->capturedCode = ERROR_STATE.code; - data->messageHas42 = (strstr(ret.state->message, "42") != NULL); - errorCatch(ret); -} - -static void test_error_thread_isolation(void **state) { - // Main thread state is clean before the test. - assert_int_equal(ERROR_STATE.code, ERROR_OK); - - thread_test_data_t data = { .capturedCode = ERROR_OK, .messageHas42 = false }; - - thread_t thread; - threadInit(&thread, helper_thread_throw); - thread.data = &data; - threadStart(&thread); - threadStop(&thread); - - // Worker saw ERROR_NOT_OK in its own ERROR_STATE. - assert_int_equal(data.capturedCode, ERROR_NOT_OK); - assert_true(data.messageHas42); - - // Main thread ERROR_STATE was not touched by the worker. - assert_int_equal(ERROR_STATE.code, ERROR_OK); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -#define CONCURRENT_THREAD_COUNT 4 - -static thread_test_data_t concurrent_data[CONCURRENT_THREAD_COUNT]; -static thread_t concurrent_threads[CONCURRENT_THREAD_COUNT]; - -static void test_error_concurrent_throw(void **state) { - for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { - concurrent_data[i].capturedCode = ERROR_OK; - concurrent_data[i].messageHas42 = false; - threadInit(&concurrent_threads[i], helper_thread_throw); - concurrent_threads[i].data = &concurrent_data[i]; - } - - for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { - threadStart(&concurrent_threads[i]); - } - - for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { - threadStop(&concurrent_threads[i]); - } - - // Every worker must have seen its own independent error. - for(int32_t i = 0; i < CONCURRENT_THREAD_COUNT; i++) { - assert_int_equal(concurrent_data[i].capturedCode, ERROR_NOT_OK); - assert_true(concurrent_data[i].messageHas42); - } - - // Main thread is still clean. - assert_int_equal(ERROR_STATE.code, ERROR_OK); - - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_errorThrow), - cmocka_unit_test(test_errorOk), - cmocka_unit_test(test_errorIsOk), - cmocka_unit_test(test_errorChain), - cmocka_unit_test(test_errorThrowNoFormatArguments), - cmocka_unit_test(test_errorThrowWithCode), - cmocka_unit_test(test_errorChainOkRetvalPassesThroughUnchanged), - cmocka_unit_test(test_errorPrintOkRetvalPassesThroughUnchanged), - cmocka_unit_test(test_errorPrintErrorRetvalPassesThroughUnchanged), - cmocka_unit_test(test_errorThrowImplNullStateAsserts), - cmocka_unit_test(test_errorThrowImplOkCodeAsserts), - cmocka_unit_test(test_errorThrowImplNullFileAsserts), - cmocka_unit_test(test_errorThrowImplNullFunctionAsserts), - cmocka_unit_test(test_errorThrowImplNegativeLineAsserts), - cmocka_unit_test(test_errorThrowImplNullMessageAsserts), - cmocka_unit_test(test_errorOkAssertsIfPreviousErrorNotCaught), - cmocka_unit_test(test_errorChainImplNullStateAsserts), - cmocka_unit_test(test_errorChainImplNullMessageAsserts), - cmocka_unit_test(test_errorCatchNullStateAsserts), - cmocka_unit_test(test_errorCatchNullMessageAsserts), - cmocka_unit_test(test_errorPrintNullStateAsserts), - cmocka_unit_test(test_errorPrintNullMessageAsserts), - cmocka_unit_test(test_errorCatch_ok), - cmocka_unit_test(test_errorCatch_error), - cmocka_unit_test(test_error_thread_isolation), - cmocka_unit_test(test_error_concurrent_throw), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/item/CMakeLists.txt b/test/item/CMakeLists.txt deleted file mode 100644 index ee723335..00000000 --- a/test/item/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_inventory.c) \ No newline at end of file diff --git a/test/item/test_inventory.c b/test/item/test_inventory.c deleted file mode 100644 index 75d13302..00000000 --- a/test/item/test_inventory.c +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/item/inventory.h" -#include "util/memory.h" - -static itemid_t ITEM_ID_POTION; -static itemid_t ITEM_ID_POTATO; -static itemid_t ITEM_ID_APPLE; - -// Registers three test items (POTION as MEDICINE, POTATO/APPLE as FOOD) -// directly into the global ITEM registry -- these tests exercise the -// inventory/backpack layer only, not the JSON-driven itemInit() pipeline. -static int_t itemFixtureSetup(void **state) { - memoryZero(&ITEM, sizeof(ITEM)); - ITEM.typeCount = ITEM_TYPE_NULL + 1; - ITEM.defCount = ITEM_ID_NULL + 1; - - const itemtype_t medicine = itemTypeGetOrCreate("MEDICINE"); - const itemtype_t food = itemTypeGetOrCreate("FOOD"); - - ITEM_ID_POTION = ITEM.defCount++; - ITEM.defs[ITEM_ID_POTION].type = medicine; - - ITEM_ID_POTATO = ITEM.defCount++; - ITEM.defs[ITEM_ID_POTATO].type = food; - - ITEM_ID_APPLE = ITEM.defCount++; - ITEM.defs[ITEM_ID_APPLE].type = food; - - return 0; -} - -static void test_inventoryInit(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - - inventoryInit(&inventory, storage, 5); - - // Should setup struct - assert_non_null(inventory.storage); - assert_int_equal(inventory.storageSize, 5); - - // Should zero the item ids. - for(size_t i = 0; i < inventory.storageSize; i++) { - assert_int_equal(inventory.storage[i].item, ITEM_ID_NULL); - assert_int_equal(inventory.storage[i].quantity, 0); - } - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryInit(NULL, storage, 5)); - - // Should fail when given NULL storage pointer - expect_assert_failure(inventoryInit(&inventory, NULL, 5)); - - // Should fail when given zero storage size - expect_assert_failure(inventoryInit(&inventory, storage, 0)); -} - -static void test_inventoryItemExists(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - inventoryInit(&inventory, storage, 5); - - // Initially should not exist - assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION)); - - // Add item - inventorySet(&inventory, ITEM_ID_POTION, 3); - assert_true(inventoryItemExists(&inventory, ITEM_ID_POTION)); - - // Remove item - inventorySet(&inventory, ITEM_ID_POTION, 0); - assert_false(inventoryItemExists(&inventory, ITEM_ID_POTION)); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryItemExists(NULL, ITEM_ID_POTION)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION)); - inventory.storageSize = 5; - - // Should fail when given ITEM_ID_NULL - expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_NULL)); - - // Should fail if item has zero quantity somehow - inventorySet(&inventory, ITEM_ID_POTION, 3); - inventory.storage[0].quantity = 0; - expect_assert_failure(inventoryItemExists(&inventory, ITEM_ID_POTION)); -} - -static void test_inventorySet(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - inventoryInit(&inventory, storage, 5); - - // Set item quantity - inventorySet(&inventory, ITEM_ID_POTION, 4); - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].quantity, 4); - - // Update item quantity - inventorySet(&inventory, ITEM_ID_POTION, 2); - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].quantity, 2); - - // Remove item by setting quantity to 0 - inventorySet(&inventory, ITEM_ID_POTION, 0); - assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL); - - // Setting multiple items should not cause issues - inventorySet(&inventory, ITEM_ID_POTION, 1); - inventorySet(&inventory, ITEM_ID_POTATO, 5); - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].quantity, 1); - assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO); - assert_int_equal(inventory.storage[1].quantity, 5); - assert_int_equal(inventory.storage[2].item, ITEM_ID_NULL); - - // Setting early item to 0 should shift others down - inventorySet(&inventory, ITEM_ID_POTION, 0);// Item 0 removed - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTATO); - assert_int_equal(inventory.storage[0].quantity, 5); - assert_int_equal(inventory.storage[1].item, ITEM_ID_NULL); - - // Setting non-existing item should add it - inventorySet(&inventory, ITEM_ID_POTION, 3); - assert_int_equal(inventory.storage[1].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[1].quantity, 3); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventorySet(NULL, ITEM_ID_POTION, 3)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventorySet(&inventory, ITEM_ID_POTION, 3)); - inventory.storageSize = 5; - - // Should fail when given ITEM_ID_NULL - expect_assert_failure(inventorySet(&inventory, ITEM_ID_NULL, 3)); -} - -static void test_inventoryAdd(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - inventoryInit(&inventory, storage, 5); - - // Add item quantity - inventoryAdd(&inventory, ITEM_ID_POTION, 4); - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].quantity, 4); - - // Add more to existing item - inventoryAdd(&inventory, ITEM_ID_POTION, 3); - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].quantity, 7); - - // Adding item that would overflow should assert - expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 250)); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryAdd(NULL, ITEM_ID_POTION, 3)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_POTION, 3)); - inventory.storageSize = 5; - - // Should fail when given ITEM_ID_NULL - expect_assert_failure(inventoryAdd(&inventory, ITEM_ID_NULL, 3)); -} - -static void test_inventoryRemove(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - inventoryInit(&inventory, storage, 5); - - // Add item and then remove it - inventorySet(&inventory, ITEM_ID_POTION, 5); - inventoryRemove(&inventory, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL); - - // Removing non-existing item should do nothing - inventoryRemove(&inventory, ITEM_ID_POTION); - assert_int_equal(inventory.storage[0].item, ITEM_ID_NULL); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryRemove(NULL, ITEM_ID_POTION)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_POTION)); - inventory.storageSize = 5; - - // Should fail when given ITEM_ID_NULL - expect_assert_failure(inventoryRemove(&inventory, ITEM_ID_NULL)); -} - -static void test_inventoryGetCount(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - inventoryInit(&inventory, storage, 5); - - // Initially should be zero - assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0); - - // Add item and check count - inventorySet(&inventory, ITEM_ID_POTION, 4); - assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 4); - - // Update item quantity and check count - inventorySet(&inventory, ITEM_ID_POTION, 2); - assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 2); - - // Remove item and check count - inventorySet(&inventory, ITEM_ID_POTION, 0); - assert_int_equal(inventoryGetCount(&inventory, ITEM_ID_POTION), 0); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryGetCount(NULL, ITEM_ID_POTION)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_POTION)); - inventory.storageSize = 5; - - // Should fail when given ITEM_ID_NULL - expect_assert_failure(inventoryGetCount(&inventory, ITEM_ID_NULL)); -} - -static void test_inventoryIsFull(void **state) { - - - inventorystack_t storage[2]; - inventory_t inventory; - inventoryInit(&inventory, storage, 2); - - // Initially should not be full - assert_false(inventoryIsFull(&inventory)); - - // Fill inventory - inventorySet(&inventory, ITEM_ID_POTION, 1); - inventorySet(&inventory, ITEM_ID_POTATO, 1); - assert_true(inventoryIsFull(&inventory)); - - // Remove one item - inventoryRemove(&inventory, ITEM_ID_POTION); - assert_false(inventoryIsFull(&inventory)); - - // Add one item back - inventorySet(&inventory, ITEM_ID_POTION, 1); - assert_true(inventoryIsFull(&inventory)); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryIsFull(NULL)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventoryIsFull(&inventory)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventoryIsFull(&inventory)); - inventory.storageSize = 2; -} - -static void test_inventoryItemFull(void **state) { - - - inventorystack_t storage[2]; - inventory_t inventory; - inventoryInit(&inventory, storage, 2); - - // Initially should not be full - assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION)); - - // Add some potions, but not too many. - inventorySet(&inventory, ITEM_ID_POTION, 100); - assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION)); - - // Fill with potions - inventorySet(&inventory, ITEM_ID_POTION, ITEM_STACK_QUANTITY_MAX); - assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION)); - - // Add potatoes, should not affect potions - inventorySet(&inventory, ITEM_ID_POTATO, 50); - assert_true(inventoryItemFull(&inventory, ITEM_ID_POTION)); - assert_false(inventoryItemFull(&inventory, ITEM_ID_POTATO)); - - // Remove some potions - inventorySet(&inventory, ITEM_ID_POTION, 200); - assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION)); - - // Fill with potatoes - inventorySet(&inventory, ITEM_ID_POTATO, ITEM_STACK_QUANTITY_MAX); - assert_false(inventoryItemFull(&inventory, ITEM_ID_POTION)); - assert_true(inventoryItemFull(&inventory, ITEM_ID_POTATO)); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventoryItemFull(NULL, ITEM_ID_POTION)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventoryItemFull(&inventory, ITEM_ID_POTION)); - inventory.storageSize = 2; -} - -static void test_inventorySort(void **state) { - - - inventorystack_t storage[5]; - inventory_t inventory; - inventoryInit(&inventory, storage, 5); - - // Add items out of order - inventorySet(&inventory, ITEM_ID_POTATO, 3); - inventorySet(&inventory, ITEM_ID_APPLE, 5); - inventorySet(&inventory, ITEM_ID_POTION, 2); - - // Sort by ID - inventorySort(&inventory, INVENTORY_SORT_BY_ID, false); - - // Check order - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO); - assert_int_equal(inventory.storage[2].item, ITEM_ID_APPLE); - - // Sort by ID reverse - inventorySort(&inventory, INVENTORY_SORT_BY_ID, true); - assert_int_equal(inventory.storage[0].item, ITEM_ID_APPLE); - assert_int_equal(inventory.storage[1].item, ITEM_ID_POTATO); - assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION); - - // Sort by type ascending - POTION (MEDICINE) sorts before POTATO/APPLE - // (both FOOD). The two FOOD items compare equal, so their relative order - // isn't guaranteed (qsort is not stable) - only the type grouping is. - inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, false); - assert_int_equal(inventory.storage[0].item, ITEM_ID_POTION); - assert_true( - (inventory.storage[1].item == ITEM_ID_POTATO && - inventory.storage[2].item == ITEM_ID_APPLE) || - (inventory.storage[1].item == ITEM_ID_APPLE && - inventory.storage[2].item == ITEM_ID_POTATO) - ); - - // Sort by type reverse - FOOD items first (order unspecified), POTION last. - inventorySort(&inventory, INVENTORY_SORT_BY_TYPE, true); - assert_int_equal(inventory.storage[2].item, ITEM_ID_POTION); - assert_true( - (inventory.storage[0].item == ITEM_ID_POTATO && - inventory.storage[1].item == ITEM_ID_APPLE) || - (inventory.storage[0].item == ITEM_ID_APPLE && - inventory.storage[1].item == ITEM_ID_POTATO) - ); - - // Should fail when given NULL inventory pointer - expect_assert_failure(inventorySort(NULL, INVENTORY_SORT_BY_ID, false)); - - // Should fail when given NULL storage pointer - inventory.storage = NULL; - expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false)); - inventory.storage = storage; - - // Should fail when given zero storage size - inventory.storageSize = 0; - expect_assert_failure(inventorySort(&inventory, INVENTORY_SORT_BY_ID, false)); - inventory.storageSize = 5; -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_inventoryInit), - cmocka_unit_test(test_inventoryItemExists), - cmocka_unit_test(test_inventorySet), - cmocka_unit_test(test_inventoryAdd), - cmocka_unit_test(test_inventoryRemove), - cmocka_unit_test(test_inventoryGetCount), - cmocka_unit_test(test_inventoryIsFull), - cmocka_unit_test(test_inventoryItemFull), - cmocka_unit_test(test_inventorySort), - }; - - return cmocka_run_group_tests(tests, itemFixtureSetup, NULL); -} \ No newline at end of file diff --git a/test/rpg/CMakeLists.txt b/test/rpg/CMakeLists.txt deleted file mode 100644 index bd2d8fb2..00000000 --- a/test/rpg/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_rpg.c) - -# Subdirs -add_subdirectory(overworld) -add_subdirectory(battle) -add_subdirectory(entity) -add_subdirectory(cutscene) \ No newline at end of file diff --git a/test/rpg/battle/CMakeLists.txt b/test/rpg/battle/CMakeLists.txt deleted file mode 100644 index b9d92cfd..00000000 --- a/test/rpg/battle/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_battle.c) diff --git a/test/rpg/battle/test_battle.c b/test/rpg/battle/test_battle.c deleted file mode 100644 index c2112ba1..00000000 --- a/test/rpg/battle/test_battle.c +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/battle/battle.h" - -// TEMPORARY: hardcoded pending a real moves.jsonc-backed registry lookup. -#define BATTLE_MOVE_ID_ATTACK ((battlemoveid_t)1) - -static battlefighter_t *addFighter( - const battlefighterteam_t team, - const battlefightercontroller_t controller, - const uint16_t attack, - const uint16_t defense, - const uint16_t speed, - const uint16_t healthMax -) { - const battlefighterstats_t stats = { - .attack = attack, .defense = defense, .speed = speed - }; - return battleAddFighter(team, controller, stats, healthMax, 0); -} - -static void test_battleStartEntersOpeningThenPreRound(void **state) { - - - battleInit(); - addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 10, 0, 10, 20 - ); - addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 10, 0, 5, 20 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - assert_int_equal(BATTLE.state, BATTLE_STATE_OPENING); - - // OPENING only transitions to PRE_ROUND; it doesn't run PRE_ROUND's logic - // in the same call. - battleUpdate(); - assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND); - assert_int_equal(BATTLE.executionCount, 0); - - // PRE_ROUND builds the execution order and moves on to selection. - battleUpdate(); - assert_int_equal(BATTLE.executionCount, 2); - assert_true( - BATTLE.state == BATTLE_STATE_PLAYER_SELECTION || - BATTLE.state == BATTLE_STATE_AI_SELECTION - ); -} - -static void test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters( - void **state -) { - - - battleInit(); - battlefighter_t *ally1 = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 100, 20 - ); - battlefighter_t *ally2 = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20 - ); - battlefighter_t *enemy = addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 50, 20 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION, positions the cursor - - // ally1 is fastest, so it's first to decide. - assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION); - assert_ptr_equal(battleGetCurrentFighter(), ally1); - - // A forced move (as a scripted-battle cutscene item would push) marks - // ally2 as already decided, so PLAYER_SELECTION must skip it. - battleQueueMove(ally2->id, BATTLE_MOVE_ID_ATTACK, enemy->id); - - battlePlayerAttack(enemy->id); - assert_int_equal(BATTLE.moves[ally1->id].move, BATTLE_MOVE_ID_ATTACK); - assert_int_equal(BATTLE.moves[ally1->id].targets[0], enemy->id); - - // ally2 already had a decision queued, and enemy is AI-controlled, so - // there's nothing left for PLAYER_SELECTION -- it should have moved on. - assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION); -} - -static void test_battleAiSelectionAutoQueuesUndecidedAiFighters(void **state) { - - - battleInit(); - battlefighter_t *ally = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20 - ); - battlefighter_t *enemy = addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION - - battlePlayerAttack(enemy->id);// ally decides, selection moves to AI - assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION); - - battleUpdate();// AI_SELECTION auto-decides for enemy - assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING); - assert_int_equal(BATTLE.moves[enemy->id].move, BATTLE_MOVE_ID_ATTACK); - assert_int_equal(BATTLE.moves[enemy->id].targets[0], ally->id); -} - -static void test_battleMovesExecutingResolvesInSpeedOrderAndFizzles( - void **state -) { - - - battleInit(); - // ally1 is fast enough to kill enemy1 before enemy1 or ally2 act. enemy2 - // keeps the battle alive so execution continues to ally2's now-moot - // attack against the already-dead enemy1. - battlefighter_t *ally1 = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 100, 20 - ); - battlefighter_t *ally2 = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20 - ); - battlefighter_t *enemy1 = addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 50, 10 - ); - battlefighter_t *enemy2 = addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 1, 100 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION (ally1 first, fastest) - - assert_ptr_equal(battleGetCurrentFighter(), ally1); - battlePlayerAttack(enemy1->id); - assert_ptr_equal(battleGetCurrentFighter(), ally2); - battlePlayerAttack(enemy1->id);// both allies target enemy1 - assert_int_equal(BATTLE.state, BATTLE_STATE_AI_SELECTION); - - battleUpdate();// AI_SELECTION - assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING); - - // Execution order: ally1 (100), enemy1 (50), ally2 (10), enemy2 (1). - battleUpdate();// ally1 kills enemy1 - assert_false(battleFighterIsAlive(enemy1)); - assert_int_equal(BATTLE.state, BATTLE_STATE_MOVES_EXECUTING);// enemy2 alive - - battleUpdate();// enemy1's own (now dead) turn is skipped - battleUpdate();// ally2's attack against the dead enemy1 fizzles silently - assert_int_equal(enemy1->health, 0);// no double-kill, no underflow - - battleUpdate();// enemy2 acts - battleUpdate();// all 4 slots processed -> POST_ROUND - assert_int_equal(BATTLE.state, BATTLE_STATE_POST_ROUND); - - battleUpdate();// POST_ROUND -> PRE_ROUND, round advances - assert_int_equal(BATTLE.round, 2); - assert_int_equal(BATTLE.state, BATTLE_STATE_PRE_ROUND); -} - -static void test_battleWinEndsInStateEnded(void **state) { - - - battleInit(); - battlefighter_t *ally = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 50, 0, 10, 20 - ); - battlefighter_t *enemy = addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 1, 0, 5, 10 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION - - battlePlayerAttack(enemy->id);// selection -> AI_SELECTION - battleUpdate();// AI_SELECTION -> MOVES_EXECUTING - battleUpdate();// ally kills the only enemy - - assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED); - assert_int_equal(BATTLE.result, BATTLE_RESULT_WIN); -} - -static void test_battleLossEndsInStateEnded(void **state) { - - - battleInit(); - battlefighter_t *ally = addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 1, 0, 5, 10 - ); - battlefighter_t *enemy = addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 50, 0, 10, 20 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION - - battlePlayerAttack(enemy->id);// selection -> AI_SELECTION - battleUpdate();// AI_SELECTION -> MOVES_EXECUTING - battleUpdate();// ally attacks first (irrelevant to outcome) - battleUpdate();// enemy kills the only ally - - assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED); - assert_int_equal(BATTLE.result, BATTLE_RESULT_LOSS); -} - -static void test_battleFleeEndsInStateEndedImmediately(void **state) { - - - battleInit(); - addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20 - ); - addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, true); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION - - battlePlayerFlee(); - assert_int_equal(BATTLE.state, BATTLE_STATE_ENDED); - assert_int_equal(BATTLE.result, BATTLE_RESULT_FLED); -} - -static void test_battleFleeUnavailableIsIgnored(void **state) { - - - battleInit(); - addFighter( - BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, 5, 0, 10, 20 - ); - addFighter( - BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI, 5, 0, 5, 20 - ); - - battleStart(BATTLE_ENCOUNTER_REGULAR, false); - battleUpdate();// OPENING -> PRE_ROUND - battleUpdate();// PRE_ROUND -> PLAYER_SELECTION - - battlePlayerFlee(); - assert_int_equal(BATTLE.state, BATTLE_STATE_PLAYER_SELECTION); - assert_int_equal(BATTLE.result, BATTLE_RESULT_NONE); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_battleStartEntersOpeningThenPreRound), - cmocka_unit_test(test_battlePlayerSelectionAdvancesAndSkipsDecidedFighters), - cmocka_unit_test(test_battleAiSelectionAutoQueuesUndecidedAiFighters), - cmocka_unit_test(test_battleMovesExecutingResolvesInSpeedOrderAndFizzles), - cmocka_unit_test(test_battleWinEndsInStateEnded), - cmocka_unit_test(test_battleLossEndsInStateEnded), - cmocka_unit_test(test_battleFleeEndsInStateEndedImmediately), - cmocka_unit_test(test_battleFleeUnavailableIsIgnored), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/cutscene/CMakeLists.txt b/test/rpg/cutscene/CMakeLists.txt deleted file mode 100644 index 52464928..00000000 --- a/test/rpg/cutscene/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_cutscene.c) -dusktest(test_cutscenesystem.c) -dusktest(test_cutscenecontrol.c) -dusktest(test_cutscenemaparea.c) diff --git a/test/rpg/cutscene/test_cutscene.c b/test/rpg/cutscene/test_cutscene.c deleted file mode 100644 index 2f0457cf..00000000 --- a/test/rpg/cutscene/test_cutscene.c +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/cutscene/cutscene.h" -#include "util/memory.h" -#include "util/string.h" - -// ============================================================ -// cutsceneRemoveFront -// ============================================================ - -static cutsceneitem_t MARKER_ITEM(const char_t *name) { - cutsceneitem_t item; - memoryZero(&item, sizeof(item)); - item.type = CUTSCENE_ITEM_TYPE_MARKER; - stringCopy(item.marker.name, name, CUTSCENE_MARKER_NAME_MAX - 1); - return item; -} - -static void test_cutsceneRemoveFrontShiftsRemainderToIndexZero( - void **state -) { - - cutsceneitem_t items[4] = { - MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c"), MARKER_ITEM("d") - }; - cutscene_t scene = { .items = items, .itemCount = 4, .itemsMax = 4 }; - - cutsceneRemoveFront(&scene, 2); - - assert_int_equal(scene.itemCount, 2); - assert_string_equal(scene.items[0].marker.name, "c"); - assert_string_equal(scene.items[1].marker.name, "d"); -} - -static void test_cutsceneRemoveFrontAllItemsLeavesEmptyScene(void **state) { - - cutsceneitem_t items[3] = { - MARKER_ITEM("a"), MARKER_ITEM("b"), MARKER_ITEM("c") - }; - cutscene_t scene = { .items = items, .itemCount = 3, .itemsMax = 3 }; - - cutsceneRemoveFront(&scene, 3); - - assert_int_equal(scene.itemCount, 0); -} - -static void test_cutsceneRemoveFrontZeroIsNoop(void **state) { - - cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") }; - cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 }; - - cutsceneRemoveFront(&scene, 0); - - assert_int_equal(scene.itemCount, 2); - assert_string_equal(scene.items[0].marker.name, "a"); - assert_string_equal(scene.items[1].marker.name, "b"); -} - -static void test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount( - void **state -) { - - cutsceneitem_t items[2] = { MARKER_ITEM("a"), MARKER_ITEM("b") }; - cutscene_t scene = { .items = items, .itemCount = 2, .itemsMax = 2 }; - - expect_assert_failure(cutsceneRemoveFront(&scene, 3)); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_cutsceneRemoveFrontShiftsRemainderToIndexZero), - cmocka_unit_test(test_cutsceneRemoveFrontAllItemsLeavesEmptyScene), - cmocka_unit_test(test_cutsceneRemoveFrontZeroIsNoop), - cmocka_unit_test(test_cutsceneRemoveFrontAssertsWhenCountExceedsItemCount), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/cutscene/test_cutscenecontrol.c b/test/rpg/cutscene/test_cutscenecontrol.c deleted file mode 100644 index 60b4d57d..00000000 --- a/test/rpg/cutscene/test_cutscenecontrol.c +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "time/time.h" -#include "util/memory.h" - -static void test_cutsceneWaitCompletesAfterItsDuration(void **state) { - - cutsceneitem_t item = { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }; - cutsceneitemdata_t data; - memoryZero(&data, sizeof(data)); - - cutsceneWaitStart(&item, &data); - - TIME.delta = 0.5f; - assert_false(cutsceneWaitUpdate(&item, &data)); - - TIME.delta = 0.6f; - assert_true(cutsceneWaitUpdate(&item, &data)); -} - -static void *lastUserData; -static uint8_t callbackCallCount; - -static void recordCallback(void *userData) { - lastUserData = userData; - callbackCallCount++; -} - -static void test_cutsceneCallbackFiresOnStartWithNullUserData(void **state) { - - callbackCallCount = 0; - lastUserData = (void *)0x1; - - cutsceneitem_t item = - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback }; - cutsceneitemdata_t data; - - cutsceneCallbackStart(&item, &data); - assert_int_equal(callbackCallCount, 1); - assert_null(lastUserData); - - // Callback items always complete immediately -- the effect already - // happened in Start, not Update. - assert_true(cutsceneCallbackUpdate(&item, &data)); - assert_int_equal(callbackCallCount, 1);// Update doesn't fire it again -} - -static void test_cutsceneCallbackNullIsNoop(void **state) { - - callbackCallCount = 0; - cutsceneitem_t item = - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = NULL }; - cutsceneitemdata_t data; - - cutsceneCallbackStart(&item, &data);// should not crash - assert_int_equal(callbackCallCount, 0); -} - -static void test_cutsceneSetPauseAppliesImmediately(void **state) { - - CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, - .setPause = CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE - }; - cutsceneitemdata_t data; - - cutsceneSetPauseStart(&item, &data); - assert_int_equal( - CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_BATTLE - ); - assert_true(cutsceneSetPauseUpdate(&item, &data)); -} - -static void test_cutsceneConcurrentCompletesOnceAllChildrenDo(void **state) { - - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_CONCURRENT, - .concurrent = { - .items = (const cutsceneitem_t[]){ - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.2f }, - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f } - }, - .count = 2 - } - }; - cutsceneitemdata_t data; - memoryZero(&data, sizeof(data)); - - cutsceneConcurrentStart(&item, &data); - - TIME.delta = 0.3f; - // Child 0 (0.2s) elapses; child 1 (0.5s) still has 0.2s left. - assert_false(cutsceneConcurrentUpdate(&item, &data)); - - TIME.delta = 0.3f; - // Child 1 now elapses too. - assert_true(cutsceneConcurrentUpdate(&item, &data)); -} - -static void test_cutsceneConcurrentDoesNotReUpdateFinishedChildren( - void **state -) { - - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_CONCURRENT, - .concurrent = { - .items = (const cutsceneitem_t[]){ - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.1f }, - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 10.0f } - }, - .count = 2 - } - }; - cutsceneitemdata_t data; - memoryZero(&data, sizeof(data)); - - cutsceneConcurrentStart(&item, &data); - - TIME.delta = 0.2f; - cutsceneConcurrentUpdate(&item, &data);// child 0 finishes - - // If child 0 were re-updated, its stored wait value would keep dropping - // further below zero -- not observable directly, but the overall result - // must not falsely report done just because child 0 keeps completing. - TIME.delta = 0.2f; - assert_false(cutsceneConcurrentUpdate(&item, &data)); -} - -static void test_cutsceneConcurrentCannotNest(void **state) { - - cutsceneitem_t outer = { - .type = CUTSCENE_ITEM_TYPE_CONCURRENT, - .concurrent = { - .items = (const cutsceneitem_t[]){ - { - .type = CUTSCENE_ITEM_TYPE_CONCURRENT, - .concurrent = { - .items = (const cutsceneitem_t[]){ - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f } - }, - .count = 1 - } - } - }, - .count = 1 - } - }; - cutsceneitemdata_t data; - - expect_assert_failure(cutsceneConcurrentStart(&outer, &data)); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_cutsceneWaitCompletesAfterItsDuration), - cmocka_unit_test(test_cutsceneCallbackFiresOnStartWithNullUserData), - cmocka_unit_test(test_cutsceneCallbackNullIsNoop), - cmocka_unit_test(test_cutsceneSetPauseAppliesImmediately), - cmocka_unit_test(test_cutsceneConcurrentCompletesOnceAllChildrenDo), - cmocka_unit_test(test_cutsceneConcurrentDoesNotReUpdateFinishedChildren), - cmocka_unit_test(test_cutsceneConcurrentCannotNest), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/cutscene/test_cutscenemaparea.c b/test/rpg/cutscene/test_cutscenemaparea.c deleted file mode 100644 index 9c70980f..00000000 --- a/test/rpg/cutscene/test_cutscenemaparea.c +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/overworld/maparea.h" -#include "util/memory.h" - -static void resetMapAreas(void) { - memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); -} - -static void test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated( - void **state -) { - - resetMapAreas(); - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, - .mapAreaAdd = { - .min = { 0, 0, 0 }, .max = { 5, 5, 0 }, - .callback = mapAreaNoopCallback, - .notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL - } - }; - cutsceneitemdata_t data; - - cutsceneMapAreaAddStart(&item, &data); - assert_true(cutsceneMapAreaAddUpdate(&item, &data)); - - uint8_t id = CUTSCENE_SYSTEM.areaLastCreated; - assert_ptr_equal(MAP_AREAS[id].callback, mapAreaNoopCallback); - assert_int_equal(MAP_AREAS[id].max.x, 5); -} - -static void test_cutsceneMapAreaRemoveClearsSlot(void **state) { - - resetMapAreas(); - uint8_t id = mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, - .mapAreaRemove = { .areaId = id } - }; - cutsceneitemdata_t data; - - cutsceneMapAreaRemoveStart(&item, &data); - assert_true(cutsceneMapAreaRemoveUpdate(&item, &data)); - assert_null(MAP_AREAS[id].callback); -} - -static void test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel( - void **state -) { - - resetMapAreas(); - cutsceneitem_t addItem = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, - .mapAreaAdd = { - .min = { 0, 0, 0 }, .max = { 5, 5, 0 }, - .callback = mapAreaNoopCallback, - .notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL - } - }; - cutsceneitemdata_t data; - cutsceneMapAreaAddStart(&addItem, &data); - uint8_t id = CUTSCENE_SYSTEM.areaLastCreated; - - cutsceneitem_t removeItem = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, - .mapAreaRemove = { .areaId = CUTSCENE_REF_AREA_LAST_CREATED } - }; - cutsceneMapAreaRemoveStart(&removeItem, &data); - assert_null(MAP_AREAS[id].callback); -} - -static void test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges( - void **state -) { - - resetMapAreas(); - uint8_t id = mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, - .mapAreaWait = { .areaIds = { id }, .count = 1 } - }; - cutsceneitemdata_t data; - - cutsceneMapAreaWaitStart(&item, &data); - assert_false(cutsceneMapAreaWaitUpdate(&item, &data)); - - MAP_AREAS[id].triggerCount++;// simulates the area's callback firing - assert_true(cutsceneMapAreaWaitUpdate(&item, &data)); -} - -static void test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges( - void **state -) { - - resetMapAreas(); - uint8_t idA = mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - uint8_t idB = mapAreaAdd( - (worldpos_t){ 10, 10, 0 }, (worldpos_t){ 15, 15, 0 }, - mapAreaNoopCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - - cutsceneitem_t item = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, - .mapAreaWait = { .areaIds = { idA, idB }, .count = 2 } - }; - cutsceneitemdata_t data; - - cutsceneMapAreaWaitStart(&item, &data); - assert_false(cutsceneMapAreaWaitUpdate(&item, &data)); - - MAP_AREAS[idB].triggerCount++;// only the second watched area fires - assert_true(cutsceneMapAreaWaitUpdate(&item, &data)); -} - -static void test_cutsceneMapAreaWaitResolvesLastCreatedSentinel( - void **state -) { - - resetMapAreas(); - cutsceneitem_t addItem = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, - .mapAreaAdd = { - .min = { 0, 0, 0 }, .max = { 5, 5, 0 }, - .callback = mapAreaNoopCallback, - .notify = MAP_AREA_NOTIFY_ALL, .trigger = MAP_TRIGGER_ALL - } - }; - cutsceneitemdata_t data; - cutsceneMapAreaAddStart(&addItem, &data); - uint8_t id = CUTSCENE_SYSTEM.areaLastCreated; - - cutsceneitem_t waitItem = { - .type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, - .mapAreaWait = { .areaIds = { CUTSCENE_REF_AREA_LAST_CREATED }, .count = 1 } - }; - cutsceneMapAreaWaitStart(&waitItem, &data); - assert_false(cutsceneMapAreaWaitUpdate(&waitItem, &data)); - - MAP_AREAS[id].triggerCount++; - assert_true(cutsceneMapAreaWaitUpdate(&waitItem, &data)); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_cutsceneMapAreaAddCreatesAreaAndStoresLastCreated), - cmocka_unit_test(test_cutsceneMapAreaRemoveClearsSlot), - cmocka_unit_test(test_cutsceneMapAreaRemoveResolvesLastCreatedSentinel), - cmocka_unit_test(test_cutsceneMapAreaWaitCompletesWhenTriggerCountChanges), - cmocka_unit_test( - test_cutsceneMapAreaWaitCompletesWhenAnyWatchedAreaChanges - ), - cmocka_unit_test(test_cutsceneMapAreaWaitResolvesLastCreatedSentinel), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/cutscene/test_cutscenesystem.c b/test/rpg/cutscene/test_cutscenesystem.c deleted file mode 100644 index 845257fd..00000000 --- a/test/rpg/cutscene/test_cutscenesystem.c +++ /dev/null @@ -1,583 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "rpg/entity/entity.h" -#include "time/time.h" -#include "util/string.h" - -static uint8_t callbackFired; - -static void recordCallback(void *userData) { - callbackFired++; -} - -// INNER is jumped into by OUTER's second item, never returning -- this is -// the "nested cutscene is a one-way jump" behavior: cutsceneCutsceneStart -// replaces CUTSCENE_SYSTEM.scene outright, and the callback item's effect -// fires in Start, not Update, so it fires the same frame the jump happens. -static cutsceneitem_t CUTSCENE_TEST_INNER_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordCallback } -}; -static cutscene_t CUTSCENE_TEST_INNER = { - .items = CUTSCENE_TEST_INNER_ITEMS, - .itemCount = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t), - .itemsMax = sizeof(CUTSCENE_TEST_INNER_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -static cutsceneitem_t CUTSCENE_TEST_OUTER_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 0.5f }, - { - .type = CUTSCENE_ITEM_TYPE_CUTSCENE, - .cutsceneRef = { .cutscene = &CUTSCENE_TEST_INNER } - } -}; -static cutscene_t CUTSCENE_TEST_OUTER = { - .items = CUTSCENE_TEST_OUTER_ITEMS, - .itemCount = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t), - .itemsMax = sizeof(CUTSCENE_TEST_OUTER_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_DEFAULT -}; - -static void test_cutsceneSystemStartSetsUpInitialItem(void **state) { - - cutsceneSystemInit(); - - cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER); - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER); - assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0); - assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_DEFAULT); -} - -static void test_cutsceneSystemNestedCutsceneIsOneWayJump(void **state) { - - cutsceneSystemInit(); - callbackFired = 0; - cutsceneSystemStartCutscene(&CUTSCENE_TEST_OUTER); - - TIME.delta = 0.1f; - cutsceneSystemUpdate();// wait not elapsed yet - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_OUTER); - assert_int_equal(callbackFired, 0); - - // The wait elapses, advancing to the nested-cutscene item, which jumps - // straight into INNER and starts its first item (the callback) -- all - // within this single update call. - TIME.delta = 1.0f; - cutsceneSystemUpdate(); - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INNER); - assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0); - assert_int_equal(callbackFired, 1); - - // INNER's only item (the callback) always reports complete -- one more - // update ends the whole cutscene. - cutsceneSystemUpdate(); - assert_null(CUTSCENE_SYSTEM.scene); - assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF); - assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE); -} - -static void test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene(void **state) { - - cutsceneSystemInit(); - - cutsceneSystemUpdate();// should not crash - assert_null(CUTSCENE_SYSTEM.scene); -} - -static cutsceneitem_t CUTSCENE_TEST_SINGLE_WAIT_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f } -}; -static cutscene_t CUTSCENE_TEST_SINGLE_WAIT = { - .items = CUTSCENE_TEST_SINGLE_WAIT_ITEMS, - .itemCount = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t), - .itemsMax = sizeof(CUTSCENE_TEST_SINGLE_WAIT_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -static void test_cutsceneSystemStartWithSetsInteractEntities(void **state) { - - cutsceneSystemInit(); - entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); - entityInit(&ENTITIES[1], ENTITY_TYPE_NPC); - - cutsceneSystemStartCutsceneWith( - &CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[1] - ); - - assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[0]); - assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[1]); - assert_ptr_equal( - cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT), &ENTITIES[0] - ); - assert_ptr_equal( - cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED), &ENTITIES[1] - ); - - TIME.delta = 2.0f; - cutsceneSystemUpdate();// ends the cutscene - assert_null(CUTSCENE_SYSTEM.refs.entityInteract);// reset on end - assert_null(CUTSCENE_SYSTEM.refs.entityInteracted); -} - -static void test_cutsceneSystemGetEntitySentinelsRequireBeingSet( - void **state -) { - - cutsceneSystemInit(); - - expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACT)); - expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_INTERACTED)); - expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_CREATED)); - expect_assert_failure(cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF)); -} - -static void test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef( - void **state -) { - - cutsceneSystemInit(); - entityInit(&ENTITIES[2], ENTITY_TYPE_NPC); - - entity_t *resolved = cutsceneSystemGetEntity(2); - assert_ptr_equal(resolved, &ENTITIES[2]); - // Resolving by direct index also updates LAST_REF. - assert_ptr_equal( - cutsceneSystemGetEntity(CUTSCENE_ENTITY_LAST_REF), &ENTITIES[2] - ); -} - -static void test_cutsceneSystemGetAreaId(void **state) { - - cutsceneSystemInit(); - // cutsceneSystemInit() zero-inits the field -- only actually starting a - // cutscene sets it to the "nothing created yet" sentinel. - CUTSCENE_SYSTEM.refs.areaLastCreated = CUTSCENE_REF_AREA_LAST_CREATED; - - expect_assert_failure(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED)); - assert_int_equal(cutsceneSystemGetAreaId(5), 5);// direct IDs pass through - - CUTSCENE_SYSTEM.refs.areaLastCreated = 3; - assert_int_equal(cutsceneSystemGetAreaId(CUTSCENE_REF_AREA_LAST_CREATED), 3); -} - -static void test_cutsceneSystemGetTextMiniId(void **state) { - - cutsceneSystemInit(); - CUTSCENE_SYSTEM.refs.textMiniLastCreated = CUTSCENE_REF_TEXT_MINI_LAST_CREATED; - - expect_assert_failure( - cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED) - ); - assert_int_equal(cutsceneSystemGetTextMiniId(4), 4); - - CUTSCENE_SYSTEM.refs.textMiniLastCreated = 1; - assert_int_equal( - cutsceneSystemGetTextMiniId(CUTSCENE_REF_TEXT_MINI_LAST_CREATED), 1 - ); -} - -static void test_cutsceneSystemDisposeResetsState(void **state) { - - cutsceneSystemInit(); - entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); - cutsceneSystemStartCutsceneWith( - &CUTSCENE_TEST_SINGLE_WAIT, &ENTITIES[0], &ENTITIES[0] - ); - - cutsceneSystemDispose(); - assert_null(CUTSCENE_SYSTEM.scene); - assert_int_equal(CUTSCENE_SYSTEM.currentItem, 0xFF); - assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NONE); - assert_null(CUTSCENE_SYSTEM.refs.entityInteract); - assert_null(CUTSCENE_SYSTEM.refs.entityInteracted); -} - -// --- loadedFile survival across cutsceneSystemPrepare ------------------- -// -// cutsceneCutsceneResolve (and cutsceneSystemLoad) stamp -// CUTSCENE_SYSTEM.loadedFile with what they just parsed loadedScene from -// - cutsceneSystemGoTo needs that to survive so it can reload the same source -// later. cutsceneCutsceneResolve's caller stamps it BEFORE calling -// cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene), so -// cutsceneSystemPrepare must not blindly clear loadedFile on every -// start - only when switching away from loadedScene to some other -// cutscene_t entirely (see cutsceneSystemPrepare's doc comment). This -// regression-tests the exact bug hit via initial.jsonc's CUTSCENE item -// into main_menu.jsonc: cutsceneCutsceneResolve stamped loadedFile, then -// the immediately following cutsceneSystemStartCutscene call wiped it -// straight back out, so a later cutsceneSystemGoTo on the still-running -// cutscene had nothing to reload from. -static void test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene( - void **state -) { - - cutsceneSystemInit(); - - // Simulate cutsceneCutsceneResolve: populate loadedScene/loadedItems - // and stamp loadedFile with its source, before the cutscene has - // actually started running. - CUTSCENE_SYSTEM.loadedItems[0] = - (cutsceneitem_t){ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }; - CUTSCENE_SYSTEM.loadedScene = (cutscene_t){ - .items = CUTSCENE_SYSTEM.loadedItems, - .itemCount = 1, - .itemsMax = CUTSCENE_LOADED_ITEMS_MAX, - .pause = CUTSCENE_PAUSE_NONE - }; - stringCopy( - CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc", - ASSET_FILE_NAME_MAX - 1 - ); - - cutsceneSystemStartCutscene(&CUTSCENE_SYSTEM.loadedScene); - - assert_string_equal( - CUTSCENE_SYSTEM.loadedFile, "cutscenes/main_menu.jsonc" - ); - - // Starting a genuinely different cutscene, though, must still - // invalidate it - there's nothing left to reload it from. - cutsceneSystemStartCutscene(&CUTSCENE_TEST_SINGLE_WAIT); - - assert_string_equal(CUTSCENE_SYSTEM.loadedFile, ""); -} - -// --- CUTSCENE_INSERT --------------------------------------------------- -// -// INSERT now splices its target's items directly into the running -// scene's own array (see cutsceneSystemInsertCutscene) rather than -// swapping CUTSCENE_SYSTEM.scene to a different cutscene_t and pushing a -// return frame - CUTSCENE_SYSTEM.scene therefore stays the same outer -// cutscene throughout every test below. cutsceneSystemNext pops each -// already-run item off the front of the array as it advances (see -// CUTSCENE_SYSTEM.currentItem's doc comment), so itemCount reflects only -// what's left to run at any given point, not a running total of -// everything ever spliced in - the itemCount assertions below are sized -// accordingly. Since the running scene's own array is what gets appended -// into, any outer cutscene used with INSERT must still declare its items -// array (and .itemsMax) with enough spare capacity for whatever ends up -// spliced into it at once, including transitively through any nested -// INSERT. - -static uint8_t insertOrderLog[8]; -static uint8_t insertOrderLogCount; - -static void recordOrder0(void *userData) { insertOrderLog[insertOrderLogCount++] = 0; } -static void recordOrder1(void *userData) { insertOrderLog[insertOrderLogCount++] = 1; } -static void recordOrder2(void *userData) { insertOrderLog[insertOrderLogCount++] = 2; } -static void recordOrderA(void *userData) { insertOrderLog[insertOrderLogCount++] = 3; } -static void recordOrderB(void *userData) { insertOrderLog[insertOrderLogCount++] = 4; } - -static cutsceneitem_t CUTSCENE_TEST_INSERT_SNIPPET_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA }, - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 2.0f }, - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB } -}; -static cutscene_t CUTSCENE_TEST_INSERT_SNIPPET = { - .items = CUTSCENE_TEST_INSERT_SNIPPET_ITEMS, - .itemCount = - sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), - .itemsMax = - sizeof(CUTSCENE_TEST_INSERT_SNIPPET_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -// 5 items to start, +3 spare slots for CUTSCENE_TEST_INSERT_SNIPPET's items. -static cutsceneitem_t CUTSCENE_TEST_INSERT_OUTER_ITEMS[8] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f }, - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder1 }, - { - .type = CUTSCENE_ITEM_TYPE_INSERT, - .insert = { .cutscene = &CUTSCENE_TEST_INSERT_SNIPPET } - }, - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 } -}; -static cutscene_t CUTSCENE_TEST_INSERT_OUTER = { - .items = CUTSCENE_TEST_INSERT_OUTER_ITEMS, - .itemCount = 5, - .itemsMax = sizeof(CUTSCENE_TEST_INSERT_OUTER_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -static void test_cutsceneInsertSplicesItemsInPlaceThenResumes(void **state) { - - insertOrderLogCount = 0; - cutsceneSystemInit(); - cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_OUTER); - - TIME.delta = 1.5f;// elapses the outer WAIT(1.0) - cutsceneSystemUpdate();// order0 done -> starts the outer WAIT - cutsceneSystemUpdate();// the WAIT elapses -> starts order1 - cutsceneSystemUpdate();// order1 done -> starts the INSERT, cascading into orderA - - // The splice happened in place - CUTSCENE_SYSTEM.scene never changed. - // order0/the WAIT/order1/the INSERT item itself have all since been - // popped off the front, leaving the snippet's 3 items (orderA now - // running, its WAIT, orderB) plus order2. - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER); - assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 4); - - TIME.delta = 2.5f;// elapses the snippet's WAIT(2.0) - cutsceneSystemUpdate();// orderA done -> starts snippet's WAIT - cutsceneSystemUpdate();// snippet's WAIT elapses -> starts orderB - cutsceneSystemUpdate();// orderB done -> continues on to order2 - - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_OUTER); - - cutsceneSystemUpdate();// order2 done -> cutscene ends naturally - assert_null(CUTSCENE_SYSTEM.scene); - - const uint8_t expected[] = { 0, 1, 3, 4, 2 }; - assert_int_equal(insertOrderLogCount, sizeof(expected)); - assert_memory_equal(insertOrderLog, expected, sizeof(expected)); -} - -static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } -}; -static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET = { - .items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS, - .itemCount = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) / - sizeof(cutsceneitem_t), - .itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET_ITEMS) / - sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -// 2 items to start, +1 spare slot for the snippet's single item. -static cutsceneitem_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS[3] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, - { - .type = CUTSCENE_ITEM_TYPE_INSERT, - .insert = { - .cutscene = &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_SNIPPET - } - } -}; -static cutscene_t CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER = { - .items = CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS, - .itemCount = 2, - .itemsMax = sizeof(CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER_ITEMS) / - sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -static void test_cutsceneInsertAsLastItemEndsCutsceneNaturally( - void **state -) { - - insertOrderLogCount = 0; - cutsceneSystemInit(); - cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER); - - cutsceneSystemUpdate();// order0 done -> starts INSERT, cascades to orderA - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_AS_LAST_ITEM_OUTER); - // order0 and the INSERT item itself have both been popped off the - // front by now, leaving just orderA (currently running). - assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1); - - // orderA is the snippet's only item, and the snippet was spliced in as - // the outer's last item -- finishing it should reach the natural end - // of the whole cutscene, not get stuck. - cutsceneSystemUpdate(); - assert_null(CUTSCENE_SYSTEM.scene); - - const uint8_t expected[] = { 0, 3 }; - assert_int_equal(insertOrderLogCount, sizeof(expected)); - assert_memory_equal(insertOrderLog, expected, sizeof(expected)); -} - -static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } -}; -static cutscene_t CUTSCENE_TEST_INSERT_NESTED_INNER = { - .items = CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS, - .itemCount = - sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t), - .itemsMax = - sizeof(CUTSCENE_TEST_INSERT_NESTED_INNER_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -// INSERT is MID's first item, so entering MID (from OUTER) cascades -// straight through into INNER within the same Start chain, one level -// deeper than a single insert. MID is only ever read from as a splice -// source (see CUTSCENE_TEST_INSERT_NESTED_OUTER) - it's never itself the -// running scene, so it needs no spare capacity of its own. -static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS[] = { - { - .type = CUTSCENE_ITEM_TYPE_INSERT, - .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_INNER } - }, - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderB } -}; -static cutscene_t CUTSCENE_TEST_INSERT_NESTED_MID = { - .items = CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS, - .itemCount = - sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t), - .itemsMax = - sizeof(CUTSCENE_TEST_INSERT_NESTED_MID_ITEMS) / sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -// 3 items to start, +2 spare for MID's items, +1 more for MID's own -// nested INSERT(INNER) once MID's items become part of this array too. -static cutsceneitem_t CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS[6] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder0 }, - { - .type = CUTSCENE_ITEM_TYPE_INSERT, - .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NESTED_MID } - }, - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrder2 } -}; -static cutscene_t CUTSCENE_TEST_INSERT_NESTED_OUTER = { - .items = CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS, - .itemCount = 3, - .itemsMax = sizeof(CUTSCENE_TEST_INSERT_NESTED_OUTER_ITEMS) / - sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NONE -}; - -static void test_cutsceneInsertNestsThroughMultipleLevels(void **state) { - - insertOrderLogCount = 0; - cutsceneSystemInit(); - cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NESTED_OUTER); - - // order0 done -> starts INSERT(MID), cascading through INSERT(INNER) -> - // orderA, all within this one Update call's Start chain - both splices - // land in OUTER's own array. - cutsceneSystemUpdate(); - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_NESTED_OUTER); - // order0 and both INSERT items themselves have all been popped off the - // front by now, leaving orderA (currently running), orderB, order2. - assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 3); - - cutsceneSystemUpdate();// orderA done -> continues on to orderB - cutsceneSystemUpdate();// orderB done -> continues on to order2 - cutsceneSystemUpdate();// order2 done -> ends naturally - assert_null(CUTSCENE_SYSTEM.scene); - - const uint8_t expected[] = { 0, 3, 4, 2 }; - assert_int_equal(insertOrderLogCount, sizeof(expected)); - assert_memory_equal(insertOrderLog, expected, sizeof(expected)); -} - -static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = recordOrderA } -}; -static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET = { - .items = CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET_ITEMS, - .itemCount = 1, - .itemsMax = 1, - .pause = CUTSCENE_PAUSE_NONE -}; - -// No spare capacity at all - itemsMax equals itemCount, leaving no room -// to splice anything into it. -static cutsceneitem_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS[1] = { - { - .type = CUTSCENE_ITEM_TYPE_INSERT, - .insert = { .cutscene = &CUTSCENE_TEST_INSERT_NO_CAPACITY_SNIPPET } - } -}; -static cutscene_t CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER = { - .items = CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER_ITEMS, - .itemCount = 1, - .itemsMax = 1, - .pause = CUTSCENE_PAUSE_NONE -}; - -static void test_cutsceneInsertAssertsWhenExceedingItemsMax(void **state) { - - cutsceneSystemInit(); - - expect_assert_failure( - cutsceneSystemStartCutscene(&CUTSCENE_TEST_INSERT_NO_CAPACITY_OUTER) - ); -} - -static void noopInsertCallback(void *userData) {} - -static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = noopInsertCallback } -}; -static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_SNIPPET = { - .items = CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS, - .itemCount = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) / - sizeof(cutsceneitem_t), - .itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_SNIPPET_ITEMS) / - sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_ALL -}; - -// 1 item to start, +1 spare slot for the snippet's single item. -static cutsceneitem_t CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS[2] = { - { - .type = CUTSCENE_ITEM_TYPE_INSERT, - .insert = { .cutscene = &CUTSCENE_TEST_INSERT_PERSIST_SNIPPET } - } -}; -static cutscene_t CUTSCENE_TEST_INSERT_PERSIST_OUTER = { - .items = CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS, - .itemCount = 1, - .itemsMax = sizeof(CUTSCENE_TEST_INSERT_PERSIST_OUTER_ITEMS) / - sizeof(cutsceneitem_t), - .pause = CUTSCENE_PAUSE_NPC -}; - -static void test_cutsceneInsertDoesNotResetPauseOrInteractEntities( - void **state -) { - - cutsceneSystemInit(); - entityInit(&ENTITIES[3], ENTITY_TYPE_PLAYER); - entityInit(&ENTITIES[4], ENTITY_TYPE_NPC); - - cutsceneSystemStartCutsceneWith( - &CUTSCENE_TEST_INSERT_PERSIST_OUTER, &ENTITIES[3], &ENTITIES[4] - ); - - // The INSERT item's Start already spliced the snippet into place and - // cascaded into it by now - and since the INSERT item itself has since - // been popped off the front, only the snippet's one item is left. - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INSERT_PERSIST_OUTER); - assert_int_equal(CUTSCENE_SYSTEM.scene->itemCount, 1); - assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteract, &ENTITIES[3]); - assert_ptr_equal(CUTSCENE_SYSTEM.refs.entityInteracted, &ENTITIES[4]); - // The snippet declares ALL, but INSERT doesn't apply it -- the outer - // cutscene's NPC pause, already in effect, stands. - assert_int_equal(CUTSCENE_SYSTEM.pause, CUTSCENE_PAUSE_NPC); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_cutsceneSystemStartSetsUpInitialItem), - cmocka_unit_test(test_cutsceneSystemNestedCutsceneIsOneWayJump), - cmocka_unit_test(test_cutsceneSystemUpdateIsNoopWithNoActiveCutscene), - cmocka_unit_test(test_cutsceneSystemStartWithSetsInteractEntities), - cmocka_unit_test(test_cutsceneSystemGetEntitySentinelsRequireBeingSet), - cmocka_unit_test(test_cutsceneSystemGetEntityDirectIndexUpdatesLastRef), - cmocka_unit_test(test_cutsceneSystemGetAreaId), - cmocka_unit_test(test_cutsceneSystemGetTextMiniId), - cmocka_unit_test(test_cutsceneSystemDisposeResetsState), - cmocka_unit_test( - test_cutsceneSystemPrepareKeepsLoadedFileWhenReenteringLoadedScene - ), - cmocka_unit_test(test_cutsceneInsertSplicesItemsInPlaceThenResumes), - cmocka_unit_test(test_cutsceneInsertAsLastItemEndsCutsceneNaturally), - cmocka_unit_test(test_cutsceneInsertNestsThroughMultipleLevels), - cmocka_unit_test(test_cutsceneInsertAssertsWhenExceedingItemsMax), - cmocka_unit_test(test_cutsceneInsertDoesNotResetPauseOrInteractEntities), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/entity/CMakeLists.txt b/test/rpg/entity/CMakeLists.txt deleted file mode 100644 index 453ff3da..00000000 --- a/test/rpg/entity/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_entity.c) -target_sources(test_entity PRIVATE entitytestfixture.c) - -dusktest(test_npc.c) -target_sources(test_npc PRIVATE entitytestfixture.c) - -dusktest(test_entityinteract.c) -target_sources(test_entityinteract PRIVATE entitytestfixture.c) - -dusktest(test_entityitem.c) -target_sources(test_entityitem PRIVATE entitytestfixture.c) diff --git a/test/rpg/entity/entitytestfixture.c b/test/rpg/entity/entitytestfixture.c deleted file mode 100644 index bbc9d447..00000000 --- a/test/rpg/entity/entitytestfixture.c +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "entitytestfixture.h" -#include "rpg/overworld/map.h" -#include "rpg/overworld/tile.h" -#include "util/memory.h" -#include "util/string.h" - -void entityTestFixtureReset(void) { - memoryZero(ENTITIES, sizeof(ENTITIES)); - - memoryZero(&MAP, sizeof(map_t)); - stringCopy(MAP.name, "test", MAP_NAME_MAX); - MAP.chunkPosition = (chunkpos_t){ 0, 0, 0 }; - - chunk_t *chunk = &MAP.chunks[0]; - chunk->position = (chunkpos_t){ 0, 0, 0 }; - for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) { - chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 }; - } - for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) { - chunk->entities[i] = 0xFF; - } - - MAP.chunkOrder[0] = chunk; -} diff --git a/test/rpg/entity/entitytestfixture.h b/test/rpg/entity/entitytestfixture.h deleted file mode 100644 index 89e591e4..00000000 --- a/test/rpg/entity/entitytestfixture.h +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "rpg/entity/entity.h" - -/** - * Resets ENTITIES[] to all-empty and builds a single minimal, synchronously - * "loaded" chunk at chunk position (0, 0, 0) on the MAP global, entirely - * TILE_SHAPE_GROUND at local Z 0. Sufficient for entityWalk/Turn/Run, chunk - * bookkeeping, and NPC movement tests -- does not exercise the real async - * chunk-loading pipeline (mapChunkLoad). - * - * World positions with x/y in [0, CHUNK_WIDTH-1]/[0, CHUNK_HEIGHT-1] and - * z == 0 fall within the fixture chunk. Individual tests may override - * specific columns afterward (e.g. to place a ramp). - */ -void entityTestFixtureReset(void); diff --git a/test/rpg/entity/test_entity.c b/test/rpg/entity/test_entity.c deleted file mode 100644 index 54a2bcf0..00000000 --- a/test/rpg/entity/test_entity.c +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "entitytestfixture.h" -#include "rpg/overworld/map.h" -#include "rpg/overworld/worldpos.h" - -static void setTile( - const worldpos_t pos, const tileshape_t shape, const uint8_t z -) { - chunktileindex_t index = worldPosToChunkTileIndex(&pos); - MAP.chunks[0].tiles[index] = (tile_t){ .shape = shape, .z = z }; -} - -static void test_entityInit(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[3], ENTITY_TYPE_ITEM); - assert_int_equal(ENTITIES[3].id, 3); - assert_int_equal(ENTITIES[3].type, ENTITY_TYPE_ITEM); - assert_int_equal(ENTITIES[3].chunkIndex, 0xFF); - assert_int_equal(ENTITIES[3].globalId, ENTITY_GLOBAL_ID_NULL); - // entityItemInit wires the interact callback -- confirms init dispatch ran. - assert_int_equal(ENTITIES[3].interact.type, ENTITY_INTERACT_CALLBACK); - - expect_assert_failure(entityInit(NULL, ENTITY_TYPE_ITEM)); - expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_NULL)); - expect_assert_failure(entityInit(&ENTITIES[0], ENTITY_TYPE_COUNT)); - - entity_t offBounds; - expect_assert_failure(entityInit(&offBounds, ENTITY_TYPE_ITEM)); -} - -static void test_entityGetAvailable(void **state) { - - entityTestFixtureReset(); - - assert_int_equal(entityGetAvailable(), 0); - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - assert_int_equal(entityGetAvailable(), 1); - entityInit(&ENTITIES[1], ENTITY_TYPE_NPC); - assert_int_equal(entityGetAvailable(), 2); -} - -static void test_entityGetAt(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 3, 4, 0 }; - - assert_ptr_equal(entityGetAt((worldpos_t){ 3, 4, 0 }), &ENTITIES[0]); - assert_null(entityGetAt((worldpos_t){ 3, 4, 1 })); - assert_null(entityGetAt((worldpos_t){ 0, 0, 0 })); -} - -static void test_entityGetByGlobalId(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[2], ENTITY_TYPE_NPC); - ENTITIES[2].globalId = 42; - - assert_ptr_equal(entityGetByGlobalId(42), &ENTITIES[2]); - assert_null(entityGetByGlobalId(43)); - assert_null(entityGetByGlobalId(ENTITY_GLOBAL_ID_NULL)); -} - -static void test_entityCanUnload(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START - 1; - assert_true(entityCanUnload(&ENTITIES[0])); - - ENTITIES[0].globalId = ENTITY_GLOBAL_ID_START; - assert_false(entityCanUnload(&ENTITIES[0])); -} - -static void test_entityCanTurnWalkRun(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - assert_true(entityCanTurn(&ENTITIES[0])); - assert_true(entityCanWalk(&ENTITIES[0])); - assert_true(entityCanRun(&ENTITIES[0])); - - ENTITIES[0].animation = ENTITY_ANIM_WALK; - assert_false(entityCanTurn(&ENTITIES[0])); - assert_false(entityCanWalk(&ENTITIES[0])); - assert_false(entityCanRun(&ENTITIES[0])); - - ENTITIES[0].animation = ENTITY_ANIM_IDLE; - ENTITIES[0].walkEndCooldown = 0.1f; - assert_false(entityCanTurn(&ENTITIES[0]));// cooldown blocks turning only - assert_true(entityCanWalk(&ENTITIES[0])); -} - -static void test_entityTurn(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - entityTurn(&ENTITIES[0], ENTITY_DIR_EAST); - assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_EAST); - assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_TURN); - - // Can't turn again mid-turn. - ENTITIES[0].direction = ENTITY_DIR_NORTH; - entityTurn(&ENTITIES[0], ENTITY_DIR_WEST); - assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH); -} - -static void test_entityWalkOnOpenGround(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 5, 0 }; - - entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH); - assert_int_equal(ENTITIES[0].position.x, 5); - assert_int_equal(ENTITIES[0].position.y, 6); - assert_int_equal(ENTITIES[0].position.z, 0); - assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_WALK); - assert_int_equal(ENTITIES[0].direction, ENTITY_DIR_NORTH); - // entityUpdateChunk should have assigned it to the fixture chunk. - assert_int_equal(ENTITIES[0].chunkIndex, 0); - assert_int_equal(MAP.chunks[0].entities[0], 0); -} - -static void test_entityWalkBlockedAtChunkEdge(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 0, 0, 0 }; - - // West/south of the loaded chunk is unloaded (TILE_NULL) -- blocked. - entityWalk(&ENTITIES[0], ENTITY_DIR_WEST); - assert_int_equal(ENTITIES[0].position.x, 0); - assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE); -} - -static void test_entityWalkBlockedByOtherEntity(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 5, 0 }; - - entityInit(&ENTITIES[1], ENTITY_TYPE_NPC); - ENTITIES[1].position = (worldpos_t){ 5, 6, 0 };// directly north - - entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH); - assert_int_equal(ENTITIES[0].position.x, 5); - assert_int_equal(ENTITIES[0].position.y, 5); - assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE); -} - -static void test_entityWalkCannotWhileNotIdle(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 5, 0 }; - ENTITIES[0].animation = ENTITY_ANIM_WALK; - - entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH); - assert_int_equal(ENTITIES[0].position.y, 5);// unchanged -} - -// Ramp at (5,5) facing north, raised GROUND at (5,6)/z1 -- walking north -// off the ramp raises the entity by one Z layer, and walking back south -// from the raised tile falls back down onto the ramp. -static void setUpRamp(void) { - setTile((worldpos_t){ 5, 5, 0 }, TILE_SHAPE_RAMP_NORTH, 0); - setTile((worldpos_t){ 5, 6, 0 }, TILE_SHAPE_GROUND, 1); -} - -static void test_entityWalkUpRamp(void **state) { - - entityTestFixtureReset(); - setUpRamp(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 5, 0 }; - - entityWalk(&ENTITIES[0], ENTITY_DIR_NORTH); - assert_int_equal(ENTITIES[0].position.x, 5); - assert_int_equal(ENTITIES[0].position.y, 6); - assert_int_equal(ENTITIES[0].position.z, 1); -} - -static void test_entityWalkFallDownRamp(void **state) { - - entityTestFixtureReset(); - setUpRamp(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 6, 1 }; - - entityWalk(&ENTITIES[0], ENTITY_DIR_SOUTH); - assert_int_equal(ENTITIES[0].position.x, 5); - assert_int_equal(ENTITIES[0].position.y, 5); - assert_int_equal(ENTITIES[0].position.z, 0); -} - -static void test_entityWalkCannotClimbRampFromTheSide(void **state) { - - entityTestFixtureReset(); - setUpRamp(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 5, 0 }; - - // Only NORTH climbs this ramp -- EAST off the ramp tile is just blocked - // ground movement, not a climb. - entityWalk(&ENTITIES[0], ENTITY_DIR_EAST); - assert_int_equal(ENTITIES[0].position.x, 6); - assert_int_equal(ENTITIES[0].position.z, 0); -} - -static void test_entityRun(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = (worldpos_t){ 5, 5, 0 }; - - entityRun(&ENTITIES[0], ENTITY_DIR_NORTH); - assert_int_equal(ENTITIES[0].position.y, 6); - assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_RUN); -} - -static void test_entitySetChunkAndUpdateChunk(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - entitySetChunk(&ENTITIES[0], 0); - assert_int_equal(ENTITIES[0].chunkIndex, 0); - assert_int_equal(MAP.chunks[0].entities[0], 0); - - entitySetChunk(&ENTITIES[0], 0xFF); - assert_int_equal(ENTITIES[0].chunkIndex, 0xFF); - assert_int_equal(MAP.chunks[0].entities[0], 0xFF); - - ENTITIES[0].position = (worldpos_t){ 3, 3, 0 }; - entityUpdateChunk(&ENTITIES[0]); - assert_int_equal(ENTITIES[0].chunkIndex, 0); -} - -static void test_entityPositionSet(void **state) { - - entityTestFixtureReset(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].animation = ENTITY_ANIM_WALK; - ENTITIES[0].walkEndCooldown = 5; - - entityPositionSet(&ENTITIES[0], (worldpos_t){ 7, 8, 0 }); - assert_int_equal(ENTITIES[0].position.x, 7); - assert_int_equal(ENTITIES[0].position.y, 8); - assert_int_equal(ENTITIES[0].animation, ENTITY_ANIM_IDLE); - assert_int_equal(ENTITIES[0].walkEndCooldown, 0); - assert_int_equal(ENTITIES[0].chunkIndex, 0);// updated as a side effect -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_entityInit), - cmocka_unit_test(test_entityGetAvailable), - cmocka_unit_test(test_entityGetAt), - cmocka_unit_test(test_entityGetByGlobalId), - cmocka_unit_test(test_entityCanUnload), - cmocka_unit_test(test_entityCanTurnWalkRun), - cmocka_unit_test(test_entityTurn), - cmocka_unit_test(test_entityWalkOnOpenGround), - cmocka_unit_test(test_entityWalkBlockedAtChunkEdge), - cmocka_unit_test(test_entityWalkBlockedByOtherEntity), - cmocka_unit_test(test_entityWalkCannotWhileNotIdle), - cmocka_unit_test(test_entityWalkUpRamp), - cmocka_unit_test(test_entityWalkFallDownRamp), - cmocka_unit_test(test_entityWalkCannotClimbRampFromTheSide), - cmocka_unit_test(test_entityRun), - cmocka_unit_test(test_entitySetChunkAndUpdateChunk), - cmocka_unit_test(test_entityPositionSet), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/entity/test_entityinteract.c b/test/rpg/entity/test_entityinteract.c deleted file mode 100644 index 4980e21f..00000000 --- a/test/rpg/entity/test_entityinteract.c +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "entitytestfixture.h" -#include "ui/rpg/textbox/uitextboxmain.h" -#include "ui/focus/uifocus.h" -#include "rpg/cutscene/cutscenesystem.h" - -static cutsceneitem_t CUTSCENE_TEST_INTERACT_ITEMS[] = { - { .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = 1.0f } -}; -static cutscene_t CUTSCENE_TEST_INTERACT = { - .items = CUTSCENE_TEST_INTERACT_ITEMS, - .itemCount = 1, - .itemsMax = 1, - .pause = CUTSCENE_PAUSE_NONE -}; - -static entity_t *player; -static entity_t *target; - -static void resetInteractFixture(void) { - entityTestFixtureReset(); - uiFocusInit(); - uiTextboxMainInit(); - uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test - cutsceneSystemInit(); - - entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); - entityInit(&ENTITIES[1], ENTITY_TYPE_NPC); - player = &ENTITIES[0]; - target = &ENTITIES[1]; - player->direction = ENTITY_DIR_NORTH; -} - -static void test_entityInteractWithNull(void **state) { - - resetInteractFixture(); - - target->interact.type = ENTITY_INTERACT_NULL; - entityInteractWith(player, target);// should not crash or change anything - assert_int_equal(target->interact.type, ENTITY_INTERACT_NULL); -} - -static void test_entityInteractWithPrintTurnsNpcToFacePlayer(void **state) { - - resetInteractFixture(); - - target->interact.type = ENTITY_INTERACT_PRINT; - target->interact.data.message = "Hello!"; - - entityInteractWith(player, target); - - assert_true(uiTextboxMainIsActive()); - assert_int_equal( - target->data.npc.interactState, NPC_INTERACT_STATE_CONVERSING - ); - // NPC turns to face the opposite of the player's facing direction. - assert_int_equal(target->direction, entityDirGetOpposite(player->direction)); -} - -static entity_t *callbackPlayerArg; -static entity_t *callbackTargetArg; -static uint8_t callbackCount; - -static void recordInteractCallback(entity_t *p, entity_t *t) { - callbackPlayerArg = p; - callbackTargetArg = t; - callbackCount++; -} - -static void test_entityInteractWithCallback(void **state) { - - resetInteractFixture(); - callbackCount = 0; - - target->interact.type = ENTITY_INTERACT_CALLBACK; - target->interact.data.callback = recordInteractCallback; - - entityInteractWith(player, target); - - assert_int_equal(callbackCount, 1); - assert_ptr_equal(callbackPlayerArg, player); - assert_ptr_equal(callbackTargetArg, target); -} - -static void test_entityInteractWithCallbackRequiresNonNull(void **state) { - - resetInteractFixture(); - - target->interact.type = ENTITY_INTERACT_CALLBACK; - target->interact.data.callback = NULL; - - expect_assert_failure(entityInteractWith(player, target)); -} - -static void test_entityInteractWithCutsceneStartsIt(void **state) { - - resetInteractFixture(); - - target->interact.type = ENTITY_INTERACT_CUTSCENE; - target->interact.data.cutscene = &CUTSCENE_TEST_INTERACT; - - entityInteractWith(player, target); - - assert_ptr_equal(CUTSCENE_SYSTEM.scene, &CUTSCENE_TEST_INTERACT); - assert_ptr_equal(CUTSCENE_SYSTEM.entityInteract, player); - assert_ptr_equal(CUTSCENE_SYSTEM.entityInteracted, target); -} - -static void test_entityInteractWithRequiresNonNullEntities(void **state) { - - resetInteractFixture(); - - expect_assert_failure(entityInteractWith(NULL, target)); - expect_assert_failure(entityInteractWith(player, NULL)); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_entityInteractWithNull), - cmocka_unit_test(test_entityInteractWithPrintTurnsNpcToFacePlayer), - cmocka_unit_test(test_entityInteractWithCallback), - cmocka_unit_test(test_entityInteractWithCallbackRequiresNonNull), - cmocka_unit_test(test_entityInteractWithCutsceneStartsIt), - cmocka_unit_test(test_entityInteractWithRequiresNonNullEntities), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/entity/test_entityitem.c b/test/rpg/entity/test_entityitem.c deleted file mode 100644 index f4936ec2..00000000 --- a/test/rpg/entity/test_entityitem.c +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "entitytestfixture.h" -#include "ui/rpg/textbox/uitextboxmain.h" -#include "ui/focus/uifocus.h" - -// An arbitrary, non-null itemid_t -- entityItemSet just stores/reports it -// verbatim, it doesn't need a real item registered in ITEM. -#define TEST_ITEM_ID ((itemid_t)1) - -static entity_t *setUpItemEntity(void) { - entityTestFixtureReset(); - uiFocusInit(); - uiTextboxMainInit(); - uiTextboxMainFocusClosed(NULL);// force-clear stale focus from a prior test - - entityInit(&ENTITIES[0], ENTITY_TYPE_ITEM); - return &ENTITIES[0]; -} - -static void test_entityItemInit(void **state) { - - entity_t *item = setUpItemEntity(); - - assert_int_equal(item->interact.type, ENTITY_INTERACT_CALLBACK); - assert_ptr_equal(item->interact.data.callback, entityItemInteract); -} - -static void test_entityItemSet(void **state) { - - entity_t *item = setUpItemEntity(); - - entityItemSet(item, TEST_ITEM_ID, 3); - assert_int_equal(item->data.item.item, TEST_ITEM_ID); - assert_int_equal(item->data.item.quantity, 3); - assert_false(item->data.item.collected); -} - -// entityItemInteract itself is NOT covered here: it unconditionally calls -// itemGive(), which calls itemGetName(), which dereferences -// LOCALE.entry->data.locale -- a real locale asset populated only by the -// async asset-loading pipeline (see localemanager.h/assetlocaleloader.h). -// That's exactly the asset-loading dependency this test pass scoped out; -// faking it cheaply isn't possible without hand-building the loader's -// internal hash format, which is more coupling than it's worth. Backpack -// bookkeeping itself is already covered by test/item/test_inventory.c-style -// tests at the inventory/backpack layer. - -static void test_entityItemMovementWaitsForCollectionAndTextbox(void **state) { - - entity_t *item = setUpItemEntity(); - - // Not collected yet -- stays. - entityItemMovement(item); - assert_int_equal(item->type, ENTITY_TYPE_ITEM); - - // Collected (set directly -- see note above on why entityItemInteract - // itself isn't driven here), but a message is still showing -- stays. - item->data.item.collected = true; - uiTextboxMainSetText("Picked up!"); - entityItemMovement(item); - assert_int_equal(item->type, ENTITY_TYPE_ITEM); - - // Once the textbox is dismissed, the entity despawns. - uiFocusPop(); - assert_false(uiTextboxMainIsActive()); - entityItemMovement(item); - assert_int_equal(item->type, ENTITY_TYPE_NULL); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_entityItemInit), - cmocka_unit_test(test_entityItemSet), - cmocka_unit_test(test_entityItemMovementWaitsForCollectionAndTextbox), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/entity/test_npc.c b/test/rpg/entity/test_npc.c deleted file mode 100644 index aaa5174b..00000000 --- a/test/rpg/entity/test_npc.c +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "entitytestfixture.h" -#include "rpg/cutscene/cutscenesystem.h" -#include "time/time.h" - -static entity_t *setUpNpc(const worldpos_t position) { - entityTestFixtureReset(); - entityInit(&ENTITIES[0], ENTITY_TYPE_NPC); - ENTITIES[0].position = position; - return &ENTITIES[0]; -} - -static void test_npcSetMoveType(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 }); - - npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN); - assert_int_equal(npc->data.npc.moveType, NPC_MOVE_TYPE_RANDOM_TURN); - // Init ran: timer was seeded within the default frequency range. - assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f); -} - -static void test_npcRandomTurnMovementFiresOnceTimerElapses(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN); - npc->data.npc.moveData.randomTurn.timer = 0.05f; - - TIME.delta = 0.01f; - npcRandomTurnMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// timer not elapsed yet - - TIME.delta = 1.0f; - npcRandomTurnMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_TURN); - // Timer is reseeded, not left at/below zero. - assert_true(npc->data.npc.moveData.randomTurn.timer > 0.0f); -} - -static void test_npcRandomWalkMovementFiresOnceTimerElapses(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK); - npc->data.npc.moveData.randomWalk.timer = 0.05f; - - TIME.delta = 0.01f; - npcRandomWalkMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_IDLE); - - TIME.delta = 1.0f; - npcRandomWalkMovement(npc); - // Open ground on all sides -- whichever direction is chosen, it moves. - assert_int_equal(npc->animation, ENTITY_ANIM_WALK); -} - -static void test_npcRandomTurnAndWalkMovementRunsBothIndependently( - void **state -) { - - entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_TURN_AND_WALK); - npcrandomturnandwalk_t *tw = &npc->data.npc.moveData.randomTurnAndWalk; - tw->turn.timer = 999.0f;// don't fire this tick - tw->walk.timer = 0.05f;// fires this tick - - TIME.delta = 1.0f; - npcRandomTurnAndWalkMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_WALK); - assert_true(tw->turn.timer > 900.0f);// unaffected by the walk firing -} - -static void test_npcMovementGatedByCutscenePause(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK); - npc->data.npc.moveData.randomWalk.timer = 0.0f; - TIME.delta = 1.0f; - - CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NPC; - npcMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked by pause - - CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE; - npcMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_WALK); -} - -static void test_npcMovementGatedByInteractState(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 8, 8, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_RANDOM_WALK); - npc->data.npc.moveData.randomWalk.timer = 0.0f; - TIME.delta = 1.0f; - - npc->data.npc.interactState = NPC_INTERACT_STATE_CONVERSING; - npcMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_IDLE);// blocked while conversing - - npc->data.npc.interactState = NPC_INTERACT_STATE_NONE; - npcMovement(npc); - assert_int_equal(npc->animation, ENTITY_ANIM_WALK); -} - -static void test_npcPathMovementFollowsAndLoopsWaypoints(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_PATH); - npcpath_t *path = &npc->data.npc.moveData.path; - npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 6, 0 }); - npcPathAddNode(&npc->data.npc, (worldpos_t){ 5, 5, 0 }); - - npcPathMovement(npc);// steps toward waypoint 0 - assert_int_equal(npc->position.x, 5); - assert_int_equal(npc->position.y, 6); - assert_int_equal(path->index, 0);// not yet considered "arrived" - - npc->animation = ENTITY_ANIM_IDLE;// simulate the walk animation finishing - npcPathMovement(npc);// arrives at waypoint 0, advances, steps toward wp 1 - assert_int_equal(path->index, 1); - assert_int_equal(npc->position.x, 5); - assert_int_equal(npc->position.y, 5); -} - -static void test_npcPathMovementNoopWithEmptyPath(void **state) { - - entity_t *npc = setUpNpc((worldpos_t){ 5, 5, 0 }); - npcSetMoveType(npc, NPC_MOVE_TYPE_PATH); - - npcPathMovement(npc); - assert_int_equal(npc->position.x, 5); - assert_int_equal(npc->position.y, 5); - assert_int_equal(npc->animation, ENTITY_ANIM_IDLE); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_npcSetMoveType), - cmocka_unit_test(test_npcRandomTurnMovementFiresOnceTimerElapses), - cmocka_unit_test(test_npcRandomWalkMovementFiresOnceTimerElapses), - cmocka_unit_test(test_npcRandomTurnAndWalkMovementRunsBothIndependently), - cmocka_unit_test(test_npcMovementGatedByCutscenePause), - cmocka_unit_test(test_npcMovementGatedByInteractState), - cmocka_unit_test(test_npcPathMovementFollowsAndLoopsWaypoints), - cmocka_unit_test(test_npcPathMovementNoopWithEmptyPath), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/overworld/CMakeLists.txt b/test/rpg/overworld/CMakeLists.txt deleted file mode 100644 index 7f8ecc94..00000000 --- a/test/rpg/overworld/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_maparea.c) - -# Subdirs \ No newline at end of file diff --git a/test/rpg/overworld/test_maparea.c b/test/rpg/overworld/test_maparea.c deleted file mode 100644 index 259200af..00000000 --- a/test/rpg/overworld/test_maparea.c +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "rpg/overworld/maparea.h" -#include "rpg/overworld/map.h" -#include "rpg/entity/entity.h" -#include "util/memory.h" - -static uint8_t callbackCount; -static uint8_t lastTrigger; - -static void recordCallback(entity_t *entity, const uint8_t trigger) { - callbackCount++; - lastTrigger = trigger; -} - -static void resetMapAreas(void) { - memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); - callbackCount = 0; - lastTrigger = 0; -} - -static void test_mapAreaInit(void **state) { - - - maparea_t area; - const worldpos_t min = { 5, 5, 0 }; - const worldpos_t max = { 0, 0, 0 }; - - // min/max should be normalized regardless of argument order. - mapAreaInit( - &area, min, max, recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - assert_int_equal(area.min.x, 0); - assert_int_equal(area.min.y, 0); - assert_int_equal(area.max.x, 5); - assert_int_equal(area.max.y, 5); - assert_int_equal(area.triggerCount, 0); - - expect_assert_failure( - mapAreaInit(&area, min, max, NULL, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL) - ); -} - -static void test_mapAreaIsInside(void **state) { - - - maparea_t area; - mapAreaInit( - &area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - - assert_true(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 0 })); - assert_true(mapAreaIsInside(&area, (worldpos_t){ 0, 0, 0 }));// inclusive min - assert_true( - mapAreaIsInside(&area, (worldpos_t){ 10, 10, 0 }) - );// inclusive max - assert_false(mapAreaIsInside(&area, (worldpos_t){ 11, 5, 0 })); - assert_false(mapAreaIsInside(&area, (worldpos_t){ 5, 5, 1 })); -} - -static void test_mapAreaShouldNotify(void **state) { - - - maparea_t area; - mapAreaInit( - &area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 10, 10, 0 }, - recordCallback, MAP_AREA_NOTIFY_PLAYER, MAP_TRIGGER_ALL - ); - - entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); - entityInit(&ENTITIES[1], ENTITY_TYPE_NPC); - entityInit(&ENTITIES[2], ENTITY_TYPE_ITEM); - - assert_true(mapAreaShouldNotify(&area, &ENTITIES[0])); - assert_false(mapAreaShouldNotify(&area, &ENTITIES[1]));// not notified - assert_false(mapAreaShouldNotify(&area, &ENTITIES[2]));// item never notifies - - area.notify = MAP_AREA_NOTIFY_ALL; - assert_true(mapAreaShouldNotify(&area, &ENTITIES[1])); -} - -static void test_mapAreaAddAndRemove(void **state) { - - resetMapAreas(); - - uint8_t id = mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - assert_int_equal(id, 0); - assert_ptr_equal(MAP_AREAS[0].callback, recordCallback); - - // Adding again should reuse the next free slot, not the same one. - uint8_t id2 = mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - assert_int_equal(id2, 1); - - mapAreaRemove(id); - assert_null(MAP_AREAS[0].callback);// slot freed - - // Removing frees the slot for reuse. - uint8_t id3 = mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - assert_int_equal(id3, 0); -} - -static void test_mapAreaCheckEntityTriggersEnterStepExit(void **state) { - - resetMapAreas(); - - mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - - entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); - ENTITIES[0].position = (worldpos_t){ 10, 10, 0 };// outside - - // Outside -> outside: no callback. - mapAreaCheckEntity(&ENTITIES[0]); - assert_int_equal(callbackCount, 0); - assert_int_equal(MAP_AREAS[0].triggerCount, 0); - - // Outside -> inside: ENTER. - ENTITIES[0].position = (worldpos_t){ 2, 2, 0 }; - mapAreaCheckEntity(&ENTITIES[0]); - assert_int_equal(callbackCount, 1); - assert_int_equal(lastTrigger, MAP_TRIGGER_ENTER); - assert_int_equal(MAP_AREAS[0].triggerCount, 1); - - // Inside -> inside: STEP, every subsequent call. - mapAreaCheckEntity(&ENTITIES[0]); - assert_int_equal(callbackCount, 2); - assert_int_equal(lastTrigger, MAP_TRIGGER_STEP); - - mapAreaCheckEntity(&ENTITIES[0]); - assert_int_equal(callbackCount, 3); - assert_int_equal(lastTrigger, MAP_TRIGGER_STEP); - - // Inside -> outside: EXIT. - ENTITIES[0].position = (worldpos_t){ 10, 10, 0 }; - mapAreaCheckEntity(&ENTITIES[0]); - assert_int_equal(callbackCount, 4); - assert_int_equal(lastTrigger, MAP_TRIGGER_EXIT); -} - -static void test_mapAreaCheckEntityRespectsTriggerMask(void **state) { - - resetMapAreas(); - - // Only interested in ENTER, not STEP or EXIT. - mapAreaAdd( - (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ENTER - ); - - entityInit(&ENTITIES[0], ENTITY_TYPE_PLAYER); - ENTITIES[0].position = (worldpos_t){ 2, 2, 0 }; - - mapAreaCheckEntity(&ENTITIES[0]);// ENTER: fires - assert_int_equal(callbackCount, 1); - - mapAreaCheckEntity(&ENTITIES[0]);// STEP: masked out - assert_int_equal(callbackCount, 1); - - ENTITIES[0].position = (worldpos_t){ 10, 10, 0 }; - mapAreaCheckEntity(&ENTITIES[0]);// EXIT: masked out - assert_int_equal(callbackCount, 1); -} - -static void test_mapAreaCanUnload(void **state) { - - resetMapAreas(); - memoryZero(&MAP, sizeof(map_t)); - - maparea_t area; - mapAreaInit( - &area, (worldpos_t){ 0, 0, 0 }, (worldpos_t){ 5, 5, 0 }, - recordCallback, MAP_AREA_NOTIFY_ALL, MAP_TRIGGER_ALL - ); - - // No chunk overlaps the area's bounds (all chunks default to position 0, - // 0, 0 after memoryZero -- move them far away first). - for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) { - MAP.chunks[i].position = (chunkpos_t){ 100, 100, 100 }; - } - assert_true(mapAreaCanUnload(&area)); - - // One chunk overlapping the area's bounds blocks unload. - MAP.chunks[0].position = (chunkpos_t){ 0, 0, 0 }; - assert_false(mapAreaCanUnload(&area)); -} - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_mapAreaInit), - cmocka_unit_test(test_mapAreaIsInside), - cmocka_unit_test(test_mapAreaShouldNotify), - cmocka_unit_test(test_mapAreaAddAndRemove), - cmocka_unit_test(test_mapAreaCheckEntityTriggersEnterStepExit), - cmocka_unit_test(test_mapAreaCheckEntityRespectsTriggerMask), - cmocka_unit_test(test_mapAreaCanUnload), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/rpg/test_rpg.c b/test/rpg/test_rpg.c deleted file mode 100644 index 7a5e36d7..00000000 --- a/test/rpg/test_rpg.c +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" - -int main(int argc, char** argv) { - const struct CMUnitTest tests[] = { - // Add RPG tests here in the future - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/save/CMakeLists.txt b/test/save/CMakeLists.txt deleted file mode 100644 index d4164abb..00000000 --- a/test/save/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_savesettings.c) -dusktest(test_saveslot.c) -target_sources(test_saveslot PRIVATE savetestfixture.c) - -dusktest(test_savedevicelinux.c) -target_sources(test_savedevicelinux PRIVATE savetestfixture.c) - -dusktest(test_savedevice.c) -target_sources(test_savedevice PRIVATE savetestfixture.c) - -dusktest(test_save.c) -target_sources(test_save PRIVATE savetestfixture.c) diff --git a/test/save/savetestfixture.c b/test/save/savetestfixture.c deleted file mode 100644 index eb1aee01..00000000 --- a/test/save/savetestfixture.c +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -// nftw()/FTW_DEPTH/FTW_PHYS are glibc extensions gated behind this. -#define _XOPEN_SOURCE 700 - -#include "savetestfixture.h" -#include "assert/assert.h" -#include "asset/asset.h" -#include "asset/loader/assetloader.h" -#include "asset/loader/assetentry.h" -#include "util/memory.h" -#include "util/string.h" -#include -#include -#include -#include -#include - -static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX]; -static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX]; -static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME; - -static const char_t *SAVE_TEST_FIXTURE_PARTY_JSON = - "{\"members\":{\"hero\":{" - "\"attack\":10,\"defense\":5,\"magic\":0,\"speed\":10,\"luck\":0," - "\"healthMax\":30,\"mpMax\":10" - "}}}"; - -static yyjson_doc *SAVE_TEST_FIXTURE_PARTY_DOC = NULL; - -errorret_t saveTestFixturePartyJsonLoadSyncStub(assetloading_t *loading) { - loading->entry->data.json = SAVE_TEST_FIXTURE_PARTY_DOC; - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); -} - -errorret_t saveTestFixturePartyJsonDisposeStub(assetentry_t *entry) { - errorOk(); -} - -void saveTestFixtureStubPartyAsset(void) { - memoryZero(&ASSET, sizeof(ASSET)); - for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) { - threadMutexInit(&ASSET.loading[i].mutex); - } - - if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) { - SAVE_TEST_FIXTURE_PARTY_DOC = yyjson_read( - SAVE_TEST_FIXTURE_PARTY_JSON, strlen(SAVE_TEST_FIXTURE_PARTY_JSON), 0 - ); - assertNotNull( - SAVE_TEST_FIXTURE_PARTY_DOC, "Failed to parse party JSON fixture" - ); - } - - ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].loadSync = - saveTestFixturePartyJsonLoadSyncStub; - ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].dispose = - saveTestFixturePartyJsonDisposeStub; -} - -void saveTestFixtureUnstubPartyAsset(void) { - if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) return; - yyjson_doc_free(SAVE_TEST_FIXTURE_PARTY_DOC); - SAVE_TEST_FIXTURE_PARTY_DOC = NULL; -} - -static int_t saveTestFixtureRemoveEntry( - const char *path, - const struct stat *statBuffer, - int typeFlag, - struct FTW *ftwBuffer -) { - return remove(path); -} - -int saveTestFixtureSetup(void **state) { - const char_t *realHome = getenv("HOME"); - SAVE_TEST_FIXTURE_HAD_REAL_HOME = realHome != NULL; - if(realHome != NULL) { - stringCopy( - SAVE_TEST_FIXTURE_REAL_HOME, realHome, - sizeof(SAVE_TEST_FIXTURE_REAL_HOME) - 1 - ); - } - - stringCopy( - SAVE_TEST_FIXTURE_HOME, "/tmp/dusk-save-test-XXXXXX", - sizeof(SAVE_TEST_FIXTURE_HOME) - 1 - ); - assertNotNull(mkdtemp(SAVE_TEST_FIXTURE_HOME), "Failed to create temp $HOME"); - - assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed"); - - saveTestFixtureStubPartyAsset(); - - errorret_t ret = saveInit(); - assertTrue(errorIsOk(ret), "saveInit failed in test fixture"); - - return 0; -} - -int saveTestFixtureTeardown(void **state) { - saveTestFixtureUnstubPartyAsset(); - - nftw( - SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16, - FTW_DEPTH | FTW_PHYS - ); - - if(SAVE_TEST_FIXTURE_HAD_REAL_HOME) { - setenv("HOME", SAVE_TEST_FIXTURE_REAL_HOME, 1); - } else { - unsetenv("HOME"); - } - - memoryZero(&SAVE, sizeof(SAVE)); - - return 0; -} - -void saveTestFixtureBlockSaveDirectory(void) { - const char_t *home = getenv("HOME"); - assertNotNull(home, "Fixture $HOME must be set before blocking it"); - - char_t path[PATH_MAX]; - stringFormat(path, sizeof(path), "%s/.dusk", home); - - FILE *file = fopen(path, "w"); - assertNotNull(file, "Failed to create blocking file"); - fclose(file); -} diff --git a/test/save/savetestfixture.h b/test/save/savetestfixture.h deleted file mode 100644 index 2ac2bde8..00000000 --- a/test/save/savetestfixture.h +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "save/save.h" - -/** - * Points $HOME at a fresh, empty temporary directory and resets the global - * SAVE state via the real saveInit(). Save tests run against the real Linux - * filesystem backend (there is no fake/mockable platform layer to swap in - - * the platform hooks are compile-time macros, not function pointers), so - * this sandboxing is what keeps tests from touching the real user's - * $HOME/.dusk/saves. Matches cmocka's CMUnitTestSetup signature - pass - * directly to cmocka_unit_test_setup_teardown. - */ -int saveTestFixtureSetup(void **state); - -/** - * Recursively removes the temporary $HOME directory tree created by - * saveTestFixtureSetup and restores the real $HOME. Matches cmocka's - * CMUnitTestTeardown signature - pass directly to - * cmocka_unit_test_setup_teardown. - */ -int saveTestFixtureTeardown(void **state); - -/** - * Places a regular file where the ".dusk" directory needs to go, so a - * subsequent availability check's mkdirp() call fails with ENOTDIR instead - * of succeeding. Used to deterministically exercise the "device unavailable - * because its directory couldn't be created" path without relying on real - * filesystem permission tricks. Must be called after saveTestFixtureSetup. - */ -void saveTestFixtureBlockSaveDirectory(void); - -/** - * Stubs the JSON asset loader's sync callback to hand back a small - * in-memory party.jsonc-shaped doc instead of doing a real file/zip read - - * saveInit()/saveSlotInit() seed the party roster via partyInit(), which - * loads party.jsonc through the real asset system, and there's no real - * asset bundle available under the test harness. No thread, no real I/O. - * Resets ASSET to a fresh, otherwise-empty state each call, so it's safe - * to call once per test. Called automatically by saveTestFixtureSetup. - */ -void saveTestFixtureStubPartyAsset(void); - -/** - * Frees the in-memory doc created by saveTestFixtureStubPartyAsset. Safe - * to call even if that function was never called. Called automatically - * by saveTestFixtureTeardown. - */ -void saveTestFixtureUnstubPartyAsset(void); diff --git a/test/save/test_save.c b/test/save/test_save.c deleted file mode 100644 index a04946b8..00000000 --- a/test/save/test_save.c +++ /dev/null @@ -1,620 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "savetestfixture.h" -#include "save/save.h" -#include "save/savedevicelinux.h" -#include "util/memory.h" -#include "util/string.h" -#include -#include - -// ============================================================ -// Helpers -// ============================================================ - -static void noopAvailabilityCallback(savedevice_t *device, void *user) {} - -// Skips saveFindAvailableDevice()'s bookkeeping (findingAvailableDevice -// etc.) and goes straight to "device 0 is current" - used by tests that -// only care about behavior once a device has been selected, not how it got -// selected. Still runs the real availability check underneath, since that -// is what actually creates the on-disk save directory via mkdirp - reads -// and writes fail without it. -static void makeDeviceAvailable(void) { - saveDeviceCheckAvailability(&SAVE.devices[0], noopAvailabilityCallback, NULL); - saveDeviceUpdate(&SAVE.devices[0]); - assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_AVAILABLE); - SAVE.deviceCurrent = 0; -} - -static bool_t g_findCallbackFired; -static savedevice_t *g_findCallbackDevice; - -static void findAvailableCallback(savedevice_t *device, void *user) { - g_findCallbackFired = true; - g_findCallbackDevice = device; -} - -static void resetFindCallbackState(void) { - g_findCallbackFired = false; - g_findCallbackDevice = NULL; -} - -// ============================================================ -// saveInit -// ============================================================ - -static void test_saveInit_setsDefaults(void **state) { - // saveTestFixtureSetup already called the real saveInit() - just assert - // on the state it left behind. - assert_int_equal(SAVE.deviceCurrent, 0xFF); - assert_int_equal(SAVE.slotCurrent, 0xFF); - assert_false(SAVE.findingAvailableDevice); - assert_false(SAVE.noAvailableDeviceFound); - assert_true(SAVE.settings.audioMasterVolume == 1.0f); - assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN); - - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - assert_int_equal(SAVE.caches[i].name[0], '\0'); - assert_int_equal(SAVE.caches[i].playerLevel, 0); - } -} - -// ============================================================ -// saveUpdate - no-op path -// ============================================================ - -static void test_saveUpdate_noopWhenNotFinding(void **state) { - errorret_t ret = saveUpdate(); - assert_true(errorIsOk(ret)); - assert_int_equal(SAVE.deviceCurrent, 0xFF); - assert_false(SAVE.findingAvailableDevice); -} - -// ============================================================ -// saveFindAvailableDevice / saveUpdate - discovery flow -// ============================================================ - -static void test_saveFindAvailableDevice_nullCallbackAsserts(void **state) { - expect_assert_failure(saveFindAvailableDevice(NULL, NULL)); -} - -static void test_saveFindAvailableDevice_reentrantAsserts(void **state) { - resetFindCallbackState(); - saveFindAvailableDevice(findAvailableCallback, NULL); - expect_assert_failure(saveFindAvailableDevice(findAvailableCallback, NULL)); -} - -static void test_saveFindAvailableDevice_deviceAlreadyAvailable(void **state) { - resetFindCallbackState(); - // Mark the device available ahead of time via a real check (this is also - // what creates the on-disk save directory that the "found device" flow - // below needs to write settings back out to). - saveDeviceCheckAvailability(&SAVE.devices[0], noopAvailabilityCallback, NULL); - saveDeviceUpdate(&SAVE.devices[0]); - assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_AVAILABLE); - - saveFindAvailableDevice(findAvailableCallback, NULL); - - // Set synchronously inside the call itself - no need to wait for update. - assert_int_equal(SAVE.deviceCurrent, 0); - assert_false(g_findCallbackFired); - - errorret_t ret = saveUpdate(); - assert_true(errorIsOk(ret)); - assert_true(g_findCallbackFired); - assert_ptr_equal(g_findCallbackDevice, &SAVE.devices[0]); - assert_false(SAVE.findingAvailableDevice); -} - -static void test_saveFindAvailableDevice_coldPath_oneUpdateResolves( - void **state -) { - resetFindCallbackState(); - - saveFindAvailableDevice(findAvailableCallback, NULL); - - // Nothing resolved synchronously yet - the Linux availability check is - // synchronous, but the callback it queues is only drained by an explicit - // saveDeviceUpdate() inside saveUpdate(). - assert_int_equal(SAVE.deviceCurrent, 0xFF); - assert_false(g_findCallbackFired); - - errorret_t ret = saveUpdate(); - assert_true(errorIsOk(ret)); - assert_true(g_findCallbackFired); - assert_ptr_equal(g_findCallbackDevice, &SAVE.devices[0]); - assert_int_equal(SAVE.deviceCurrent, 0); - assert_false(SAVE.findingAvailableDevice); -} - -static void test_saveFindAvailableDevice_allUnavailable_firesNullCallback( - void **state -) { - unsetenv("HOME"); - resetFindCallbackState(); - - saveFindAvailableDevice(findAvailableCallback, NULL); - - errorret_t ret = saveUpdate(); - assert_true(errorIsOk(ret)); - assert_true(g_findCallbackFired); - assert_null(g_findCallbackDevice); - assert_int_equal(SAVE.deviceCurrent, 0xFF); - assert_false(SAVE.findingAvailableDevice); -} - -// ============================================================ -// saveOnDeviceAvailabilityChecked -// ============================================================ - -static void test_saveOnDeviceAvailabilityChecked_nullDeviceAsserts( - void **state -) { - SAVE.findingAvailableDevice = true; - expect_assert_failure(saveOnDeviceAvailabilityChecked(NULL, NULL)); -} - -static void test_saveOnDeviceAvailabilityChecked_notFindingAsserts( - void **state -) { - SAVE.findingAvailableDevice = false; - expect_assert_failure( - saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL) - ); -} - -static void test_saveOnDeviceAvailabilityChecked_ignoresIfAlreadyResolved( - void **state -) { - SAVE.findingAvailableDevice = true; - SAVE.deviceCurrent = 0; - SAVE.devices[0].state = SAVE_DEVICE_STATE_UNAVAILABLE; - - saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL); - - // Already resolved - unavailable status on a re-delivered callback must - // not clobber the winning device or flip noAvailableDeviceFound. - assert_int_equal(SAVE.deviceCurrent, 0); - assert_false(SAVE.noAvailableDeviceFound); -} - -static void test_saveOnDeviceAvailabilityChecked_singleDeviceUnavailable( - void **state -) { - SAVE.findingAvailableDevice = true; - SAVE.deviceCurrent = 0xFF; - SAVE.devices[0].state = SAVE_DEVICE_STATE_UNAVAILABLE; - - saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL); - - // SAVE_DEVICE_COUNT is 1 on Linux, so an unavailable device 0 is always - // "the last device" - there's no next device to chain to. - assert_true(SAVE.noAvailableDeviceFound); - assert_int_equal(SAVE.deviceCurrent, 0xFF); -} - -static void test_saveOnDeviceAvailabilityChecked_deviceAvailable( - void **state -) { - SAVE.findingAvailableDevice = true; - SAVE.deviceCurrent = 0xFF; - SAVE.devices[0].state = SAVE_DEVICE_STATE_AVAILABLE; - - saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL); - - assert_int_equal(SAVE.deviceCurrent, 0); - assert_false(SAVE.noAvailableDeviceFound); -} - -// ============================================================ -// saveSaveSettings / saveLoadSettings -// ============================================================ - -static void test_saveSaveSettings_noCurrentDeviceAsserts(void **state) { - expect_assert_failure(saveSaveSettings()); -} - -static void test_saveSaveSettings_notDirty_isNoop(void **state) { - // Documents a real, currently-shipping bug: nothing anywhere in the - // codebase ever sets SAVE.settingsDirty = true, so saveSaveSettings() is - // permanently a no-op in practice. This test locks in that (surprising) - // behavior rather than the presumably-intended one, so it fails loudly - // if someone changes the no-op condition without meaning to. - makeDeviceAvailable(); - assert_false(SAVE.settingsDirty); - SAVE.settings.audioMasterVolume = 12.0f; - - errorret_t ret = saveSaveSettings(); - assert_true(errorIsOk(ret)); - - savesettings_t onDisk; - saveSettingsInit(&onDisk); - errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk); - assert_true(errorIsOk(readRet)); - assert_true(onDisk.audioMasterVolume == 1.0f);// never actually written -} - -static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag( - void **state -) { - makeDeviceAvailable(); - SAVE.settingsDirty = true;// nothing in real code sets this - forced here - SAVE.settings.audioMasterVolume = 5.5f; - - errorret_t ret = saveSaveSettings(); - assert_true(errorIsOk(ret)); - assert_false(SAVE.settingsDirty); - - savesettings_t onDisk; - saveSettingsInit(&onDisk); - errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk); - assert_true(errorIsOk(readRet)); - assert_true(onDisk.audioMasterVolume == 5.5f); -} - -static void test_saveLoadSettings_noCurrentDeviceAsserts(void **state) { - expect_assert_failure(saveLoadSettings()); -} - -static void test_saveLoadSettings_readsAndClearsDirtyFlag(void **state) { - makeDeviceAvailable(); - - savesettings_t onDisk; - saveSettingsInit(&onDisk); - onDisk.audioMasterVolume = 9.25f; - errorret_t writeRet = saveDeviceSettingsWrite(&SAVE.devices[0], &onDisk); - assert_true(errorIsOk(writeRet)); - - SAVE.settingsDirty = true; - SAVE.settings.audioMasterVolume = 0.0f; - - errorret_t ret = saveLoadSettings(); - assert_true(errorIsOk(ret)); - assert_true(SAVE.settings.audioMasterVolume == 9.25f); - assert_false(SAVE.settingsDirty); -} - -// ============================================================ -// saveSaveSlot / saveLoadSlot -// ============================================================ - -static void test_saveSaveSlot_noCurrentDeviceAsserts(void **state) { - expect_assert_failure(saveSaveSlot()); -} - -static void test_saveSaveSlot_invalidSlotIndexAsserts(void **state) { - makeDeviceAvailable(); - // slotCurrent is still 0xFF (unset) from saveInit(). - expect_assert_failure(saveSaveSlot()); -} - -static void test_saveSaveSlot_success(void **state) { - makeDeviceAvailable(); - SAVE.slotCurrent = 1; - saveSlotInit(&SAVE.slot); - stringCopy( - SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name) - ); - SAVE.slot.cachedData.playerLevel = 12; - SAVE.slotDirty = true; - - errorret_t ret = saveSaveSlot(); - assert_true(errorIsOk(ret)); - assert_false(SAVE.slotDirty); - assert_true(stringEquals(SAVE.caches[1].name, "Hero")); - assert_int_equal(SAVE.caches[1].playerLevel, 12); - - // No pre-init needed - saveSlotReadJSON() (via saveDeviceSlotRead) fully - // resets/overwrites its destination struct regardless of prior content. - saveslot_t onDisk; - errorret_t readRet = saveDeviceSlotRead(&SAVE.devices[0], &onDisk, 1); - assert_true(errorIsOk(readRet)); - assert_true(stringEquals(onDisk.cachedData.name, "Hero")); - assert_int_equal(onDisk.cachedData.playerLevel, 12); -} - -static void test_saveLoadSlot_noCurrentDeviceAsserts(void **state) { - expect_assert_failure(saveLoadSlot()); -} - -static void test_saveLoadSlot_invalidSlotIndexAsserts(void **state) { - makeDeviceAvailable(); - expect_assert_failure(saveLoadSlot()); -} - -static void test_saveLoadSlot_success(void **state) { - makeDeviceAvailable(); - - // Write slot 2's on-disk file via SAVE.slot itself (saveSlotInit() only - // ever operates on SAVE.slot now), then reset it again below - the - // write and the load-under-test don't overlap in time. - SAVE.slotCurrent = 2; - saveSlotInit(&SAVE.slot); - stringCopy( - SAVE.slot.cachedData.name, "Zelda", sizeof(SAVE.slot.cachedData.name) - ); - SAVE.slot.cachedData.playerLevel = 30; - errorret_t writeRet = saveDeviceSlotWrite(&SAVE.devices[0], &SAVE.slot, 2); - assert_true(errorIsOk(writeRet)); - - SAVE.slotCurrent = 2; - saveSlotInit(&SAVE.slot); - SAVE.slotDirty = true; - - errorret_t ret = saveLoadSlot(); - assert_true(errorIsOk(ret)); - assert_false(SAVE.slotDirty); - assert_true(stringEquals(SAVE.slot.cachedData.name, "Zelda")); - assert_int_equal(SAVE.slot.cachedData.playerLevel, 30); - assert_true(stringEquals(SAVE.caches[2].name, "Zelda")); - assert_int_equal(SAVE.caches[2].playerLevel, 30); -} - -static void test_saveLoadSlot_deviceReadFails_cacheStaysStale(void **state) { - makeDeviceAvailable(); - - char_t slotPath[512]; - errorret_t pathRet = saveDeviceLinuxGetSlotPath( - slotPath, sizeof(slotPath), 0 - ); - assert_true(errorIsOk(pathRet)); - FILE *file = fopen(slotPath, "w"); - assert_non_null(file); - fprintf(file, "not valid json {{{"); - fclose(file); - - SAVE.slotCurrent = 0; - saveSlotInit(&SAVE.slot); - SAVE.caches[0].playerLevel = 77;// sentinel - - errorret_t ret = saveLoadSlot(); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - // The device read failed before the cache-sync line ever ran. - assert_int_equal(SAVE.caches[0].playerLevel, 77); -} - -// ============================================================ -// saveLoadAllSlots -// ============================================================ - -static void test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex(void **state) { - makeDeviceAvailable(); - - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - SAVE.slotCurrent = i; - saveSlotInit(&SAVE.slot); - SAVE.slot.cachedData.playerLevel = (int32_t)(i + 1); - errorret_t saveRet = saveSaveSlot(); - assert_true(errorIsOk(saveRet)); - } - - errorret_t ret = saveLoadAllSlots(); - assert_true(errorIsOk(ret)); - assert_int_equal(SAVE.slotCurrent, SAVE_SLOT_COUNT - 1); - - for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) { - assert_int_equal(SAVE.caches[i].playerLevel, (int32_t)(i + 1)); - } -} - -static void test_saveLoadAllSlots_missingFileLeavesInitDefaults(void **state) { - makeDeviceAvailable(); - - SAVE.slotCurrent = 0; - saveSlotInit(&SAVE.slot); - SAVE.slot.cachedData.playerLevel = 5; - errorret_t saveRet = saveSaveSlot(); - assert_true(errorIsOk(saveRet)); - // Slots 1 and 2 are never written - no file exists for them. - - errorret_t ret = saveLoadAllSlots(); - assert_true(errorIsOk(ret)); - - assert_int_equal(SAVE.caches[0].playerLevel, 5); - // Missing-file slots fall back to saveSlotInit()'s default (0), not the - // JSON-missing-field default of 1 that a corrupt-but-present file with no - // playerLevel key would produce - these are two different defaults. - assert_int_equal(SAVE.caches[1].playerLevel, 0); - assert_int_equal(SAVE.caches[2].playerLevel, 0); -} - -static void test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex( - void **state -) { - makeDeviceAvailable(); - - SAVE.slotCurrent = 0; - saveSlotInit(&SAVE.slot); - errorret_t saveRet = saveSaveSlot(); - assert_true(errorIsOk(saveRet)); - - char_t slotPath[512]; - errorret_t pathRet = saveDeviceLinuxGetSlotPath( - slotPath, sizeof(slotPath), 1 - ); - assert_true(errorIsOk(pathRet)); - FILE *file = fopen(slotPath, "w"); - assert_non_null(file); - fprintf(file, "not valid json {{{"); - fclose(file); - - errorret_t ret = saveLoadAllSlots(); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); - - // The loop bails out as soon as slot 1 fails - it never reaches slot 2. - assert_int_equal(SAVE.slotCurrent, 1); -} - -// ============================================================ -// saveDispose -// ============================================================ - -static void test_saveDispose_succeeds(void **state) { - errorret_t ret = saveDispose(); - assert_true(errorIsOk(ret)); -} - -// ============================================================ -// saveUpdate - the unconditional settings write-back on device found -// ============================================================ - -static void test_saveUpdate_foundDevice_rewritesSettingsUnconditionally( - void **state -) { - // Documents another surprising bit of current behavior: as soon as a - // device is found, saveUpdate() writes SAVE.settings straight back out to - // it, regardless of settingsDirty - independent of (and unguarded by) the - // same dirty-flag check saveSaveSettings() itself honors. - resetFindCallbackState(); - SAVE.settings.audioMasterVolume = 3.25f; - assert_false(SAVE.settingsDirty); - - saveFindAvailableDevice(findAvailableCallback, NULL); - errorret_t ret = saveUpdate(); - assert_true(errorIsOk(ret)); - assert_true(g_findCallbackFired); - - savesettings_t onDisk; - saveSettingsInit(&onDisk); - errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk); - assert_true(errorIsOk(readRet)); - assert_true(onDisk.audioMasterVolume == 3.25f); -} - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test_setup_teardown( - test_saveInit_setsDefaults, saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveUpdate_noopWhenNotFinding, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveFindAvailableDevice_nullCallbackAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveFindAvailableDevice_reentrantAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveFindAvailableDevice_deviceAlreadyAvailable, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveFindAvailableDevice_coldPath_oneUpdateResolves, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveFindAvailableDevice_allUnavailable_firesNullCallback, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveOnDeviceAvailabilityChecked_nullDeviceAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveOnDeviceAvailabilityChecked_notFindingAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveOnDeviceAvailabilityChecked_ignoresIfAlreadyResolved, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveOnDeviceAvailabilityChecked_singleDeviceUnavailable, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveOnDeviceAvailabilityChecked_deviceAvailable, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveSaveSettings_noCurrentDeviceAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveSaveSettings_notDirty_isNoop, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveSaveSettings_dirtyForced_writesAndClearsFlag, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadSettings_noCurrentDeviceAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadSettings_readsAndClearsDirtyFlag, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveSaveSlot_noCurrentDeviceAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveSaveSlot_invalidSlotIndexAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveSaveSlot_success, saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadSlot_noCurrentDeviceAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadSlot_invalidSlotIndexAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadSlot_success, saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadSlot_deviceReadFails_cacheStaysStale, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadAllSlots_missingFileLeavesInitDefaults, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveDispose_succeeds, saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveUpdate_foundDevice_rewritesSettingsUnconditionally, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/save/test_savedevice.c b/test/save/test_savedevice.c deleted file mode 100644 index 7f4bf449..00000000 --- a/test/save/test_savedevice.c +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "savetestfixture.h" -#include "save/savedevice.h" -#include "save/slot/saveslot.h" -#include "save/settings/savesettings.h" -#include "save/save.h" -#include "util/memory.h" -#include "util/string.h" - -// ============================================================ -// saveDeviceInit -// ============================================================ - -static void test_saveDeviceInit_nullAsserts(void **state) { - expect_assert_failure(saveDeviceInit(NULL)); -} - -static void test_saveDeviceInit_setsUnknownState(void **state) { - savedevice_t device; - memorySet(&device, 0xAB, sizeof(device)); - - errorret_t ret = saveDeviceInit(&device); - assert_true(errorIsOk(ret)); - assert_int_equal(device.state, SAVE_DEVICE_STATE_UNKNOWN); - assert_false(device.fireCallback); -} - -// ============================================================ -// saveDeviceUpdate / saveDeviceFireCallback -// ============================================================ - -static void test_saveDeviceUpdate_nullAsserts(void **state) { - expect_assert_failure(saveDeviceUpdate(NULL)); -} - -static bool_t g_updateCallbackFired; -static void *g_updateCallbackUser; - -static void updateCallback(savedevice_t *device, void *user) { - g_updateCallbackFired = true; - g_updateCallbackUser = user; -} - -static void test_saveDeviceUpdate_firesQueuedCallbackOnce(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - device.stateCallback = updateCallback; - device.user = (void *)0x1234; - device.fireCallback = true; - - g_updateCallbackFired = false; - g_updateCallbackUser = NULL; - - errorret_t ret = saveDeviceUpdate(&device); - assert_true(errorIsOk(ret)); - assert_true(g_updateCallbackFired); - assert_ptr_equal(g_updateCallbackUser, (void *)0x1234); - assert_false(device.fireCallback); - - // A second update with nothing queued must not re-fire the callback. - g_updateCallbackFired = false; - ret = saveDeviceUpdate(&device); - assert_true(errorIsOk(ret)); - assert_false(g_updateCallbackFired); -} - -static void test_saveDeviceFireCallback_nullAsserts(void **state) { - expect_assert_failure(saveDeviceFireCallback(NULL)); -} - -static void test_saveDeviceFireCallback_setsFlag(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - saveDeviceFireCallback(&device); - assert_true(device.fireCallback); -} - -static void test_saveDeviceFireCallback_assertsIfAlreadyQueued(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - saveDeviceFireCallback(&device); - expect_assert_failure(saveDeviceFireCallback(&device)); -} - -// ============================================================ -// saveDeviceCheckAvailability -// ============================================================ - -static void test_saveDeviceCheckAvailability_nullAsserts(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - expect_assert_failure( - saveDeviceCheckAvailability(NULL, updateCallback, NULL) - ); - expect_assert_failure(saveDeviceCheckAvailability(&device, NULL, NULL)); -} - -static void test_saveDeviceCheckAvailability_assertsWhileAlreadyChecking( - void **state -) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - // Directly force the device into a mid-check state without going through - // the real (synchronous, on this platform) availability check - this - // simulates what a genuinely async platform would look like mid-flight. - device.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; - - expect_assert_failure( - saveDeviceCheckAvailability(&device, updateCallback, NULL) - ); -} - -static void test_saveDeviceCheckAvailability_resolvesSynchronouslyOnLinux( - void **state -) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - g_updateCallbackFired = false; - saveDeviceCheckAvailability(&device, updateCallback, NULL); - - // The Linux backend resolves availability synchronously inside the check - // call itself, but the callback is only *queued* (fireCallback), not - // fired, until an explicit saveDeviceUpdate() drains it. - assert_int_equal(device.state, SAVE_DEVICE_STATE_AVAILABLE); - assert_true(device.fireCallback); - assert_false(g_updateCallbackFired); - - errorret_t updateRet = saveDeviceUpdate(&device); - assert_true(errorIsOk(updateRet)); - assert_true(g_updateCallbackFired); -} - -// ============================================================ -// saveDeviceSlotWrite / saveDeviceSlotRead / saveDeviceSettingsWrite / -// saveDeviceSettingsRead - dispatch to the platform's four hooks. -// ============================================================ - -static void test_saveDeviceSlotWrite_nullAsserts(void **state) { - savedevice_t device; - // Content is never touched - both calls assert on a NULL argument - // before either device or slot would actually be read. - saveslot_t slot; - - expect_assert_failure(saveDeviceSlotWrite(NULL, &slot, 0)); - expect_assert_failure(saveDeviceSlotWrite(&device, NULL, 0)); -} - -static void test_saveDeviceSlotRead_nullAsserts(void **state) { - savedevice_t device; - saveslot_t slot; - - expect_assert_failure(saveDeviceSlotRead(NULL, &slot, 0)); - expect_assert_failure(saveDeviceSlotRead(&device, NULL, 0)); -} - -static void test_saveDeviceSettingsWrite_nullAsserts(void **state) { - savedevice_t device; - savesettings_t settings; - saveSettingsInit(&settings); - - expect_assert_failure(saveDeviceSettingsWrite(NULL, &settings)); - expect_assert_failure(saveDeviceSettingsWrite(&device, NULL)); -} - -static void test_saveDeviceSettingsRead_nullAsserts(void **state) { - savedevice_t device; - savesettings_t settings; - saveSettingsInit(&settings); - - expect_assert_failure(saveDeviceSettingsRead(NULL, &settings)); - expect_assert_failure(saveDeviceSettingsRead(&device, NULL)); -} - -static void test_saveDeviceSlot_dispatchesToPlatform(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - // The save directory must exist before a write can succeed - normally - // saveDeviceCheckAvailability does this via the platform's mkdirp. - saveDeviceCheckAvailability(&device, updateCallback, NULL); - saveDeviceUpdate(&device); - - // saveSlotInit() only ever operates on SAVE.slot now, so that's the - // write source here. - saveSlotInit(&SAVE.slot); - SAVE.slot.cachedData.playerLevel = 21; - - errorret_t writeRet = saveDeviceSlotWrite(&device, &SAVE.slot, 0); - assert_true(errorIsOk(writeRet)); - - // No pre-init needed - saveSlotReadJSON() (via saveDeviceSlotRead) fully - // resets/overwrites its destination struct regardless of prior content. - saveslot_t read; - errorret_t readRet = saveDeviceSlotRead(&device, &read, 0); - assert_true(errorIsOk(readRet)); - - assert_int_equal(read.cachedData.playerLevel, 21); -} - -static void test_saveDeviceSettings_dispatchesToPlatform(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - // The save directory must exist before a write can succeed - normally - // saveDeviceCheckAvailability does this via the platform's mkdirp. - saveDeviceCheckAvailability(&device, updateCallback, NULL); - saveDeviceUpdate(&device); - - savesettings_t written; - saveSettingsInit(&written); - written.audioMasterVolume = 0.25f;// exactly representable, safe to compare - - errorret_t writeRet = saveDeviceSettingsWrite(&device, &written); - assert_true(errorIsOk(writeRet)); - - savesettings_t read; - saveSettingsInit(&read); - errorret_t readRet = saveDeviceSettingsRead(&device, &read); - assert_true(errorIsOk(readRet)); - - assert_true(read.audioMasterVolume == 0.25f); -} - -// ============================================================ -// saveDeviceDispose -// ============================================================ - -static void test_saveDeviceDispose_nullAsserts(void **state) { - expect_assert_failure(saveDeviceDispose(NULL)); -} - -static void test_saveDeviceDispose_succeeds(void **state) { - savedevice_t device; - errorret_t initRet = saveDeviceInit(&device); - assert_true(errorIsOk(initRet)); - - errorret_t ret = saveDeviceDispose(&device); - assert_true(errorIsOk(ret)); -} - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test_setup_teardown( - test_saveDeviceInit_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceInit_setsUnknownState, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveDeviceUpdate_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceUpdate_firesQueuedCallbackOnce, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceFireCallback_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceFireCallback_setsFlag, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceFireCallback_assertsIfAlreadyQueued, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveDeviceCheckAvailability_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceCheckAvailability_assertsWhileAlreadyChecking, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceCheckAvailability_resolvesSynchronouslyOnLinux, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveDeviceSlotWrite_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceSlotRead_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceSettingsWrite_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceSettingsRead_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceSlot_dispatchesToPlatform, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceSettings_dispatchesToPlatform, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_saveDeviceDispose_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_saveDeviceDispose_succeeds, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/save/test_savedevicelinux.c b/test/save/test_savedevicelinux.c deleted file mode 100644 index a9ba2ea2..00000000 --- a/test/save/test_savedevicelinux.c +++ /dev/null @@ -1,455 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "savetestfixture.h" -#include "save/savedevice.h" -#include "save/savedevicelinux.h" -#include "save/slot/saveslot.h" -#include "save/settings/savesettings.h" -#include "save/save.h" -#include "util/memory.h" -#include "util/string.h" -#include -#include - -// ============================================================ -// Helper: drives a device through saveDeviceCheckAvailability + -// saveDeviceUpdate (the real public flow), returning once the callback has -// fired. -// ============================================================ - -static bool_t g_callbackFired; -static savedevice_t *g_callbackDevice; - -static void checkAvailabilityCallback(savedevice_t *device, void *user) { - g_callbackFired = true; - g_callbackDevice = device; -} - -static void checkAvailabilityAndDrain(savedevice_t *device) { - g_callbackFired = false; - g_callbackDevice = NULL; - saveDeviceCheckAvailability(device, checkAvailabilityCallback, NULL); - saveDeviceUpdate(device); -} - -// ============================================================ -// saveDeviceLinuxGetDirectory / GetSettingsPath / GetSlotPath -// ============================================================ - -static void test_getDirectory_success(void **state) { - char_t path[512]; - errorret_t ret = saveDeviceLinuxGetDirectory(path, sizeof(path)); - assert_true(errorIsOk(ret)); - - char_t expected[512]; - stringFormat(expected, sizeof(expected), "%s/.dusk/saves", getenv("HOME")); - assert_true(stringEquals(path, expected)); -} - -static void test_getDirectory_homeUnset_errors(void **state) { - unsetenv("HOME"); - - char_t path[512]; - errorret_t ret = saveDeviceLinuxGetDirectory(path, sizeof(path)); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_getDirectory_nullAsserts(void **state) { - expect_assert_failure(saveDeviceLinuxGetDirectory(NULL, 512)); -} - -static void test_getSettingsPath_success(void **state) { - char_t path[512]; - errorret_t ret = saveDeviceLinuxGetSettingsPath(path, sizeof(path)); - assert_true(errorIsOk(ret)); - - char_t expected[512]; - stringFormat( - expected, sizeof(expected), "%s/.dusk/saves/settings.json", getenv("HOME") - ); - assert_true(stringEquals(path, expected)); -} - -static void test_getSettingsPath_homeUnset_errors(void **state) { - unsetenv("HOME"); - - char_t path[512]; - errorret_t ret = saveDeviceLinuxGetSettingsPath(path, sizeof(path)); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_getSlotPath_success(void **state) { - char_t path[512]; - errorret_t ret = saveDeviceLinuxGetSlotPath(path, sizeof(path), 2); - assert_true(errorIsOk(ret)); - - char_t expected[512]; - stringFormat( - expected, sizeof(expected), "%s/.dusk/saves/slot2.json", getenv("HOME") - ); - assert_true(stringEquals(path, expected)); -} - -static void test_getSlotPath_homeUnset_errors(void **state) { - unsetenv("HOME"); - - char_t path[512]; - errorret_t ret = saveDeviceLinuxGetSlotPath(path, sizeof(path), 0); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -// ============================================================ -// saveDeviceLinuxCheckAvailability -// ============================================================ - -static void test_checkAvailability_wrongStateAsserts(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - device.state = SAVE_DEVICE_STATE_UNKNOWN; - - expect_assert_failure(saveDeviceLinuxCheckAvailability(&device)); -} - -static void test_checkAvailability_nullAsserts(void **state) { - expect_assert_failure(saveDeviceLinuxCheckAvailability(NULL)); -} - -static void test_checkAvailability_success(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - device.state = SAVE_DEVICE_STATE_UNKNOWN; - - checkAvailabilityAndDrain(&device); - - assert_int_equal(device.state, SAVE_DEVICE_STATE_AVAILABLE); - assert_null(device.reasonKey); - assert_true(g_callbackFired); - assert_ptr_equal(g_callbackDevice, &device); - - // The directory should now actually exist on disk (under the sandboxed - // $HOME the fixture set up). - char_t path[512]; - errorret_t pathRet = saveDeviceLinuxGetDirectory(path, sizeof(path)); - assert_true(errorIsOk(pathRet)); - FILE *dir = fopen(path, "r"); - // Directories can't be fopen'd for reading data, but a NULL here would - // mean the path doesn't exist at all - a non-NULL/EISDIR failure both - // indicate the directory exists. - if(dir != NULL) fclose(dir); -} - -static void test_checkAvailability_homeUnset(void **state) { - unsetenv("HOME"); - - savedevice_t device; - memoryZero(&device, sizeof(device)); - device.state = SAVE_DEVICE_STATE_UNKNOWN; - - checkAvailabilityAndDrain(&device); - - assert_int_equal(device.state, SAVE_DEVICE_STATE_UNAVAILABLE); - assert_true(stringEquals(device.reasonKey, "save.linux.no_home")); - assert_true(g_callbackFired); -} - -static void test_checkAvailability_mkdirpFails(void **state) { - saveTestFixtureBlockSaveDirectory(); - - savedevice_t device; - memoryZero(&device, sizeof(device)); - device.state = SAVE_DEVICE_STATE_UNKNOWN; - - checkAvailabilityAndDrain(&device); - - assert_int_equal(device.state, SAVE_DEVICE_STATE_UNAVAILABLE); - assert_true(stringEquals(device.reasonKey, "save.linux.mkdirp_failed")); - assert_true(g_callbackFired); -} - -// ============================================================ -// saveDeviceLinuxSlotWrite / saveDeviceLinuxSlotRead -// ============================================================ - -static void test_slotWrite_nullAsserts(void **state) { - savedevice_t device; - // Content is never touched - both calls assert on a NULL argument - // before either device or slot would actually be read. - saveslot_t slot; - - expect_assert_failure(saveDeviceLinuxSlotWrite(NULL, &slot, 0)); - expect_assert_failure(saveDeviceLinuxSlotWrite(&device, NULL, 0)); -} - -static void test_slotRead_nullAsserts(void **state) { - savedevice_t device; - saveslot_t slot; - - expect_assert_failure(saveDeviceLinuxSlotRead(NULL, &slot, 0)); - expect_assert_failure(saveDeviceLinuxSlotRead(&device, NULL, 0)); -} - -static void test_slotWriteRead_roundTrip(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - // The save directory must exist before a write can succeed - normally - // saveDeviceLinuxCheckAvailability does this via mkdirp. - checkAvailabilityAndDrain(&device); - - // saveSlotInit() only ever operates on SAVE.slot now, so that's the - // write source here. - saveSlotInit(&SAVE.slot); - stringCopy( - SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name) - ); - SAVE.slot.cachedData.playerLevel = 9; - - errorret_t writeRet = saveDeviceLinuxSlotWrite(&device, &SAVE.slot, 1); - assert_true(errorIsOk(writeRet)); - - // No pre-init needed - saveSlotReadJSON() (via saveDeviceLinuxSlotRead) - // fully resets/overwrites its destination struct regardless of prior - // content. - saveslot_t read; - errorret_t readRet = saveDeviceLinuxSlotRead(&device, &read, 1); - assert_true(errorIsOk(readRet)); - - assert_true(stringEquals(read.cachedData.name, "Hero")); - assert_int_equal(read.cachedData.playerLevel, 9); -} - -static void test_slotRead_noFileYet_leavesSlotUntouched(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - - // Deliberately not saveSlotInit()'d - a missing slot file means - // saveDeviceLinuxSlotRead never even calls saveSlotReadJSON, so slot - // must come in exactly as the caller left it and go out unchanged. - saveslot_t slot; - memoryZero(&slot, sizeof(slot)); - stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name)); - slot.cachedData.playerLevel = 555; - - errorret_t ret = saveDeviceLinuxSlotRead(&device, &slot, 2); - assert_true(errorIsOk(ret)); - - assert_true(stringEquals(slot.cachedData.name, "sentinel")); - assert_int_equal(slot.cachedData.playerLevel, 555); -} - -static void test_slotRead_corruptFile_errors(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - - // The directory must exist before we can drop a file into it - normally - // saveDeviceLinuxCheckAvailability does this via mkdirp. - savedevice_t tmpDevice; - memoryZero(&tmpDevice, sizeof(tmpDevice)); - tmpDevice.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; - saveDeviceLinuxCheckAvailability(&tmpDevice); - assert_int_equal(tmpDevice.state, SAVE_DEVICE_STATE_AVAILABLE); - - char_t slotPath[512]; - errorret_t pathRet = - saveDeviceLinuxGetSlotPath(slotPath, sizeof(slotPath), 0); - assert_true(errorIsOk(pathRet)); - - FILE *file = fopen(slotPath, "w"); - assert_non_null(file); - fprintf(file, "not valid json {{{"); - fclose(file); - - saveslot_t slot; - - errorret_t ret = saveDeviceLinuxSlotRead(&device, &slot, 0); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -// ============================================================ -// saveDeviceLinuxSettingsWrite / saveDeviceLinuxSettingsRead -// ============================================================ - -static void test_settingsWrite_nullAsserts(void **state) { - savedevice_t device; - savesettings_t settings; - saveSettingsInit(&settings); - - expect_assert_failure(saveDeviceLinuxSettingsWrite(NULL, &settings)); - expect_assert_failure(saveDeviceLinuxSettingsWrite(&device, NULL)); -} - -static void test_settingsRead_nullAsserts(void **state) { - savedevice_t device; - savesettings_t settings; - saveSettingsInit(&settings); - - expect_assert_failure(saveDeviceLinuxSettingsRead(NULL, &settings)); - expect_assert_failure(saveDeviceLinuxSettingsRead(&device, NULL)); -} - -static void test_settingsWriteRead_roundTrip(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - // The save directory must exist before a write can succeed - normally - // saveDeviceLinuxCheckAvailability does this via mkdirp. - checkAvailabilityAndDrain(&device); - - savesettings_t written; - saveSettingsInit(&written); - written.audioMasterVolume = 3.25f;// exactly representable, safe to compare - - errorret_t writeRet = saveDeviceLinuxSettingsWrite(&device, &written); - assert_true(errorIsOk(writeRet)); - - savesettings_t read; - saveSettingsInit(&read); - - errorret_t readRet = saveDeviceLinuxSettingsRead(&device, &read); - assert_true(errorIsOk(readRet)); - assert_true(read.audioMasterVolume == 3.25f); -} - -static void test_settingsRead_noFileYet_leavesUntouched(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - - savesettings_t settings; - saveSettingsInit(&settings); - settings.audioMasterVolume = 7.75f; - - errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings); - assert_true(errorIsOk(ret)); - assert_true(settings.audioMasterVolume == 7.75f); -} - -static void test_settingsRead_corruptFile_errors(void **state) { - savedevice_t device; - memoryZero(&device, sizeof(device)); - - savedevice_t tmpDevice; - memoryZero(&tmpDevice, sizeof(tmpDevice)); - tmpDevice.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; - saveDeviceLinuxCheckAvailability(&tmpDevice); - assert_int_equal(tmpDevice.state, SAVE_DEVICE_STATE_AVAILABLE); - - char_t settingsPath[512]; - errorret_t pathRet = - saveDeviceLinuxGetSettingsPath(settingsPath, sizeof(settingsPath)); - assert_true(errorIsOk(pathRet)); - - FILE *file = fopen(settingsPath, "w"); - assert_non_null(file); - fprintf(file, "not valid json {{{"); - fclose(file); - - savesettings_t settings; - saveSettingsInit(&settings); - - errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test_setup_teardown( - test_getDirectory_success, saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_getDirectory_homeUnset_errors, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_getDirectory_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_getSettingsPath_success, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_getSettingsPath_homeUnset_errors, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_getSlotPath_success, saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_getSlotPath_homeUnset_errors, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_checkAvailability_wrongStateAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_checkAvailability_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_checkAvailability_success, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_checkAvailability_homeUnset, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_checkAvailability_mkdirpFails, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_slotWrite_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_slotRead_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_slotWriteRead_roundTrip, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_slotRead_noFileYet_leavesSlotUntouched, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_slotRead_corruptFile_errors, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - - cmocka_unit_test_setup_teardown( - test_settingsWrite_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_settingsRead_nullAsserts, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_settingsWriteRead_roundTrip, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_settingsRead_noFileYet_leavesUntouched, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - cmocka_unit_test_setup_teardown( - test_settingsRead_corruptFile_errors, - saveTestFixtureSetup, saveTestFixtureTeardown - ), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/save/test_savesettings.c b/test/save/test_savesettings.c deleted file mode 100644 index 4d761fb8..00000000 --- a/test/save/test_savesettings.c +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "save/settings/savesettings.h" -#include "save/savejson.h" -#include "util/memory.h" -#include -#include - -// ============================================================ -// Helpers - wrap the writeInit/readInit macros (which can only be used -// inside a function returning errorret_t) so tests can call them plainly. -// ============================================================ - -static errorret_t settingsToJSON( - savesettings_t *settings, - char_t **outJson, - size_t *outLen -) { - writeInit(); - - errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object); - if(errorIsNotOk(writeResult)) { - yyjson_mut_doc_free(doc); - errorChain(writeResult); - } - - *outJson = yyjson_mut_write(doc, 0, outLen); - yyjson_mut_doc_free(doc); - assertNotNull(*outJson, "Failed to write settings JSON"); - - errorOk(); -} - -static errorret_t settingsFromJSON( - savesettings_t *settings, const char_t *json -) { - readInit(json, strlen(json)); - errorret_t readResult = saveSettingsReadJSON(settings, object); - yyjson_doc_free(jsonDoc); - errorChain(readResult); - errorOk(); -} - -// ============================================================ -// saveSettingsInit -// ============================================================ - -static void test_saveSettingsInit_resetsAudioMasterVolume(void **state) { - savesettings_t settings; - memoryZero(&settings, sizeof(settings)); - settings.audioMasterVolume = 42.0f; - - saveSettingsInit(&settings); - - assert_true(settings.audioMasterVolume == 1.0f); -} - -static void test_saveSettingsInit_nullAsserts(void **state) { - expect_assert_failure(saveSettingsInit(NULL)); -} - -// ============================================================ -// saveSettingsWriteJSON / saveSettingsReadJSON -// ============================================================ - -static void test_saveSettingsWriteJSON_nullAsserts(void **state) { - savesettings_t settings; - saveSettingsInit(&settings); - writeInit(); - - expect_assert_failure(saveSettingsWriteJSON(NULL, doc, object)); - expect_assert_failure(saveSettingsWriteJSON(&settings, NULL, object)); - expect_assert_failure(saveSettingsWriteJSON(&settings, doc, NULL)); - - yyjson_mut_doc_free(doc); -} - -static void test_saveSettingsReadJSON_nullAsserts(void **state) { - savesettings_t settings; - saveSettingsInit(&settings); - writeInit(); - - size_t len; - char_t *json = yyjson_mut_write(doc, 0, &len); - yyjson_mut_doc_free(doc); - assert_non_null(json); - - yyjson_doc *readDoc = yyjson_read(json, len, 0); - yyjson_val *readObject = yyjson_doc_get_root(readDoc); - - expect_assert_failure(saveSettingsReadJSON(NULL, readObject)); - expect_assert_failure(saveSettingsReadJSON(&settings, NULL)); - - yyjson_doc_free(readDoc); - free(json); -} - -static void test_saveSettingsWriteReadJSON_roundTrip(void **state) { - savesettings_t written; - saveSettingsInit(&written); - written.audioMasterVolume = 2.5f; - - char_t *json; - size_t len; - errorret_t writeRet = settingsToJSON(&written, &json, &len); - assert_true(errorIsOk(writeRet)); - - savesettings_t read; - saveSettingsInit(&read); - read.audioMasterVolume = -1.0f;// sentinel, should be overwritten - - errorret_t readRet = settingsFromJSON(&read, json); - assert_true(errorIsOk(readRet)); - assert_true(read.audioMasterVolume == 2.5f); - - free(json); -} - -static void test_saveSettingsReadJSON_missingKeyUsesDefault(void **state) { - savesettings_t settings; - saveSettingsInit(&settings); - settings.audioMasterVolume = 999.0f;// sentinel - - errorret_t ret = settingsFromJSON(&settings, "{\"version\":1}"); - assert_true(errorIsOk(ret)); - assert_true(settings.audioMasterVolume == 1.0f); -} - -// ============================================================ -// saveSettingsReadJSON - version enforcement -// ============================================================ - -static void test_saveSettingsReadJSON_versionMissing_errors(void **state) { - savesettings_t settings; - saveSettingsInit(&settings); - - errorret_t ret = settingsFromJSON(&settings, "{}"); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_saveSettingsReadJSON_versionMismatch_errors(void **state) { - savesettings_t settings; - saveSettingsInit(&settings); - - errorret_t ret = settingsFromJSON(&settings, "{\"version\":999}"); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_saveSettingsInit_resetsAudioMasterVolume), - cmocka_unit_test(test_saveSettingsInit_nullAsserts), - - cmocka_unit_test(test_saveSettingsWriteJSON_nullAsserts), - cmocka_unit_test(test_saveSettingsReadJSON_nullAsserts), - cmocka_unit_test(test_saveSettingsWriteReadJSON_roundTrip), - cmocka_unit_test(test_saveSettingsReadJSON_missingKeyUsesDefault), - cmocka_unit_test(test_saveSettingsReadJSON_versionMissing_errors), - cmocka_unit_test(test_saveSettingsReadJSON_versionMismatch_errors), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/save/test_saveslot.c b/test/save/test_saveslot.c deleted file mode 100644 index fab7b8a0..00000000 --- a/test/save/test_saveslot.c +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "save/slot/saveslot.h" -#include "save/save.h" -#include "save/savejson.h" -#include "savetestfixture.h" -#include "util/memory.h" -#include "util/string.h" -#include -#include - -// saveSlotInit() seeds the party roster via partyInit(), which loads -// party.jsonc through the real asset system - stub it out (see -// savetestfixture.h) rather than needing a real asset bundle here. -static int assetGroupSetup(void **state) { - saveTestFixtureStubPartyAsset(); - return 0; -} - -static int assetGroupTeardown(void **state) { - saveTestFixtureUnstubPartyAsset(); - return 0; -} - -// ============================================================ -// Helpers - wrap the writeInit/readInit macros (which can only be used -// inside a function returning errorret_t) so tests can call them plainly. -// ============================================================ - -static errorret_t slotToJSON( - saveslot_t *slot, char_t **outJson, size_t *outLen -) { - writeInit(); - - errorret_t writeResult = saveSlotWriteJSON(slot, doc, object); - if(errorIsNotOk(writeResult)) { - yyjson_mut_doc_free(doc); - errorChain(writeResult); - } - - *outJson = yyjson_mut_write(doc, 0, outLen); - yyjson_mut_doc_free(doc); - assertNotNull(*outJson, "Failed to write slot JSON"); - - errorOk(); -} - -static errorret_t slotFromJSON(saveslot_t *slot, const char_t *json) { - readInit(json, strlen(json)); - errorret_t readResult = saveSlotReadJSON(slot, object); - yyjson_doc_free(jsonDoc); - errorChain(readResult); - errorOk(); -} - -// ============================================================ -// saveSlotInit - only ever operates on SAVE.slot now. -// ============================================================ - -static void test_saveSlotInit_defaults(void **state) { - memorySet(&SAVE.slot, 0xFF, sizeof(SAVE.slot)); - - saveSlotInit(&SAVE.slot); - - assert_int_equal(SAVE.slot.version, 1); - assert_int_equal(SAVE.slot.dataType, 0); - assert_int_equal(SAVE.slot.cachedData.name[0], '\0'); - assert_true(SAVE.slot.cachedData.time.time == 0.0); - assert_int_equal(SAVE.slot.cachedData.playerLevel, 0); -} - -// ============================================================ -// saveSlotInUse / saveSlotHasSaved -// ============================================================ - -static void test_saveSlotInUse(void **state) { - saveSlotInit(&SAVE.slot); - - assert_false(saveSlotInUse(&SAVE.slot.cachedData)); - - stringCopy( - SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name) - ); - assert_true(saveSlotInUse(&SAVE.slot.cachedData)); -} - -static void test_saveSlotInUse_nullAsserts(void **state) { - expect_assert_failure(saveSlotInUse(NULL)); -} - -static void test_saveSlotHasSaved(void **state) { - saveSlotInit(&SAVE.slot); - - assert_false(saveSlotHasSaved(&SAVE.slot.cachedData)); - - // Writing to JSON stamps the current time as a side effect, so even a - // pure serialize (no device write) flips "has ever saved" to true. - char_t *json; - size_t len; - errorret_t ret = slotToJSON(&SAVE.slot, &json, &len); - assert_true(errorIsOk(ret)); - assert_true(saveSlotHasSaved(&SAVE.slot.cachedData)); - - free(json); -} - -static void test_saveSlotHasSaved_nullAsserts(void **state) { - expect_assert_failure(saveSlotHasSaved(NULL)); -} - -// ============================================================ -// saveSlotWriteJSON -// ============================================================ - -static void test_saveSlotWriteJSON_nullAsserts(void **state) { - // Content is never touched - both calls assert on a NULL doc/object - // argument before slot itself would actually be read. - saveslot_t slot; - writeInit(); - - expect_assert_failure(saveSlotWriteJSON(NULL, doc, object)); - expect_assert_failure(saveSlotWriteJSON(&slot, NULL, object)); - expect_assert_failure(saveSlotWriteJSON(&slot, doc, NULL)); - - yyjson_mut_doc_free(doc); -} - -// ============================================================ -// saveSlotReadJSON -// ============================================================ - -static void test_saveSlotReadJSON_nullAsserts(void **state) { - saveslot_t slot; - - yyjson_doc *readDoc = yyjson_read("{}", 2, 0); - yyjson_val *readObject = yyjson_doc_get_root(readDoc); - - expect_assert_failure(saveSlotReadJSON(NULL, readObject)); - expect_assert_failure(saveSlotReadJSON(&slot, NULL)); - - yyjson_doc_free(readDoc); -} - -static void test_saveSlotReadJSON_roundTrip(void **state) { - // saveSlotInit() only ever operates on SAVE.slot now, so that's the - // write source here. - saveSlotInit(&SAVE.slot); - SAVE.slot.version = 7;// deliberately non-default, not part of the JSON schema - stringCopy( - SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name) - ); - SAVE.slot.cachedData.playerLevel = 42; - // An in-use slot (non-empty name) requires a non-empty map name too - - // see saveSlotNewGame/requireStrMin("mapName", 1) in saveSlotVer1ReadJSON. - stringCopy( - SAVE.slot.cachedData.mapName, "overworld", - sizeof(SAVE.slot.cachedData.mapName) - ); - - char_t *json; - size_t len; - errorret_t writeRet = slotToJSON(&SAVE.slot, &json, &len); - assert_true(errorIsOk(writeRet)); - - const dusktimeepoch_t writtenTime = SAVE.slot.cachedData.time; - - // No pre-init needed - saveSlotReadJSON() fully resets/overwrites its - // destination struct regardless of prior content. - saveslot_t read; - errorret_t readRet = slotFromJSON(&read, json); - assert_true(errorIsOk(readRet)); - - assert_true(stringEquals(read.cachedData.name, "Hero")); - assert_int_equal(read.cachedData.playerLevel, 42); - assert_true(stringEquals(read.cachedData.mapName, "overworld")); - assert_true(read.cachedData.time.time == writtenTime.time); - - // version/dataType are struct-only bookkeeping, never serialized - the - // reader resets them to the default regardless of the writer's version. - assert_int_equal(read.version, 1); - - free(json); -} - -static void test_saveSlotReadJSON_blankSlot_shortCircuits(void **state) { - // Deliberately not saveSlotInit()'d - saveSlotReadJSON() fully resets its - // destination struct itself before parsing, so these sentinel values are - // only here to prove they get overwritten, not preserved. - saveslot_t slot; - memorySet(&slot, 0, sizeof(slot)); - stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name)); - slot.cachedData.playerLevel = 999; - - // No "name" key (or an empty one) means a blank/never-used slot (see - // saveSlotInUse) - the reader returns right after reading that, without - // needing or validating time/mapName/partyMembers/partyOrder at all, so - // this succeeds despite omitting fields that would otherwise be required. - errorret_t ret = slotFromJSON(&slot, "{\"version\":1}"); - assert_true(errorIsOk(ret)); - - assert_int_equal(slot.cachedData.name[0], '\0'); - assert_int_equal(slot.cachedData.mapName[0], '\0'); - // Not readInt32's own default of 1 - the early return happens before - // playerLevel is ever read, leaving the zero-init saveSlotCurrentReadJSON - // seeds ver1 with (see the memoryZero in saveSlotCurrentReadJSON). - assert_int_equal(slot.cachedData.playerLevel, 0); -} - -static void test_saveSlotReadJSON_inUseMissingMapName_errors(void **state) { - saveslot_t slot; - - // "name" is present (in use) but "mapName" is missing - a real save - // (via saveSlotNewGame) always has both, so this is corrupt. - errorret_t ret = slotFromJSON( - &slot, - "{\"version\":1,\"name\":\"Hero\"," - "\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}" - ); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) { - saveslot_t slot; - - errorret_t ret = slotFromJSON( - &slot, "{\"version\":1,\"name\":\"Hero\",\"playerLevel\":5}" - ); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_saveSlotReadJSON_nameTooLong_errors(void **state) { - saveslot_t slot; - - errorret_t ret = slotFromJSON( - &slot, - "{\"version\":1,\"name\":\"WayTooLongAName\"," - "\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}" - ); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) { - saveslot_t slot; - - // A non-object root has no keys, so every field falls back to its - // default - except "version" and "time", which are required and error - // instead. - errorret_t ret = slotFromJSON(&slot, "[]"); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -// ============================================================ -// saveSlotReadJSON - version enforcement -// ============================================================ - -static void test_saveSlotReadJSON_versionMissing_errors(void **state) { - saveslot_t slot; - - errorret_t ret = slotFromJSON( - &slot, - "{\"name\":\"Hero\"," - "\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}" - ); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -static void test_saveSlotReadJSON_versionMismatch_errors(void **state) { - saveslot_t slot; - - errorret_t ret = slotFromJSON( - &slot, - "{\"version\":999,\"name\":\"Hero\"," - "\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}" - ); - assert_true(errorIsNotOk(ret)); - errorCatch(ret); -} - -int main(void) { - assertInit(); - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_saveSlotInit_defaults), - - cmocka_unit_test(test_saveSlotInUse), - cmocka_unit_test(test_saveSlotInUse_nullAsserts), - cmocka_unit_test(test_saveSlotHasSaved), - cmocka_unit_test(test_saveSlotHasSaved_nullAsserts), - - cmocka_unit_test(test_saveSlotWriteJSON_nullAsserts), - - cmocka_unit_test(test_saveSlotReadJSON_nullAsserts), - cmocka_unit_test(test_saveSlotReadJSON_roundTrip), - cmocka_unit_test(test_saveSlotReadJSON_blankSlot_shortCircuits), - cmocka_unit_test(test_saveSlotReadJSON_inUseMissingMapName_errors), - cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors), - cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors), - cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors), - cmocka_unit_test(test_saveSlotReadJSON_versionMissing_errors), - cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors), - }; - - return cmocka_run_group_tests(tests, assetGroupSetup, assetGroupTeardown); -} diff --git a/test/thread/CMakeLists.txt b/test/thread/CMakeLists.txt deleted file mode 100644 index 9c865744..00000000 --- a/test/thread/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_thread.c) diff --git a/test/thread/test_thread.c b/test/thread/test_thread.c deleted file mode 100644 index 585ab348..00000000 --- a/test/thread/test_thread.c +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "thread/thread.h" -#include "util/memory.h" - -// --- Helpers --- - -static void helper_noop(thread_t *thread) { - // intentionally empty: one-shot thread that exits immediately -} - -static void helper_loop(thread_t *thread) { - while(!threadShouldStop(thread)) {} -} - -static void helper_write_data(thread_t *thread) { - int32_t *value = (int32_t *)thread->data; - *value = 42; -} - -// --- thread_t tests --- - -static void test_threadInit(void **state) { - thread_t thread; - threadInit(&thread, helper_noop); - - assert_int_equal(thread.state, THREAD_STATE_STOPPED); - assert_ptr_equal(thread.callback, helper_noop); - assert_null(thread.data); -} - -static void test_thread_start_stop(void **state) { - thread_t thread; - threadInit(&thread, helper_noop); - threadStart(&thread); - threadStop(&thread); - - assert_int_equal(thread.state, THREAD_STATE_STOPPED); -} - -static void test_thread_should_stop(void **state) { - // threadStop blocks until STOPPED - if threadShouldStop is broken the - // looping callback never exits and this test hangs / times out. - thread_t thread; - threadInit(&thread, helper_loop); - threadStart(&thread); - threadStop(&thread); - - assert_int_equal(thread.state, THREAD_STATE_STOPPED); -} - -static void test_thread_data(void **state) { - int32_t value = 0; - - thread_t thread; - threadInit(&thread, helper_write_data); - thread.data = &value; - threadStart(&thread); - threadStop(&thread); - - // After threadStop the callback has definitely run. - assert_int_equal(value, 42); -} - -static void test_thread_restart(void **state) { - // A thread can be started, stopped, and started again. - thread_t thread; - threadInit(&thread, helper_noop); - - threadStart(&thread); - threadStop(&thread); - assert_int_equal(thread.state, THREAD_STATE_STOPPED); - - // Re-initialise so threadId / state are reset, then start again. - threadInit(&thread, helper_noop); - threadStart(&thread); - threadStop(&thread); - assert_int_equal(thread.state, THREAD_STATE_STOPPED); -} - -// --- threadmutex_t tests --- - -static void test_threadMutex_lock_unlock(void **state) { - threadmutex_t mutex; - threadMutexInit(&mutex); - - threadMutexLock(&mutex); - threadMutexUnlock(&mutex); - - threadMutexDispose(&mutex); -} - -// Shared data for try-lock test. Uses volatile phase to coordinate the two -// threads without introducing a second mutex. -typedef struct { - threadmutex_t *target; - volatile int32_t phase; - bool_t resultWhileLocked; - bool_t resultAfterUnlock; -} trylock_data_t; - -static void helper_trylock(thread_t *thread) { - trylock_data_t *data = (trylock_data_t *)thread->data; - - // Phase 1: main holds the lock - trylock must fail. - while(data->phase != 1) {} - data->resultWhileLocked = threadMutexTryLock(data->target); - data->phase = 2; - - // Phase 3: main released the lock - trylock must succeed. - while(data->phase != 3) {} - data->resultAfterUnlock = threadMutexTryLock(data->target); - if(data->resultAfterUnlock) { - threadMutexUnlock(data->target); - } - data->phase = 4; -} - -static void test_threadMutex_try_lock(void **state) { - threadmutex_t mutex; - threadMutexInit(&mutex); - - trylock_data_t data = { - .target = &mutex, - .phase = 0, - .resultWhileLocked = false, - .resultAfterUnlock = false - }; - - thread_t thread; - threadInit(&thread, helper_trylock); - thread.data = &data; - threadStart(&thread); - - // Hold the lock, then let the helper try. - threadMutexLock(&mutex); - data.phase = 1; - while(data.phase != 2) {} - assert_false(data.resultWhileLocked); - - // Release, then let the helper try again. - threadMutexUnlock(&mutex); - data.phase = 3; - while(data.phase != 4) {} - assert_true(data.resultAfterUnlock); - - threadStop(&thread); - threadMutexDispose(&mutex); -} - -// Mutual-exclusion test: N threads each increment a shared counter M times -// under a mutex. The final value must be exactly N*M. -#define MUTEX_THREADS 4 -#define MUTEX_ITERATIONS 10000 - -typedef struct { - threadmutex_t *mutex; - int32_t *counter; -} counter_data_t; - -static counter_data_t counter_thread_data[MUTEX_THREADS]; -static thread_t counter_threads[MUTEX_THREADS]; - -static void helper_increment(thread_t *thread) { - counter_data_t *data = (counter_data_t *)thread->data; - for(int32_t i = 0; i < MUTEX_ITERATIONS; i++) { - threadMutexLock(data->mutex); - (*data->counter)++; - threadMutexUnlock(data->mutex); - } -} - -static void test_threadMutex_mutual_exclusion(void **state) { - threadmutex_t mutex; - threadMutexInit(&mutex); - int32_t counter = 0; - - for(int32_t i = 0; i < MUTEX_THREADS; i++) { - counter_thread_data[i].mutex = &mutex; - counter_thread_data[i].counter = &counter; - threadInit(&counter_threads[i], helper_increment); - counter_threads[i].data = &counter_thread_data[i]; - } - - for(int32_t i = 0; i < MUTEX_THREADS; i++) { - threadStart(&counter_threads[i]); - } - - for(int32_t i = 0; i < MUTEX_THREADS; i++) { - threadStop(&counter_threads[i]); - } - - assert_int_equal(counter, MUTEX_THREADS * MUTEX_ITERATIONS); - - threadMutexDispose(&mutex); -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_threadInit), - cmocka_unit_test(test_thread_start_stop), - cmocka_unit_test(test_thread_should_stop), - cmocka_unit_test(test_thread_data), - cmocka_unit_test(test_thread_restart), - cmocka_unit_test(test_threadMutex_lock_unlock), - cmocka_unit_test(test_threadMutex_try_lock), - cmocka_unit_test(test_threadMutex_mutual_exclusion), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/test/time/CMakeLists.txt b/test/time/CMakeLists.txt deleted file mode 100644 index 06f100b5..00000000 --- a/test/time/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_time.c) \ No newline at end of file diff --git a/test/time/test_time.c b/test/time/test_time.c deleted file mode 100644 index edddb2e4..00000000 --- a/test/time/test_time.c +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include - -// Mocks -static uint64_t SDL_GETTICKS_TICKS = 0; -static bool_t SDL_GETTICKS_CALLED = false; - -uint64_t SDL_GetTicks64(void) { - SDL_GETTICKS_CALLED = true; - return SDL_GETTICKS_TICKS; -} - -// Mocked includes -#include "util/memory.h" -#include "time/time.h" - -// Tests -static void test_timeInit(void **state) { - - // Fill time with garbage - memorySet(&TIME, 0xFF, sizeof(TIME)); - - // Init - timeInit(); - - // Inital struct is set. - assert_float_equal(TIME.time, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.dynamicTime, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.dynamicDelta, DUSK_TIME_STEP, 0.0001f); - assert_false(TIME.dynamicUpdate); -} - -static void test_timeUpdate(void **state) { - - - // Init first - timeInit(); - - // First time update, calls SDL_GetTicks - SDL_GETTICKS_TICKS = 14; // Simulate 15ms elapsed (just barely not one step) - SDL_GETTICKS_CALLED = false; - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - assert_true(TIME.dynamicUpdate); - assert_float_equal(TIME.dynamicDelta, 0.014f, 0.0001f); - assert_float_equal( - TIME.dynamicTime, DUSK_TIME_STEP + TIME.dynamicDelta, 0.0001f - ); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); // Not changed - assert_float_equal(TIME.time, DUSK_TIME_STEP, 0.0001f); // Not changed - - // Simulate total 16ms elapsed (2ms more) - SDL_GETTICKS_TICKS = 16; - SDL_GETTICKS_CALLED = false; - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - assert_false(TIME.dynamicUpdate); - assert_float_equal(TIME.dynamicDelta, 0.002f, 0.0001f); - assert_float_equal(TIME.dynamicTime, DUSK_TIME_STEP * 2, 0.0001f); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.time, DUSK_TIME_STEP * 2, 0.0001f); // Stepped once - - // Second test, half step (16 + 8) - SDL_GETTICKS_TICKS = 24; - SDL_GETTICKS_CALLED = false; - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - assert_true(TIME.dynamicUpdate); - assert_float_equal(TIME.dynamicDelta, 0.008f, 0.0001f); - assert_float_equal(TIME.dynamicTime, DUSK_TIME_STEP + 0.024f, 0.0001f); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.time, DUSK_TIME_STEP * 2, 0.0001f);// Unchanged. - - // Third test, another half step, leading to non dynamic update - SDL_GETTICKS_TICKS = 32; - SDL_GETTICKS_CALLED = false; - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - assert_false(TIME.dynamicUpdate); - assert_float_equal(TIME.dynamicDelta, 0.008f, 0.0001f); - assert_float_equal(TIME.dynamicTime, DUSK_TIME_STEP + 0.032f, 0.0001f); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.time, DUSK_TIME_STEP * 3, 0.0001f); - - // Fourth test, large jump, should only step once - SDL_GETTICKS_TICKS = 100; // Simulate 100ms elapsed - SDL_GETTICKS_CALLED = false; - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - assert_false(TIME.dynamicUpdate); - assert_float_equal(TIME.dynamicDelta, 0.068f, 0.0001f); - assert_float_equal(TIME.dynamicTime, DUSK_TIME_STEP + 0.100f, 0.0001f); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.time, DUSK_TIME_STEP * 4, 0.0001f); - - // Fifth test, despite a small time passing the game should not start - // trying to run ahead - SDL_GETTICKS_TICKS = 104; // Simulate 4ms elapsed - SDL_GETTICKS_CALLED = false; - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - assert_true(TIME.dynamicUpdate); - assert_float_equal(TIME.dynamicDelta, 0.004f, 0.0001f); - assert_float_equal(TIME.dynamicTime, DUSK_TIME_STEP + 0.104f, 0.0001f); - assert_float_equal(TIME.delta, DUSK_TIME_STEP, 0.0001f); - assert_float_equal(TIME.time, DUSK_TIME_STEP * 4, 0.0001f); - - // Time can stand still if needed - SDL_GETTICKS_CALLED = false; - SDL_GETTICKS_TICKS = 104; // No time elapsed - timeUpdate(); - assert_true(SDL_GETTICKS_CALLED); - - // Time cannot go backwards - SDL_GETTICKS_TICKS = 90; - SDL_GETTICKS_CALLED = false; - expect_assert_failure(timeUpdate()); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_timeInit), - cmocka_unit_test(test_timeUpdate), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/util/CMakeLists.txt b/test/util/CMakeLists.txt deleted file mode 100644 index 325295c1..00000000 --- a/test/util/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -include(dusktest) - -# Tests -dusktest(test_array.c) -dusktest(test_math.c) -dusktest(test_memory.c) -dusktest(test_string.c) -dusktest(test_sort.c) \ No newline at end of file diff --git a/test/util/test_array.c b/test/util/test_array.c deleted file mode 100644 index da46709d..00000000 --- a/test/util/test_array.c +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "util/memory.h" -#include "util/array.h" - -static void test_arrayReverse(void **state) { - - - uint8_t array1[] = { 1, 2, 3, 4, 5 }; - arrayReverse(array1, 5, sizeof(uint8_t)); - uint8_t expected1[] = {5,4,3,2,1}; - assert_memory_equal(array1, expected1, sizeof(uint8_t) * 5); - - uint8_t array2[] = { 10, 20, 30, 40 }; - arrayReverse(array2, 4, sizeof(uint8_t)); - uint8_t expected2[] = {40,30,20,10}; - assert_memory_equal(array2, expected2, sizeof(uint8_t) * 4); - - uint8_t array3[] = { 7 }; - arrayReverse(array3, 1, sizeof(uint8_t)); - uint8_t expected3[] = {7}; - assert_memory_equal(array3, expected3, sizeof(uint8_t) * 1); - - uint8_t array4[] = {}; - arrayReverse(array4, 0, sizeof(uint8_t)); - uint8_t expected4[] = {}; - assert_memory_equal(array4, expected4, sizeof(uint8_t) * 0); - - // Can reverse arrays of larger element sizes - uint16_t array5[] = { 1000, 2000, 3000 }; - arrayReverse(array5, 3, sizeof(uint16_t)); - uint16_t expected5[] = {3000,2000,1000}; - assert_memory_equal(array5, expected5, sizeof(uint16_t) * 3); - - uint64_t array6[] = { 100000, 200000, 300000, 400000 }; - arrayReverse(array6, 4, sizeof(uint64_t)); - uint64_t expected6[] = {400000,300000,200000,100000}; - assert_memory_equal(array6, expected6, sizeof(uint64_t) * 4); - - // Will only sort provided array length, anything after is untouched - uint8_t array7[] = { 1, 2, 3, 4, 5, 6, 7 }; - arrayReverse(array7, 5, sizeof(uint8_t)); - uint8_t expected7[] = {5,4,3,2,1,6,7}; - assert_memory_equal(array7, expected7, sizeof(uint8_t) * 7); - - // Should fail when given NULL array pointer - expect_assert_failure(arrayReverse(NULL, 5, sizeof(uint8_t))); - - // Should fail when given zero size - expect_assert_failure(arrayReverse(array1, 5, 0)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_arrayReverse), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/util/test_math.c b/test/util/test_math.c deleted file mode 100644 index df5672da..00000000 --- a/test/util/test_math.c +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "util/math.h" - -static void test_mathNextPowTwo(void **state) { - - - // Positive integers - assert_int_equal(mathNextPowTwo(0), 1); - assert_int_equal(mathNextPowTwo(1), 1); - assert_int_equal(mathNextPowTwo(2), 2); - assert_int_equal(mathNextPowTwo(3), 4); - assert_int_equal(mathNextPowTwo(4), 4); - assert_int_equal(mathNextPowTwo(5), 8); - assert_int_equal(mathNextPowTwo(15), 16); - assert_int_equal(mathNextPowTwo(16), 16); - assert_int_equal(mathNextPowTwo(17), 32); - assert_int_equal(mathNextPowTwo(31), 32); - assert_int_equal(mathNextPowTwo(32), 32); - assert_int_equal(mathNextPowTwo(33), 64); - assert_int_equal(mathNextPowTwo(63), 64); - assert_int_equal(mathNextPowTwo(64), 64); - assert_int_equal(mathNextPowTwo(65), 128); - - // Large values - assert_int_equal(mathNextPowTwo(1000), 1024); - assert_int_equal(mathNextPowTwo(4095), 4096); - - // Zero - assert_int_equal(mathNextPowTwo(0), 1); - - // Max Value - assert_int_equal(mathNextPowTwo(0xFFFFFFFF), 1); - - // Edge: value just below and above a power of two - assert_int_equal(mathNextPowTwo(255), 256); - assert_int_equal(mathNextPowTwo(257), 512); -} - -static void test_mathMax(void **state) { - // Test positive integers - assert_int_equal(mathMax(1, 2), 2); - assert_int_equal(mathMax(2, 1), 2); - - // Test negative integers - assert_int_equal(mathMax(-1, -2), -1); - assert_int_equal(mathMax(-4, -2), -2); - - // Test 0 - assert_int_equal(mathMax(0, -1), 0); - assert_int_equal(mathMax(-1, 0), 0); - - // Test large values - assert_int_equal(mathMax(1000000, 999999), 1000000); - assert_int_equal(mathMax(999999, 1000000), 1000000); - - // Test mixed negative and positive - assert_int_equal(mathMax(-32, 5), 5); - assert_int_equal(mathMax(5, -32), 5); - - // Floats with floats - assert_float_equal(mathMax(1.5f, 2.5f), 2.5f, 0.0001f); - assert_float_equal(mathMax(2.5f, 1.5f), 2.5f, 0.0001f); - assert_float_equal(mathMax(-1.5f, -2.5f), -1.5f, 0.0001f); - assert_float_equal(mathMax(-2.5f, -1.5f), -1.5f, 0.0001f); - assert_float_equal(mathMax(0.0f, -1.5f), 0.0f, 0.0001f); - assert_float_equal(mathMax(-1.5f, 0.0f), 0.0f, 0.0001f); - assert_float_equal(mathMax(500.0f, -333.0f), 500.0f, 0.0001f); - assert_float_equal(mathMax(-333.0f, 500.0f), 500.0f, 0.0001f); - - // Floats with integers - assert_float_equal(mathMax(1, 2.5f), 2.5f, 0.0001f); - assert_float_equal(mathMax(2.5f, 1), 2.5f, 0.0001f); - assert_float_equal(mathMax(-1, -2.5f), -1.0f, 0.0001f); - assert_float_equal(mathMax(-2.5f, -1), -1.0f, 0.0001f); - assert_float_equal(mathMax(0, -1.5f), 0.0f, 0.0001f); - assert_float_equal(mathMax(-1.5f, 0), 0.0f, 0.0001f); - assert_float_equal(mathMax(500, -333.5f), 500.0f, 0.0001f); - assert_float_equal(mathMax(-333.5f, 500), 500.0f, 0.0001f); - - // Equal values - assert_int_equal(mathMax(5, 5), 5); - assert_float_equal(mathMax(2.5f, 2.5f), 2.5f, 0.0001f); - - // Special float values - assert_float_equal(mathMax(-0.0f, 0.0f), 0.0f, 0.0001f); -} - -static void test_mathMin(void **state) { - - - // Positive integers - assert_int_equal(mathMin(1, 2), 1); - assert_int_equal(mathMin(2, 1), 1); - assert_int_equal(mathMin(0, 5), 0); - assert_int_equal(mathMin(5, 0), 0); - - // Negative integers - assert_int_equal(mathMin(-1, -2), -2); - assert_int_equal(mathMin(-2, -1), -2); - assert_int_equal(mathMin(-5, -10), -10); - assert_int_equal(mathMin(-10, -5), -10); - - // Mixed negative and positive - assert_int_equal(mathMin(-5, 32), -5); - assert_int_equal(mathMin(32, -5), -5); - - // Large values - assert_int_equal(mathMin(1000000, 999999), 999999); - assert_int_equal(mathMin(999999, 1000000), 999999); - - // Floats with floats - assert_float_equal(mathMin(1.5f, 2.5f), 1.5f, 0.0001f); - assert_float_equal(mathMin(2.5f, 1.5f), 1.5f, 0.0001f); - assert_float_equal(mathMin(-1.5f, -2.5f), -2.5f, 0.0001f); - assert_float_equal(mathMin(-2.5f, -1.5f), -2.5f, 0.0001f); - assert_float_equal(mathMin(0.0f, -1.5f), -1.5f, 0.0001f); - assert_float_equal(mathMin(-1.5f, 0.0f), -1.5f, 0.0001f); - assert_float_equal(mathMin(500.0f, -333.0f), -333.0f, 0.0001f); - assert_float_equal(mathMin(-333.0f, 500.0f), -333.0f, 0.0001f); - - // Floats with integers - assert_float_equal(mathMin(1, 2.5f), 1.0f, 0.0001f); - assert_float_equal(mathMin(2.5f, 1), 1.0f, 0.0001f); - assert_float_equal(mathMin(-1, -2.5f), -2.5f, 0.0001f); - assert_float_equal(mathMin(-2.5f, -1), -2.5f, 0.0001f); - assert_float_equal(mathMin(0, -1.5f), -1.5f, 0.0001f); - assert_float_equal(mathMin(-1.5f, 0), -1.5f, 0.0001f); - assert_float_equal(mathMin(500, -333.5f), -333.5f, 0.0001f); - assert_float_equal(mathMin(-333.5f, 500), -333.5f, 0.0001f); - - // Equal values - assert_int_equal(mathMin(5, 5), 5); - assert_float_equal(mathMin(2.5f, 2.5f), 2.5f, 0.0001f); - - // Special float values - assert_float_equal(mathMin(-0.0f, 0.0f), -0.0f, 0.0001f); -} - -static void test_mathClamp(void **state) { - - - // Positive Integer bounds. - assert_int_equal(mathClamp(5, 1, 10), 5); // Within bounds - assert_int_equal(mathClamp(0, 1, 10), 1); // Below lower bound - assert_int_equal(mathClamp(15, 1, 10), 10); // Above upper bound - - // Negative Integer bounds. - assert_int_equal(mathClamp(-5, -10, -1), -5); // Within bounds - assert_int_equal(mathClamp(-15, -10, -1), -10); // Below lower bound - assert_int_equal(mathClamp(0, -10, -1), -1); // Above upper bound - - // Mixed Integer bounds. - assert_int_equal(mathClamp(0, -5, 5), 0); // Within bounds - assert_int_equal(mathClamp(-10, -5, 5), -5); // Below lower bound - assert_int_equal(mathClamp(10, -5, 5), 5); // Above upper bound - - // Positive Float bounds. - assert_float_equal(mathClamp(5.5f, 1.0f, 10.0f), 5.5f, 0.0001f); // Within - assert_float_equal(mathClamp(0.5f, 1.0f, 10.0f), 1.0f, 0.0001f); // Below - assert_float_equal(mathClamp(15.5f, 1.0f, 10.0f), 10.0f, 0.0001f); // Above - - // Negative Float bounds. - assert_float_equal(mathClamp(-5.5f, -10.0f, -1.0f), -5.5f, 0.0001f); // Within - assert_float_equal(mathClamp(-15.5f, -10.0f, -1.0f), -10.0f, 0.0001f);// Below - assert_float_equal(mathClamp(0.0f, -10.0f, -1.0f), -1.0f, 0.0001f); // Above - - // Mixed Float bounds. - assert_float_equal(mathClamp(0.0f, -5.0f, 5.0f), 0.0f, 0.0001f); // Within - assert_float_equal(mathClamp(-10.0f, -5.0f, 5.0f), -5.0f, 0.0001f); // Below - assert_float_equal(mathClamp(10.0f, -5.0f, 5.0f), 5.0f, 0.0001f); // Above - - // With integers and floats mixed - assert_float_equal(mathClamp(5, 1.0f, 10.0f), 5.0f, 0.0001f); // Within - assert_float_equal(mathClamp(0, 1.0f, 10.0f), 1.0f, 0.0001f); // Below - assert_float_equal(mathClamp(15, 1.0f, 10.0f), 10.0f, 0.0001f); // Above - assert_float_equal(mathClamp(5.5f, 1, 10), 5.5f, 0.0001f); // Within - assert_float_equal(mathClamp(0.5f, 1, 10), 1.0f, 0.0001f); // Below - assert_float_equal(mathClamp(15.5f, 1, 10), 10.0f, 0.0001f); // Above - assert_float_equal(mathClamp(-5, -10.0f, -1.0f), -5.0f, 0.0001f); // Within - assert_float_equal(mathClamp(-15, -10.0f, -1.0f), -10.0f, 0.0001f);// Below - assert_float_equal(mathClamp(0, -10.0f, -1.0f), -1.0f, 0.0001f); // Above - assert_float_equal(mathClamp(-5.5f, -10, -1), -5.5f, 0.0001f); // Within - assert_float_equal(mathClamp(-15.5f, -10, -1), -10.0f, 0.0001f);// Below - assert_float_equal(mathClamp(0.0f, -10, -1), -1.0f, 0.0001f); // Above - - // Edge: value equals min or max - assert_int_equal(mathClamp(1, 1, 10), 1); - assert_int_equal(mathClamp(10, 1, 10), 10); - assert_float_equal(mathClamp(1.0f, 1.0f, 10.0f), 1.0f, 0.0001f); - assert_float_equal(mathClamp(10.0f, 1.0f, 10.0f), 10.0f, 0.0001f); - - // Edge: min > max (undefined, but should return min or max) - assert_int_equal(mathClamp(5, 10, 1), 1); // Should clamp to upper - assert_float_equal(mathClamp(5.0f, 10.0f, 1.0f), 1.0f, 0.0001f); -} - -static void test_mathAbs(void **state) { - - - // Positive integers - assert_int_equal(mathAbs(5), 5); - assert_int_equal(mathAbs(0), 0); - - // Negative integers - assert_int_equal(mathAbs(-5), 5); - assert_int_equal(mathAbs(-100), 100); - - // Positive floats - assert_float_equal(mathAbs(5.5f), 5.5f, 0.0001f); - assert_float_equal(mathAbs(0.0f), 0.0f, 0.0001f); - - // Negative floats - assert_float_equal(mathAbs(-5.5f), 5.5f, 0.0001f); - assert_float_equal(mathAbs(-100.25f), 100.25f, 0.0001f); - - // Edge: -0.0f - assert_float_equal(mathAbs(-0.0f), 0.0f, 0.0001f); - - // INT_MIN edge case is NOT handled due to two's complement overflow - assert_int_equal(mathAbs(INT32_MIN + 1), 2147483647); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_mathNextPowTwo), - cmocka_unit_test(test_mathMax), - cmocka_unit_test(test_mathMin), - cmocka_unit_test(test_mathClamp), - cmocka_unit_test(test_mathAbs), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/util/test_memory.c b/test/util/test_memory.c deleted file mode 100644 index fe8b3e83..00000000 --- a/test/util/test_memory.c +++ /dev/null @@ -1,536 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "util/memory.h" - -static void test_memoryAllocate(void **state) { - - - // Memory should allocate - void *ptr = memoryAllocate(128); - assert_non_null(ptr); - - // Allocating should be tracked - assert_int_equal(memoryGetAllocatedCount(), 1); - - void *ptr2 = memoryAllocate(256); - assert_non_null(ptr2); - assert_int_equal(memoryGetAllocatedCount(), 2); - - // Free memory - memoryFree(ptr); - assert_int_equal(memoryGetAllocatedCount(), 1); - memoryFree(ptr2); - assert_int_equal(memoryGetAllocatedCount(), 0); - - // Should not be able to allocate 0 bytes - expect_assert_failure(memoryAllocate(0)); - - // Should not be able to allocate more memory than possible - expect_assert_failure(memoryAllocate(SIZE_MAX)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryFree(void **state) { - - - // Create some memory - void *ptr = memoryAllocate(64); - assert_non_null(ptr); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Free - memoryFree(ptr); - - // Allocated count should decrease - assert_int_equal(memoryGetAllocatedCount(), 0); - - ptr = memoryAllocate(32); - assert_non_null(ptr); - void *ptr2 = memoryAllocate(32); - assert_non_null(ptr2); - assert_int_equal(memoryGetAllocatedCount(), 2); - - // Free both - memoryFree(ptr); - assert_int_equal(memoryGetAllocatedCount(), 1); - memoryFree(ptr2); - assert_int_equal(memoryGetAllocatedCount(), 0); - - // Expect unable to free NULL - expect_assert_failure(memoryFree(NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryCopy(void **state) { - - - // Create some memory - size_t size = 32; - void *src = memoryAllocate(size); - void *dest = memoryAllocate(size); - assert_non_null(src); - assert_non_null(dest); - - // Fill source with known pattern and copy - memorySet(src, 0xAB, size); - memoryCopy(dest, src, size); - assert_memory_equal(src, dest, size); - - // Should not be able to copy to NULL - expect_assert_failure(memoryCopy(NULL, src, size)); - - // Should not be able to copy from NULL - expect_assert_failure(memoryCopy(dest, NULL, size)); - - // Cannot copy 0 bytes - expect_assert_failure(memoryCopy(dest, src, 0)); - - // Cannot copy to itself - expect_assert_failure(memoryCopy(src, src, size)); - - // Overlapping regions (should assert) - void *overlap = memoryAllocate(size * 2); - expect_assert_failure( - memoryCopy((uint8_t*)overlap, (uint8_t*)overlap + 8, size) - ); - expect_assert_failure( - memoryCopy((uint8_t*)overlap + 8, (uint8_t*)overlap, size) - ); - memoryFree(overlap); - - memoryFree(src); - memoryFree(dest); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memorySet(void **state) { - - - // Allocate memory - size_t size = 16; - void *ptr = memoryAllocate(size); - assert_non_null(ptr); - - // Fill with pattern - memorySet(ptr, 0xCD, size); - uint8_t expected[16]; - for(size_t i = 0; i < size; i++) { - expected[i] = 0xCD; - } - assert_memory_equal(ptr, expected, size); - - // Cannot set to NULL - expect_assert_failure(memorySet(NULL, 0x00, size)); - - // Canot set 0 bytes - expect_assert_failure(memorySet(ptr, 0x00, 0)); - - // Set with value outside 0-255 (should be cast to uint8_t) - memorySet(ptr, 0x1FF, size); - for(size_t i = 0; i < size; i++) { - assert_int_equal(((uint8_t*)ptr)[i], 0xFF); - } - - memoryFree(ptr); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryZero(void **state) { - - - // Create some memory - size_t size = 20; - void *ptr = memoryAllocate(size); - assert_non_null(ptr); - - // Fill and then zero - memorySet(ptr, 0xEF, size); - memoryZero(ptr, size); - - // All memory should be zeroed - uint8_t expected[20] = {0}; - assert_memory_equal(ptr, expected, size); - - // Cannot zero to NULL pointer - expect_assert_failure(memoryZero(NULL, size)); - - // Cannot zero 0 bytes - expect_assert_failure(memoryZero(ptr, 0)); - - memoryFree(ptr); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryCopyRangeSafe(void **state) { - - - // Create some memory - size_t size = 10; - void *src = memoryAllocate(size); - void *dest = memoryAllocate(size); - assert_non_null(src); - assert_non_null(dest); - - // Initialize source with known pattern - for(size_t i = 0; i < size; i++) { - ((uint8_t*)src)[i] = (uint8_t)(i + 1); // 1, 2, ..., 10 - } - - // Copy a range from src[2] to src[7] (5 bytes) into dest - memoryCopyRangeSafe(dest, (uint8_t*)src + 2, (uint8_t*)src + 7, size); - uint8_t expected[10] = {3, 4, 5, 6, 7}; // Expected data in dest - assert_memory_equal(dest, expected, 5); - - // Copy entire buffer - memoryCopyRangeSafe(dest, src, (uint8_t*)src + size, size); - assert_memory_equal(dest, src, size); - - // Cannot copy to NULL destination - expect_assert_failure( - memoryCopyRangeSafe(NULL, src, (uint8_t*)src + 5, size) - ); - - // Cannot copy from NULL start - expect_assert_failure( - memoryCopyRangeSafe(dest, NULL, (uint8_t*)src + 5, size) - ); - - // Cannot copy from NULL end - expect_assert_failure(memoryCopyRangeSafe(dest, (uint8_t*)src, NULL, size)); - - // Start and end are the same - expect_assert_failure( - memoryCopyRangeSafe(dest, (uint8_t*)src, (uint8_t*)src, size) - ); - - // End is before start - expect_assert_failure( - memoryCopyRangeSafe(dest, (uint8_t*)src + 5, (uint8_t*)src + 2, size) - ); - - // Size to copy exceeds maximum - expect_assert_failure( - memoryCopyRangeSafe(dest, (uint8_t*)src, (uint8_t*)src + 10, 5) - ); - - memoryFree(src); - memoryFree(dest); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryMove(void **state) { - - - // Create some memory - size_t size = 15; - void *ptr = memoryAllocate(size + 5); // Extra space for overlap - assert_non_null(ptr); - - // Initialize memory with known pattern - for(size_t i = 0; i < size + 5; i++) { - ((uint8_t*)ptr)[i] = (uint8_t)(i + 1); // 1, 2, ..., size+5 - } - - // Move overlapping region: from ptr[0..14] to ptr[5..19] - memoryMove((uint8_t*)ptr + 5, ptr, size); - - // Expected pattern after move - uint8_t expected[20]; - for(size_t i = 0; i < 5; i++) { - expected[i] = (uint8_t)(i + 1); // Original first 5 bytes - } - for(size_t i = 5; i < 20; i++) { - expected[i] = (uint8_t)(i - 4); // Moved bytes - } - assert_memory_equal(ptr, expected, size + 5); - - // Overlap backward: move ptr[5..19] to ptr[0..14] - for(size_t i = 0; i < size + 5; i++) { - ((uint8_t*)ptr)[i] = (uint8_t)(i + 1); - } - memoryMove(ptr, (uint8_t*)ptr + 5, size); - for(size_t i = 0; i < size; i++) { - assert_int_equal(((uint8_t*)ptr)[i], (uint8_t)(i + 6)); - } - - // Cannot move to NULL - expect_assert_failure(memoryMove(NULL, ptr, size)); - - // Cannot move from NULL - expect_assert_failure(memoryMove(ptr, NULL, size)); - - // Cannot move 0 bytes - expect_assert_failure(memoryMove(ptr, ptr, 0)); - - // Cannot move to itself - expect_assert_failure(memoryMove(ptr, ptr, size)); - - memoryFree(ptr); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryCompare(void **state) { - - - // Create two memory blocks - size_t size = 8; - void *a = memoryAllocate(size); - void *b = memoryAllocate(size); - assert_non_null(a); - assert_non_null(b); - - // Initialize both with same data - for(size_t i = 0; i < size; i++) { - ((uint8_t*)a)[i] = (uint8_t)(i + 1); - ((uint8_t*)b)[i] = (uint8_t)(i + 1); - } - - // They should be equal - assert_int_equal(memoryCompare(a, b, size), 0); - - // Change b and compare again - ((uint8_t*)b)[size - 1] = 0xFF; - assert_true(memoryCompare(a, b, size) < 0); - - // Change a and compare again - ((uint8_t*)a)[size - 1] = 0xFF; - ((uint8_t*)a)[size - 2] = 0xFE; - assert_true(memoryCompare(a, b, size) > 0); - - // CAnnot compare with NULL a - expect_assert_failure(memoryCompare(NULL, b, size)); - - // Cannot compare with NULL b - expect_assert_failure(memoryCompare(a, NULL, size)); - - // Comparing with self always equal - assert_int_equal(memoryCompare(a, a, size), 0); - assert_int_equal(memoryCompare(b, b, size), 0); - - // Comparison of 0 bytes is always true - assert_int_equal(memoryCompare(a, b, 0), 0); - - // Compare different sizes (should assert, so we only test the API contract) - // Not possible with current API, as size is explicit, but document intent. - - memoryFree(a); - memoryFree(b); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryReallocate(void **state) { - - - size_t initialSize = 16; - void *ptr = memoryAllocate(initialSize); - assert_non_null(ptr); - - // Reallocate to a larger size - size_t newSize = 32; - memoryReallocate(&ptr, newSize); - assert_non_null(ptr); - - // Reallocate to a smaller size - size_t smallerSize = 8; - memoryReallocate(&ptr, smallerSize); - assert_non_null(ptr); - - // Cannot realloc to size 0 - expect_assert_failure(memoryReallocate(&ptr, 0)); - - // Cannot realloc NULL pointer - expect_assert_failure(memoryReallocate(NULL, 16)); - - // Cannot reallocate more memory than possible - expect_assert_failure(memoryReallocate(&ptr, SIZE_MAX)); - - // All we really care about is that the pointer is valid after reallocations - memoryFree(ptr); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryResize(void **state) { - - - size_t initialSize = 32; - void *ptr = memoryAllocate(initialSize); - assert_non_null(ptr); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Initialize memory - for(size_t i = 0; i < initialSize; i++) { - ((uint8_t*)ptr)[i] = (uint8_t)(i + 1); - } - - // Equal size shouldn't touch the pointer - void *oldPtr = ptr; - memoryResize(&ptr, initialSize, initialSize); - assert_non_null(ptr); - assert_ptr_equal(ptr, oldPtr); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Reallocate to a larger size, it should not leak. - size_t newSize = 64; - memoryResize(&ptr, initialSize, newSize); - assert_non_null(ptr); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Check that old data is preserved - for(size_t i = 0; i < initialSize; i++) { - assert_int_equal(((uint8_t*)ptr)[i], (uint8_t)(i + 1)); - } - - // Reallocate to a smaller size - size_t smallerSize = 16; - memoryResize(&ptr, newSize, smallerSize); - assert_non_null(ptr); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Check that data is still correct up to the smaller size - for(size_t i = 0; i < smallerSize; i++) { - assert_int_equal(((uint8_t*)ptr)[i], (uint8_t)(i + 1)); - } - - // Cannot realloc to size 0 - expect_assert_failure(memoryResize(&ptr, smallerSize, 0)); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Cannot take NULL - expect_assert_failure(memoryResize(NULL, smallerSize, 16)); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // memoryResize with oldsize of 0 not possible - expect_assert_failure(memoryResize(&ptr, 0, 16)); - assert_int_equal(memoryGetAllocatedCount(), 1); - - // Cannot resize more memory than possible - expect_assert_failure(memoryResize(&ptr, smallerSize, SIZE_MAX)); - assert_int_equal(memoryGetAllocatedCount(), 1); - - memoryFree(ptr); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_memoryCopyInterleaved(void **state) { - - - // Basic: copy 4 uint32_t values from contiguous source into every-other dest - // slot (stride = 2*sizeof(uint32_t)). - uint32_t src[4] = { 0x11111111, 0x22222222, 0x33333333, 0x44444444 }; - uint32_t dest[8]; - memoryZero(dest, sizeof(dest)); - - memoryCopyInterleaved( - dest, sizeof(uint32_t) * 2, - src, sizeof(uint32_t), - sizeof(uint32_t), 4 - ); - - assert_int_equal(dest[0], 0x11111111); - assert_int_equal(dest[2], 0x22222222); - assert_int_equal(dest[4], 0x33333333); - assert_int_equal(dest[6], 0x44444444); - // Skipped slots should be untouched. - assert_int_equal(dest[1], 0); - assert_int_equal(dest[3], 0); - assert_int_equal(dest[5], 0); - assert_int_equal(dest[7], 0); - - // Strided source: pick every second element into a contiguous destination. - uint32_t src2[8] = { 0xA, 0xFF, 0xB, 0xFF, 0xC, 0xFF, 0xD, 0xFF }; - uint32_t dest2[4]; - memoryZero(dest2, sizeof(dest2)); - - memoryCopyInterleaved( - dest2, sizeof(uint32_t), - src2, sizeof(uint32_t) * 2, - sizeof(uint32_t), 4 - ); - - assert_int_equal(dest2[0], 0xA); - assert_int_equal(dest2[1], 0xB); - assert_int_equal(dest2[2], 0xC); - assert_int_equal(dest2[3], 0xD); - - // Multi-byte element: copy structs field-by-field pattern. - typedef struct { uint32_t x; uint32_t y; } pair_t; - uint32_t yVals[3] = { 10, 20, 30 }; - pair_t pairs[3]; - memoryZero(pairs, sizeof(pairs)); - - memoryCopyInterleaved( - &pairs[0].y, sizeof(pair_t), - yVals, sizeof(uint32_t), - sizeof(uint32_t), 3 - ); - - assert_int_equal(pairs[0].y, 10); - assert_int_equal(pairs[1].y, 20); - assert_int_equal(pairs[2].y, 30); - assert_int_equal(pairs[0].x, 0); - assert_int_equal(pairs[1].x, 0); - assert_int_equal(pairs[2].x, 0); - - // Count of 0 is a no-op. - uint32_t untouched[2] = { 0xDEAD, 0xBEEF }; - memoryCopyInterleaved(untouched, 4, src, 4, 4, 0); - assert_int_equal(untouched[0], 0xDEAD); - - // NULL dest - expect_assert_failure(memoryCopyInterleaved(NULL, 8, src, 4, 4, 2)); - - // NULL src - expect_assert_failure(memoryCopyInterleaved(dest, 8, NULL, 4, 4, 2)); - - // elementSize 0 - expect_assert_failure(memoryCopyInterleaved(dest, 8, src, 4, 0, 2)); - - // destStride smaller than elementSize - expect_assert_failure(memoryCopyInterleaved(dest, 2, src, 4, 4, 2)); - - // srcStride smaller than elementSize - expect_assert_failure(memoryCopyInterleaved(dest, 8, src, 2, 4, 2)); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_memoryAllocate), - cmocka_unit_test(test_memoryFree), - cmocka_unit_test(test_memoryCopy), - cmocka_unit_test(test_memorySet), - cmocka_unit_test(test_memoryZero), - cmocka_unit_test(test_memoryCopyRangeSafe), - cmocka_unit_test(test_memoryMove), - cmocka_unit_test(test_memoryCompare), - cmocka_unit_test(test_memoryReallocate), - cmocka_unit_test(test_memoryResize), - cmocka_unit_test(test_memoryCopyInterleaved), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/util/test_sort.c b/test/util/test_sort.c deleted file mode 100644 index 7ec671df..00000000 --- a/test/util/test_sort.c +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "util/sort.h" -#include "util/memory.h" - -typedef struct { - int_t key; - char value; -} teststruct_t; - -void testSortMethod( - void (*sortMethod)( - void *array, - const size_t count, - const size_t size, - const sortcompare_t compare - ), - const char *methodName -) { - // Create an array of integers - size_t count = 5; - int_t *array = (int_t*)memoryAllocate(count * sizeof(int_t)); - assert_non_null(array); - - // Initialize with unsorted data - array[0] = 5; - array[1] = 3; - array[2] = 4; - array[3] = 1; - array[4] = 2; - - // Comparator function - int_t compareInts(const void *a, const void *b) { - int_t intA = *(const int_t*)a; - int_t intB = *(const int_t*)b; - return (intA > intB) - (intA < intB); - } - - // Sort the array - sortMethod(array, count, sizeof(int_t), compareInts); - - // Check that the array is sorted - for(size_t i = 0; i < count; i++) { - assert_int_equal(array[i], (int_t)(i + 1)); - } - - // Test with custom struct comparator - teststruct_t unsortedStructs[4] = { - {3, 'b'}, - {1, 'd'}, - {4, 'a'}, - {2, 'c'} - }; - teststruct_t structArray[4]; - - // Test with number comparator - memoryCopy(structArray, unsortedStructs, sizeof(unsortedStructs)); - int_t compareStructsKeys(const void *a, const void *b) { - const teststruct_t *structA = (const teststruct_t*)a; - const teststruct_t *structB = (const teststruct_t*)b; - return (structA->key > structB->key) - (structA->key < structB->key); - } - - sortMethod(structArray, 4, sizeof(teststruct_t), compareStructsKeys); - for(size_t i = 0; i < 4; i++) { - assert_int_equal(structArray[i].key, (int_t)(i + 1)); - } - - // Test with char comparator - memoryCopy(structArray, unsortedStructs, sizeof(unsortedStructs)); - int_t compareStructsValues(const void *a, const void *b) { - const teststruct_t *structA = (const teststruct_t*)a; - const teststruct_t *structB = (const teststruct_t*)b; - return - (structA->value > structB->value) - (structA->value < structB->value); - } - sortMethod(structArray, 4, sizeof(teststruct_t), compareStructsValues); - char expectedValues[4] = {'a', 'b', 'c', 'd'}; - for(size_t i = 0; i < 4; i++) { - assert_int_equal(structArray[i].value, expectedValues[i]); - } - - // Cannot sort NULL base - expect_assert_failure(sortMethod(NULL, count, sizeof(int_t), compareInts)); - - // Cannot sort 0 elements - expect_assert_failure(sortMethod(array, 0, sizeof(int_t), compareInts)); - - // Cannot sort with size 0 - expect_assert_failure(sortMethod(array, count, 0, compareInts)); - - // Cannot sort with NULL comparator - expect_assert_failure(sortMethod(array, count, sizeof(int_t), NULL)); - - memoryFree(array); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_sortBubble(void **state) { - testSortMethod(sortBubble, "sortBubble"); -} - -static void test_sortQuick(void **state) { - testSortMethod(sortQuick, "sortQuick"); -} - -static void test_sortArrayU8(void **state) { - - - // Create an array of uint8_t - size_t count = 6; - uint8_t array[6] = {5, 2, 4, 1, 3, 0}; - - // Sort the array - sortArrayU8(array, count); - - // Check that the array is sorted - for(size_t i = 0; i < count; i++) { - assert_int_equal(array[i], (uint8_t)i); - } - - // Cannot sort NULL array - expect_assert_failure(sortArrayU8(NULL, count)); - - // Cannot sort 0 elements - expect_assert_failure(sortArrayU8(array, 0)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_sortBubble), - cmocka_unit_test(test_sortQuick), - cmocka_unit_test(test_sortArrayU8), - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/test/util/test_string.c b/test/util/test_string.c deleted file mode 100644 index e616d0a9..00000000 --- a/test/util/test_string.c +++ /dev/null @@ -1,913 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "dusktest.h" -#include "util/string.h" -#include "util/memory.h" - -static void test_stringIsWhitespace(void **state) { - assert_true(stringIsWhitespace(' ')); // Space is whitespace - assert_true(stringIsWhitespace('\t')); // Tab is whitespace - assert_true(stringIsWhitespace('\n')); // Newline is whitespace - assert_true(stringIsWhitespace('\r')); // Carriage return is whitespace - assert_true(stringIsWhitespace('\v')); // Vertical tab is whitespace - assert_true(stringIsWhitespace('\f')); // Form feed is whitespace - assert_false(stringIsWhitespace('A')); // 'A' is not whitespace - assert_false(stringIsWhitespace('1')); // '1' is not whitespace - assert_false(stringIsWhitespace('_')); // '_' is not whitespace - assert_false(stringIsWhitespace('-')); // '-' is not whitespace - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringCopy(void **state) { - char dest[16]; - stringCopy(dest, "hello", sizeof(dest)); - assert_string_equal(dest, "hello"); // Normal copy - - // Empty string - stringCopy(dest, "", sizeof(dest)); - assert_string_equal(dest, ""); // Should copy empty string - - // Euffer just fits - stringCopy(dest, "123456789012345", 16); // 15 chars + null - assert_string_equal(dest, "123456789012345"); // Should fit exactly - - // Cannot copy from NULL source - expect_assert_failure(stringCopy(dest, NULL, sizeof(dest))); - - // Cannot copy to NULL destination - expect_assert_failure(stringCopy(NULL, "hello", sizeof(dest))); - - // Must have destSize > 0 - expect_assert_failure(stringCopy(dest, "hello", 0)); - - // Must have destSize large enough for source - expect_assert_failure(stringCopy(dest, "this string is too long", 10)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringCompare(void **state) { - - - // Check for equality - assert_int_equal(stringCompare("abc", "abc"), 0); - assert_int_equal(stringCompare("", ""), 0); - assert_int_equal(stringCompare("a", "a"), 0); - assert_int_equal(stringCompare("longer string", "longer string"), 0); - - // Check for less than cases - assert_true(stringCompare("", "a") < 0); - assert_true(stringCompare("abc", "abd") < 0); - - // Check for greater than cases - assert_true(stringCompare("abd", "abc") > 0); - assert_true(stringCompare("a", "") > 0); - - // Cannot compare NULL strings - expect_assert_failure(stringCompare(NULL, "abc")); - expect_assert_failure(stringCompare("abc", NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringCompareInsensitive(void **state) { - - - assert_int_equal(stringCompareInsensitive("abc", "ABC"), 0); - assert_true(stringCompareInsensitive("abc", "abd") < 0); - assert_true(stringCompareInsensitive("abd", "abc") > 0); - - assert_true(stringCompareInsensitive("", "A") < 0); - assert_true(stringCompareInsensitive("A", "") > 0); - - // Cannot compare NULL strings - expect_assert_failure(stringCompareInsensitive(NULL, "abc")); - expect_assert_failure(stringCompareInsensitive("abc", NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringTrim(void **state) { - - - char buf[32]; - stringCopy(buf, " hello ", sizeof(buf)); - - stringTrim(buf); - assert_string_equal(buf, "hello"); // Trims spaces - - // Non-space whitespace - stringCopy(buf, "\t\nhello world\r\n", sizeof(buf)); - stringTrim(buf); - assert_string_equal(buf, "hello world"); - - // Only whitespace - stringCopy(buf, " ", sizeof(buf)); - stringTrim(buf); - assert_string_equal(buf, ""); - - // No whitespace - stringCopy(buf, "no-trim", sizeof(buf)); - stringTrim(buf); - assert_string_equal(buf, "no-trim"); - - // Left whitespace only - stringCopy(buf, " left", sizeof(buf)); - stringTrim(buf); - assert_string_equal(buf, "left"); - - // Right whitespace only - stringCopy(buf, "right ", sizeof(buf)); - stringTrim(buf); - assert_string_equal(buf, "right"); - - // Cannot trim NULL string - expect_assert_failure(stringTrim(NULL)); - - // Can trim empty string - stringCopy(buf, "", sizeof(buf)); - stringTrim(buf); - assert_string_equal(buf, ""); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringFindLastChar(void **state) { - - - const char *str = "hello world"; - char *result = stringFindLastChar(str, 'o'); - assert_non_null(result); - assert_int_equal(*result, 'o'); - assert_int_equal(result - str, 7); // Position of last 'o' - - result = stringFindLastChar(str, 'h'); - assert_non_null(result); - assert_int_equal(*result, 'h'); - assert_int_equal(result - str, 0); // Position of 'h' - - result = stringFindLastChar(str, 'z'); - assert_null(result); // 'z' not found - - // Cannot search NULL string - expect_assert_failure(stringFindLastChar(NULL, 'a')); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringFormat(void **state) { - - - // Test a string format - char buffer[32]; - int32_t len = stringFormat(buffer, sizeof(buffer), "Hello %s!", "World"); - assert_int_equal(len, 12); - assert_string_equal(buffer, "Hello World!"); - - // Test a number format - len = stringFormat(buffer, sizeof(buffer), "Number: %d", 42); - assert_int_equal(len, 10); - assert_string_equal(buffer, "Number: 42"); - - // Test floats with different precision - len = stringFormat(buffer, sizeof(buffer), "Float: %.2f", 3.14159); - assert_int_equal(len, 11); - assert_string_equal(buffer, "Float: 3.14"); - - len = stringFormat(buffer, sizeof(buffer), "Float: %.4f", 3.14159); - assert_int_equal(len, 13); - assert_string_equal(buffer, "Float: 3.1416"); - - // Test integer formatting - len = stringFormat(buffer, sizeof(buffer), "Hex: 0x%X", 255); - assert_int_equal(len, 9); - assert_string_equal(buffer, "Hex: 0xFF"); - - // Test empty format - len = stringFormat(buffer, sizeof(buffer), ""); - assert_int_equal(len, 0); - assert_string_equal(buffer, ""); - - // Test no substitutions - len = stringFormat(buffer, sizeof(buffer), "No substitutions"); - assert_int_equal(len, 16); - assert_string_equal(buffer, "No substitutions"); - - // Test buffer too small - expect_assert_failure(stringFormat(buffer, 5, "This is too long")); - expect_assert_failure(stringFormat( - buffer, sizeof(buffer), "Hello %s", - "This string is way too long to fit within the buffer that we have " - "allocated" - )); - - // Test NULL destination, simply returns required length - len = stringFormat(NULL, 0, "Hello %s!", "World"); - assert_int_equal(len, 12); - - len = stringFormat(NULL, 0, "Number: %d", 42); - assert_int_equal(len, 10); - - // Cannot have NULL format - expect_assert_failure(stringFormat(buffer, sizeof(buffer), NULL)); - - // Must have destSize > 0 if dest is not NULL - expect_assert_failure(stringFormat(buffer, 0, "Hello")); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringToI32(void **state) { - - char_t buffer[64]; - int32_t value; - bool_t result; - - // Standard integers - stringCopy(buffer, "12345", sizeof(buffer)); - assert_true(result = stringToI32(buffer, &value)); - assert_int_equal(value, 12345); - - stringCopy(buffer, "-6789", sizeof(buffer)); - assert_true(result = stringToI32(buffer, &value)); - assert_int_equal(value, -6789); - - stringCopy(buffer, "+123", sizeof(buffer)); - assert_true(result = stringToI32(buffer, &value)); - assert_int_equal(value, 123); - - // Cannot convert invalid strings - stringCopy(buffer, "abc", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "123abc", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, " 123", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "123 ", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, " 1234 ", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "+-123", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "\t123", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "12 34", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "--123", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "++123", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "+", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "-", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - // Non ascii numbers - stringCopy(buffer, "12345", sizeof(buffer)); // Fullwidth digits - assert_false(result = stringToI32(buffer, &value)); - - // Cannot take math - stringCopy(buffer, "100+20", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "50-10", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "5*6", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "20/4", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - // Cannot handle decimal points - stringCopy(buffer, "12.34", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - // Cannot handle non-numbers - stringCopy(buffer, "12-34", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "e", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "3.273390607896142e+150 ", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - stringCopy(buffer, "9999999999999999999999999", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - // Cannot handle really low numbers - stringCopy(buffer, "-9999999999999999999999999", sizeof(buffer)); - assert_false(result = stringToI32(buffer, &value)); - - // Cannot handle items outside int32 range - stringCopy(buffer, "2147483648", sizeof(buffer)); // INT32_MAX + 1 - assert_false(result = stringToI32(buffer, &value)); - stringCopy(buffer, "-2147483649", sizeof(buffer)); // INT32_MIN - 1 - assert_false(result = stringToI32(buffer, &value)); - - // CAN handle INT32 limits - stringCopy(buffer, "2147483647", sizeof(buffer)); // INT32_MAX - assert_true(result = stringToI32(buffer, &value)); - assert_int_equal(value, 2147483647); - stringCopy(buffer, "-2147483648", sizeof(buffer)); // INT32_MIN - assert_true(result = stringToI32(buffer, &value)); - assert_int_equal(value, -2147483648); - - // Cannot have NULL string - expect_assert_failure(stringToI32(NULL, &value)); - - // Cannot have NULL output pointer - stringCopy(buffer, "123", sizeof(buffer)); - expect_assert_failure(stringToI32(buffer, NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringToI64(void **state) { - - char_t buffer[64]; - int64_t value; - bool_t result; - - // Standard integers - stringCopy(buffer, "12345678901234", sizeof(buffer)); - assert_true(result = stringToI64(buffer, &value)); - assert_int_equal(value, 12345678901234LL); - stringCopy(buffer, "-67890123456789", sizeof(buffer)); - assert_true(result = stringToI64(buffer, &value)); - assert_int_equal(value, -67890123456789LL); - - // Cannot convert invalid strings - stringCopy(buffer, "abc", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - stringCopy(buffer, "123abc", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - // Cannot convert invalid strings - stringCopy(buffer, "abc", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "123abc", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, " 123", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "123 ", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, " 1234 ", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "+-123", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "\t123", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "12 34", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "--123", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "++123", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "+", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "-", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - // Non ascii numbers - stringCopy(buffer, "12345", sizeof(buffer)); // Fullwidth digits - assert_false(result = stringToI64(buffer, &value)); - - // Cannot take math - stringCopy(buffer, "100+20", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "50-10", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "5*6", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "20/4", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - // Cannot handle decimal points - stringCopy(buffer, "12.34", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - // Cannot handle non-numbers - stringCopy(buffer, "12-34", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "e", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "3.273390607896142e+150 ", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - stringCopy(buffer, "9999999999999999999999999", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - // Cannot handle really low numbers - stringCopy(buffer, "-9999999999999999999999999", sizeof(buffer)); - assert_false(result = stringToI64(buffer, &value)); - - // Cannot handle items outside int64 range - stringCopy(buffer, "9223372036854775808", sizeof(buffer)); // INT64_MAX + 1 - assert_false(result = stringToI64(buffer, &value)); - stringCopy(buffer, "-9223372036854775809", sizeof(buffer)); // INT64_MIN - 1 - assert_false(result = stringToI64(buffer, &value)); - - // CAN handle INT64 limits - stringCopy(buffer, "9223372036854775807", sizeof(buffer)); // INT64_MAX - assert_true(result = stringToI64(buffer, &value)); - assert_int_equal(value, 9223372036854775807LL); - stringCopy(buffer, "-9223372036854775808", sizeof(buffer)); // INT64_MIN - assert_true(result = stringToI64(buffer, &value)); - assert_int_equal(value, -9223372036854775808LL); - - // Cannot have NULL string - expect_assert_failure(stringToI64(NULL, &value)); - - // Cannot have NULL output pointer - stringCopy(buffer, "123", sizeof(buffer)); - expect_assert_failure(stringToI64(buffer, NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringToI16(void **state) { - - - char_t buffer[64]; - int16_t value; - bool_t result; - - // Standard integers (within int16 range) - stringCopy(buffer, "12345", sizeof(buffer)); - assert_true(result = stringToI16(buffer, &value)); - assert_int_equal(value, (int16_t)12345); - - stringCopy(buffer, "-23456", sizeof(buffer)); - assert_true(result = stringToI16(buffer, &value)); - assert_int_equal(value, (int16_t)-23456); - - // Cannot convert invalid strings - stringCopy(buffer, "abc", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "123abc", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, " 123", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "123 ", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, " 1234 ", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "+-123", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "\t123", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "12 34", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "--123", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "++123", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "+", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "-", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - // Non-ascii numbers - stringCopy(buffer, "12345", sizeof(buffer)); // Fullwidth digits - assert_false(result = stringToI16(buffer, &value)); - - // Cannot take math - stringCopy(buffer, "100+20", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "50-10", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "5*6", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "20/4", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - // Cannot handle decimal points - stringCopy(buffer, "12.34", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - // Cannot handle non-numbers / mixed formats - stringCopy(buffer, "12-34", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "e", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - stringCopy(buffer, "3.273390607896142e+150 ", sizeof(buffer)); - assert_false(result = stringToI16(buffer, &value)); - - // Out of int16 range (overflow) - stringCopy(buffer, "32768", sizeof(buffer)); // INT16_MAX + 1 - assert_false(result = stringToI16(buffer, &value)); - - // Out of int16 range (underflow) - stringCopy(buffer, "-32769", sizeof(buffer)); // INT16_MIN - 1 - assert_false(result = stringToI16(buffer, &value)); - - // CAN handle int16 limits - stringCopy(buffer, "32767", sizeof(buffer)); // INT16_MAX - assert_true(result = stringToI16(buffer, &value)); - assert_int_equal(value, (int16_t)32767); - - stringCopy(buffer, "-32768", sizeof(buffer)); // INT16_MIN - assert_true(result = stringToI16(buffer, &value)); - assert_int_equal(value, (int16_t)-32768); - - // Cannot have NULL string - expect_assert_failure(stringToI16(NULL, &value)); - - // Cannot have NULL output pointer - stringCopy(buffer, "123", sizeof(buffer)); - expect_assert_failure(stringToI16(buffer, NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringToU16(void **state) { - - - char_t buffer[64]; - uint16_t value; - bool_t result; - - // Standard integers (within uint16 range) - stringCopy(buffer, "0", sizeof(buffer)); - assert_true(result = stringToU16(buffer, &value)); - assert_int_equal(value, (uint16_t)0); - - stringCopy(buffer, "12345", sizeof(buffer)); - assert_true(result = stringToU16(buffer, &value)); - assert_int_equal(value, (uint16_t)12345); - - // Cannot convert invalid strings - stringCopy(buffer, "abc", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "123abc", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, " 123", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "123 ", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, " 1234 ", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "+-123", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "\t123", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "12 34", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "--123", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "++123", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "+", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - // Unsigned: no minus sign - stringCopy(buffer, "-", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "-1", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - // Non-ascii numbers - stringCopy(buffer, "12345", sizeof(buffer)); // Fullwidth digits - assert_false(result = stringToU16(buffer, &value)); - - // Cannot take math - stringCopy(buffer, "100+20", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "50-10", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "5*6", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "20/4", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - // Cannot handle decimal points - stringCopy(buffer, "12.34", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - // Cannot handle non-numbers / mixed formats - stringCopy(buffer, "12-34", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "e", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - stringCopy(buffer, "3.273390607896142e+150 ", sizeof(buffer)); - assert_false(result = stringToU16(buffer, &value)); - - // Out of uint16 range (overflow) - stringCopy(buffer, "65536", sizeof(buffer)); // UINT16_MAX + 1 - assert_false(result = stringToU16(buffer, &value)); - - // CAN handle uint16 limits - stringCopy(buffer, "65535", sizeof(buffer)); // UINT16_MAX - assert_true(result = stringToU16(buffer, &value)); - assert_int_equal(value, (uint16_t)65535); - - // Cannot have NULL string - expect_assert_failure(stringToU16(NULL, &value)); - - // Cannot have NULL output pointer - stringCopy(buffer, "123", sizeof(buffer)); - expect_assert_failure(stringToU16(buffer, NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringToF32(void **state) { - - - char_t buffer[64]; - float value; - bool_t result; - - // Standard values - stringCopy(buffer, "0", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - assert_float_equal(value, 0.0f, 0.000001f); - - stringCopy(buffer, "123.5", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - assert_float_equal(value, 123.5f, 0.000001f); - - stringCopy(buffer, "-45.25", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - assert_float_equal(value, -45.25f, 0.000001f); - - stringCopy(buffer, "1.0", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - assert_float_equal(value, 1.0f, 0.000001f); - - // Integers should also parse - stringCopy(buffer, "42", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - assert_float_equal(value, 42.0f, 0.000001f); - - stringCopy(buffer, "-7", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - assert_float_equal(value, -7.0f, 0.000001f); - - // Cannot do European format - stringCopy(buffer, "123,45", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Cannot convert invalid strings - stringCopy(buffer, "abc", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "123abc", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, " ", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, " 1.23", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "1.23 ", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, " 1.23 ", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Invalid sign usage - stringCopy(buffer, "+-1.2", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "--1.2", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "++1.2", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "+", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "-", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Multiple decimal points - stringCopy(buffer, "1.2.3", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Non-ascii digits - stringCopy(buffer, "123.45", sizeof(buffer)); // Fullwidth - assert_false(result = stringToF32(buffer, &value)); - - // Cannot take math - stringCopy(buffer, "1+2", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "5-3", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "2*3", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "6/2", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Scientific notation allowed - stringCopy(buffer, "1e3", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - - stringCopy(buffer, "3.4E10", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - - // Overflow (beyond float32 max) - stringCopy(buffer, "3.5e38", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Underflow (too small, magnitude) - stringCopy(buffer, "1e-50", sizeof(buffer)); - assert_false(result = stringToF32(buffer, &value)); - - // Edge of float32 range (decimal form) - stringCopy(buffer, "340282346638528859811704183484516925440.0", - sizeof(buffer)); // ~FLT_MAX - assert_true(result = stringToF32(buffer, &value)); - - // Valid near-limits (implementation-dependent, keep conservative) - stringCopy(buffer, "3.4028234e38", sizeof(buffer)); - assert_true(result = stringToF32(buffer, &value)); - - // Cannot have NULL string - expect_assert_failure(stringToF32(NULL, &value)); - - // Cannot have NULL output pointer - stringCopy(buffer, "1.23", sizeof(buffer)); - expect_assert_failure(stringToF32(buffer, NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringEndsWith(void **state) { - - - assert_true(stringEndsWith("hello.c", ".c")); - assert_true(stringEndsWith("Hello World!", "World!")); - - assert_false(stringEndsWith("document.pdf", ".doc")); - assert_false(stringEndsWith("image.jpeg", ".png")); - assert_false(stringEndsWith("short", "longer")); - - // Cannot have NULL strings - expect_assert_failure(stringEndsWith(NULL, "world")); - expect_assert_failure(stringEndsWith("hello", NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -static void test_stringEndsWithCaseInsensitive(void **state) { - - assert_true(stringEndsWithCaseInsensitive("hello.C", ".c")); - assert_true(stringEndsWithCaseInsensitive("Hello World!", "world!")); - assert_false(stringEndsWithCaseInsensitive("document.pdf", ".DOC")); - assert_false(stringEndsWithCaseInsensitive("image.jpeg", ".PNG")); - assert_false(stringEndsWithCaseInsensitive("short", "LONGER")); - - // Cannot have NULL strings - expect_assert_failure(stringEndsWithCaseInsensitive(NULL, "WORLD")); - expect_assert_failure(stringEndsWithCaseInsensitive("hello", NULL)); - - // Expect no leaks - assert_int_equal(memoryGetAllocatedCount(), 0); -} - -int main(int argc, char **argv) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_stringIsWhitespace), - cmocka_unit_test(test_stringCopy), - cmocka_unit_test(test_stringCompare), - cmocka_unit_test(test_stringCompareInsensitive), - cmocka_unit_test(test_stringTrim), - cmocka_unit_test(test_stringFindLastChar), - cmocka_unit_test(test_stringFormat), - cmocka_unit_test(test_stringToI32), - cmocka_unit_test(test_stringToI64), - cmocka_unit_test(test_stringToI16), - cmocka_unit_test(test_stringToU16), - cmocka_unit_test(test_stringToF32), - cmocka_unit_test(test_stringEndsWith), - cmocka_unit_test(test_stringEndsWithCaseInsensitive), - }; - return cmocka_run_group_tests(tests, NULL, NULL); -} \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt deleted file mode 100644 index fa3f544a..00000000 --- a/tools/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -add_subdirectory(run_python) \ No newline at end of file diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py deleted file mode 100644 index 7f488a23..00000000 --- a/tools/asset/chunk/__main__.py +++ /dev/null @@ -1,624 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -""" -Generates DCF + companion DMF files from raw chunk JSON files, or upgrades -legacy DCF files (version 1 or 2) to the current version. - -JSON input (assetsraw/maps//chunks/chunk_X_Y_Z.json): - { - "tiles": [ - { "pos": [x, y, z], "type": , "tile": } - ], - "meshes": [ - { "file": "house_5_3.dmf", "pos": [x, y, z] } - ], - "entities": [ - { "type": "global", "globalId": , "pos": [x, y, z] }, - { "type": "item", "itemId": , "quantity": , "pos": [x, y, z] } - ], - "areas": [ - { - "min": [x, y, z], "max": [x, y, z], - "callbackId": , "notify": , "trigger": - } - ] - } - - Tiles absent from the array default to TILE_SHAPE_NULL. Each (x, y) - column may only have ONE real tile - chunks store one tile per column, - not one per (x, y, z), and that tile records its own local z (see the - version 4 tile format below). If two entries share the same (x, y) with - different z, the later one in the array wins and a warning is printed. - Mesh files are located by searching under assets/meshes/ and referenced by - path from the assets root in the DCF. - - is the map's name, up to 9 characters (MAP_NAME_MAX in - src/dusk/rpg/overworld/map.h reserves 1 byte for the null terminator). - - "entities" spawns things into the world when this chunk loads. A "global" - entity is spawned via mapSpawnEntity() - globalId indexes - ENTITY_GLOBAL_LIST (src/dusk/rpg/entity/global/entitygloballist.h) and is - deduped automatically if already spawned, so it's safe to declare on a - chunk that streams in more than once. An "item" entity has no persistent - identity - it respawns fresh every time this chunk (re)loads, including - after being picked up, since nothing tracks "already collected" yet. - itemId is a raw ITEM_ID_* value (see src/dusk/rpg/item/item.json for the - name -> id mapping, same convention as the tile "type" ints above). - - "areas" declares map trigger regions (see rpg/overworld/maparea.h) owned - by this chunk - they're removed when the chunk unloads and re-added if it - streams back in. callbackId indexes MAP_AREA_CALLBACK_LIST - (src/dusk/rpg/overworld/global/mapareagloballist.h; 0 is reserved and - invalid). notify is bitwise MAP_AREA_NOTIFY_PLAYER(1)|NOTIFY_NPC(2). - trigger is bitwise MAP_TRIGGER_STEP(1)|ENTER(2)|EXIT(4). - - Output DCF is derived automatically: - assetsraw/maps//chunks/chunk_X_Y_Z.json - -> assets/maps//chunks/X_Y_Z.dcf - -Version 5 DCF format (after 8-byte header): - tile_t tiles[CHUNK_WIDTH * CHUNK_HEIGHT] (one per x/y column) - each tile: uint32_t shape, uint8_t z, 3 padding bytes (8 bytes total, - matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; }) - uint8_t meshCount - for each model: - null-terminated string (relative asset path to .json model) - float32[3] (x, y, z offset) - uint8_t entitySpawnCount - for each entity spawn: - uint8_t kind (0 = global entity, 1 = item entity) - uint16_t a (globalId if kind 0, itemId if kind 1) - uint8_t b (unused if kind 0, quantity if kind 1) - int16_t x, y, z (world position, 3 fields) - uint8_t areaSpawnCount - for each area spawn: - int16_t minX, minY, minZ (3 fields) - int16_t maxX, maxY, maxZ (3 fields) - uint16_t callbackId - uint8_t notify - uint8_t trigger - -DMF format: - Bytes 0-3: DMF\\x00 - Bytes 4-7: uint32_t version = 1 (little-endian) - Bytes 8-11: uint32_t vertCount (little-endian) - Bytes 12+: meshvertex_t vertices[vertCount] - Each vertex: uv[2] + pos[3] = 5 x float32 LE = 20 bytes - -Usage: - python3 -m tools.asset.chunk # process all assetsraw/maps/*/chunks/*.json - python3 -m tools.asset.chunk # process one JSON file - python3 -m tools.asset.chunk # upgrade legacy (v1/v2) DCF in-place -""" - -import argparse -import json -import math -import os -import struct -import sys - -_HERE = os.path.dirname(os.path.abspath(__file__)) -PROJECT_ROOT = os.path.normpath(os.path.join(_HERE, '..', '..', '..')) -ASSETSRAW_DIR = os.path.join(PROJECT_ROOT, 'assetsraw') -ASSETS_DIR = os.path.join(PROJECT_ROOT, 'assets') - -CHUNK_WIDTH = 16 -CHUNK_HEIGHT = 16 -CHUNK_DEPTH = 8 -# One tile per (x, y) column - the column's local Z (0..CHUNK_DEPTH-1) is -# carried on the tile itself instead of storing a tile per (x, y, z). -CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT # 256 - -# World-space Z distance of one Z-layer/story, in world-float space. -# Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h. -WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2) - -CHUNK_MESH_COUNT_MAX = 10 -CHUNK_MESH_NAME_MAX = 64 -CHUNK_ENTITY_SPAWN_COUNT_MAX = 8 -CHUNK_AREA_COUNT_MAX = 4 - -# Matches MAP_NAME_MAX in src/dusk/rpg/overworld/map.h (buffer size, -# including the null terminator - so map names are at most 9 characters). -MAP_NAME_MAX = 10 - -ENTITY_SPAWN_KIND_GLOBAL = 0 -ENTITY_SPAWN_KIND_ITEM = 1 - -# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded -# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment). -TILE_STRUCT_FORMAT = '/chunks/chunk_X_Y_Z.json -> - - Raises ValueError if json_path isn't under that structure, or if - is longer than MAP_NAME_MAX - 1 characters.""" - rel = os.path.relpath(os.path.abspath(json_path), ASSETSRAW_DIR) - parts = rel.split(os.sep) - if len(parts) != 4 or parts[0] != 'maps' or parts[2] != 'chunks': - raise ValueError( - f"Chunk JSON must live at assetsraw/maps//chunks/*.json, got: " - f"{json_path}" - ) - map_name = parts[1] - if len(map_name) > MAP_NAME_MAX - 1: - raise ValueError( - f"Map name too long (> {MAP_NAME_MAX - 1} characters): {map_name}" - ) - return map_name - - -def derive_dcf_path(json_path): - """assetsraw/maps//chunks/chunk_X_Y_Z.json - -> assets/maps//chunks/X_Y_Z.dcf""" - base = os.path.splitext(os.path.basename(json_path))[0] - if base.startswith('chunk_'): - base = base[len('chunk_'):] - rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR) - return os.path.join(ASSETS_DIR, rel_dir, base + '.dcf') - - -def write_dmf(path, vertex_bytes): - vert_count = len(vertex_bytes) // VERTEX_SIZE - buf = bytearray() - buf += DMF_MAGIC - buf += struct.pack(' CHUNK_ENTITY_SPAWN_COUNT_MAX: - raise ValueError( - f"Too many entity spawns ({len(entity_spawns)}) - max " - f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}" - ) - if len(area_spawns) > CHUNK_AREA_COUNT_MAX: - raise ValueError( - f"Too many area spawns ({len(area_spawns)}) - max " - f"{CHUNK_AREA_COUNT_MAX}" - ) - - buf = bytearray() - buf += FILE_MAGIC - buf += b'\x00' - buf += struct.pack('= CHUNK_MESH_NAME_MAX: - raise ValueError( - f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}" - ) - buf += encoded + b'\x00' - buf += struct.pack('<3f', offset[0], offset[1], offset[2]) - - buf += struct.pack(' DCF + DMF -# --------------------------------------------------------------------------- - -def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z): - """Six vertices (2 CCW triangles) for a quad with per-corner Z heights. - Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1). - NORTH=+Y, EAST=+X (matches entityDirGetRelative).""" - verts = [ - (u0, v0, fx, fy, sw_z), - (u1, v0, fx+1, fy, se_z), - (u1, v1, fx+1, fy+1, ne_z), - (u0, v0, fx, fy, sw_z), - (u1, v1, fx+1, fy+1, ne_z), - (u0, v1, fx, fy+1, nw_z), - ] - buf = bytearray() - for v in verts: - buf += struct.pack('<5f', *v) - return bytes(buf) - - -# Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z. -# NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1. -# Diagonal outer-corner ramps: only the named corner raised, rest at 0. -# Diagonal inner-corner ramps: only the corner opposite the named one is -# lowered to 0, the rest (including the named corner) stay raised at 1. -_RAMP_CORNERS = { - TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0), - TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high - TILE_SHAPE_RAMP_SOUTH: (1.0, 1.0, 0.0, 0.0), # south=high, north=low - TILE_SHAPE_RAMP_EAST: (0.0, 1.0, 1.0, 0.0), # west=low, east=high - TILE_SHAPE_RAMP_WEST: (1.0, 0.0, 0.0, 1.0), # west=high, east=low - TILE_SHAPE_RAMP_NORTHEAST: (0.0, 0.0, 1.0, 0.0), # only NE raised - TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised - TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised - TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised - TILE_SHAPE_RAMP_NORTHEAST_INNER: (0.0, 1.0, 1.0, 1.0), # only SW low - TILE_SHAPE_RAMP_NORTHWEST_INNER: (1.0, 0.0, 1.0, 1.0), # only SE low - TILE_SHAPE_RAMP_SOUTHEAST_INNER: (1.0, 1.0, 1.0, 0.0), # only NW low - TILE_SHAPE_RAMP_SOUTHWEST_INNER: (1.0, 1.0, 0.0, 1.0), # only NE low -} - - -def build_terrain_verts(tiles_bytes): - """Generate quads for every non-null tile, shaped to match tile type.""" - buf = bytearray() - for y in range(CHUNK_HEIGHT): - for x in range(CHUNK_WIDTH): - idx = tile_index(x, y) - tile_type, z = struct.unpack_from( - ' look up matching model JSON - for mesh in data.get('meshes', []): - filename = mesh['file'] - pos = mesh.get('pos', [0, 0, 0]) - model_filename = os.path.splitext(filename)[0] + '.json' - rel = find_model(model_filename) - if rel is None: - raise ValueError( - f"Model not found under assets/models/: {model_filename}" - ) - model_names.append(rel) - mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2]))) - print(f' Resolved {filename} -> {rel}') - - entity_spawns = [] - for spawn in data.get('entities', []): - pos = tuple(int(v) for v in spawn['pos']) - if spawn['type'] == 'global': - entity_spawns.append({ - 'kind': ENTITY_SPAWN_KIND_GLOBAL, - 'globalId': int(spawn['globalId']), - 'pos': pos, - }) - elif spawn['type'] == 'item': - entity_spawns.append({ - 'kind': ENTITY_SPAWN_KIND_ITEM, - 'itemId': int(spawn['itemId']), - 'quantity': int(spawn['quantity']), - 'pos': pos, - }) - else: - raise ValueError(f"Unknown entity spawn type: {spawn['type']}") - - area_spawns = [] - for area in data.get('areas', []): - area_spawns.append({ - 'min': tuple(int(v) for v in area['min']), - 'max': tuple(int(v) for v in area['max']), - 'callbackId': int(area['callbackId']), - 'notify': int(area['notify']), - 'trigger': int(area['trigger']), - }) - - write_dcf( - dcf_path, bytes(tiles), model_names, mesh_offsets, - entity_spawns, area_spawns - ) - - -def process_json(json_path): - dcf_path = derive_dcf_path(json_path) - os.makedirs(os.path.dirname(dcf_path), exist_ok=True) - print(f"{json_path} -> {dcf_path}") - from_json(json_path, dcf_path) - - -# --------------------------------------------------------------------------- -# Legacy DCF (v1/v2) -> current version -# --------------------------------------------------------------------------- - -def collapse_legacy_tiles(legacy_tiles, path): - """Collapse the old one-tile-per-(x,y,z) grid into the current - one-tile-per-(x,y) format. If a column has more than one non-null tile - across its Z layers, the highest Z wins and a warning is printed.""" - tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE) - for y in range(CHUNK_HEIGHT): - for x in range(CHUNK_WIDTH): - found = [] - for z in range(_LEGACY_CHUNK_DEPTH): - legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT - shape = struct.unpack_from( - ' 1: - print( - f' WARNING: {path}: column ({x}, {y}) has tiles at ' - f'z={[z for z, _ in found]} - only one tile per column ' - f'is kept, z={found[-1][0]} wins (highest Z)' - ) - z, shape = found[-1] - struct.pack_into( - TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE, - shape, z - ) - return bytes(tiles) - - -def read_legacy_dcf(path): - with open(path, 'rb') as f: - data = f.read() - if data[:3] != FILE_MAGIC: - raise ValueError(f"{path}: not a DCF file") - version = struct.unpack_from(' 0: - meshes.append(verts) - else: - mesh_count = data[offset] - offset += 1 - for _ in range(mesh_count): - vert_count = struct.unpack_from(' 0: - meshes.append(verts) - return tiles, meshes - - -def upgrade_dcf(path): - print(f"Upgrading legacy DCF {path} ...") - tiles, meshes = read_legacy_dcf(path) - print( - f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, " - f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}" - ) - base = os.path.splitext(os.path.basename(path))[0] - terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks') - os.makedirs(terrain_dir, exist_ok=True) - mesh_names = [] - for idx, verts in enumerate(meshes): - dmf_name = f'chunk_{base}_{idx}.dmf' - write_dmf(os.path.join(terrain_dir, dmf_name), verts) - mesh_names.append(f'meshes/chunks/{dmf_name}') - write_dcf(path, tiles, mesh_names) - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser( - description="Generate DCF + companion DMF files from raw chunk JSON, " - "or upgrade legacy DCF files to the current version" - ) - parser.add_argument( - 'path', nargs='?', - help='Chunk JSON file to process, or legacy DCF file to upgrade. ' - 'If omitted, processes all assetsraw/maps/*/chunks/*.json' - ) - args = parser.parse_args() - - if args.path is None: - maps_dir = os.path.join(ASSETSRAW_DIR, 'maps') - if not os.path.isdir(maps_dir): - print(f"No directory found: {maps_dir}") - sys.exit(1) - json_files = [] - for map_name in sorted(os.listdir(maps_dir)): - chunks_dir = os.path.join(maps_dir, map_name, 'chunks') - if not os.path.isdir(chunks_dir): - continue - json_files.extend(sorted( - os.path.join(chunks_dir, f) - for f in os.listdir(chunks_dir) - if f.endswith('.json') - )) - if not json_files: - print(f"No JSON files found under {maps_dir}") - sys.exit(0) - for p in json_files: - process_json(p) - return - - if os.path.splitext(args.path)[1].lower() == '.json': - process_json(args.path) - else: - upgrade_dcf(args.path) - - -if __name__ == '__main__': - main() diff --git a/tools/asset/dmf/__init__.py b/tools/asset/dmf/__init__.py deleted file mode 100644 index 4e1b4307..00000000 --- a/tools/asset/dmf/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT diff --git a/tools/asset/dmf/__main__.py b/tools/asset/dmf/__main__.py deleted file mode 100644 index e761e9e1..00000000 --- a/tools/asset/dmf/__main__.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -""" -Writes DMF (Dusk Mesh Format) files. - -DMF format: - Bytes 0-3: DMF\x00 (magic) - Bytes 4-7: uint32_t version = 1 (little-endian) - Bytes 8-11: uint32_t vertCount (little-endian) - Bytes 12+: meshvertex_t vertices[vertCount] - Each vertex is 20 bytes: uv[2] + pos[3] (5 floats, LE) - -Usage: - python3 -m tools.asset.dmf - Reads raw vertex bytes from vertices.bin and writes a DMF file. -""" - -import argparse -import struct -import os - -MAGIC = b'DMF\x00' -VERSION = 1 -VERTEX_SIZE = 20 - - -def write_dmf(path, vertex_bytes): - if len(vertex_bytes) % VERTEX_SIZE != 0: - raise ValueError( - f"Vertex data size {len(vertex_bytes)} is not a " - f"multiple of {VERTEX_SIZE}" - ) - vert_count = len(vertex_bytes) // VERTEX_SIZE - buf = bytearray() - buf += MAGIC - buf += struct.pack(' --output - Every file under is added, using its path relative to - as the zip entry name. - -Called unconditionally on every CMake build (see the DUSK_ASSETS_BUILT -target) rather than gated behind file-level build-system dependencies - -CMake's CONFIGURE_DEPENDS glob only notices added/removed files on the -NEXT configure, so a plain custom_command DEPENDS list can miss edits to -existing files or lag a build behind on adds/removes. Instead this script -itself decides whether anything actually needs repacking: it writes a -manifest of every input file's (mtime, size) next to the output on each -successful pack, and skips the real work if a fresh manifest compares -equal to it and the output still exists. The manifest also carries the -format version, so a manifest written by an older version of this script -(a stale single/dual-archive dusk.dsk sitting from before a format -change) never compares equal and always triggers a fresh pack. -""" - -import argparse -import json -import os -import struct -import zipfile -import zlib -import io - -MAGIC = b'DSK' -VERSION = 2 -HEADER_FORMAT = '<4sIII' -HEADER_SIZE = struct.calcsize(HEADER_FORMAT) - - -def build_zip_blob(root, relative_paths): - buf = io.BytesIO() - with zipfile.ZipFile(buf, 'w', compression=zipfile.ZIP_STORED) as zf: - for relative_path in sorted(relative_paths): - zf.write(os.path.join(root, relative_path), arcname=relative_path) - return buf.getvalue() - - -def manifest_path(output_path): - return output_path + '.manifest.json' - - -def compute_manifest(input_dir, relative_paths): - entries = {} - for relative_path in relative_paths: - st = os.stat(os.path.join(input_dir, relative_path)) - entries[relative_path] = [st.st_mtime_ns, st.st_size] - return {'version': VERSION, 'files': entries} - - -def load_manifest(path): - try: - with open(path, 'r', encoding='utf-8') as f: - return json.load(f) - except (OSError, ValueError): - return None - - -def pack(input_dir, output_path): - relative_paths = [] - for dirpath, _dirnames, filenames in os.walk(input_dir): - for filename in filenames: - full_path = os.path.join(dirpath, filename) - relative_paths.append( - os.path.relpath(full_path, input_dir).replace(os.sep, '/') - ) - - manifest = compute_manifest(input_dir, relative_paths) - manifest_file = manifest_path(output_path) - if os.path.isfile(output_path) and load_manifest(manifest_file) == manifest: - print(f'{output_path} is up to date, skipping pack') - return - - blob = build_zip_blob(input_dir, relative_paths) - - header = struct.pack( - HEADER_FORMAT, MAGIC, VERSION, len(blob), zlib.crc32(blob) & 0xFFFFFFFF - ) - - with open(output_path, 'wb') as f: - f.write(header) - f.write(blob) - - with open(manifest_file, 'w', encoding='utf-8') as f: - json.dump(manifest, f) - - print( - f'Wrote {output_path}: {len(relative_paths)} file(s) ' - f'({len(blob)} bytes), {len(header) + len(blob)} bytes total' - ) - - -def main(): - parser = argparse.ArgumentParser( - description='Pack the assets directory into a dusk.dsk (DSK2) archive.' - ) - parser.add_argument('--input', required=True, help='Assets directory to pack') - parser.add_argument('--output', required=True, help='Path to write dusk.dsk to') - args = parser.parse_args() - - pack(args.input, args.output) - - -if __name__ == '__main__': - main() diff --git a/tools/color.py b/tools/color.py deleted file mode 100644 index 39c7f47a..00000000 --- a/tools/color.py +++ /dev/null @@ -1,79 +0,0 @@ -import argparse -import csv -import os - -parser = argparse.ArgumentParser(description="Color CSV to .h defines") -parser.add_argument("--csv", required=True, help="Path to color CSV file") -parser.add_argument("--output", required=True, help="Path to output .h file") -args = parser.parse_args() - -# Load colors -colors = {} -with open(args.csv, newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - if "name" not in reader.fieldnames: - raise ValueError("CSV must have a 'name' column") - for row in reader: - name = row["name"] - r, g, b = row["r"], row["g"], row["b"] - a = row["a"] if "a" in row and row["a"] != "" else "1.0" - for ch in (r, g, b, a): - if not (0.0 <= float(ch) <= 1.0): - raise ValueError(f"Color channel {ch!r} for '{name}' out of range [0.0, 1.0]") - colors[name] = (r, g, b, a) - -# Build output -out = [ - "#pragma once", - '#include "dusk.h"', - "", - "typedef float_t colorchannelf_t;", - "typedef uint8_t colorchannel8_t;", - "", - "typedef struct { colorchannelf_t r, g, b; } color3f_t;", - "typedef struct { colorchannelf_t r, g, b, a; } color4f_t;", - "typedef struct { colorchannel8_t r, g, b; } color3b_t;", - "typedef struct { colorchannel8_t r, g, b, a; } color4b_t;", - "typedef color4b_t color_t;", - "", - "#define color3f(r, g, b) ((color3f_t){ r, g, b })", - "#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })", - "#define color3b(r, g, b) ((color3b_t){ r, g, b })", - "#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })", - "#define color(r, g, b, a) color4b(r, g, b, a)", - "#define colorHex(hex) color4b(((hex >> 24) & 0xFF), ((hex >> 16) & 0xFF), ((hex >> 8) & 0xFF), (hex & 0xFF))", - "", -] - -js = [] -for name, (r, g, b, a) in colors.items(): - r8, g8, b8, a8 = (int(float(ch) * 255) for ch in (r, g, b, a)) - macro = "COLOR_" + name.upper() - camel = "".join(p[0].upper() + p[1:].lower() for p in name.split("_")) - - out += [ - f"// {name}", - f"#define {macro}_4B color4b({r8}, {g8}, {b8}, {a8})", - f"#define {macro}_3B color3b({r8}, {g8}, {b8})", - f"#define {macro}_3F color3f({r}f, {g}f, {b}f)", - f"#define {macro}_4F color4f({r}f, {g}f, {b}f, {a}f)", - f"#define {macro} {macro}_4B", - "", - ] - js += [ - f"Color.{name.lower()} = function() {{", - f" return new Color({r8}, {g8}, {b8}, {a8});", - "};", - "", - ] - -out.append("// JS color functions") -out.append("#define COLOR_SCRIPT \\") -for line in "\n".join(js).rstrip().splitlines(): - out.append(f' "{line}\\n" \\') -out[-1] = out[-1].rstrip(" \\") -out.append("") - -os.makedirs(os.path.dirname(args.output), exist_ok=True) -with open(args.output, "w", encoding="utf-8") as f: - f.write("\n".join(out)) diff --git a/tools/editor/common/dtf.js b/tools/editor/common/dtf.js deleted file mode 100644 index 3017b819..00000000 --- a/tools/editor/common/dtf.js +++ /dev/null @@ -1,175 +0,0 @@ -"use strict"; - -// DTF – Dusk Texture Format -// -// Header (13 bytes): -// [0–2] "DTF" magic -// [3] 0x01 version -// [4–7] uint32 width (little-endian) -// [8–11] uint32 height (little-endian) -// [12] uint8 format -// -// Formats: -// 0x01 Alpha – 1 byte per pixel (alpha channel only) -// 0x03 RGB – 3 bytes per pixel (no alpha) -// 0x04 RGBA – 4 bytes per pixel -// -// Followed by width × height × bpp bytes of tightly-packed pixel data. - -const DTF = (() => { - const MAGIC = [0x44, 0x54, 0x46]; // "DTF" - const VERSION = 0x01; - const FORMAT_ALPHA = 0x01; - const FORMAT_RGB = 0x03; - const FORMAT_RGBA = 0x04; - const HEADER_SIZE = 13; - - // Bytes per pixel for each format. - const BPP = { - [FORMAT_ALPHA]: 1, - [FORMAT_RGB]: 3, - [FORMAT_RGBA]: 4, - }; - - // Encode RGBA source pixels into a DTF ArrayBuffer at the given format. - // When format is FORMAT_ALPHA and redAsAlpha is true, the red channel is - // used as the alpha value instead of the actual alpha channel. - function encode(width, height, rgbaData, format, redAsAlpha) { - if(format === undefined) format = FORMAT_RGBA; - const bpp = BPP[format]; - if(bpp === undefined) throw new Error(`Unknown DTF format: 0x${format.toString(16)}`); - - const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData); - const buf = new ArrayBuffer(HEADER_SIZE + width * height * bpp); - const bytes = new Uint8Array(buf); - const view = new DataView(buf); - - bytes[0] = MAGIC[0]; - bytes[1] = MAGIC[1]; - bytes[2] = MAGIC[2]; - bytes[3] = VERSION; - view.setUint32(4, width, true); - view.setUint32(8, height, true); - bytes[12] = format; - - let dst = HEADER_SIZE; - for(let i = 0; i < width * height; i++) { - const o = i * 4; - switch (format) { - case FORMAT_ALPHA: - bytes[dst++] = redAsAlpha ? src[o] : src[o + 3]; - break; - case FORMAT_RGB: - bytes[dst++] = src[o]; - bytes[dst++] = src[o + 1]; - bytes[dst++] = src[o + 2]; - break; - default: // FORMAT_RGBA - bytes[dst++] = src[o]; - bytes[dst++] = src[o + 1]; - bytes[dst++] = src[o + 2]; - bytes[dst++] = src[o + 3]; - } - } - - return buf; - } - - // Decode a DTF ArrayBuffer. Always returns RGBA pixel data for internal use. - // Alpha-format files decode as {R=0, G=0, B=0, A=alpha} so the alpha channel - // is preserved and toImageData() can display it correctly. - function decode(buffer) { - const bytes = new Uint8Array(buffer); - const view = new DataView(buffer); - - if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DTF"); - if(bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2]) { - throw new Error("Invalid DTF magic bytes – not a DTF file"); - } - - const version = bytes[3]; - if(version !== VERSION) { - throw new Error(`Unsupported DTF version: 0x${version.toString(16).padStart(2, "0")}`); - } - - const width = view.getUint32(4, true); - const height = view.getUint32(8, true); - const format = bytes[12]; - const bpp = BPP[format]; - - if(bpp === undefined) { - throw new Error(`Unsupported DTF format: 0x${format.toString(16).padStart(2, "0")}`); - } - - const expected = HEADER_SIZE + width * height * bpp; - if(bytes.length < expected) { - throw new Error(`DTF pixel data truncated (expected ${expected} bytes, got ${bytes.length})`); - } - - const rgba = new Uint8ClampedArray(width * height * 4); - let src = HEADER_SIZE; - for(let i = 0; i < width * height; i++) { - const o = i * 4; - switch (format) { - case FORMAT_ALPHA: - rgba[o] = rgba[o + 1] = rgba[o + 2] = 0; - rgba[o + 3] = bytes[src++]; - break; - case FORMAT_RGB: - rgba[o] = bytes[src++]; - rgba[o + 1] = bytes[src++]; - rgba[o + 2] = bytes[src++]; - rgba[o + 3] = 255; - break; - default: // FORMAT_RGBA - rgba[o] = bytes[src++]; - rgba[o + 1] = bytes[src++]; - rgba[o + 2] = bytes[src++]; - rgba[o + 3] = bytes[src++]; - } - } - - return { width, height, format, data: rgba }; - } - - // Convert RGBA source pixels to a display-ready ImageData for the given format. - // Shows exactly how the texture will look after a DTF encode/decode round-trip. - // Alpha → grayscale from alpha channel (or red if redAsAlpha), out-alpha=255 - // RGB → discard alpha, fully opaque - // RGBA → pass-through - function toImageData(width, height, rgbaData, format, redAsAlpha) { - const src = rgbaData instanceof Uint8ClampedArray ? rgbaData : new Uint8ClampedArray(rgbaData); - const out = new Uint8ClampedArray(width * height * 4); - - for(let i = 0; i < width * height; i++) { - const o = i * 4; - switch (format) { - case FORMAT_ALPHA: { - const v = redAsAlpha ? src[o] : src[o + 3]; - out[o] = out[o + 1] = out[o + 2] = v; - out[o + 3] = v; // use value as canvas alpha so background shows through transparent areas - break; - } - case FORMAT_RGB: - out[o] = src[o]; - out[o + 1] = src[o + 1]; - out[o + 2] = src[o + 2]; - out[o + 3] = 255; - break; - default: // FORMAT_RGBA - out[o] = src[o]; - out[o + 1] = src[o + 1]; - out[o + 2] = src[o + 2]; - out[o + 3] = src[o + 3]; - } - } - - return new ImageData(out, width, height); - } - - return Object.freeze({ - encode, decode, toImageData, - FORMAT_ALPHA, FORMAT_RGB, FORMAT_RGBA, - VERSION, HEADER_SIZE, BPP, - }); -})(); diff --git a/tools/editor/common/png.js b/tools/editor/common/png.js deleted file mode 100644 index fcaecce0..00000000 --- a/tools/editor/common/png.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; - -// DuskPNG – PNG export using pngjs (window.PNG) -// Falls back to canvas.toBlob if pngjs is unavailable. - -const DuskPNG = (() => { - function _pngAvailable() { - return typeof PNG !== "undefined" && PNG.sync && typeof PNG.sync.write === "function"; - } - - // Encode RGBA pixel data into a PNG Buffer via pngjs. - // Returns a Uint8Array (Buffer) if pngjs is available, otherwise null. - function encode(width, height, rgbaData) { - if(!_pngAvailable()) return null; - - const png = new PNG({ width, height }); - const src = rgbaData instanceof Uint8ClampedArray - ? rgbaData - : new Uint8ClampedArray(rgbaData); - - for(let i = 0; i < src.length; i++) { - png.data[i] = src[i]; - } - - return PNG.sync.write(png); // returns a Buffer (Uint8Array subclass) - } - - // Trigger a browser download of the RGBA data as a PNG file. - function download(filename, width, height, rgbaData) { - const buf = encode(width, height, rgbaData); - - if(buf) { - // pngjs path - const blob = new Blob([buf], { type: "image/png" }); - _triggerDownload(URL.createObjectURL(blob), filename); - } else { - // Canvas fallback - const canvas = Object.assign(document.createElement("canvas"), { width, height }); - const ctx = canvas.getContext("2d"); - const src = rgbaData instanceof Uint8ClampedArray - ? rgbaData - : new Uint8ClampedArray(rgbaData); - ctx.putImageData(new ImageData(src, width, height), 0, 0); - canvas.toBlob(blob => _triggerDownload(URL.createObjectURL(blob), filename), "image/png"); - } - } - - function _triggerDownload(url, filename) { - const a = Object.assign(document.createElement("a"), { href: url, download: filename }); - a.click(); - URL.revokeObjectURL(url); - } - - return Object.freeze({ encode, download }); -})(); diff --git a/tools/editor/index.html b/tools/editor/index.html deleted file mode 100644 index 91a27fec..00000000 --- a/tools/editor/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - Dusk Editor Tools - - - - - - -
    - -
    -

    Editor Tools

    -

    Asset creation and data authoring tools for the Dusk game engine.

    -
    - -
    - - -
    - -
    - - - diff --git a/tools/editor/styles/components/bg-swatches.css b/tools/editor/styles/components/bg-swatches.css deleted file mode 100644 index e80b896b..00000000 --- a/tools/editor/styles/components/bg-swatches.css +++ /dev/null @@ -1,28 +0,0 @@ -/* Component – Background swatches */ - -.bg-swatches { - display: flex; - flex-wrap: wrap; - gap: 5px; -} - -.bg-swatch { - width: 26px; - height: 26px; - border-radius: var(--radius-sm); - border: 2px solid transparent; - cursor: pointer; - padding: 0; - flex-shrink: 0; - overflow: hidden; - transition: border-color var(--speed); -} - -.bg-swatch:hover { - border-color: var(--text-muted); -} - -.bg-swatch.active, -.bg-swatch.active:hover { - border-color: var(--accent); -} diff --git a/tools/editor/styles/components/btn.css b/tools/editor/styles/components/btn.css deleted file mode 100644 index 37cdc1d1..00000000 --- a/tools/editor/styles/components/btn.css +++ /dev/null @@ -1,38 +0,0 @@ -/* Component – Button */ - -.btn { - display: block; - width: 100%; - background: var(--bg-raised); - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text); - padding: 0.4rem 0.75rem; - font-size: 0.825rem; - cursor: pointer; - text-align: center; - font-family: inherit; - transition: background var(--speed), border-color var(--speed); -} - -.btn + .btn { - margin-top: 0.375rem; -} - -.btn:hover:not(:disabled) { - border-color: var(--accent-dim); -} - -.btn:disabled { - opacity: 0.35; - cursor: not-allowed; -} - -.btn-primary { - background: var(--accent-dim); - border-color: var(--accent); -} - -.btn-primary:hover:not(:disabled) { - background: var(--accent); -} diff --git a/tools/editor/styles/components/control-row.css b/tools/editor/styles/components/control-row.css deleted file mode 100644 index 8ffa92f7..00000000 --- a/tools/editor/styles/components/control-row.css +++ /dev/null @@ -1,13 +0,0 @@ -/* Component – Control row */ - -.control-row { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; -} - -.control-row label { - color: var(--text-muted); - min-width: 44px; -} diff --git a/tools/editor/styles/components/empty-state.css b/tools/editor/styles/components/empty-state.css deleted file mode 100644 index 30e7bcb2..00000000 --- a/tools/editor/styles/components/empty-state.css +++ /dev/null @@ -1,16 +0,0 @@ -/* Component – Empty state */ - -.empty-state { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - border-style: dashed; - padding: 3rem; - text-align: center; - color: var(--text-muted); -} - -.empty-state p { - margin-top: 0.5rem; - font-size: 0.875rem; -} diff --git a/tools/editor/styles/components/file-name.css b/tools/editor/styles/components/file-name.css deleted file mode 100644 index 3f45daf7..00000000 --- a/tools/editor/styles/components/file-name.css +++ /dev/null @@ -1,8 +0,0 @@ -/* Component – File name */ - -.file-name { - font-size: 0.775rem; - color: var(--text-muted); - word-break: break-all; - line-height: 1.4; -} diff --git a/tools/editor/styles/components/hero.css b/tools/editor/styles/components/hero.css deleted file mode 100644 index ea7fc20f..00000000 --- a/tools/editor/styles/components/hero.css +++ /dev/null @@ -1,16 +0,0 @@ -/* Component – Hero section */ - -.hero { - padding: var(--gap) 0; -} - -.hero h1 { - font-size: 1.75rem; - font-weight: 700; - margin-bottom: 0.5rem; -} - -.hero p { - color: var(--text-muted); - max-width: 540px; -} diff --git a/tools/editor/styles/components/info-table.css b/tools/editor/styles/components/info-table.css deleted file mode 100644 index 59545517..00000000 --- a/tools/editor/styles/components/info-table.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Component – Info table */ - -.info-table { - width: 100%; - border-collapse: collapse; - font-size: 0.8rem; -} - -.info-table td { - padding: 0.15rem 0; - vertical-align: top; -} - -.info-table td:first-child { - color: var(--text-muted); - width: 50px; - font-size: 0.75rem; - padding-right: 0.75rem; -} diff --git a/tools/editor/styles/components/load-area.css b/tools/editor/styles/components/load-area.css deleted file mode 100644 index 171b8609..00000000 --- a/tools/editor/styles/components/load-area.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Component – Load area */ - -.load-area { - border: 1px dashed var(--border); - border-radius: var(--radius); - padding: 0.6rem; - text-align: center; - cursor: pointer; - font-size: 0.825rem; - color: var(--text-muted); - transition: border-color var(--speed), color var(--speed); - line-height: 1.5; -} - -.load-area:hover, -.load-area.drag-over { - border-color: var(--accent-dim); - color: var(--text); -} diff --git a/tools/editor/styles/components/panel-inputs.css b/tools/editor/styles/components/panel-inputs.css deleted file mode 100644 index 64d5f802..00000000 --- a/tools/editor/styles/components/panel-inputs.css +++ /dev/null @@ -1,47 +0,0 @@ -/* Component – Panel form inputs */ - -.tool-panel select { - width: 100%; - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text); - padding: 0.25rem 0.4rem; - font-size: 0.875rem; - font-family: inherit; - transition: border-color var(--speed); - cursor: pointer; -} - -.tool-panel select:focus { - outline: none; - border-color: var(--accent-dim); -} - -.tool-panel input[type="checkbox"] { - accent-color: var(--accent); - cursor: pointer; - flex-shrink: 0; -} - -.tool-panel input[type="number"] { - width: 52px; - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text); - padding: 0.25rem 0.4rem; - font-size: 0.875rem; - font-family: inherit; - transition: border-color var(--speed); -} - -.tool-panel input[type="number"]:focus { - outline: none; - border-color: var(--accent-dim); -} - -.unit { - color: var(--text-muted); - font-size: 0.875rem; -} diff --git a/tools/editor/styles/components/panel-section.css b/tools/editor/styles/components/panel-section.css deleted file mode 100644 index d39ccd24..00000000 --- a/tools/editor/styles/components/panel-section.css +++ /dev/null @@ -1,13 +0,0 @@ -/* Component – Panel section */ - -.panel-section { - padding: 0.875rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - border-bottom: 1px solid var(--border); -} - -.panel-section:last-child { - border-bottom: none; -} diff --git a/tools/editor/styles/components/preview-empty.css b/tools/editor/styles/components/preview-empty.css deleted file mode 100644 index ba5ae4f7..00000000 --- a/tools/editor/styles/components/preview-empty.css +++ /dev/null @@ -1,14 +0,0 @@ -/* Component – Preview empty state */ - -.preview-empty { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: var(--text-muted); - font-size: 0.875rem; - text-align: center; - white-space: nowrap; - user-select: none; - pointer-events: none; -} diff --git a/tools/editor/styles/components/preview-scroll.css b/tools/editor/styles/components/preview-scroll.css deleted file mode 100644 index f9539b12..00000000 --- a/tools/editor/styles/components/preview-scroll.css +++ /dev/null @@ -1,16 +0,0 @@ -/* Component – Preview scroll */ - -.preview-scroll { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 1.5rem; -} - -.preview-scroll canvas { - image-rendering: pixelated; - image-rendering: crisp-edges; - display: block; - max-width: none; -} diff --git a/tools/editor/styles/components/site-header.css b/tools/editor/styles/components/site-header.css deleted file mode 100644 index b9f8ff85..00000000 --- a/tools/editor/styles/components/site-header.css +++ /dev/null @@ -1,43 +0,0 @@ -/* Component – Site header */ - -.site-header { - background: var(--bg-surface); - border-bottom: 1px solid var(--border); - padding: 0 var(--gap); - display: flex; - align-items: center; - gap: 1rem; - height: 52px; -} - -.site-header .logo { - font-weight: 700; - font-size: 1rem; - color: var(--text); - letter-spacing: 0.03em; -} - -.site-header .logo span { - color: var(--accent); -} - -.site-header nav { - display: flex; - gap: 0.25rem; - margin-left: auto; -} - -.site-header nav a { - padding: 0.3rem 0.75rem; - border-radius: var(--radius); - color: var(--text-muted); - font-size: 0.875rem; - transition: background var(--speed), color var(--speed); -} - -.site-header nav a:hover, -.site-header nav a.active { - background: var(--bg-raised); - color: var(--text); - text-decoration: none; -} diff --git a/tools/editor/styles/components/tool-card.css b/tools/editor/styles/components/tool-card.css deleted file mode 100644 index b4772262..00000000 --- a/tools/editor/styles/components/tool-card.css +++ /dev/null @@ -1,30 +0,0 @@ -/* Component – Tool card */ - -.tool-card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 1.25rem; - display: flex; - flex-direction: column; - gap: 0.4rem; - transition: border-color var(--speed); - text-decoration: none; - color: inherit; -} - -.tool-card:hover { - border-color: var(--accent-dim); - text-decoration: none; -} - -.tool-card .tool-name { - font-weight: 600; - font-size: 0.95rem; -} - -.tool-card .tool-desc { - color: var(--text-muted); - font-size: 0.85rem; - line-height: 1.5; -} diff --git a/tools/editor/styles/components/tool-panel.css b/tools/editor/styles/components/tool-panel.css deleted file mode 100644 index bffac7f2..00000000 --- a/tools/editor/styles/components/tool-panel.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Component – Tool panel */ - -.tool-panel { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - width: 210px; - flex-shrink: 0; - overflow: hidden; - display: flex; - flex-direction: column; -} diff --git a/tools/editor/styles/components/tool-preview.css b/tools/editor/styles/components/tool-preview.css deleted file mode 100644 index 55ec7795..00000000 --- a/tools/editor/styles/components/tool-preview.css +++ /dev/null @@ -1,15 +0,0 @@ -/* Component – Tool preview */ - -.tool-preview { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - flex: 1; - position: relative; - min-height: 300px; - transition: border-color var(--speed); -} - -.tool-preview.drag-over { - border-color: var(--accent-dim); -} diff --git a/tools/editor/styles/components/warning-list.css b/tools/editor/styles/components/warning-list.css deleted file mode 100644 index a175f192..00000000 --- a/tools/editor/styles/components/warning-list.css +++ /dev/null @@ -1,24 +0,0 @@ -/* Component – Warning list */ - -.warning-list { - list-style: none; - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.warning-list li { - font-size: 0.775rem; - color: #e8a030; - line-height: 1.4; - padding-left: 1.1rem; - position: relative; -} - -.warning-list li::before { - content: "!"; - position: absolute; - left: 0; - font-weight: 700; - color: #e8a030; -} diff --git a/tools/editor/styles/components/warnings-section.css b/tools/editor/styles/components/warnings-section.css deleted file mode 100644 index 0ffe30e1..00000000 --- a/tools/editor/styles/components/warnings-section.css +++ /dev/null @@ -1,5 +0,0 @@ -/* Component – Warnings section */ - -.warnings-section { - border-color: #6b4a00; -} diff --git a/tools/editor/styles/elements.css b/tools/editor/styles/elements.css deleted file mode 100644 index afc29db7..00000000 --- a/tools/editor/styles/elements.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Elements – Bare HTML */ - -body { - background: var(--bg); - color: var(--text); - font-family: system-ui, -apple-system, sans-serif; - font-size: 14px; - line-height: 1.6; - min-height: 100vh; -} - -a { - color: var(--accent); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} diff --git a/tools/editor/styles/generic.css b/tools/editor/styles/generic.css deleted file mode 100644 index b9b6fce0..00000000 --- a/tools/editor/styles/generic.css +++ /dev/null @@ -1,9 +0,0 @@ -/* Generic – Reset */ - -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -[hidden] { display: none !important; } diff --git a/tools/editor/styles/main.css b/tools/editor/styles/main.css deleted file mode 100644 index 1a6fec0b..00000000 --- a/tools/editor/styles/main.css +++ /dev/null @@ -1,32 +0,0 @@ -/* ITCSS entry point */ - -@import "settings.css"; -@import "generic.css"; -@import "elements.css"; - -/* Objects */ -@import "objects/section-label.css"; -@import "objects/page.css"; -@import "objects/section.css"; -@import "objects/tool-grid.css"; -@import "objects/tool-workspace.css"; - -/* Components */ -@import "components/site-header.css"; -@import "components/hero.css"; -@import "components/tool-card.css"; -@import "components/empty-state.css"; -@import "components/tool-panel.css"; -@import "components/panel-section.css"; -@import "components/control-row.css"; -@import "components/panel-inputs.css"; -@import "components/btn.css"; -@import "components/bg-swatches.css"; -@import "components/tool-preview.css"; -@import "components/preview-scroll.css"; -@import "components/preview-empty.css"; -@import "components/load-area.css"; -@import "components/file-name.css"; -@import "components/info-table.css"; -@import "components/warnings-section.css"; -@import "components/warning-list.css"; diff --git a/tools/editor/styles/objects/page.css b/tools/editor/styles/objects/page.css deleted file mode 100644 index cfd1726b..00000000 --- a/tools/editor/styles/objects/page.css +++ /dev/null @@ -1,7 +0,0 @@ -/* Object – Page container */ - -.page { - max-width: 1400px; - margin: 0 auto; - padding: var(--gap); -} diff --git a/tools/editor/styles/objects/section-label.css b/tools/editor/styles/objects/section-label.css deleted file mode 100644 index 1708deff..00000000 --- a/tools/editor/styles/objects/section-label.css +++ /dev/null @@ -1,13 +0,0 @@ -/* Object – Section label */ - -.section-label { - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -.section > .section-label { - margin-bottom: 1rem; -} diff --git a/tools/editor/styles/objects/section.css b/tools/editor/styles/objects/section.css deleted file mode 100644 index b15d11b7..00000000 --- a/tools/editor/styles/objects/section.css +++ /dev/null @@ -1,4 +0,0 @@ -/* Object – Page section */ -.section { - margin-top: var(--gap); -} diff --git a/tools/editor/styles/objects/tool-grid.css b/tools/editor/styles/objects/tool-grid.css deleted file mode 100644 index 974f866e..00000000 --- a/tools/editor/styles/objects/tool-grid.css +++ /dev/null @@ -1,7 +0,0 @@ -/* Object – Tool grid */ - -.tool-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 1rem; -} diff --git a/tools/editor/styles/objects/tool-workspace.css b/tools/editor/styles/objects/tool-workspace.css deleted file mode 100644 index 0363a3bd..00000000 --- a/tools/editor/styles/objects/tool-workspace.css +++ /dev/null @@ -1,7 +0,0 @@ -/* Object – Tool workspace */ - -.tool-workspace { - margin-top: var(--gap); - display: flex; - gap: 1rem; -} diff --git a/tools/editor/styles/settings.css b/tools/editor/styles/settings.css deleted file mode 100644 index a770dd7e..00000000 --- a/tools/editor/styles/settings.css +++ /dev/null @@ -1,17 +0,0 @@ -:root { - --bg: #121217; /* deep dark base */ - --bg-surface: #1a1a22; /* subtle elevation */ - --bg-raised: #23232d; /* layered surfaces */ - --border: #2e2e3a; /* soft border contrast */ - - --text: #e6e6f0; /* high readability */ - --text-muted: #9a9ab0; /* muted secondary text */ - - --accent: #8b5cf6; /* vibrant purple */ - --accent-dim: #6d46c7; /* deeper purple for hover/active */ - - --radius: 6px; - --radius-sm: 4px; - --gap: 1.5rem; - --speed: 0.15s; -} \ No newline at end of file diff --git a/tools/editor/texture-padder/index.html b/tools/editor/texture-padder/index.html deleted file mode 100644 index ae4357ec..00000000 --- a/tools/editor/texture-padder/index.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - Texture Padder – Dusk Editor - - - - - - -
    - -
    -

    Texture Padder

    -

    Load an image or .dtf file and pad its dimensions to meet power-of-two or minimum size requirements.

    -
    - -
    - - - - - -
    -
    Load an image or .dtf file to get started
    - -
    - -
    -
    - - - - - - - diff --git a/tools/editor/texture-padder/texture-padder.js b/tools/editor/texture-padder/texture-padder.js deleted file mode 100644 index b8ed1aa4..00000000 --- a/tools/editor/texture-padder/texture-padder.js +++ /dev/null @@ -1,275 +0,0 @@ -"use strict"; - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -// Returns the smallest power of two >= n. -function nextPow2(n) { - if(n <= 0) return 1; - let p = 1; - while (p < n) p <<= 1; - return p; -} - -// ─── State ─────────────────────────────────────────────────────────────────── - -const state = { - pixels: null, // Uint8ClampedArray RGBA at original resolution - width: 0, - height: 0, - scale: 1, - bg: "grid", - padWidthPow2: true, - padHeightPow2: true, - minWidth4: true, - minHeight4: true, - format: DTF.FORMAT_RGBA, - filename: "texture", -}; - -// ─── DOM refs ──────────────────────────────────────────────────────────────── - -const canvas = document.getElementById("canvas"); -const ctx = canvas.getContext("2d"); -const fileInput = document.getElementById("file-input"); -const loadLabel = document.getElementById("load-label"); -const scaleInput = document.getElementById("scale-input"); -const bgSwatches = document.getElementById("bg-swatches"); -const previewArea = document.getElementById("preview-area"); -const previewEmpty = document.getElementById("preview-empty"); -const previewScroll = document.getElementById("preview-scroll"); -const infoSection = document.getElementById("info-section"); -const fileNameEl = document.getElementById("file-name"); -const infoFilename = document.getElementById("info-filename"); -const infoInputSize = document.getElementById("info-input-size"); -const infoOutputSize = document.getElementById("info-output-size"); -const formatSelect = document.getElementById("format-select"); -const btnDtf = document.getElementById("btn-dtf"); -const btnPng = document.getElementById("btn-png"); -const infoDtfSize = document.getElementById("info-dtf-size"); -const chkPadWidthPow2 = document.getElementById("chk-pad-width-pow2"); -const chkPadHeightPow2 = document.getElementById("chk-pad-height-pow2"); -const chkMinWidth4 = document.getElementById("chk-min-width-4"); -const chkMinHeight4 = document.getElementById("chk-min-height-4"); - -// ─── Padding logic ─────────────────────────────────────────────────────────── - -function computeOutputSize() { - let outW = state.width; - let outH = state.height; - // Apply minimum-size constraints first so pow2 rounding accounts for them. - if(state.minWidth4 && outW < 4) outW = 4; - if(state.minHeight4 && outH < 4) outH = 4; - if(state.padWidthPow2) outW = nextPow2(outW); - if(state.padHeightPow2) outH = nextPow2(outH); - return { outW, outH }; -} - -function buildPaddedPixels() { - const { outW, outH } = computeOutputSize(); - const src = state.pixels; - // Zero-filled Uint8ClampedArray = fully transparent black padding. - const out = new Uint8ClampedArray(outW * outH * 4); - for(let y = 0; y < state.height; y++) { - for(let x = 0; x < state.width; x++) { - const si = (y * state.width + x) * 4; - const di = (y * outW + x) * 4; - out[di] = src[si]; - out[di + 1] = src[si + 1]; - out[di + 2] = src[si + 2]; - out[di + 3] = src[si + 3]; - } - } - return { pixels: out, width: outW, height: outH }; -} - -// ─── Rendering ─────────────────────────────────────────────────────────────── - -const CHECKER_CELL = 8; - -function drawCheckerboard(w, h) { - for(let y = 0; y < h; y += CHECKER_CELL) { - for(let x = 0; x < w; x += CHECKER_CELL) { - ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff"; - ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y)); - } - } -} - -function render() { - if(!state.pixels) return; - - const { pixels, width, height } = buildPaddedPixels(); - const cw = width * state.scale; - const ch = height * state.scale; - - canvas.width = cw; - canvas.height = ch; - - if(state.bg === "grid") { - drawCheckerboard(cw, ch); - } else { - ctx.fillStyle = state.bg; - ctx.fillRect(0, 0, cw, ch); - } - - const src = Object.assign(document.createElement("canvas"), { width, height }); - src.getContext("2d").putImageData(new ImageData(pixels, width, height), 0, 0); - - ctx.imageSmoothingEnabled = false; - ctx.drawImage(src, 0, 0, cw, ch); -} - -// ─── Info update ───────────────────────────────────────────────────────────── - -function updateInfo() { - if(!state.pixels) return; - const { outW, outH } = computeOutputSize(); - const bytes = DTF.HEADER_SIZE + outW * outH * DTF.BPP[state.format]; - infoInputSize.textContent = `${state.width} × ${state.height}`; - infoOutputSize.textContent = `${outW} × ${outH}`; - infoDtfSize.textContent = `${(bytes / 1024).toFixed(1)} KB`; -} - -// ─── Loading ───────────────────────────────────────────────────────────────── - -function loadStandardImage(file) { - const url = URL.createObjectURL(file); - const img = new Image(); - - img.onload = () => { - const offscreen = Object.assign(document.createElement("canvas"), { - width: img.naturalWidth, - height: img.naturalHeight, - }); - offscreen.getContext("2d").drawImage(img, 0, 0); - const imageData = offscreen.getContext("2d").getImageData(0, 0, img.naturalWidth, img.naturalHeight); - applyImageData(img.naturalWidth, img.naturalHeight, imageData.data, file.name); - URL.revokeObjectURL(url); - }; - - img.onerror = () => { - alert("Failed to load image. The file may be corrupt or unsupported."); - URL.revokeObjectURL(url); - }; - - img.src = url; -} - -function loadDTF(file) { - const reader = new FileReader(); - reader.onload = e => { - try { - const { width, height, data } = DTF.decode(e.target.result); - applyImageData(width, height, data, file.name); - } catch (err) { - alert(`Failed to load DTF: ${err.message}`); - } - }; - reader.readAsArrayBuffer(file); -} - -function applyImageData(width, height, data, filename) { - state.pixels = new Uint8ClampedArray(data); - state.width = width; - state.height = height; - state.filename = filename.replace(/\.[^/.]+$/, ""); - - fileNameEl.textContent = filename; - infoFilename.textContent = filename; - infoSection.hidden = false; - updateInfo(); - - previewEmpty.hidden = true; - previewScroll.hidden = false; - - btnDtf.disabled = false; - btnPng.disabled = false; - - render(); -} - -function handleFile(file) { - if(!file) return; - if(file.name.toLowerCase().endsWith(".dtf")) { - loadDTF(file); - } else { - loadStandardImage(file); - } -} - -// ─── Export ─────────────────────────────────────────────────────────────────── - -function exportDTF() { - if(!state.pixels) return; - const { pixels, width, height } = buildPaddedPixels(); - const buf = DTF.encode(width, height, pixels, state.format); - const blob = new Blob([buf], { type: "application/octet-stream" }); - const url = URL.createObjectURL(blob); - const a = Object.assign(document.createElement("a"), { - href: url, - download: `${state.filename}.dtf`, - }); - a.click(); - URL.revokeObjectURL(url); -} - -function exportPNG() { - if(!state.pixels) return; - const { pixels, width, height } = buildPaddedPixels(); - DuskPNG.download(`${state.filename}.png`, width, height, pixels); -} - -// ─── Event listeners ───────────────────────────────────────────────────────── - -fileInput.addEventListener("change", e => handleFile(e.target.files[0])); - -scaleInput.addEventListener("input", () => { - const v = Math.max(1, Math.min(10, parseInt(scaleInput.value, 10) || 1)); - scaleInput.value = v; - state.scale = v; - render(); -}); - -bgSwatches.addEventListener("click", e => { - const btn = e.target.closest(".bg-swatch"); - if(!btn) return; - bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active")); - btn.classList.add("active"); - state.bg = btn.dataset.bg; - render(); -}); - -formatSelect.addEventListener("change", () => { - state.format = parseInt(formatSelect.value, 10); - updateInfo(); -}); - -btnDtf.addEventListener("click", exportDTF); -btnPng.addEventListener("click", exportPNG); - -chkPadWidthPow2.addEventListener("change", () => { state.padWidthPow2 = chkPadWidthPow2.checked; updateInfo(); render(); }); -chkPadHeightPow2.addEventListener("change", () => { state.padHeightPow2 = chkPadHeightPow2.checked; updateInfo(); render(); }); -chkMinWidth4.addEventListener("change", () => { state.minWidth4 = chkMinWidth4.checked; updateInfo(); render(); }); -chkMinHeight4.addEventListener("change", () => { state.minHeight4 = chkMinHeight4.checked; updateInfo(); render(); }); - -// Drag-and-drop -function setupDrop(target) { - target.addEventListener("dragover", e => { - e.preventDefault(); - loadLabel.classList.add("drag-over"); - previewArea.classList.add("drag-over"); - }); - target.addEventListener("dragleave", () => { - loadLabel.classList.remove("drag-over"); - previewArea.classList.remove("drag-over"); - }); - target.addEventListener("drop", e => { - e.preventDefault(); - loadLabel.classList.remove("drag-over"); - previewArea.classList.remove("drag-over"); - handleFile(e.dataTransfer.files[0]); - }); -} - -setupDrop(loadLabel); -setupDrop(previewArea); diff --git a/tools/editor/texture/index.html b/tools/editor/texture/index.html deleted file mode 100644 index edf52130..00000000 --- a/tools/editor/texture/index.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - Texture Creator – Dusk Editor - - - - - - -
    - -
    -

    Texture Creator

    -

    Load an image or existing .dtf file, then export as Dusk Texture Format (.dtf) or PNG.

    -
    - -
    - - - - - -
    -
    Load an image or .dtf file to get started
    - -
    - -
    -
    - - - - - - - diff --git a/tools/editor/texture/texture.js b/tools/editor/texture/texture.js deleted file mode 100644 index 84776685..00000000 --- a/tools/editor/texture/texture.js +++ /dev/null @@ -1,279 +0,0 @@ -"use strict"; - -// ─── State ─────────────────────────────────────────────────────────────────── - -const state = { - pixels: null, // Uint8ClampedArray RGBA at original resolution - width: 0, - height: 0, - scale: 1, - bg: "grid", - format: DTF.FORMAT_RGBA, // output DTF format - redAsAlpha: false, // Alpha format: use red channel instead of alpha - filename: "texture", // basename without extension -}; - -// ─── DOM refs ──────────────────────────────────────────────────────────────── - -const canvas = document.getElementById("canvas"); -const ctx = canvas.getContext("2d"); -const fileInput = document.getElementById("file-input"); -const loadLabel = document.getElementById("load-label"); -const scaleInput = document.getElementById("scale-input"); -const bgSwatches = document.getElementById("bg-swatches"); -const previewArea = document.getElementById("preview-area"); -const previewEmpty = document.getElementById("preview-empty"); -const previewScroll = document.getElementById("preview-scroll"); -const infoSection = document.getElementById("info-section"); -const fileNameEl = document.getElementById("file-name"); -const infoFilename = document.getElementById("info-filename"); -const infoSize = document.getElementById("info-size"); -const infoFormat = document.getElementById("info-format"); -const btnDtf = document.getElementById("btn-dtf"); -const btnPng = document.getElementById("btn-png"); -const formatSelect = document.getElementById("format-select"); -const infoDtfSize = document.getElementById("info-dtf-size"); -const warningsSection = document.getElementById("warnings-section"); -const warningList = document.getElementById("warning-list"); -const redAsAlphaRow = document.getElementById("red-as-alpha-row"); -const redAsAlphaCheck = document.getElementById("red-as-alpha"); - -// ─── Rendering ─────────────────────────────────────────────────────────────── - -const CHECKER_CELL = 8; - -function drawCheckerboard(w, h) { - for(let y = 0; y < h; y += CHECKER_CELL) { - for(let x = 0; x < w; x += CHECKER_CELL) { - ctx.fillStyle = ((x / CHECKER_CELL + y / CHECKER_CELL) % 2 === 0) ? "#c8c8c8" : "#ffffff"; - ctx.fillRect(x, y, Math.min(CHECKER_CELL, w - x), Math.min(CHECKER_CELL, h - y)); - } - } -} - -function render() { - if(!state.pixels) return; - - const { pixels, width, height, scale, bg, format } = state; - const cw = width * scale; - const ch = height * scale; - - canvas.width = cw; - canvas.height = ch; - - // 1. Background - if(bg === "grid") { - drawCheckerboard(cw, ch); - } else { - ctx.fillStyle = bg; - ctx.fillRect(0, 0, cw, ch); - } - - // 2. Composite image on top at scaled size (pixelated) - const src = Object.assign(document.createElement("canvas"), { width, height }); - src.getContext("2d").putImageData(DTF.toImageData(width, height, pixels, format, state.redAsAlpha), 0, 0); - - ctx.imageSmoothingEnabled = false; - ctx.drawImage(src, 0, 0, cw, ch); -} - -// ─── Loading ───────────────────────────────────────────────────────────────── - -function loadStandardImage(file) { - const url = URL.createObjectURL(file); - const img = new Image(); - - img.onload = () => { - const offscreen = Object.assign(document.createElement("canvas"), { - width: img.naturalWidth, - height: img.naturalHeight, - }); - offscreen.getContext("2d").drawImage(img, 0, 0); - const imageData = offscreen.getContext("2d").getImageData(0, 0, img.naturalWidth, img.naturalHeight); - - applyImageData( - img.naturalWidth, - img.naturalHeight, - imageData.data, - file.name, - file.type || "image", - ); - URL.revokeObjectURL(url); - }; - - img.onerror = () => { - showError("Failed to load image. The file may be corrupt or unsupported."); - URL.revokeObjectURL(url); - }; - - img.src = url; -} - -function loadDTF(file) { - const reader = new FileReader(); - reader.onload = e => { - try { - const { width, height, format, data } = DTF.decode(e.target.result); - const label = format === DTF.FORMAT_ALPHA ? "DTF (Alpha)" - : format === DTF.FORMAT_RGB ? "DTF (RGB)" - : "DTF (RGBA)"; - applyImageData(width, height, data, file.name, label, format); - } catch (err) { - showError(`Failed to load DTF: ${err.message}`); - } - }; - reader.readAsArrayBuffer(file); -} - -function updateWarnings() { - const warnings = []; - - if(state.pixels) { - const { width, height } = state; - const isPow2 = n => n > 0 && (n & (n - 1)) === 0; - - if(width < 4) warnings.push(`Width is below 4 px (${width})`); - if(height < 4) warnings.push(`Height is below 4 px (${height})`); - if(!isPow2(width)) warnings.push(`Width is not a power of two (${width})`); - if(!isPow2(height)) warnings.push(`Height is not a power of two (${height})`); - - const bytes = DTF.HEADER_SIZE + width * height * DTF.BPP[state.format]; - if(bytes > 256 * 1024) { - warnings.push(`Output exceeds 256 KB (${(bytes / 1024).toFixed(1)} KB)`); - } - } - - warningList.replaceChildren( - ...warnings.map(msg => Object.assign(document.createElement("li"), { textContent: msg })), - ); - warningsSection.hidden = warnings.length === 0; -} - -function updateDtfSize() { - if(!state.pixels) return; - const bytes = DTF.HEADER_SIZE + state.width * state.height * DTF.BPP[state.format]; - infoDtfSize.textContent = `${(bytes / 1024).toFixed(1)} KB`; - updateWarnings(); -} - -function applyImageData(width, height, data, filename, formatLabel, format) { - state.pixels = new Uint8ClampedArray(data); // defensive copy - state.width = width; - state.height = height; - state.filename = filename.replace(/\.[^/.]+$/, ""); - - // Sync format selector when loading an existing DTF - if(format !== undefined) { - state.format = format; - formatSelect.value = format; - redAsAlphaRow.hidden = format !== DTF.FORMAT_ALPHA; - } - - // Sidebar info - fileNameEl.textContent = filename; - infoFilename.textContent = filename; - infoSize.textContent = `${width} × ${height}`; - infoFormat.textContent = formatLabel; - infoSection.hidden = false; - updateDtfSize(); - - // Show canvas - previewEmpty.hidden = true; - previewScroll.hidden = false; - - // Enable export - btnDtf.disabled = false; - btnPng.disabled = false; - - render(); -} - -function handleFile(file) { - if(!file) return; - if(file.name.toLowerCase().endsWith(".dtf")) { - loadDTF(file); - } else { - loadStandardImage(file); - } -} - -function showError(msg) { - alert(msg); -} - -// ─── Export ─────────────────────────────────────────────────────────────────── - -function exportDTF() { - if(!state.pixels) return; - const buf = DTF.encode(state.width, state.height, state.pixels, state.format, state.redAsAlpha); - const blob = new Blob([buf], { type: "application/octet-stream" }); - const url = URL.createObjectURL(blob); - const a = Object.assign(document.createElement("a"), { - href: url, - download: `${state.filename}.dtf`, - }); - a.click(); - URL.revokeObjectURL(url); -} - -function exportPNG() { - if(!state.pixels) return; - DuskPNG.download(`${state.filename}.png`, state.width, state.height, state.pixels); -} - -// ─── Event listeners ───────────────────────────────────────────────────────── - -fileInput.addEventListener("change", e => handleFile(e.target.files[0])); - -scaleInput.addEventListener("input", () => { - const v = Math.max(1, Math.min(10, parseInt(scaleInput.value, 10) || 1)); - scaleInput.value = v; - state.scale = v; - render(); -}); - -bgSwatches.addEventListener("click", e => { - const btn = e.target.closest(".bg-swatch"); - if(!btn) return; - bgSwatches.querySelectorAll(".bg-swatch").forEach(b => b.classList.remove("active")); - btn.classList.add("active"); - state.bg = btn.dataset.bg; - render(); -}); - -btnDtf.addEventListener("click", exportDTF); -btnPng.addEventListener("click", exportPNG); - -formatSelect.addEventListener("change", () => { - state.format = parseInt(formatSelect.value, 10); - redAsAlphaRow.hidden = state.format !== DTF.FORMAT_ALPHA; - updateDtfSize(); - render(); -}); - -redAsAlphaCheck.addEventListener("change", () => { - state.redAsAlpha = redAsAlphaCheck.checked; - render(); -}); - -// Drag-and-drop on the load label -function setupDrop(target) { - target.addEventListener("dragover", e => { - e.preventDefault(); - loadLabel.classList.add("drag-over"); - previewArea.classList.add("drag-over"); - }); - target.addEventListener("dragleave", () => { - loadLabel.classList.remove("drag-over"); - previewArea.classList.remove("drag-over"); - }); - target.addEventListener("drop", e => { - e.preventDefault(); - loadLabel.classList.remove("drag-over"); - previewArea.classList.remove("drag-over"); - handleFile(e.dataTransfer.files[0]); - }); -} - -setupDrop(loadLabel); -setupDrop(previewArea); diff --git a/tools/input.py b/tools/input.py deleted file mode 100644 index d7ad0786..00000000 --- a/tools/input.py +++ /dev/null @@ -1,58 +0,0 @@ -import argparse -import csv -import os - -parser = argparse.ArgumentParser(description="Input CSV to .h defines") -parser.add_argument("--csv", required=True, help="Path to Input CSV file") -parser.add_argument("--output", required=True, help="Path to output .h file") -args = parser.parse_args() - -def id_enum(name): - return "INPUT_ACTION_" + name.upper() - -# Load CSV -input_ids = [] -with open(args.csv, newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - if "id" not in reader.fieldnames: - raise ValueError("CSV must have an 'id' column") - for row in reader: - input_id = row["id"] - if input_id not in input_ids: - input_ids.append(input_id) - -# Assign enum values -id_values = {input_id: i + 1 for i, input_id in enumerate(input_ids)} - -# Build output -out = [ - "#pragma once", - '#include "dusk.h"', - "", - "typedef enum {", - " INPUT_ACTION_NULL = 0x0,", - "", -] -for input_id in input_ids: - out.append(f" {id_enum(input_id)} = 0x{id_values[input_id]:x},") -out += [ - "", - f" INPUT_ACTION_COUNT = 0x{len(input_ids) + 1:x}", - "} inputaction_t;", - "", - "static const char_t *INPUT_ACTION_IDS[] = {", -] -for input_id in input_ids: - out.append(f" [{id_enum(input_id)}] = \"{input_id}\",") -out += [ - "};", - "", - "static const char_t *INPUT_ACTION_SCRIPT =", -] -for input_id in input_ids: - out.append(f" \"var {id_enum(input_id)} = {id_values[input_id]};\\n\"") -out += [";", ""] - -os.makedirs(os.path.dirname(args.output), exist_ok=True) -with open(args.output, "w", encoding="utf-8") as f: - f.write("\n".join(out)) diff --git a/tools/makedolphiniso.py b/tools/makedolphiniso.py deleted file mode 100644 index 5456320e..00000000 --- a/tools/makedolphiniso.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -""" -Builds GameCube / Wii disc images (NTSC-J, NTSC-U, PAL) using xorrisofs / -mkisofs + Swiss-GC system-area headers. - -The Swiss-GC headers contain a working GCN apploader that boots any DOL via -the El Torito catalog - no custom PPC assembly needed. - -Headers are cached in /../buildtools/iso/ and downloaded from the -Swiss-GC repository on first use. - -Disc layout (produced by xorrisofs): - sectors 0-15 system area (boot.bin + bi2 + apploader, from Swiss header) - sector 16 ISO 9660 PVD - sector 17 El Torito Boot Record - ... directory records, file data - Dusk.dol (El Torito boot image - loaded by apploader) - dusk.dsk (asset archive - found at runtime via ISO 9660) -""" - -import argparse -import os -import shutil -import struct -import subprocess -import sys -import urllib.request - -SWISS_BASE = ( - "https://raw.githubusercontent.com/emukidid/swiss-gc/" - "6f78e41940d2699b69ec62636d57411ae2857f1e/buildtools/iso/" -) - -REGIONS = [ - ('j', 'J', 'NTSC-J', 0), - ('u', 'E', 'NTSC-U', 1), - ('e', 'P', 'PAL', 2), -] - - -def _buildtools_iso_dir(): - here = os.path.dirname(os.path.abspath(__file__)) - return os.path.join(here, '..', 'buildtools', 'iso') - - -def _get_hdr(region_suffix): - """Return the raw bytes of the Swiss system-area header for *region_suffix* - ('j', 'u', or 'e'), downloading it if necessary.""" - cache_dir = _buildtools_iso_dir() - os.makedirs(cache_dir, exist_ok=True) - path = os.path.join(cache_dir, f'eltorito-{region_suffix}.hdr') - if not os.path.exists(path): - url = SWISS_BASE + f'eltorito-{region_suffix}.hdr' - print(f'Downloading {url} ...', flush=True) - with urllib.request.urlopen(url) as resp: - data = resp.read() - with open(path, 'wb') as f: - f.write(data) - with open(path, 'rb') as f: - return f.read() - - -def _patch_hdr(hdr_data, game_id, title): - """Patch game ID (bytes 0-5) and title (bytes 0x20-0x3FF) into the - system-area header. Everything else (magic, bi2, apploader) is unchanged.""" - hdr = bytearray(hdr_data) - hdr[0:6] = game_id.encode('ascii') - tb = title.encode('ascii')[:0x3E0] - hdr[0x20:0x20 + len(tb)] = tb - # zero remaining title bytes so stale Swiss title doesn't bleed through - hdr[0x20 + len(tb):0x400] = bytes(0x3E0 - len(tb)) - return bytes(hdr) - - -def _find_mkisofs(): - for tool in ('xorrisofs', 'mkisofs'): - if shutil.which(tool): - return tool - sys.exit('Error: neither xorrisofs nor mkisofs found. ' - 'Install xorriso (apt install xorriso) or mkisofs.') - - -def _build_iso(platform, game_id, title, region_suffix, dol_path, dsk_path, - out_path): - hdr_data = _patch_hdr(_get_hdr(region_suffix), game_id, title) - - # Write patched header to a temp file alongside the output so we have a - # stable path (avoids tempfile cleanup races on some systems). - hdr_tmp = out_path + '.hdr.tmp' - try: - with open(hdr_tmp, 'wb') as f: - f.write(hdr_data) - - mkisofs = _find_mkisofs() - dol_name = os.path.basename(dol_path) - - cmd = [ - mkisofs, - '-R', '-J', - '-G', hdr_tmp, - '-no-emul-boot', - '-eltorito-platform', 'PPC', - '-b', dol_name, - '-o', out_path, - dol_path, - dsk_path, - ] - subprocess.run(cmd, check=True) - finally: - if os.path.exists(hdr_tmp): - os.remove(hdr_tmp) - - size = os.path.getsize(out_path) - print(f'Created {out_path} ({size:,} bytes)') - - -def main(): - ap = argparse.ArgumentParser( - description='Build GCN/Wii disc images from a DOL and asset file.') - ap.add_argument('platform', choices=['GCN', 'WII']) - ap.add_argument('dol', help='Path to Dusk.dol') - ap.add_argument('dsk', help='Path to dusk.dsk asset archive') - ap.add_argument('title', help='Game title string') - ap.add_argument('build_dir', help='Output directory') - args = ap.parse_args() - - id_prefix = 'G' if args.platform == 'GCN' else 'R' - - for region_suffix, region_char, region_name, _ in REGIONS: - game_id = f'{id_prefix}DK{region_char}01' - out_path = os.path.join(args.build_dir, f'Dusk-{region_name}.iso') - _build_iso(args.platform, game_id, args.title, - region_suffix, args.dol, args.dsk, out_path) - - -if __name__ == '__main__': - main() diff --git a/tools/run_python/CMakeLists.txt b/tools/run_python/CMakeLists.txt deleted file mode 100644 index abd8808a..00000000 --- a/tools/run_python/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -function(dusk_run_python CMAKE_TARGET_NAME PYTHON_MODULE) - find_package(Python3 COMPONENTS Interpreter REQUIRED) - - add_custom_target(${CMAKE_TARGET_NAME} ALL - WORKING_DIRECTORY ${DUSK_ROOT_DIR} - COMMAND - ${Python3_EXECUTABLE} -m ${PYTHON_MODULE} ${ARGN} - ) -endfunction() diff --git a/tools/server.py b/tools/server.py deleted file mode 100755 index c55f5b8f..00000000 --- a/tools/server.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2026 Dominic Masters -# -# This software is released under the MIT License. -# https://opensource.org/licenses/MIT - -import argparse -import functools -import http.server -import os - -EDITOR_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor") - -parser = argparse.ArgumentParser(description="Dusk editor tools server") -parser.add_argument("--host", default="localhost", help="Host to bind to") -parser.add_argument("--port", type=int, default=3000, help="Port to listen on") -args = parser.parse_args() - -handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=EDITOR_DIR) - -with http.server.HTTPServer((args.host, args.port), handler) as server: - print(f"Dusk editor tools: http://{args.host}:{args.port}/") - server.serve_forever()