Chunks as json

This commit is contained in:
2026-07-11 17:43:26 -05:00
parent 60dfb89b53
commit 9551e222dc
39 changed files with 1949 additions and 13252 deletions
+25
View File
@@ -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." }
]
}
Binary file not shown.
+1
View File
@@ -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]}]}
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+1
View File
@@ -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]}]}
Binary file not shown.
+1
View File
@@ -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], [1, 0], [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], [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], [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], [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], [0, 0], [0, 0], [0, 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], [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]], "meshes": [{"model": "models/chunks/chunk_1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_3_2.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [7.0, 2.0, 0.0]}, {"model": "models/buildings/house_4_2.json", "offset": [1.0, 8.0, 0.0]}, {"model": "models/buildings/house_1_1.json", "offset": [11.0, 10.0, 0.0]}, {"model": "models/buildings/house_5_2.json", "offset": [13.0, 6.0, 0.0]}]}
Binary file not shown.
+1
View File
@@ -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]}]}
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_2_0_0_0.json", "offset": [0.0, 0.0, 0.0]}]}
+7 -1
View File
@@ -2,6 +2,12 @@
"name": "Test Map",
"entities": [
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 }
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 },
{
"type": "npc",
"position": [8, 8, 1],
"path": [[4, 4, 0], [10, 10, 1], [4, 4, 0], [10, 10, 1]],
"cutscene": "test_npc"
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+136 -97
View File
@@ -8,40 +8,17 @@
#include "assetchunkloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "util/string.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/asset.h"
#include "yyjson.h"
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->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) {
+21 -14
View File
@@ -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.
+1
View File
@@ -6,6 +6,7 @@
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscene.c
cutscenesystem.c
)
+444
View File
@@ -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();
}
+117
View File
@@ -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/<name>.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
);
@@ -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."),
);
+1 -2
View File
@@ -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)
add_subdirectory(item)
+58 -1
View File
@@ -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();
+12 -1
View File
@@ -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);
@@ -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
)
-17
View File
@@ -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
@@ -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
@@ -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
+18 -18
View File
@@ -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;
+3 -3
View File
@@ -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.
-43
View File
@@ -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;
-16
View File
@@ -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
);
-6
View File
@@ -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);
+55 -151
View File
@@ -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/<map>/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/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/chunks/X_Y_Z.dcf
Output chunk JSON is derived automatically, preserving the map
subdirectory:
assetsraw/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/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": "<path to .json model, relative to assets/>",
"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 <file.json> # process one JSON file
python3 -m tools.asset.chunk <file.dcf> # 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 = '<IB3x'
TILE_SIZE = struct.calcsize(TILE_STRUCT_FORMAT)
VERTEX_SIZE = 20
# Legacy (v1/v2) DCFs stored one uint32 shape per (x, y, z) triple across
# the full 3D grid, at whatever CHUNK_DEPTH was in effect when they were
# written (4). Frozen here, independent of the live CHUNK_DEPTH above, so
# a future depth change can't corrupt parsing of genuinely old files.
_LEGACY_CHUNK_DEPTH = 4
_LEGACY_TILE_SIZE = 4
_LEGACY_CHUNK_TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * _LEGACY_CHUNK_DEPTH
TILE_SHAPE_NULL = 0
TILE_SHAPE_GROUND = 1
TILE_SHAPE_RAMP_NORTH = 2
@@ -105,9 +102,7 @@ TILE_SHAPE_RAMP_NORTHWEST_INNER = 11
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
FILE_MAGIC = b'DCF'
DMF_MAGIC = b'DMF\x00'
VERSION_OUT = 4
DMF_VERSION = 1
@@ -143,13 +138,13 @@ def write_model_json(path, mesh_rel, texture_rel, color):
print(f' Wrote model JSON {path}')
def derive_dcf_path(json_path):
"""assetsraw/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/chunks/X_Y_Z.dcf"""
def derive_chunk_json_path(json_path):
"""assetsraw/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/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('<I', VERSION_OUT)
buf += tiles
buf += struct.pack('<B', mesh_count)
tile_list = []
for i in range(CHUNK_TILE_COUNT):
shape, z = struct.unpack_from(TILE_STRUCT_FORMAT, tiles, i * TILE_SIZE)
tile_list.append([shape, z])
meshes = []
for name, offset in zip(mesh_names, mesh_offsets):
encoded = name.encode('ascii')
if len(encoded) >= 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(
'<I', legacy_tiles, legacy_idx * _LEGACY_TILE_SIZE
)[0]
if shape != TILE_SHAPE_NULL:
found.append((z, shape))
if not found:
continue
if len(found) > 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('<I', data, 4)[0]
if version not in (1, 2):
raise ValueError(f"{path}: expected version 1 or 2, got {version}")
offset = 8
tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
offset += tiles_size
meshes = []
if version == 1:
vert_count = struct.unpack_from('<I', data, offset)[0]
offset += 4
verts = data[offset:offset + vert_count * VERTEX_SIZE]
if len(verts) != vert_count * VERTEX_SIZE:
raise ValueError(f"{path}: truncated vertex data")
if vert_count > 0:
meshes.append(verts)
else:
mesh_count = data[offset]
offset += 1
for _ in range(mesh_count):
vert_count = struct.unpack_from('<I', data, offset)[0]
offset += 4
verts = data[offset:offset + vert_count * VERTEX_SIZE]
if len(verts) != vert_count * VERTEX_SIZE:
raise ValueError(f"{path}: truncated vertex data")
offset += vert_count * VERTEX_SIZE
if vert_count > 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__':