From 9551e222dccdc40d7f317054dace6cf089feff48 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 11 Jul 2026 17:43:26 -0500 Subject: [PATCH] Chunks as json --- assets/cutscene/test_npc.json | 25 + assets/map/testmap/chunks/-1_0_0.dcf | Bin 2235 -> 0 bytes assets/map/testmap/chunks/-1_0_0.json | 1 + assets/map/testmap/chunks/0_0_0.dcf | Bin 2146 -> 0 bytes assets/map/testmap/chunks/0_0_0.json | 1046 +++++++ assets/map/testmap/chunks/0_1_0.dcf | Bin 2234 -> 0 bytes assets/map/testmap/chunks/0_1_0.json | 1 + assets/map/testmap/chunks/1_0_0.dcf | Bin 2322 -> 0 bytes assets/map/testmap/chunks/1_0_0.json | 1 + assets/map/testmap/chunks/1_1_0.dcf | Bin 2234 -> 0 bytes assets/map/testmap/chunks/1_1_0.json | 1 + assets/map/testmap/chunks/2_0_0.dcf | Bin 2102 -> 0 bytes assets/map/testmap/chunks/2_0_0.json | 1 + assets/map/testmap/map.json | 8 +- .../map/testmap/chunks/chunk_-1_0_0.json | 1992 ------------- assetsraw/map/testmap/chunks/chunk_0_0_0.json | 2183 -------------- assetsraw/map/testmap/chunks/chunk_0_1_0.json | 1929 ------------- assetsraw/map/testmap/chunks/chunk_1_0_0.json | 2107 -------------- assetsraw/map/testmap/chunks/chunk_1_1_0.json | 2028 ------------- assetsraw/map/testmap/chunks/chunk_2_0_0.json | 2543 ----------------- .../asset/loader/chunk/assetchunkloader.c | 233 +- .../asset/loader/chunk/assetchunkloader.h | 35 +- src/dusk/rpg/cutscene/CMakeLists.txt | 1 + src/dusk/rpg/cutscene/cutscene.c | 444 +++ src/dusk/rpg/cutscene/cutscene.h | 117 + src/dusk/rpg/cutscene/scene/testcutscene.h | 17 - src/dusk/rpg/entity/CMakeLists.txt | 3 +- src/dusk/rpg/entity/entity.c | 59 +- src/dusk/rpg/entity/entity.h | 13 +- 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 | 30 - src/dusk/rpg/overworld/chunk.c | 36 +- src/dusk/rpg/overworld/chunk.h | 6 +- src/dusk/rpg/overworld/map.c | 43 - src/dusk/rpg/overworld/map.h | 16 - src/dusk/rpg/rpg.c | 6 - tools/asset/chunk/__main__.py | 206 +- 39 files changed, 1949 insertions(+), 13252 deletions(-) create mode 100644 assets/cutscene/test_npc.json delete mode 100644 assets/map/testmap/chunks/-1_0_0.dcf create mode 100644 assets/map/testmap/chunks/-1_0_0.json delete mode 100644 assets/map/testmap/chunks/0_0_0.dcf create mode 100644 assets/map/testmap/chunks/0_0_0.json delete mode 100644 assets/map/testmap/chunks/0_1_0.dcf create mode 100644 assets/map/testmap/chunks/0_1_0.json delete mode 100644 assets/map/testmap/chunks/1_0_0.dcf create mode 100644 assets/map/testmap/chunks/1_0_0.json delete mode 100644 assets/map/testmap/chunks/1_1_0.dcf create mode 100644 assets/map/testmap/chunks/1_1_0.json delete mode 100644 assets/map/testmap/chunks/2_0_0.dcf create mode 100644 assets/map/testmap/chunks/2_0_0.json delete mode 100644 assetsraw/map/testmap/chunks/chunk_-1_0_0.json delete mode 100644 assetsraw/map/testmap/chunks/chunk_0_0_0.json delete mode 100644 assetsraw/map/testmap/chunks/chunk_0_1_0.json delete mode 100644 assetsraw/map/testmap/chunks/chunk_1_0_0.json delete mode 100644 assetsraw/map/testmap/chunks/chunk_1_1_0.json delete mode 100644 assetsraw/map/testmap/chunks/chunk_2_0_0.json create mode 100644 src/dusk/rpg/cutscene/cutscene.c 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 diff --git a/assets/cutscene/test_npc.json b/assets/cutscene/test_npc.json new file mode 100644 index 00000000..13d53bbe --- /dev/null +++ b/assets/cutscene/test_npc.json @@ -0,0 +1,25 @@ +{ + "pause": "DEFAULT", + "items": [ + { "type": "text", "text": "Test Two." }, + { "type": "entityAdd", "entityType": "npc", "position": [4, 4, 0] }, + { + "type": "textMini", + "text": "Hello!", + "position": [4, 4, 0], + "duration": 3.0 + }, + { + "type": "emoji", + "entityIndex": "lastCreated", + "emojiType": "exclamation", + "duration": 2.0 + }, + { + "type": "entityWalkTo", + "entityIndex": "lastCreated", + "positions": [[8, 2, 0]] + }, + { "type": "text", "text": "Done." } + ] +} diff --git a/assets/map/testmap/chunks/-1_0_0.dcf b/assets/map/testmap/chunks/-1_0_0.dcf deleted file mode 100644 index 70bb62209e75de265f3e91e145eb939fa13cea15..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2235 zcmZ>9c4J^+U|?VbVh|WCG{iNyNm{!Tx6KflQTyo=4zvzudYO-&p6F#S%$|X^ALb51 zG`c$p@d=qnsyre4(B(-rpOAWV^9k_@nKvpA3Bl2Dz#k4Qx%nxnImP9Mt*5=YP?Cj30$oM14Dy7ND~gVX7R=_wIFo@ Vj(F4>#T&uYIsnxj0GiFf003?mG?f4V diff --git a/assets/map/testmap/chunks/-1_0_0.json b/assets/map/testmap/chunks/-1_0_0.json new file mode 100644 index 00000000..6601176e --- /dev/null +++ b/assets/map/testmap/chunks/-1_0_0.json @@ -0,0 +1 @@ +{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_-1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_4_4.json", "offset": [2.0, 1.0, 0.0]}, {"model": "models/buildings/house_6_3.json", "offset": [1.0, 9.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [12.0, 6.0, 0.0]}]} \ No newline at end of file diff --git a/assets/map/testmap/chunks/0_0_0.dcf b/assets/map/testmap/chunks/0_0_0.dcf deleted file mode 100644 index eccdb75949aa81127f031acf4528f6750656d4fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2146 zcmZ>9c4J^+U|?VbVh|W;8sZAVB(2;@*k*`|;c7pv{6#Hu*`N``L?X=&RZpoqn32o@ znMEvSMbZailIxC9c4J^+U|?VbVh|W;8sZA_NlM*MzReJw==S3>4_zIMk4rtV;xKbyG%kHGd2||= zdSb1w znwJfw;tk>rf!IJVt2jRoB_Ti+CzWRAq-5r$7wc!_mlmhSTg02d)Hc{NFn}-v0}i#u b@y0N<297{w4tUfW#T&uYIsnyy)iVG9`nfam diff --git a/assets/map/testmap/chunks/0_1_0.json b/assets/map/testmap/chunks/0_1_0.json new file mode 100644 index 00000000..c8e43fc7 --- /dev/null +++ b/assets/map/testmap/chunks/0_1_0.json @@ -0,0 +1 @@ +{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_0_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_8_4.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_3_3.json", "offset": [11.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [3.0, 11.0, 0.0]}]} \ No newline at end of file diff --git a/assets/map/testmap/chunks/1_0_0.dcf b/assets/map/testmap/chunks/1_0_0.dcf deleted file mode 100644 index f9f7654c958f4a2fbe56df7be1117e7cf60ba87f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2322 zcmZ>9c4J^+U|?VbVh|W;8sZ8}l3wn@v>76T%YJnAxYW@~e5Bh?t8k^2x#;l)SYz@=fM&(n1(7&D$UGE$;?YH*3ZZ9c4J^+U|?VbVh|XmQNsbkBTUjN+z8u@>Ry;T(DlK@Nu|-v!No^67hN5?Jh6Oq zb8zv|%|lm*E>A2U-5gweV$B;>2MNK^a2TB7Fq*yxXS~olzp&)yr=;c->nCTF=4C^v zctapGh&Ry7D$dVCNf=PYNu`-NDVcfc#rhffrNyc7rtzjQwGIwI4Is>bL#i3=9AuWiz}0 diff --git a/assets/map/testmap/chunks/1_1_0.json b/assets/map/testmap/chunks/1_1_0.json new file mode 100644 index 00000000..990b457b --- /dev/null +++ b/assets/map/testmap/chunks/1_1_0.json @@ -0,0 +1 @@ +{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_5_5.json", "offset": [3.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [10.0, 3.0, 0.0]}, {"model": "models/buildings/house_3_1.json", "offset": [1.0, 12.0, 0.0]}]} \ No newline at end of file diff --git a/assets/map/testmap/chunks/2_0_0.dcf b/assets/map/testmap/chunks/2_0_0.dcf deleted file mode 100644 index d26e7bb528801010fb62e7275a921da5084cf6c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2102 zcmZ>9c4J^+U|?VbVh|XmN5f&LqyuJXdL@>|=PyXCBS>QM0kQVN)D33)iSaM8`$1&} z6A%+ilWIO8^{_IFSelS|q{@Tr1p#7dQq3P#4+-DVaG+B-loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) { - errorOk(); - } - - assertNull(loading->loading.chunk.data, "Data already defined?"); - - assetfile_t *file = &loading->loading.chunk.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 chunk file." - ); - - loading->loading.chunk.data = data; - loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; errorOk(); } @@ -54,12 +31,141 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) { switch(loading->loading.chunk.state) { case ASSET_CHUNK_LOADING_STATE_INITIAL: - loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC; + loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_JSON; + loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; errorOk(); - case ASSET_CHUNK_LOADING_STATE_PARSE: - break; + case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: { + // Lock the chunk's JSON as a sub-asset. The entry key is prefixed + // with "json:" to avoid a type-collision with the chunk 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); + + yyjson_val *tilesVal = yyjson_obj_get(root, "tiles"); + if( + !tilesVal || !yyjson_is_arr(tilesVal) || + yyjson_arr_size(tilesVal) != CHUNK_TILE_COUNT + ) { + assetUnlockEntry(jsonEntry); + loading->entry->state = ASSET_ENTRY_STATE_ERROR; + errorThrow( + "Chunk JSON 'tiles' must have exactly %d entries", + CHUNK_TILE_COUNT + ); + } + + out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t)); + size_t tileIdx, tileMax; + yyjson_val *tileVal; + yyjson_arr_foreach(tilesVal, tileIdx, tileMax, tileVal) { + yyjson_val *shapeVal = yyjson_arr_get(tileVal, 0); + yyjson_val *zVal = yyjson_arr_get(tileVal, 1); + if( + !yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) != 2 || + !yyjson_is_int(shapeVal) || !yyjson_is_int(zVal) + ) { + memoryFree(out->tiles); + out->tiles = NULL; + assetUnlockEntry(jsonEntry); + loading->entry->state = ASSET_ENTRY_STATE_ERROR; + errorThrow("Chunk JSON tile entries must be [shape, z] arrays"); + } + out->tiles[tileIdx] = (tile_t){ + .shape = (tileshape_t)yyjson_get_int(shapeVal), + .z = (uint8_t)yyjson_get_int(zVal) + }; + } + + yyjson_val *meshesVal = yyjson_obj_get(root, "meshes"); + out->meshCount = 0; + if(meshesVal && yyjson_is_arr(meshesVal)) { + size_t meshCount = yyjson_arr_size(meshesVal); + if(meshCount > CHUNK_MESH_COUNT_MAX) { + memoryFree(out->tiles); + out->tiles = NULL; + assetUnlockEntry(jsonEntry); + loading->entry->state = ASSET_ENTRY_STATE_ERROR; + errorThrow( + "Chunk JSON 'meshes' exceeds CHUNK_MESH_COUNT_MAX (%d)", + CHUNK_MESH_COUNT_MAX + ); + } + + size_t meshIdx, meshMax; + yyjson_val *meshVal; + yyjson_arr_foreach(meshesVal, meshIdx, meshMax, meshVal) { + yyjson_val *modelVal = yyjson_obj_get(meshVal, "model"); + if(!modelVal || !yyjson_is_str(modelVal)) { + memoryFree(out->tiles); + out->tiles = NULL; + assetUnlockEntry(jsonEntry); + loading->entry->state = ASSET_ENTRY_STATE_ERROR; + errorThrow("Chunk JSON mesh entry missing 'model' string"); + } + const char_t *modelStr = yyjson_get_str(modelVal); + size_t modelLen = yyjson_get_len(modelVal); + if(modelLen >= CHUNK_MESH_NAME_MAX) { + memoryFree(out->tiles); + out->tiles = NULL; + assetUnlockEntry(jsonEntry); + loading->entry->state = ASSET_ENTRY_STATE_ERROR; + errorThrow( + "Chunk JSON model path '%s' exceeds max length", modelStr + ); + } + memoryCopy(out->modelNames[meshIdx], modelStr, modelLen + 1); + + vec3 offset = { 0.0f, 0.0f, 0.0f }; + yyjson_val *offsetVal = yyjson_obj_get(meshVal, "offset"); + if( + offsetVal && yyjson_is_arr(offsetVal) && + yyjson_arr_size(offsetVal) == 3 + ) { + size_t offIdx, offMax; + yyjson_val *offElem; + yyjson_arr_foreach(offsetVal, offIdx, offMax, offElem) { + if(yyjson_is_num(offElem)) { + offset[offIdx] = (float_t)yyjson_get_num(offElem); + } + } + } + glm_vec3_copy(offset, out->meshOffsets[meshIdx]); + } + out->meshCount = (uint8_t)meshCount; + } + + assetUnlockEntry(jsonEntry); + + if(out->meshCount == 0) { + loading->entry->state = ASSET_ENTRY_STATE_LOADED; + errorOk(); + } + + loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS; + loading->loading.chunk.modelIndex = 0; + loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; + errorOk(); + } case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS: while(loading->loading.chunk.modelIndex < out->meshCount) { @@ -91,73 +197,6 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) { default: errorOk(); } - - uint8_t *data = loading->loading.chunk.data; - assertNotNull(data, "Chunk data should have been loaded by now."); - - if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') { - memoryFree(data); - assetLoaderErrorThrow(loading, "Invalid chunk file header"); - } - - uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4)); - if(version != ASSET_CHUNK_FILE_VERSION) { - memoryFree(data); - assetLoaderErrorThrow( - loading, "Unsupported chunk file version %u", version - ); - } - - size_t offset = 8; - - size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t); - out->tiles = memoryAllocate(tileSize); - memoryCopy(out->tiles, data + offset, tileSize); - offset += tileSize; - - for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) { - uint32_t *shape = (uint32_t *)&out->tiles[t].shape; - *shape = endianLittleToHost32(*shape); - } - - out->meshCount = data[offset]; - offset += sizeof(uint8_t); - assertTrue( - out->meshCount <= CHUNK_MESH_COUNT_MAX, - "Chunk mesh count exceeds maximum." - ); - - for(uint8_t m = 0; m < out->meshCount; m++) { - uint8_t nameLen = 0; - while( - data[offset + nameLen] != '\0' && - nameLen < CHUNK_MESH_NAME_MAX - 1 - ) { - nameLen++; - } - memoryCopy(out->modelNames[m], data + offset, nameLen); - out->modelNames[m][nameLen] = '\0'; - offset += nameLen + 1; - - memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3)); - offset += sizeof(vec3); - out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]); - out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]); - out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]); - } - - memoryFree(data); - loading->loading.chunk.data = NULL; - - if(out->meshCount == 0) { - loading->entry->state = ASSET_ENTRY_STATE_LOADED; - errorOk(); - } - - loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS; - loading->loading.chunk.modelIndex = 0; - loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC; - errorOk(); } errorret_t assetChunkDispose(assetentry_t *entry) { diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.h b/src/dusk/asset/loader/chunk/assetchunkloader.h index db591a7f..ecee3d63 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.h +++ b/src/dusk/asset/loader/chunk/assetchunkloader.h @@ -9,8 +9,6 @@ #include "asset/assetfile.h" #include "rpg/overworld/chunk.h" -#define ASSET_CHUNK_FILE_VERSION 4 - typedef struct assetloading_s assetloading_t; typedef struct assetentry_s assetentry_t; @@ -20,16 +18,12 @@ typedef struct { typedef enum { ASSET_CHUNK_LOADING_STATE_INITIAL, - ASSET_CHUNK_LOADING_STATE_READ_FILE, - ASSET_CHUNK_LOADING_STATE_PARSE, - ASSET_CHUNK_LOADING_STATE_LOAD_MODELS, - ASSET_CHUNK_LOADING_STATE_DONE + ASSET_CHUNK_LOADING_STATE_LOAD_JSON, + ASSET_CHUNK_LOADING_STATE_LOAD_MODELS } assetchunkloadingstate_t; typedef struct { - assetfile_t file; assetchunkloadingstate_t state; - uint8_t *data; uint8_t modelIndex; } assetchunkloaderloading_t; @@ -42,9 +36,9 @@ typedef struct { } assetchunkoutput_t; /** - * Asynchronous loader for chunk assets. Reads the raw DCF file bytes into - * the loading buffer so the sync phase can parse without blocking the - * main thread on I/O. + * Asynchronous loader for chunk assets. No-op - the chunk's JSON file is + * loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync), + * which handles its own async file I/O. * * @param loading Loading information for the asset being loaded. * @return Error code indicating success or failure of the load operation. @@ -52,9 +46,22 @@ typedef struct { errorret_t assetChunkLoaderAsync(assetloading_t *loading); /** - * Synchronous loader for chunk assets. Validates the DCF binary previously - * read by the async phase and populates the output assetchunkoutput_t with - * tile data and model paths. + * Synchronous loader for chunk assets. Locks and parses the chunk's JSON + * file (tiles plus referenced model paths/offsets), then locks each + * referenced model asset before marking the entry loaded. + * + * Expected JSON shape: + * { + * "tiles": [ [shape, z], ... ] // exactly CHUNK_TILE_COUNT entries, + * // one per (x, y) column, x-major + * "meshes": [ + * { "model": "models/chunks/chunk_0_0_0_0.json", + * "offset": [0.0, 0.0, 0.0] } + * ] + * } + * "meshes" is optional; each entry's "offset" defaults to [0, 0, 0] if + * omitted. By convention mesh index 0 is the chunk's terrain and the + * rest are props (see sceneOverworldDrawChunksBase/Props). * * @param loading Loading information for the asset being loaded. * @return Error code indicating success or failure of the load operation. diff --git a/src/dusk/rpg/cutscene/CMakeLists.txt b/src/dusk/rpg/cutscene/CMakeLists.txt index ccecedc5..a7ced42c 100644 --- a/src/dusk/rpg/cutscene/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/CMakeLists.txt @@ -6,6 +6,7 @@ # Sources target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + cutscene.c cutscenesystem.c ) diff --git a/src/dusk/rpg/cutscene/cutscene.c b/src/dusk/rpg/cutscene/cutscene.c new file mode 100644 index 00000000..798f22d0 --- /dev/null +++ b/src/dusk/rpg/cutscene/cutscene.c @@ -0,0 +1,444 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "cutscene.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "asset/asset.h" +#include "asset/loader/assetloader.h" +#include "asset/loader/assetentry.h" +#include "asset/loader/json/assetjsonloader.h" +#include "rpg/cutscene/cutscenesystem.h" +#include "rpg/entity/entitytype.h" +#include "rpg/entity/entitydir.h" +#include "ui/rpg/uiemoji.h" +#include "yyjson.h" + +static cutscenejsoncacheentry_t CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_MAX]; +static uint32_t CUTSCENE_JSON_CACHE_COUNT; + +errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause) { + assertNotNull(outPause, "Output pause pointer cannot be NULL"); + if(!val || !yyjson_is_str(val)) { + errorThrow("Cutscene pause value must be a string"); + } + + const char_t *str = yyjson_get_str(val); + if(stringEquals(str, "NONE")) { + *outPause = CUTSCENE_PAUSE_NONE; + } else if(stringEquals(str, "DEFAULT")) { + *outPause = CUTSCENE_PAUSE_DEFAULT; + } else if(stringEquals(str, "ALL")) { + *outPause = CUTSCENE_PAUSE_ALL; + } else { + errorThrow("Unknown cutscene pause value '%s'", str); + } + + errorOk(); +} + +errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex) { + assertNotNull(outIndex, "Output entity index pointer cannot be NULL"); + + if(val && yyjson_is_str(val)) { + const char_t *str = yyjson_get_str(val); + if(stringEquals(str, "interact")) { + *outIndex = CUTSCENE_ENTITY_INTERACT; + } else if(stringEquals(str, "interacted")) { + *outIndex = CUTSCENE_ENTITY_INTERACTED; + } else if(stringEquals(str, "lastCreated")) { + *outIndex = CUTSCENE_ENTITY_LAST_CREATED; + } else { + errorThrow("Unknown entity index sentinel '%s'", str); + } + errorOk(); + } + + if(!val || !yyjson_is_int(val)) { + errorThrow("Entity index must be a number or sentinel string"); + } + *outIndex = (uint8_t)yyjson_get_int(val); + errorOk(); +} + +errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos) { + assertNotNull(outPos, "Output position pointer cannot be NULL"); + if(!val || !yyjson_is_arr(val) || yyjson_arr_size(val) != 3) { + errorThrow("Position must be a [x, y, z] array"); + } + + worldunit_t comps[3]; + size_t idx, max; + yyjson_val *elem; + yyjson_arr_foreach(val, idx, max, elem) { + if(!yyjson_is_num(elem)) { + errorThrow("Position elements must be numbers"); + } + comps[idx] = (worldunit_t)yyjson_get_num(elem); + } + + *outPos = (worldpos_t){ comps[0], comps[1], comps[2] }; + errorOk(); +} + +errorret_t cutsceneItemCreateFromJson( + yyjson_val *obj, + cutsceneitem_t *outItem, + worldpos_t *waypoints, + const uint8_t waypointsMax +) { + assertNotNull(obj, "Cutscene item JSON object cannot be NULL"); + assertNotNull(outItem, "Output item pointer cannot be NULL"); + + yyjson_val *typeVal = yyjson_obj_get(obj, "type"); + if(!typeVal || !yyjson_is_str(typeVal)) { + errorThrow("Cutscene item JSON missing 'type' string"); + } + const char_t *typeStr = yyjson_get_str(typeVal); + + memoryZero(outItem, sizeof(cutsceneitem_t)); + + if(stringEquals(typeStr, "text")) { + yyjson_val *textVal = yyjson_obj_get(obj, "text"); + if(!textVal || !yyjson_is_str(textVal)) { + errorThrow("Cutscene 'text' item missing 'text' string"); + } + + outItem->type = CUTSCENE_ITEM_TYPE_TEXT; + stringCopy( + outItem->text.text, yyjson_get_str(textVal), CUTSCENE_TEXT_MAX_CHARS + ); + } else if(stringEquals(typeStr, "textMini")) { + yyjson_val *textVal = yyjson_obj_get(obj, "text"); + if(!textVal || !yyjson_is_str(textVal)) { + errorThrow("Cutscene 'textMini' item missing 'text' string"); + } + + worldpos_t pos; + errorChain( + cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos) + ); + + yyjson_val *durVal = yyjson_obj_get(obj, "duration"); + if(!durVal || !yyjson_is_num(durVal)) { + errorThrow("Cutscene 'textMini' item missing 'duration' number"); + } + + outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI; + stringCopy( + outItem->textMini.text, yyjson_get_str(textVal), + CUTSCENE_TEXT_MINI_MAX_CHARS + ); + outItem->textMini.position[0] = (float_t)pos.x; + outItem->textMini.position[1] = (float_t)pos.y; + outItem->textMini.position[2] = (float_t)pos.z; + outItem->textMini.duration = (float_t)yyjson_get_num(durVal); + } else if(stringEquals(typeStr, "textMiniHide")) { + yyjson_val *indexVal = yyjson_obj_get(obj, "index"); + if(!indexVal || !yyjson_is_int(indexVal)) { + errorThrow("Cutscene 'textMiniHide' item missing 'index' number"); + } + + outItem->type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE; + outItem->textMiniHide.index = (uint8_t)yyjson_get_int(indexVal); + } else if(stringEquals(typeStr, "wait")) { + yyjson_val *durVal = yyjson_obj_get(obj, "duration"); + if(!durVal || !yyjson_is_num(durVal)) { + errorThrow("Cutscene 'wait' item missing 'duration' number"); + } + + outItem->type = CUTSCENE_ITEM_TYPE_WAIT; + outItem->wait = (float_t)yyjson_get_num(durVal); + } else if(stringEquals(typeStr, "entityAdd")) { + yyjson_val *entityTypeVal = yyjson_obj_get(obj, "entityType"); + if(!entityTypeVal || !yyjson_is_str(entityTypeVal)) { + errorThrow("Cutscene 'entityAdd' item missing 'entityType' string"); + } + const char_t *entityTypeStr = yyjson_get_str(entityTypeVal); + + entitytype_t entityType; + if(stringEquals(entityTypeStr, "player")) { + entityType = ENTITY_TYPE_PLAYER; + } else if(stringEquals(entityTypeStr, "npc")) { + entityType = ENTITY_TYPE_NPC; + } else if(stringEquals(entityTypeStr, "item")) { + entityType = ENTITY_TYPE_ITEM; + } else { + errorThrow( + "Cutscene 'entityAdd' has unknown entityType '%s'", entityTypeStr + ); + } + + worldpos_t pos; + errorChain( + cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos) + ); + + outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_ADD; + outItem->entityAdd.entityType = entityType; + outItem->entityAdd.position = pos; + } else if(stringEquals(typeStr, "entityRemove")) { + uint8_t entityIndex; + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "entityIndex"), &entityIndex + )); + + outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_REMOVE; + outItem->entityRemove.entityIndex = entityIndex; + } else if(stringEquals(typeStr, "entityTurn")) { + uint8_t entityIndex; + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "entityIndex"), &entityIndex + )); + + yyjson_val *dirVal = yyjson_obj_get(obj, "direction"); + if(!dirVal || !yyjson_is_str(dirVal)) { + errorThrow("Cutscene 'entityTurn' item missing 'direction' string"); + } + const char_t *dirStr = yyjson_get_str(dirVal); + + entitydir_t direction; + if(stringEquals(dirStr, "north")) { + direction = ENTITY_DIR_NORTH; + } else if(stringEquals(dirStr, "east")) { + direction = ENTITY_DIR_EAST; + } else if(stringEquals(dirStr, "south")) { + direction = ENTITY_DIR_SOUTH; + } else if(stringEquals(dirStr, "west")) { + direction = ENTITY_DIR_WEST; + } else { + errorThrow("Cutscene 'entityTurn' has unknown direction '%s'", dirStr); + } + + outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TURN; + outItem->entityTurn.entityIndex = entityIndex; + outItem->entityTurn.direction = direction; + } else if(stringEquals(typeStr, "entityWalkTo")) { + uint8_t entityIndex; + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "entityIndex"), &entityIndex + )); + + yyjson_val *positionsVal = yyjson_obj_get(obj, "positions"); + if(!positionsVal || !yyjson_is_arr(positionsVal)) { + errorThrow("Cutscene 'entityWalkTo' item missing 'positions' array"); + } + size_t count = yyjson_arr_size(positionsVal); + if(count == 0 || count > waypointsMax) { + errorThrow( + "Cutscene 'entityWalkTo' 'positions' must have 1-%d entries", + waypointsMax + ); + } + assertNotNull(waypoints, "Waypoint storage cannot be NULL"); + + size_t idx, max; + yyjson_val *posVal; + yyjson_arr_foreach(positionsVal, idx, max, posVal) { + errorChain(cutsceneJsonParseWorldPos(posVal, &waypoints[idx])); + } + + bool_t walkAround = true; + yyjson_val *walkAroundVal = yyjson_obj_get(obj, "walkAround"); + if(walkAroundVal && yyjson_is_bool(walkAroundVal)) { + walkAround = yyjson_get_bool(walkAroundVal); + } + + outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO; + outItem->entityWalkTo.entityIndex = entityIndex; + outItem->entityWalkTo.positions = waypoints; + outItem->entityWalkTo.count = (uint8_t)count; + outItem->entityWalkTo.walkAround = walkAround; + } else if(stringEquals(typeStr, "entityWalkToEntity")) { + uint8_t entityIndex, targetEntityIndex; + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "entityIndex"), &entityIndex + )); + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "targetEntityIndex"), &targetEntityIndex + )); + + yyjson_val *offsetXVal = yyjson_obj_get(obj, "offsetX"); + yyjson_val *offsetYVal = yyjson_obj_get(obj, "offsetY"); + if( + !offsetXVal || !yyjson_is_num(offsetXVal) || + !offsetYVal || !yyjson_is_num(offsetYVal) + ) { + errorThrow( + "Cutscene 'entityWalkToEntity' item missing " + "'offsetX'/'offsetY' numbers" + ); + } + + outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY; + outItem->entityWalkToEntity.entityIndex = entityIndex; + outItem->entityWalkToEntity.targetEntityIndex = targetEntityIndex; + outItem->entityWalkToEntity.offsetX = + (worldunit_t)yyjson_get_num(offsetXVal); + outItem->entityWalkToEntity.offsetY = + (worldunit_t)yyjson_get_num(offsetYVal); + } else if(stringEquals(typeStr, "entityTeleport")) { + uint8_t entityIndex; + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "entityIndex"), &entityIndex + )); + + worldpos_t pos; + errorChain( + cutsceneJsonParseWorldPos(yyjson_obj_get(obj, "position"), &pos) + ); + + outItem->type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT; + outItem->entityTeleport.entityIndex = entityIndex; + outItem->entityTeleport.target = pos; + } else if(stringEquals(typeStr, "emoji")) { + uint8_t entityIndex; + errorChain(cutsceneJsonParseEntityIndex( + yyjson_obj_get(obj, "entityIndex"), &entityIndex + )); + + yyjson_val *emojiTypeVal = yyjson_obj_get(obj, "emojiType"); + if(!emojiTypeVal || !yyjson_is_str(emojiTypeVal)) { + errorThrow("Cutscene 'emoji' item missing 'emojiType' string"); + } + const char_t *emojiTypeStr = yyjson_get_str(emojiTypeVal); + + uiemojitype_t emojiType; + if(stringEquals(emojiTypeStr, "question")) { + emojiType = UI_EMOJI_QUESTION_MARK; + } else if(stringEquals(emojiTypeStr, "exclamation")) { + emojiType = UI_EMOJI_EXCLAMATION_MARK; + } else { + errorThrow( + "Cutscene 'emoji' has unknown emojiType '%s'", emojiTypeStr + ); + } + + yyjson_val *durVal = yyjson_obj_get(obj, "duration"); + if(!durVal || !yyjson_is_num(durVal)) { + errorThrow("Cutscene 'emoji' item missing 'duration' number"); + } + + outItem->type = CUTSCENE_ITEM_TYPE_EMOJI; + outItem->emoji.entityIndex = entityIndex; + outItem->emoji.emojiType = emojiType; + outItem->emoji.duration = (float_t)yyjson_get_num(durVal); + } else if(stringEquals(typeStr, "shake")) { + yyjson_val *amountVal = yyjson_obj_get(obj, "amount"); + yyjson_val *durVal = yyjson_obj_get(obj, "duration"); + if( + !amountVal || !yyjson_is_int(amountVal) || + !durVal || !yyjson_is_num(durVal) + ) { + errorThrow("Cutscene 'shake' item missing 'amount'/'duration' numbers"); + } + + outItem->type = CUTSCENE_ITEM_TYPE_SHAKE; + outItem->shake.amount = (uint8_t)yyjson_get_int(amountVal); + outItem->shake.duration = (float_t)yyjson_get_num(durVal); + } else if(stringEquals(typeStr, "setPause")) { + cutscenepause_t pause; + errorChain( + cutsceneJsonParsePause(yyjson_obj_get(obj, "pause"), &pause) + ); + + outItem->type = CUTSCENE_ITEM_TYPE_SET_PAUSE; + outItem->setPause = pause; + } else { + errorThrow("Cutscene item JSON has unknown 'type': %s", typeStr); + } + + errorOk(); +} + +errorret_t cutsceneGetByName( + const char_t *name, const cutscene_t **outCutscene +) { + assertNotNull(name, "Cutscene name cannot be NULL"); + assertNotNull(outCutscene, "Output cutscene pointer cannot be NULL"); + assertStrLenMax(name, CUTSCENE_JSON_NAME_MAX, "Cutscene name too long"); + + for(uint32_t i = 0; i < CUTSCENE_JSON_CACHE_COUNT; i++) { + if(!stringEquals(CUTSCENE_JSON_CACHE[i].name, name)) continue; + *outCutscene = &CUTSCENE_JSON_CACHE[i].cutscene; + errorOk(); + } + + if(CUTSCENE_JSON_CACHE_COUNT >= CUTSCENE_JSON_CACHE_MAX) { + errorThrow( + "Too many cutscenes loaded: exceeds CUTSCENE_JSON_CACHE_MAX (%d)", + CUTSCENE_JSON_CACHE_MAX + ); + } + + char_t path[CUTSCENE_JSON_NAME_MAX + 24]; + stringFormat(path, sizeof(path), "cutscene/%s.json", name); + + assetentry_t *entry = assetLock(path, 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); + + cutscenepause_t pause = CUTSCENE_PAUSE_DEFAULT; + yyjson_val *pauseVal = yyjson_obj_get(root, "pause"); + if(pauseVal) { + errorret_t pauseRet = cutsceneJsonParsePause(pauseVal, &pause); + if(errorIsNotOk(pauseRet)) { + assetUnlockEntry(entry); + errorChain(pauseRet); + } + } + + yyjson_val *itemsVal = yyjson_obj_get(root, "items"); + if(!itemsVal || !yyjson_is_arr(itemsVal)) { + assetUnlockEntry(entry); + errorThrow("Cutscene '%s' JSON missing 'items' array", name); + } + + size_t itemCount = yyjson_arr_size(itemsVal); + if(itemCount == 0 || itemCount > CUTSCENE_JSON_ITEM_COUNT_MAX) { + assetUnlockEntry(entry); + errorThrow( + "Cutscene '%s' 'items' must have 1-%d entries", + name, CUTSCENE_JSON_ITEM_COUNT_MAX + ); + } + + cutscenejsoncacheentry_t *cache = + &CUTSCENE_JSON_CACHE[CUTSCENE_JSON_CACHE_COUNT]; + memoryZero(cache, sizeof(cutscenejsoncacheentry_t)); + stringCopy(cache->name, name, CUTSCENE_JSON_NAME_MAX); + + size_t idx, max; + yyjson_val *itemVal; + yyjson_arr_foreach(itemsVal, idx, max, itemVal) { + errorret_t itemRet = cutsceneItemCreateFromJson( + itemVal, &cache->items[idx], cache->waypoints[idx], + CUTSCENE_JSON_WAYPOINT_COUNT_MAX + ); + if(errorIsNotOk(itemRet)) { + assetUnlockEntry(entry); + errorChain(itemRet); + } + } + + assetUnlockEntry(entry); + + cache->cutscene.items = cache->items; + cache->cutscene.itemCount = (uint8_t)itemCount; + cache->cutscene.pause = pause; + cache->cutscene.dataSize = 0; + + CUTSCENE_JSON_CACHE_COUNT++; + *outCutscene = &cache->cutscene; + errorOk(); +} diff --git a/src/dusk/rpg/cutscene/cutscene.h b/src/dusk/rpg/cutscene/cutscene.h index 03effe1f..d4cb5062 100644 --- a/src/dusk/rpg/cutscene/cutscene.h +++ b/src/dusk/rpg/cutscene/cutscene.h @@ -9,6 +9,8 @@ #include "rpg/cutscene/item/cutsceneitem.h" #include "rpg/cutscene/cutscenepause.h" +typedef struct yyjson_val yyjson_val; + typedef struct cutscene_s { const cutsceneitem_t *items; uint8_t itemCount; @@ -230,3 +232,118 @@ typedef struct cutscene_s { ), \ CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \ CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED) + +#define CUTSCENE_JSON_NAME_MAX 32 +#define CUTSCENE_JSON_ITEM_COUNT_MAX 32 +#define CUTSCENE_JSON_WAYPOINT_COUNT_MAX 8 +#define CUTSCENE_JSON_CACHE_MAX 16 + +// One slot of the cutsceneGetByName cache: a parsed cutscene plus the +// backing storage its items point into (items themselves, and the +// waypoint lists any "entityWalkTo" items reference). +typedef struct { + char_t name[CUTSCENE_JSON_NAME_MAX]; + cutscene_t cutscene; + cutsceneitem_t items[CUTSCENE_JSON_ITEM_COUNT_MAX]; + worldpos_t waypoints + [CUTSCENE_JSON_ITEM_COUNT_MAX][CUTSCENE_JSON_WAYPOINT_COUNT_MAX]; +} cutscenejsoncacheentry_t; + +/** + * Parses a cutscene pause value ("NONE", "DEFAULT" or "ALL") from a + * yyjson string value. + * + * @param val The yyjson value to parse. + * @param outPause Output pointer, set to the parsed pause flags. + * @return Any error that occurs (missing/invalid/unknown value). + */ +errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause); + +/** + * Parses an entityIndex-shaped yyjson value - a raw number, or one of + * "interact", "interacted", "lastCreated" - into a raw uint8_t index or + * the matching CUTSCENE_ENTITY_* sentinel (see cutscenesystem.h). + * + * @param val The yyjson value to parse. + * @param outIndex Output pointer, set to the parsed index. + * @return Any error that occurs (missing/invalid/unknown value). + */ +errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex); + +/** + * Parses a [x, y, z] yyjson array into a worldpos_t. + * + * @param val The yyjson value to parse. + * @param outPos Output pointer, set to the parsed position. + * @return Any error that occurs (missing/invalid value). + */ +errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos); + +/** + * Parses a single cutscene item from a yyjson object into outItem. + * "type" selects the item shape (required): + * { "type": "text", "text": "Hello!" } + * { "type": "textMini", "text": "Hi", "position": [x, y, z], + * "duration": 3.0 } + * { "type": "textMiniHide", "index": 0 } + * { "type": "wait", "duration": 1.5 } + * { "type": "entityAdd", "entityType": "npc", "position": [x, y, z] } + * { "type": "entityRemove", "entityIndex": 0 } + * { "type": "entityTurn", "entityIndex": 0, "direction": "south" } + * { "type": "entityWalkTo", "entityIndex": 0, + * "positions": [[x, y, z], ...], "walkAround": true } + * { "type": "entityWalkToEntity", "entityIndex": 0, + * "targetEntityIndex": 1, "offsetX": 1, "offsetY": 0 } + * { "type": "entityTeleport", "entityIndex": 0, "position": [x, y, z] } + * { "type": "emoji", "entityIndex": 0, "emojiType": "exclamation", + * "duration": 2.0 } + * { "type": "shake", "amount": 2, "duration": 0.5 } + * { "type": "setPause", "pause": "ALL" } + * "entityIndex"/"targetEntityIndex" accept a raw number, or one of + * "interact", "interacted", "lastCreated" for the matching + * CUTSCENE_ENTITY_* sentinel. "entityType" uses the same strings as + * entityCreateFromJson ("player", "npc", "item"); "direction" is one of + * "north"/"east"/"south"/"west"; "emojiType" is "question" or + * "exclamation"; "pause" is "NONE", "DEFAULT" or "ALL". + * + * Not supported (would need persistent string/array storage or function + * pointers this parser doesn't provide): itemGive, concurrent, fade, + * map area items, callbacks, nested cutscene references. + * + * @param obj The yyjson object describing a single cutscene item. + * @param outItem Output pointer, filled with the parsed item. + * @param waypoints Backing storage for an "entityWalkTo" item's waypoint + * list; must remain valid for as long as the parsed item is used. + * Unused (may be NULL) for any other item type. + * @param waypointsMax Capacity of waypoints. + * @return Any error that occurs (missing/invalid/unknown fields). + */ +errorret_t cutsceneItemCreateFromJson( + yyjson_val *obj, + cutsceneitem_t *outItem, + worldpos_t *waypoints, + const uint8_t waypointsMax +); + +/** + * Loads (or returns the cached result of an earlier load of) the named + * cutscene from assets/cutscene/.json, parsing each entry of its + * "items" array with cutsceneItemCreateFromJson. Parsed once per name + * and cached for the lifetime of the process; holds at most + * CUTSCENE_JSON_CACHE_MAX distinct names, each with at most + * CUTSCENE_JSON_ITEM_COUNT_MAX items. + * + * File shape: + * { "pause": "DEFAULT", "items": [ { "type": "text", ... }, ... ] } + * "pause" is optional (one of "NONE", "DEFAULT", "ALL"; defaults to + * "DEFAULT" when absent). + * + * @param name The cutscene's file name (without extension), under + * assets/cutscene/. + * @param outCutscene Output pointer, set to the loaded cutscene. + * @return Any error that occurs (missing/malformed file, cache full, + * unsupported/invalid item). + */ +errorret_t cutsceneGetByName( + const char_t *name, const cutscene_t **outCutscene +); diff --git a/src/dusk/rpg/cutscene/scene/testcutscene.h b/src/dusk/rpg/cutscene/scene/testcutscene.h index e692d1a4..cff2fe0e 100755 --- a/src/dusk/rpg/cutscene/scene/testcutscene.h +++ b/src/dusk/rpg/cutscene/scene/testcutscene.h @@ -11,21 +11,4 @@ CUTSCENE(TEST_ONE, 0, DEFAULT, CUTSCENE_TEXT("Test One."), -); - -CUTSCENE(TEST_TWO, 0, DEFAULT, - CUTSCENE_TEXT("Test Two."), - CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0), - CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f), - CUTSCENE_EMOJI( - CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f - ), - CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0), - // CUTSCENE_CONCURRENT( - // CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0), - // CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0), - // ), - // CUTSCENE_ITEM_GIVE("POTATO", 3), - // CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT), - CUTSCENE_TEXT("Done."), ); \ No newline at end of file diff --git a/src/dusk/rpg/entity/CMakeLists.txt b/src/dusk/rpg/entity/CMakeLists.txt index da55a228..06ab5f2f 100644 --- a/src/dusk/rpg/entity/CMakeLists.txt +++ b/src/dusk/rpg/entity/CMakeLists.txt @@ -15,5 +15,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} add_subdirectory(anim) add_subdirectory(interact) add_subdirectory(npc) -add_subdirectory(item) -add_subdirectory(global) \ No newline at end of file +add_subdirectory(item) \ No newline at end of file diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c index f2dda4e4..3c012f8e 100644 --- a/src/dusk/rpg/entity/entity.c +++ b/src/dusk/rpg/entity/entity.c @@ -14,6 +14,7 @@ #include "rpg/overworld/maparea.h" #include "rpg/overworld/chunk.h" #include "rpg/overworld/tile.h" +#include "rpg/cutscene/cutscene.h" #include "yyjson.h" entity_t ENTITIES[ENTITY_COUNT]; @@ -399,6 +400,53 @@ errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity) { errorThrow("A player entity has already been spawned"); } + // NPC path waypoints, resolved up front for the same reason as above. + worldpos_t path[NPC_PATH_COUNT_MAX]; + uint8_t pathCount = 0; + if(type == ENTITY_TYPE_NPC) { + yyjson_val *pathVal = yyjson_obj_get(obj, "path"); + if(pathVal) { + if(!yyjson_is_arr(pathVal)) { + errorThrow("Entity JSON 'path' must be an array"); + } + size_t count = yyjson_arr_size(pathVal); + if(count == 0 || count > NPC_PATH_COUNT_MAX) { + errorThrow( + "Entity JSON 'path' must have 1-%d waypoints", NPC_PATH_COUNT_MAX + ); + } + + size_t pathIdx, pathLen; + yyjson_val *pathElem; + yyjson_arr_foreach(pathVal, pathIdx, pathLen, pathElem) { + if(!yyjson_is_arr(pathElem) || yyjson_arr_size(pathElem) != 3) { + errorThrow("Entity JSON 'path' entries must be [x, y, z] arrays"); + } + worldunit_t comps[3]; + size_t compIdx, compLen; + yyjson_val *compElem; + yyjson_arr_foreach(pathElem, compIdx, compLen, compElem) { + if(!yyjson_is_num(compElem)) { + errorThrow("Entity JSON 'path' elements must be numbers"); + } + comps[compIdx] = (worldunit_t)yyjson_get_num(compElem); + } + path[pathIdx] = (worldpos_t){ comps[0], comps[1], comps[2] }; + } + pathCount = (uint8_t)count; + } + } + + // Interact cutscene, resolved up front for the same reason as above. + const cutscene_t *cutscene = NULL; + yyjson_val *cutsceneVal = yyjson_obj_get(obj, "cutscene"); + if(cutsceneVal) { + if(!yyjson_is_str(cutsceneVal)) { + errorThrow("Entity JSON 'cutscene' must be a string"); + } + errorChain(cutsceneGetByName(yyjson_get_str(cutsceneVal), &cutscene)); + } + uint8_t index = entityGetAvailable(); if(index == 0xFF) errorThrow("No available entity slots"); @@ -407,7 +455,16 @@ errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity) { entity->direction = direction; entityPositionSet(entity, (worldpos_t){ pos[0], pos[1], pos[2] }); if(type == ENTITY_TYPE_ITEM) entityItemSet(entity, itemId, itemQuantity); - if(type == ENTITY_TYPE_PLAYER) entity->globalId = ENTITY_GLOBAL_ID_PLAYER; + if(pathCount > 0) { + npcSetMoveType(entity, NPC_MOVE_TYPE_PATH); + for(uint8_t i = 0; i < pathCount; i++) { + npcPathAddNode(&entity->data.npc, path[i]); + } + } + if(cutscene != NULL) { + entity->interact.type = ENTITY_INTERACT_CUTSCENE; + entity->interact.data.cutscene = cutscene; + } *outEntity = entity; errorOk(); diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h index c721e8f5..38f83e21 100644 --- a/src/dusk/rpg/entity/entity.h +++ b/src/dusk/rpg/entity/entity.h @@ -186,10 +186,21 @@ void entityPositionSet(entity_t *entity, const worldpos_t pos); * regardless of how it was spawned), and it is an error to spawn a * second one while one is already loaded. * + * When "type" is "npc", an optional "path" field sets it up with + * NPC_MOVE_TYPE_PATH movement: + * { "type": "npc", "position": [x, y, z], + * "path": [[4, 4, 0], [10, 10, 1]] } + * "path" must have 1-NPC_PATH_COUNT_MAX waypoints. + * + * Any entity type may set an optional "cutscene" field to wire up an + * interact component that starts the named cutscene (resolved via + * cutsceneGetByName) when interacted with: + * { "type": "npc", "position": [x, y, z], "cutscene": "test_npc" } + * * @param obj The yyjson object describing the entity. * @param outEntity Output pointer, set to the newly spawned entity on * success. * @return Any error that occurs (missing/invalid fields, unknown item, - * duplicate player, no free slots). + * duplicate player, unknown/malformed cutscene, no free slots). */ errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity); \ 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 2207763c..00000000 --- a/src/dusk/rpg/entity/global/entitygloballist.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 "entityglobaldefs.h" -#include "rpg/cutscene/scene/testcutscene.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 }); - - create->entity->interact.type = ENTITY_INTERACT_CUTSCENE; - create->entity->interact.data.cutscene = CUTSCENE_REFERENCE(TEST_TWO); -} - -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)), -}; - -//EOF diff --git a/src/dusk/rpg/overworld/chunk.c b/src/dusk/rpg/overworld/chunk.c index 93466700..c50f248a 100644 --- a/src/dusk/rpg/overworld/chunk.c +++ b/src/dusk/rpg/overworld/chunk.c @@ -125,11 +125,11 @@ void chunkUnload(chunk_t *chunk) { memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); - if(chunk->dcfEntry != NULL) { - eventUnsubscribe(&chunk->dcfEntry->onLoaded, chunkLoaded); - eventUnsubscribe(&chunk->dcfEntry->onError, chunkLoadError); - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; + if(chunk->dataEntry != NULL) { + eventUnsubscribe(&chunk->dataEntry->onLoaded, chunkLoaded); + eventUnsubscribe(&chunk->dataEntry->onError, chunkLoadError); + assetUnlockEntry(chunk->dataEntry); + chunk->dataEntry = NULL; } // modelEntries are borrowed pointers, not independently locked - the @@ -147,11 +147,11 @@ errorret_t chunkLoad(chunk_t *chunk) { chunkLoadQueueRemove(chunk); if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL; - if(chunk->dcfEntry != NULL) { - eventUnsubscribe(&chunk->dcfEntry->onLoaded, chunkLoaded); - eventUnsubscribe(&chunk->dcfEntry->onError, chunkLoadError); - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; + if(chunk->dataEntry != NULL) { + eventUnsubscribe(&chunk->dataEntry->onLoaded, chunkLoaded); + eventUnsubscribe(&chunk->dataEntry->onError, chunkLoadError); + assetUnlockEntry(chunk->dataEntry); + chunk->dataEntry = NULL; } memorySet(chunk->entities, 0xFF, sizeof(chunk->entities)); @@ -160,7 +160,7 @@ errorret_t chunkLoad(chunk_t *chunk) { char_t path[MAP_FILE_PATH_MAX + 64]; stringFormat( path, sizeof(path), - "map/%s/chunks/%d_%d_%d.dcf", + "map/%s/chunks/%d_%d_%d.json", MAP.name, (int32_t)chunk->position.x, (int32_t)chunk->position.y, @@ -197,7 +197,7 @@ void chunkLoadNext(void) { char_t path[MAP_FILE_PATH_MAX + 64]; stringFormat( path, sizeof(path), - "map/%s/chunks/%d_%d_%d.dcf", + "map/%s/chunks/%d_%d_%d.json", MAP.name, (int32_t)chunk->position.x, (int32_t)chunk->position.y, @@ -206,7 +206,7 @@ void chunkLoadNext(void) { assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CHUNK, NULL); assertNotNull(entry, "Failed to get chunk asset entry"); - chunk->dcfEntry = entry; + chunk->dataEntry = 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 @@ -323,7 +323,7 @@ void chunkLoadError(void *params, void *user) { assertNotNull(user, "chunkLoadError: user cannot be NULL"); assetentry_t *entry = (assetentry_t *)params; chunk_t *chunk = (chunk_t *)user; - if(chunk->dcfEntry != entry) return; + if(chunk->dataEntry != entry) return; consolePrint( "Chunk load error: %d %d %d", (int32_t)chunk->position.x, @@ -332,8 +332,8 @@ void chunkLoadError(void *params, void *user) { ); eventUnsubscribe(&entry->onLoaded, chunkLoaded); eventUnsubscribe(&entry->onError, chunkLoadError); - assetUnlockEntry(chunk->dcfEntry); - chunk->dcfEntry = NULL; + assetUnlockEntry(chunk->dataEntry); + chunk->dataEntry = NULL; memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles)); if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL; @@ -345,7 +345,7 @@ void chunkLoaded(void *params, void *user) { assertNotNull(user, "chunkLoaded: user cannot be NULL"); assetentry_t *entry = (assetentry_t *)params; chunk_t *chunk = (chunk_t *)user; - if(chunk->dcfEntry != entry) return; + if(chunk->dataEntry != entry) return; uint8_t meshCount = entry->data.chunk.meshCount; memoryCopy( @@ -387,7 +387,7 @@ void chunkLoaded(void *params, void *user) { } eventUnsubscribe(&entry->onLoaded, chunkLoaded); eventUnsubscribe(&entry->onError, chunkLoadError); - // Deliberately keep chunk->dcfEntry locked and set - it is what keeps the + // Deliberately keep chunk->dataEntry locked and set - it is what keeps the // chunk asset entry (and therefore its model locks) alive for as long as // this chunk_t is displaying it. Released in chunkUnload instead. chunk->meshCount = meshCount; diff --git a/src/dusk/rpg/overworld/chunk.h b/src/dusk/rpg/overworld/chunk.h index 98107efd..88cf9140 100644 --- a/src/dusk/rpg/overworld/chunk.h +++ b/src/dusk/rpg/overworld/chunk.h @@ -19,7 +19,7 @@ typedef struct chunk_s { chunkpos_t position; tile_t tiles[CHUNK_TILE_COUNT]; - assetentry_t *dcfEntry; + assetentry_t *dataEntry; uint8_t meshCount; char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX]; @@ -113,7 +113,7 @@ void chunkLoadNext(void); void chunkLoadQueueRemove(chunk_t *chunk); /** - * Callback invoked when a chunk DCF asset fails to load. Fills the + * Callback invoked when a chunk JSON asset fails to load. Fills the * chunk tiles with TILE_SHAPE_GROUND as a fallback. * Always invoked on the main thread. * @@ -123,7 +123,7 @@ void chunkLoadQueueRemove(chunk_t *chunk); void chunkLoadError(void *params, void *user); /** - * Callback invoked when a chunk DCF asset finishes loading. + * Callback invoked when a chunk JSON asset finishes loading. * Always invoked on the main thread. * * @param params The loaded assetentry_t. diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index 133f914a..0de15802 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -17,7 +17,6 @@ #include "util/string.h" #include "rpg/overworld/chunk.h" #include "rpg/entity/entity.h" -#include "rpg/entity/global/entityglobal.h" #include "yyjson.h" map_t MAP; @@ -93,48 +92,6 @@ errorret_t mapDispose() { errorOk(); } -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 mapDefLoadError(void *params, void *user) { assertNotNull(params, "mapDefLoadError: params cannot be NULL"); assetentry_t *entry = (assetentry_t *)params; diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h index 7e6f684e..ea15fd74 100644 --- a/src/dusk/rpg/overworld/map.h +++ b/src/dusk/rpg/overworld/map.h @@ -7,7 +7,6 @@ #pragma once #include "error/error.h" -#include "rpg/entity/entity.h" #define MAP_FILE_PATH_MAX 32 #define MAP_DISPLAY_NAME_MAX 64 @@ -92,18 +91,3 @@ void mapDefLoadError(void *params, void *user); * @param user Unused. */ void mapDefLoaded(void *params, void *user); - -/** - * 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 -); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index 85896907..035a61bb 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -7,12 +7,10 @@ #include "rpg.h" #include "entity/entity.h" -#include "rpg/entity/npc/npcpath.h" #include "rpg/overworld/map.h" #include "rpg/overworld/chunk.h" #include "rpg/overworld/maparea.h" #include "rpg/cutscene/cutscenesystem.h" -#include "rpg/cutscene/scene/testcutscene.h" #include "rpg/item/backpack.h" #include "rpg/battle/party.h" #include "ui/rpg/textbox/uitextboxminilist.h" @@ -43,10 +41,6 @@ errorret_t rpgInit(void) { // Init world errorChain(chunkPositionSet((chunkpos_t){ 0, 0, 0 })); - // Player is defined in the map's JSON (see entityCreateFromJson) and - // spawns asynchronously; rpgCameraUpdate picks it up once it appears. - mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 }); - // TEST: Give the player a starting assortment of items. backpackAdd(itemGetIdByName("POTION"), 5); backpackAdd(itemGetIdByName("POTATO"), 3); diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py index d34508ed..0afb3b4b 100644 --- a/tools/asset/chunk/__main__.py +++ b/tools/asset/chunk/__main__.py @@ -4,8 +4,7 @@ # 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. +Generates chunk JSON + companion DMF mesh files from raw chunk JSON files. JSON input (assetsraw//chunks/chunk_X_Y_Z.json, one subdirectory per map - e.g. assetsraw/overworld/chunks/): @@ -21,22 +20,28 @@ map - e.g. assetsraw/overworld/chunks/): 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 + chunk JSON 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. + path from the assets root in the output chunk JSON. - Output DCF is derived automatically, preserving the map subdirectory: - assetsraw//chunks/chunk_X_Y_Z.json -> assets//chunks/X_Y_Z.dcf + Output chunk JSON is derived automatically, preserving the map + subdirectory: + assetsraw//chunks/chunk_X_Y_Z.json -> assets//chunks/X_Y_Z.json -Version 4 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) +Chunk JSON format (loaded at runtime by assetChunkLoaderSync): + { + "tiles": [ [shape, z], ... ] // exactly CHUNK_WIDTH * CHUNK_HEIGHT + // entries, one per (x, y) column, + // x-major (matches tile_t: { shape, z }) + "meshes": [ + { "model": "", + "offset": [x, y, z] } + ] + } + By convention mesh index 0 (when present) is the chunk's auto-generated + terrain and the rest are props (see sceneOverworldDrawChunksBase/Props + on the C side). DMF format: Bytes 0-3: DMF\\x00 @@ -48,7 +53,6 @@ DMF format: Usage: python3 -m tools.asset.chunk # process all assetsraw/*/chunks/*.json python3 -m tools.asset.chunk # process one JSON file - python3 -m tools.asset.chunk # upgrade legacy (v1/v2) DCF in-place """ import json @@ -76,20 +80,13 @@ WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2) CHUNK_MESH_COUNT_MAX = 10 CHUNK_MESH_NAME_MAX = 64 -# 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). +# Internal working format for a column's (shape, z) pair, packed/unpacked +# with struct purely so build_terrain_verts can index into it uniformly - +# unrelated to the on-disk format, which is JSON. TILE_STRUCT_FORMAT = '/chunks/chunk_X_Y_Z.json -> assets//chunks/X_Y_Z.dcf""" +def derive_chunk_json_path(json_path): + """assetsraw//chunks/chunk_X_Y_Z.json -> assets//chunks/X_Y_Z.json""" 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') + return os.path.join(ASSETS_DIR, rel_dir, base + '.json') def write_dmf(path, vertex_bytes): @@ -164,36 +159,36 @@ def write_dmf(path, vertex_bytes): print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes') -def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None): - """Write a current-version DCF referencing the given DMF asset paths.""" +def write_chunk_json(chunk_json_path, tiles, mesh_names, mesh_offsets=None): + """Write a chunk JSON file referencing the given model asset paths.""" mesh_count = len(mesh_names) if mesh_offsets is None: mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count - buf = bytearray() - buf += FILE_MAGIC - buf += b'\x00' - buf += struct.pack('= CHUNK_MESH_NAME_MAX: + if len(name) >= 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]) - with open(dcf_path, 'wb') as f: - f.write(buf) + meshes.append({'model': name, 'offset': list(offset)}) + + obj = {'tiles': tile_list, 'meshes': meshes} + os.makedirs(os.path.dirname(chunk_json_path), exist_ok=True) + with open(chunk_json_path, 'w') as f: + json.dump(obj, f) print( - f' Wrote DCF {dcf_path}: ' - f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes' + f' Wrote chunk JSON {chunk_json_path}: {mesh_count} mesh(es)' ) # --------------------------------------------------------------------------- -# JSON -> DCF + DMF +# JSON -> chunk JSON + DMF # --------------------------------------------------------------------------- def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z): @@ -264,7 +259,7 @@ def build_terrain_verts(tiles_bytes): return bytes(buf) -def from_json(json_path, dcf_path): +def from_json(json_path, chunk_json_path): with open(json_path) as f: data = json.load(f) @@ -289,7 +284,7 @@ def from_json(json_path, dcf_path): model_names = [] mesh_offsets = [] - dcf_base = os.path.splitext(os.path.basename(dcf_path))[0] + chunk_base = os.path.splitext(os.path.basename(chunk_json_path))[0] # Terrain mesh + model (only written if non-empty) terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks') @@ -297,10 +292,10 @@ def from_json(json_path, dcf_path): os.makedirs(terrain_dir, exist_ok=True) os.makedirs(models_dir, exist_ok=True) if terrain_verts: - dmf_name = f'chunk_{dcf_base}_0.dmf' + dmf_name = f'chunk_{chunk_base}_0.dmf' write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts) mesh_rel = f'meshes/chunks/{dmf_name}' - json_name = f'chunk_{dcf_base}_0.json' + json_name = f'chunk_{chunk_base}_0.json' write_model_json( os.path.join(models_dir, json_name), mesh_rel, @@ -324,105 +319,14 @@ def from_json(json_path, dcf_path): mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2]))) print(f' Resolved {filename} -> {rel}') - write_dcf(dcf_path, bytes(tiles), model_names, mesh_offsets) + write_chunk_json(chunk_json_path, bytes(tiles), model_names, mesh_offsets) 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) + chunk_json_path = derive_chunk_json_path(json_path) + os.makedirs(os.path.dirname(chunk_json_path), exist_ok=True) + print(f"{json_path} -> {chunk_json_path}") + from_json(json_path, chunk_json_path) # --------------------------------------------------------------------------- @@ -448,10 +352,10 @@ def main(): return src = args[0] - if os.path.splitext(src)[1].lower() == '.json': - process_json(src) - else: - upgrade_dcf(src) + if os.path.splitext(src)[1].lower() != '.json': + print(f"Expected a .json input file, got: {src}") + sys.exit(1) + process_json(src) if __name__ == '__main__':