Compare commits
3 Commits
we-ball
...
item-as-file
| Author | SHA1 | Date | |
|---|---|---|---|
| 9551e222dc | |||
| 60dfb89b53 | |||
| b08ad308e4 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,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." }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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]}]}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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]}]}
|
||||||
@@ -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]}]}
|
||||||
@@ -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]}]}
|
||||||
@@ -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]}]}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "Test Map",
|
||||||
|
"entities": [
|
||||||
|
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
|
||||||
|
{ "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
@@ -8,40 +8,17 @@
|
|||||||
#include "assetchunkloader.h"
|
#include "assetchunkloader.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/endian.h"
|
#include "util/string.h"
|
||||||
#include "asset/loader/assetloading.h"
|
#include "asset/loader/assetloading.h"
|
||||||
#include "asset/loader/assetentry.h"
|
#include "asset/loader/assetentry.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
assertNotMainThread("Should be called from an async thread.");
|
||||||
|
|
||||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +31,141 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
|
|
||||||
switch(loading->loading.chunk.state) {
|
switch(loading->loading.chunk.state) {
|
||||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_JSON;
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: {
|
||||||
break;
|
// 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:
|
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
||||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
||||||
@@ -91,73 +197,6 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
default:
|
default:
|
||||||
errorOk();
|
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) {
|
errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
#include "asset/assetfile.h"
|
#include "asset/assetfile.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
#define ASSET_CHUNK_FILE_VERSION 4
|
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
@@ -20,16 +18,12 @@ typedef struct {
|
|||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
ASSET_CHUNK_LOADING_STATE_LOAD_JSON,
|
||||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS
|
||||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_DONE
|
|
||||||
} assetchunkloadingstate_t;
|
} assetchunkloadingstate_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
assetfile_t file;
|
|
||||||
assetchunkloadingstate_t state;
|
assetchunkloadingstate_t state;
|
||||||
uint8_t *data;
|
|
||||||
uint8_t modelIndex;
|
uint8_t modelIndex;
|
||||||
} assetchunkloaderloading_t;
|
} assetchunkloaderloading_t;
|
||||||
|
|
||||||
@@ -42,9 +36,9 @@ typedef struct {
|
|||||||
} assetchunkoutput_t;
|
} assetchunkoutput_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
* Asynchronous loader for chunk assets. No-op - the chunk's JSON file is
|
||||||
* the loading buffer so the sync phase can parse without blocking the
|
* loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync),
|
||||||
* main thread on I/O.
|
* which handles its own async file I/O.
|
||||||
*
|
*
|
||||||
* @param loading Loading information for the asset being loaded.
|
* @param loading Loading information for the asset being loaded.
|
||||||
* @return Error code indicating success or failure of the load operation.
|
* @return Error code indicating success or failure of the load operation.
|
||||||
@@ -52,9 +46,22 @@ typedef struct {
|
|||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
* Synchronous loader for chunk assets. Locks and parses the chunk's JSON
|
||||||
* read by the async phase and populates the output assetchunkoutput_t with
|
* file (tiles plus referenced model paths/offsets), then locks each
|
||||||
* tile data and model paths.
|
* 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.
|
* @param loading Loading information for the asset being loaded.
|
||||||
* @return Error code indicating success or failure of the load operation.
|
* @return Error code indicating success or failure of the load operation.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
|
#include "rpg/item/item.h"
|
||||||
#include "rpg/rpg.h"
|
#include "rpg/rpg.h"
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
@@ -39,6 +40,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
// errorChain(saveInit());
|
// errorChain(saveInit());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
|
errorChain(itemInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
errorChain(rpgInit());
|
errorChain(rpgInit());
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
cutscene.c
|
||||||
cutscenesystem.c
|
cutscenesystem.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();
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
#include "rpg/cutscene/cutscenepause.h"
|
#include "rpg/cutscene/cutscenepause.h"
|
||||||
|
|
||||||
|
typedef struct yyjson_val yyjson_val;
|
||||||
|
|
||||||
typedef struct cutscene_s {
|
typedef struct cutscene_s {
|
||||||
const cutsceneitem_t *items;
|
const cutsceneitem_t *items;
|
||||||
uint8_t itemCount;
|
uint8_t itemCount;
|
||||||
@@ -165,10 +167,10 @@ typedef struct cutscene_s {
|
|||||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||||
|
|
||||||
#define CUTSCENE_ITEM_GIVE(ITEM_ID, QUANTITY) \
|
#define CUTSCENE_ITEM_GIVE(ITEM_NAME, QUANTITY) \
|
||||||
{ \
|
{ \
|
||||||
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
|
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
|
||||||
.itemGive = { .item = ITEM_ID, .quantity = QUANTITY } \
|
.itemGive = { .itemName = ITEM_NAME, .quantity = QUANTITY } \
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs all listed items simultaneously and waits until all are done.
|
// Runs all listed items simultaneously and waits until all are done.
|
||||||
@@ -230,3 +232,118 @@ typedef struct cutscene_s {
|
|||||||
), \
|
), \
|
||||||
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
|
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
|
||||||
CUTSCENE_MAP_AREA_REMOVE(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
|
||||||
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
#include "rpg/entity/entitypathstep.h"
|
#include "rpg/entity/entitypathstep.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
void cutsceneEntityWalkToEntityStart(
|
void cutsceneEntityWalkToEntityStart(
|
||||||
const cutsceneitem_t *item,
|
const cutsceneitem_t *item,
|
||||||
@@ -35,7 +35,7 @@ bool_t cutsceneEntityWalkToEntityUpdate(
|
|||||||
};
|
};
|
||||||
|
|
||||||
worldunit_t z;
|
worldunit_t z;
|
||||||
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
if(chunkGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
||||||
|
|
||||||
return entityPathStep(entity, dest, true);
|
return entityPathStep(entity, dest, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/item/item.h"
|
||||||
#include "rpg/item/itemgive.h"
|
#include "rpg/item/itemgive.h"
|
||||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ void cutsceneItemGiveStart(
|
|||||||
const cutsceneitem_t *item,
|
const cutsceneitem_t *item,
|
||||||
cutsceneitemdata_t *data
|
cutsceneitemdata_t *data
|
||||||
) {
|
) {
|
||||||
itemGive(item->itemGive.item, item->itemGive.quantity);
|
itemid_t itemId = itemGetIdByName(item->itemGive.itemName);
|
||||||
|
itemGive(itemId, item->itemGive.quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t cutsceneItemGiveUpdate(
|
bool_t cutsceneItemGiveUpdate(
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "rpg/item/item.h"
|
#include "dusk.h"
|
||||||
|
|
||||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
itemid_t item;
|
const char_t *itemName;
|
||||||
uint8_t quantity;
|
uint8_t quantity;
|
||||||
} cutsceneitemgive_t;
|
} cutsceneitemgive_t;
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,3 @@
|
|||||||
CUTSCENE(TEST_ONE, 0, DEFAULT,
|
CUTSCENE(TEST_ONE, 0, DEFAULT,
|
||||||
CUTSCENE_TEXT("Test One."),
|
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(ITEM_ID_POTATO, 3),
|
|
||||||
// CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
|
|
||||||
CUTSCENE_TEXT("Done."),
|
|
||||||
);
|
|
||||||
@@ -16,4 +16,3 @@ add_subdirectory(anim)
|
|||||||
add_subdirectory(interact)
|
add_subdirectory(interact)
|
||||||
add_subdirectory(npc)
|
add_subdirectory(npc)
|
||||||
add_subdirectory(item)
|
add_subdirectory(item)
|
||||||
add_subdirectory(global)
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "rpg/overworld/tile.h"
|
#include "rpg/overworld/tile.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "entityanimwalk.h"
|
#include "entityanimwalk.h"
|
||||||
@@ -19,7 +19,7 @@ const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
float_t entityAnimTileZOffset(const worldpos_t pos) {
|
float_t entityAnimTileZOffset(const worldpos_t pos) {
|
||||||
return tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
|
return tileShapeIsRamp(chunkGetTile(pos).shape) ? 0.5f : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityAnimUpdate(entity_t *entity) {
|
void entityAnimUpdate(entity_t *entity) {
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
#include "entity.h"
|
#include "entity.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "rpg/overworld/map.h"
|
|
||||||
#include "rpg/overworld/maparea.h"
|
#include "rpg/overworld/maparea.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "rpg/overworld/tile.h"
|
#include "rpg/overworld/tile.h"
|
||||||
|
#include "rpg/cutscene/cutscene.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
entity_t ENTITIES[ENTITY_COUNT];
|
entity_t ENTITIES[ENTITY_COUNT];
|
||||||
|
|
||||||
@@ -88,8 +90,8 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get tile under foot
|
// Get tile under foot
|
||||||
tile_t tileCurrent = mapGetTile(entity->position);
|
tile_t tileCurrent = chunkGetTile(entity->position);
|
||||||
tile_t tileNew = mapGetTile(newPos);
|
tile_t tileNew = chunkGetTile(newPos);
|
||||||
bool_t fall = false;
|
bool_t fall = false;
|
||||||
bool_t raise = false;
|
bool_t raise = false;
|
||||||
|
|
||||||
@@ -138,7 +140,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
|||||||
tileNew = TILE_NULL;
|
tileNew = TILE_NULL;
|
||||||
worldpos_t abovePos = newPos;
|
worldpos_t abovePos = newPos;
|
||||||
abovePos.z += 1;
|
abovePos.z += 1;
|
||||||
tile_t tileAbove = mapGetTile(abovePos);
|
tile_t tileAbove = chunkGetTile(abovePos);
|
||||||
|
|
||||||
if(
|
if(
|
||||||
tileAbove.shape != TILE_SHAPE_NULL &&
|
tileAbove.shape != TILE_SHAPE_NULL &&
|
||||||
@@ -152,7 +154,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
|||||||
// Falling down?
|
// Falling down?
|
||||||
worldpos_t belowPos = newPos;
|
worldpos_t belowPos = newPos;
|
||||||
belowPos.z -= 1;
|
belowPos.z -= 1;
|
||||||
tile_t tileBelow = mapGetTile(belowPos);
|
tile_t tileBelow = chunkGetTile(belowPos);
|
||||||
if(
|
if(
|
||||||
tileBelow.shape != TILE_SHAPE_NULL &&
|
tileBelow.shape != TILE_SHAPE_NULL &&
|
||||||
tileShapeIsRamp(tileBelow.shape) &&
|
tileShapeIsRamp(tileBelow.shape) &&
|
||||||
@@ -282,7 +284,7 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
|||||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||||
|
|
||||||
if(entity->chunkIndex != 0xFF) {
|
if(entity->chunkIndex != 0xFF) {
|
||||||
chunk_t *old = mapGetChunk(entity->chunkIndex);
|
chunk_t *old = chunkGet(entity->chunkIndex);
|
||||||
if(old != NULL) {
|
if(old != NULL) {
|
||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
if(old->entities[i] != entity->id) continue;
|
if(old->entities[i] != entity->id) continue;
|
||||||
@@ -295,7 +297,7 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
|||||||
entity->chunkIndex = chunkIndex;
|
entity->chunkIndex = chunkIndex;
|
||||||
|
|
||||||
if(chunkIndex != 0xFF) {
|
if(chunkIndex != 0xFF) {
|
||||||
chunk_t *next = mapGetChunk(chunkIndex);
|
chunk_t *next = chunkGet(chunkIndex);
|
||||||
if(next != NULL) {
|
if(next != NULL) {
|
||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
if(next->entities[i] != 0xFF) continue;
|
if(next->entities[i] != 0xFF) continue;
|
||||||
@@ -311,6 +313,159 @@ void entityUpdateChunk(entity_t *entity) {
|
|||||||
|
|
||||||
chunkpos_t cp;
|
chunkpos_t cp;
|
||||||
worldPosToChunkPos(&entity->position, &cp);
|
worldPosToChunkPos(&entity->position, &cp);
|
||||||
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
chunkindex_t ci = chunkGetIndexAt(cp);
|
||||||
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity) {
|
||||||
|
assertNotNull(obj, "Entity JSON object cannot be NULL");
|
||||||
|
assertNotNull(outEntity, "Output entity pointer cannot be NULL");
|
||||||
|
|
||||||
|
yyjson_val *typeVal = yyjson_obj_get(obj, "type");
|
||||||
|
if(!typeVal || !yyjson_is_str(typeVal)) {
|
||||||
|
errorThrow("Entity JSON missing 'type' string");
|
||||||
|
}
|
||||||
|
const char_t *typeStr = yyjson_get_str(typeVal);
|
||||||
|
|
||||||
|
entitytype_t type;
|
||||||
|
if(stringEquals(typeStr, "player")) {
|
||||||
|
type = ENTITY_TYPE_PLAYER;
|
||||||
|
} else if(stringEquals(typeStr, "npc")) {
|
||||||
|
type = ENTITY_TYPE_NPC;
|
||||||
|
} else if(stringEquals(typeStr, "item")) {
|
||||||
|
type = ENTITY_TYPE_ITEM;
|
||||||
|
} else {
|
||||||
|
errorThrow("Entity JSON has unknown 'type': %s", typeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *posVal = yyjson_obj_get(obj, "position");
|
||||||
|
if(!posVal || !yyjson_is_arr(posVal) || yyjson_arr_size(posVal) != 3) {
|
||||||
|
errorThrow("Entity JSON missing 'position' [x, y, z] array");
|
||||||
|
}
|
||||||
|
worldunit_t pos[3];
|
||||||
|
size_t posIdx, posLen;
|
||||||
|
yyjson_val *posElem;
|
||||||
|
yyjson_arr_foreach(posVal, posIdx, posLen, posElem) {
|
||||||
|
if(!yyjson_is_num(posElem)) {
|
||||||
|
errorThrow("Entity JSON 'position' elements must be numbers");
|
||||||
|
}
|
||||||
|
pos[posIdx] = (worldunit_t)yyjson_get_num(posElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
entitydir_t direction = ENTITY_DIR_SOUTH;
|
||||||
|
yyjson_val *dirVal = yyjson_obj_get(obj, "direction");
|
||||||
|
if(dirVal && yyjson_is_str(dirVal)) {
|
||||||
|
const char_t *dirStr = yyjson_get_str(dirVal);
|
||||||
|
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("Entity JSON has unknown 'direction': %s", dirStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item entities require a valid item reference, resolved up front so a
|
||||||
|
// bad reference fails before an entity slot is ever claimed.
|
||||||
|
itemid_t itemId = ITEM_ID_NULL;
|
||||||
|
uint8_t itemQuantity = 1;
|
||||||
|
if(type == ENTITY_TYPE_ITEM) {
|
||||||
|
yyjson_val *itemVal = yyjson_obj_get(obj, "item");
|
||||||
|
if(!itemVal || !yyjson_is_str(itemVal)) {
|
||||||
|
errorThrow("Entity JSON with type 'item' missing 'item' string");
|
||||||
|
}
|
||||||
|
const char_t *itemStr = yyjson_get_str(itemVal);
|
||||||
|
itemId = itemGetIdByName(itemStr);
|
||||||
|
if(itemId == ITEM_ID_NULL) {
|
||||||
|
errorThrow("Entity JSON references unknown item '%s'", itemStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *quantityVal = yyjson_obj_get(obj, "quantity");
|
||||||
|
if(quantityVal && yyjson_is_int(quantityVal)) {
|
||||||
|
itemQuantity = (uint8_t)yyjson_get_int(quantityVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only one player may exist at a time - it always holds the reserved
|
||||||
|
// ENTITY_GLOBAL_ID_PLAYER global ID, so that's how callers (e.g. the
|
||||||
|
// camera) find it regardless of how it was spawned.
|
||||||
|
if(
|
||||||
|
type == ENTITY_TYPE_PLAYER &&
|
||||||
|
entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER) != NULL
|
||||||
|
) {
|
||||||
|
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");
|
||||||
|
|
||||||
|
entity_t *entity = &ENTITIES[index];
|
||||||
|
entityInit(entity, type);
|
||||||
|
entity->direction = direction;
|
||||||
|
entityPositionSet(entity, (worldpos_t){ pos[0], pos[1], pos[2] });
|
||||||
|
if(type == ENTITY_TYPE_ITEM) entityItemSet(entity, itemId, itemQuantity);
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "npc/npc.h"
|
#include "npc/npc.h"
|
||||||
|
|
||||||
typedef struct map_s map_t;
|
typedef struct map_s map_t;
|
||||||
|
typedef struct yyjson_val yyjson_val;
|
||||||
|
|
||||||
typedef uint16_t entityglobalid_t;
|
typedef uint16_t entityglobalid_t;
|
||||||
|
|
||||||
@@ -165,3 +166,41 @@ void entityUpdateChunk(entity_t *entity);
|
|||||||
* @param pos The world position to place the entity at.
|
* @param pos The world position to place the entity at.
|
||||||
*/
|
*/
|
||||||
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
void entityPositionSet(entity_t *entity, const worldpos_t pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an entity descriptor from a yyjson object and spawns it as a new
|
||||||
|
* entity in an available slot. Expected shape:
|
||||||
|
* { "type": "npc", "position": [x, y, z], "direction": "south" }
|
||||||
|
* "type" must be one of "player", "npc", "item". "direction" is optional
|
||||||
|
* (one of "north", "east", "south", "west") and defaults to
|
||||||
|
* ENTITY_DIR_SOUTH when absent.
|
||||||
|
*
|
||||||
|
* When "type" is "item", two extra fields apply:
|
||||||
|
* { "type": "item", "position": [x, y, z], "item": "POTION",
|
||||||
|
* "quantity": 1 }
|
||||||
|
* "item" (required) is the item's string ID, resolved via
|
||||||
|
* itemGetIdByName. "quantity" (optional) defaults to 1.
|
||||||
|
*
|
||||||
|
* When "type" is "player", the spawned entity is assigned the reserved
|
||||||
|
* ENTITY_GLOBAL_ID_PLAYER global ID (so entityGetByGlobalId can find it
|
||||||
|
* 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, 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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
@@ -11,12 +11,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
backpack.c
|
backpack.c
|
||||||
itemgive.c
|
itemgive.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# Item Definitions
|
|
||||||
dusk_run_python(
|
|
||||||
dusk_item_json_defs
|
|
||||||
tools.item
|
|
||||||
--json ${CMAKE_CURRENT_SOURCE_DIR}/item.json
|
|
||||||
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/item/itemdef.h
|
|
||||||
)
|
|
||||||
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_item_json_defs)
|
|
||||||
@@ -11,69 +11,69 @@
|
|||||||
backpack_t BACKPACK;
|
backpack_t BACKPACK;
|
||||||
|
|
||||||
void backpackInit() {
|
void backpackInit() {
|
||||||
for(uint8_t i = 0; i < ITEM_TYPE_COUNT; i++) {
|
for(uint32_t i = 0; i < ITEM_TYPE_COUNT_MAX; i++) {
|
||||||
inventoryInit(
|
inventoryInit(
|
||||||
&BACKPACK.inventories[i],
|
&BACKPACK.inventories[i],
|
||||||
BACKPACK.storage[i],
|
BACKPACK.storage[i],
|
||||||
ITEM_TYPE_COUNT_MAX
|
INVENTORY_CAPACITY_MAX
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory_t *backpackGetInventory(const itemtype_t type) {
|
inventory_t *backpackGetInventory(const itemtypeid_t type) {
|
||||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||||
return &BACKPACK.inventories[type];
|
return &BACKPACK.inventories[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
void backpackAdd(const itemid_t item, const uint8_t quantity) {
|
void backpackAdd(const itemid_t item, const uint8_t quantity) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity);
|
inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
void backpackRemove(const itemid_t item) {
|
void backpackRemove(const itemid_t item) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
inventoryRemove(backpackGetInventory(ITEMS[item].type), item);
|
inventoryRemove(backpackGetInventory(ITEMS[item].type), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
void backpackSet(const itemid_t item, const uint8_t quantity) {
|
void backpackSet(const itemid_t item, const uint8_t quantity) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity);
|
inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t backpackGetCount(const itemid_t item) {
|
uint8_t backpackGetCount(const itemid_t item) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item);
|
return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t backpackItemExists(const itemid_t item) {
|
bool_t backpackItemExists(const itemid_t item) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item);
|
return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t backpackIsFull(const itemtype_t type) {
|
bool_t backpackIsFull(const itemtypeid_t type) {
|
||||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||||
return inventoryIsFull(backpackGetInventory(type));
|
return inventoryIsFull(backpackGetInventory(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t backpackItemFull(const itemid_t item) {
|
bool_t backpackItemFull(const itemid_t item) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item);
|
return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item);
|
||||||
}
|
}
|
||||||
|
|
||||||
void backpackSort(
|
void backpackSort(
|
||||||
const itemtype_t type,
|
const itemtypeid_t type,
|
||||||
const inventorysort_t sortBy,
|
const inventorysort_t sortBy,
|
||||||
const bool_t reverse
|
const bool_t reverse
|
||||||
) {
|
) {
|
||||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||||
inventorySort(backpackGetInventory(type), sortBy, reverse);
|
inventorySort(backpackGetInventory(type), sortBy, reverse);
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
#include "inventory.h"
|
#include "inventory.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inventorystack_t storage[ITEM_TYPE_COUNT][ITEM_TYPE_COUNT_MAX];
|
inventorystack_t storage[ITEM_TYPE_COUNT_MAX][INVENTORY_CAPACITY_MAX];
|
||||||
inventory_t inventories[ITEM_TYPE_COUNT];
|
inventory_t inventories[ITEM_TYPE_COUNT_MAX];
|
||||||
} backpack_t;
|
} backpack_t;
|
||||||
|
|
||||||
extern backpack_t BACKPACK;
|
extern backpack_t BACKPACK;
|
||||||
@@ -26,7 +26,7 @@ void backpackInit();
|
|||||||
* @param type The item type.
|
* @param type The item type.
|
||||||
* @returns Pointer to the inventory for that type.
|
* @returns Pointer to the inventory for that type.
|
||||||
*/
|
*/
|
||||||
inventory_t *backpackGetInventory(const itemtype_t type);
|
inventory_t *backpackGetInventory(const itemtypeid_t type);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a quantity of an item to the backpack.
|
* Adds a quantity of an item to the backpack.
|
||||||
@@ -73,7 +73,7 @@ bool_t backpackItemExists(const itemid_t item);
|
|||||||
* @param type The item type to check.
|
* @param type The item type to check.
|
||||||
* @returns true if the type's inventory is full.
|
* @returns true if the type's inventory is full.
|
||||||
*/
|
*/
|
||||||
bool_t backpackIsFull(const itemtype_t type);
|
bool_t backpackIsFull(const itemtypeid_t type);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an item's stack is full in the backpack.
|
* Checks if an item's stack is full in the backpack.
|
||||||
@@ -91,7 +91,7 @@ bool_t backpackItemFull(const itemid_t item);
|
|||||||
* @param reverse Whether to sort in reverse order.
|
* @param reverse Whether to sort in reverse order.
|
||||||
*/
|
*/
|
||||||
void backpackSort(
|
void backpackSort(
|
||||||
const itemtype_t type,
|
const itemtypeid_t type,
|
||||||
const inventorysort_t sortBy,
|
const inventorysort_t sortBy,
|
||||||
const bool_t reverse
|
const bool_t reverse
|
||||||
);
|
);
|
||||||
@@ -188,8 +188,8 @@ int_t inventorySortByIdReverse(const void *a, const void *b) {
|
|||||||
int_t inventorySortByType(const void *a, const void *b) {
|
int_t inventorySortByType(const void *a, const void *b) {
|
||||||
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
||||||
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
||||||
const itemtype_t typeA = ITEMS[stackA->item].type;
|
const itemtypeid_t typeA = ITEMS[stackA->item].type;
|
||||||
const itemtype_t typeB = ITEMS[stackB->item].type;
|
const itemtypeid_t typeB = ITEMS[stackB->item].type;
|
||||||
if(typeA < typeB) return -1;
|
if(typeA < typeB) return -1;
|
||||||
if(typeA > typeB) return 1;
|
if(typeA > typeB) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -198,8 +198,8 @@ int_t inventorySortByType(const void *a, const void *b) {
|
|||||||
int_t inventorySortByTypeReverse(const void *a, const void *b) {
|
int_t inventorySortByTypeReverse(const void *a, const void *b) {
|
||||||
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
||||||
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
||||||
const itemtype_t typeA = ITEMS[stackA->item].type;
|
const itemtypeid_t typeA = ITEMS[stackA->item].type;
|
||||||
const itemtype_t typeB = ITEMS[stackB->item].type;
|
const itemtypeid_t typeB = ITEMS[stackB->item].type;
|
||||||
if(typeA < typeB) return 1;
|
if(typeA < typeB) return 1;
|
||||||
if(typeA > typeB) return -1;
|
if(typeA > typeB) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "rpg/item/item.h"
|
#include "rpg/item/item.h"
|
||||||
|
|
||||||
#define ITEM_STACK_QUANTITY_MAX 99
|
#define ITEM_STACK_QUANTITY_MAX 99
|
||||||
|
#define INVENTORY_CAPACITY_MAX 250
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
INVENTORY_SORT_BY_ID,
|
INVENTORY_SORT_BY_ID,
|
||||||
|
|||||||
+132
-1
@@ -7,8 +7,125 @@
|
|||||||
|
|
||||||
#include "item.h"
|
#include "item.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
|
#define ITEM_JSON_PATH "item.json"
|
||||||
|
|
||||||
|
itemdef_t ITEMS[ITEM_COUNT_MAX];
|
||||||
|
uint32_t ITEM_COUNT;
|
||||||
|
itemtype_t ITEM_TYPES[ITEM_TYPE_COUNT_MAX];
|
||||||
|
uint32_t ITEM_TYPE_COUNT;
|
||||||
|
|
||||||
|
errorret_t itemInit(void) {
|
||||||
|
memoryZero(ITEMS, sizeof(ITEMS));
|
||||||
|
memoryZero(ITEM_TYPES, sizeof(ITEM_TYPES));
|
||||||
|
ITEM_COUNT = 0;
|
||||||
|
ITEM_TYPE_COUNT = 0;
|
||||||
|
|
||||||
|
assetentry_t *jsonEntry = assetLock(
|
||||||
|
ITEM_JSON_PATH, ASSET_LOADER_TYPE_JSON, NULL
|
||||||
|
);
|
||||||
|
errorret_t ret = assetRequireLoaded(jsonEntry);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorChain(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||||
|
if(!yyjson_is_arr(root)) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("item.json root must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *entry;
|
||||||
|
yyjson_arr_foreach(root, idx, max, entry) {
|
||||||
|
if(ITEM_COUNT >= ITEM_COUNT_MAX - 1) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow(
|
||||||
|
"Too many items defined: exceeds ITEM_COUNT_MAX (%d)",
|
||||||
|
ITEM_COUNT_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_val *idVal = yyjson_obj_get(entry, "id");
|
||||||
|
yyjson_val *typeVal = yyjson_obj_get(entry, "type");
|
||||||
|
yyjson_val *nameVal = yyjson_obj_get(entry, "name");
|
||||||
|
yyjson_val *weightVal = yyjson_obj_get(entry, "weight");
|
||||||
|
|
||||||
|
if(!idVal || !yyjson_is_str(idVal)) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("Item entry %zu missing 'id' string", idx);
|
||||||
|
}
|
||||||
|
if(!typeVal || !yyjson_is_str(typeVal)) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("Item entry %zu missing 'type' string", idx);
|
||||||
|
}
|
||||||
|
if(!nameVal || !yyjson_is_str(nameVal)) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("Item entry %zu missing 'name' string", idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *idStr = yyjson_get_str(idVal);
|
||||||
|
size_t idLen = yyjson_get_len(idVal);
|
||||||
|
const char_t *typeStr = yyjson_get_str(typeVal);
|
||||||
|
size_t typeLen = yyjson_get_len(typeVal);
|
||||||
|
const char_t *nameStr = yyjson_get_str(nameVal);
|
||||||
|
size_t nameLen = yyjson_get_len(nameVal);
|
||||||
|
|
||||||
|
if(idLen >= ITEM_STRING_MAX) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("Item id '%s' exceeds max length", idStr);
|
||||||
|
}
|
||||||
|
if(nameLen + 10 >= ITEM_STRING_MAX) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("Item name '%s' exceeds max length", nameStr);
|
||||||
|
}
|
||||||
|
if(typeLen >= ITEM_STRING_MAX) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow("Item type '%s' exceeds max length", typeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemtypeid_t typeId = itemResolveType(typeStr, typeLen);
|
||||||
|
if(typeId == ITEM_TYPE_NULL) {
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorThrow(
|
||||||
|
"Too many item types defined: exceeds ITEM_TYPE_COUNT_MAX (%d)",
|
||||||
|
ITEM_TYPE_COUNT_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ITEM_COUNT++;
|
||||||
|
itemid_t id = (itemid_t)ITEM_COUNT;
|
||||||
|
itemdef_t *def = &ITEMS[id];
|
||||||
|
def->id = id;
|
||||||
|
def->type = typeId;
|
||||||
|
def->weight = (weightVal && yyjson_is_num(weightVal)) ?
|
||||||
|
(float_t)yyjson_get_num(weightVal) : 0.0f;
|
||||||
|
memoryCopy(def->idName, idStr, idLen + 1);
|
||||||
|
stringFormat(def->name, ITEM_STRING_MAX - 1, "item.%s.name", nameStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
assetUnlockEntry(jsonEntry);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
itemid_t itemGetIdByName(const char_t *name) {
|
||||||
|
assertNotNull(name, "Item name cannot be NULL");
|
||||||
|
|
||||||
|
for(uint32_t i = 1; i <= ITEM_COUNT; i++) {
|
||||||
|
if(stringEquals(ITEMS[i].idName, name)) return (itemid_t)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ITEM_ID_NULL;
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t itemGetName(
|
errorret_t itemGetName(
|
||||||
const itemid_t item,
|
const itemid_t item,
|
||||||
@@ -16,7 +133,7 @@ errorret_t itemGetName(
|
|||||||
const size_t bufferSize
|
const size_t bufferSize
|
||||||
) {
|
) {
|
||||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||||
|
|
||||||
errorChain(assetLocaleGetString(
|
errorChain(assetLocaleGetString(
|
||||||
&LOCALE.entry->data.locale,
|
&LOCALE.entry->data.locale,
|
||||||
@@ -28,3 +145,17 @@ errorret_t itemGetName(
|
|||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemtypeid_t itemResolveType(const char_t *name, const size_t nameLen) {
|
||||||
|
for(uint32_t i = 1; i <= ITEM_TYPE_COUNT; i++) {
|
||||||
|
if(stringEquals(ITEM_TYPES[i].name, name)) return (itemtypeid_t)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ITEM_TYPE_COUNT >= ITEM_TYPE_COUNT_MAX - 1) return ITEM_TYPE_NULL;
|
||||||
|
|
||||||
|
ITEM_TYPE_COUNT++;
|
||||||
|
itemtypeid_t typeId = (itemtypeid_t)ITEM_TYPE_COUNT;
|
||||||
|
ITEM_TYPES[typeId].id = typeId;
|
||||||
|
memoryCopy(ITEM_TYPES[typeId].name, name, nameLen + 1);
|
||||||
|
return typeId;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,62 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "rpg/item/itemdef.h"
|
|
||||||
|
#define ITEM_COUNT_MAX 256
|
||||||
|
#define ITEM_TYPE_COUNT_MAX 16
|
||||||
|
#define ITEM_STRING_MAX 48
|
||||||
|
|
||||||
|
typedef uint16_t itemid_t;
|
||||||
|
typedef uint8_t itemtypeid_t;
|
||||||
|
|
||||||
|
#define ITEM_ID_NULL ((itemid_t)0)
|
||||||
|
#define ITEM_TYPE_NULL ((itemtypeid_t)0)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
itemid_t id;
|
||||||
|
itemtypeid_t type;
|
||||||
|
float_t weight;
|
||||||
|
char_t idName[ITEM_STRING_MAX];
|
||||||
|
char_t name[ITEM_STRING_MAX];
|
||||||
|
} itemdef_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
itemtypeid_t id;
|
||||||
|
char_t name[ITEM_STRING_MAX];
|
||||||
|
} itemtype_t;
|
||||||
|
|
||||||
|
extern itemdef_t ITEMS[ITEM_COUNT_MAX];
|
||||||
|
extern uint32_t ITEM_COUNT;
|
||||||
|
extern itemtype_t ITEM_TYPES[ITEM_TYPE_COUNT_MAX];
|
||||||
|
extern uint32_t ITEM_TYPE_COUNT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads assets/item.json and parses it into the ITEMS/ITEM_TYPES tables.
|
||||||
|
* Must be called once, before any other item/backpack function.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs (missing/malformed JSON, or too many
|
||||||
|
* items/types defined for ITEM_COUNT_MAX/ITEM_TYPE_COUNT_MAX).
|
||||||
|
*/
|
||||||
|
errorret_t itemInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up an item's numeric ID from its JSON "id" string.
|
||||||
|
*
|
||||||
|
* @param name The item's string ID (e.g. "POTION"), case-sensitive.
|
||||||
|
* @return The matching item ID, or ITEM_ID_NULL if not found.
|
||||||
|
*/
|
||||||
|
itemid_t itemGetIdByName(const char_t *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a type name string to its numeric type ID, registering it as
|
||||||
|
* a new type in ITEM_TYPES if not already known.
|
||||||
|
*
|
||||||
|
* @param name The type's string name (e.g. "MEDICINE").
|
||||||
|
* @param nameLen Length of name, excluding the null terminator.
|
||||||
|
* @return The resolved type ID, or ITEM_TYPE_NULL if ITEM_TYPE_COUNT_MAX
|
||||||
|
* would be exceeded.
|
||||||
|
*/
|
||||||
|
itemtypeid_t itemResolveType(const char_t *name, const size_t nameLen);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the localized display name for an item.
|
* Gets the localized display name for an item.
|
||||||
|
|||||||
@@ -6,6 +6,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "chunk.h"
|
#include "chunk.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 "console/console.h"
|
||||||
|
#include "event/event.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
#include "rpg/overworld/map.h"
|
||||||
|
|
||||||
|
chunk_t CHUNKS[MAP_CHUNK_COUNT];
|
||||||
|
chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT];
|
||||||
|
|
||||||
|
static chunkpos_t CHUNK_POSITION;
|
||||||
|
static chunk_t *CHUNK_LOAD_QUEUE[MAP_CHUNK_COUNT];
|
||||||
|
static uint32_t CHUNK_LOAD_QUEUE_COUNT;
|
||||||
|
static chunk_t *CHUNK_LOADING;
|
||||||
|
|
||||||
uint32_t chunkGetTileIndex(const chunkpos_t position) {
|
uint32_t chunkGetTileIndex(const chunkpos_t position) {
|
||||||
return (position.y * CHUNK_WIDTH) + position.x;
|
return (position.y * CHUNK_WIDTH) + position.x;
|
||||||
@@ -14,3 +32,366 @@ uint32_t chunkGetTileIndex(const chunkpos_t position) {
|
|||||||
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
|
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b) {
|
||||||
return (a.x == b.x) && (a.y == b.y) && (a.z == b.z);
|
return (a.x == b.x) && (a.y == b.y) && (a.z == b.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t chunksLoadGrid(void) {
|
||||||
|
CHUNK_POSITION = (chunkpos_t){ 0, 0, 0 };
|
||||||
|
|
||||||
|
chunkindex_t i = 0;
|
||||||
|
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||||
|
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||||
|
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||||
|
chunk_t *chunk = &CHUNKS[i++];
|
||||||
|
chunk->position = (chunkpos_t){
|
||||||
|
(chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
|
||||||
|
};
|
||||||
|
errorChain(chunkLoad(chunk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkRebuildOrder();
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunksUnloadAll(void) {
|
||||||
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
|
chunkUnload(&CHUNKS[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t chunkPositionSet(const chunkpos_t newPos) {
|
||||||
|
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||||
|
if(chunkPositionIsEqual(newPos, CHUNK_POSITION)) errorOk();
|
||||||
|
|
||||||
|
// Separate loaded chunks into "keep" and "free" buckets.
|
||||||
|
chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
|
||||||
|
uint32_t freedCount = 0;
|
||||||
|
|
||||||
|
// Use a boolean grid so the inner load loop can check O(1).
|
||||||
|
bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
|
||||||
|
memoryZero(posLoaded, sizeof(posLoaded));
|
||||||
|
|
||||||
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
|
chunk_t *chunk = &CHUNKS[i];
|
||||||
|
chunkunit_t rx = chunk->position.x - newPos.x;
|
||||||
|
chunkunit_t ry = chunk->position.y - newPos.y;
|
||||||
|
chunkunit_t rz = chunk->position.z - newPos.z;
|
||||||
|
if(
|
||||||
|
rx >= 0 && rx < MAP_CHUNK_WIDTH &&
|
||||||
|
ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
|
||||||
|
rz >= 0 && rz < MAP_CHUNK_DEPTH
|
||||||
|
) {
|
||||||
|
posLoaded[rx][ry][rz] = true;
|
||||||
|
} else {
|
||||||
|
chunkUnload(chunk);
|
||||||
|
chunksFreed[freedCount++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
||||||
|
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
||||||
|
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
||||||
|
if(posLoaded[x][y][z]) continue;
|
||||||
|
assertTrue(freedCount > 0, "No free chunk slot available.");
|
||||||
|
chunk_t *chunk = &CHUNKS[chunksFreed[--freedCount]];
|
||||||
|
chunk->position = (chunkpos_t){
|
||||||
|
newPos.x + (chunkunit_t)x,
|
||||||
|
newPos.y + (chunkunit_t)y,
|
||||||
|
newPos.z + (chunkunit_t)z
|
||||||
|
};
|
||||||
|
errorChain(chunkLoad(chunk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CHUNK_POSITION = newPos;
|
||||||
|
chunkRebuildOrder();
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunkUnload(chunk_t *chunk) {
|
||||||
|
chunkLoadQueueRemove(chunk);
|
||||||
|
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||||
|
if(chunk->entities[i] == 0xFF) continue;
|
||||||
|
entity_t *entity = &ENTITIES[chunk->entities[i]];
|
||||||
|
if(!entityCanUnload(entity)) {
|
||||||
|
entitySetChunk(entity, 0xFF);
|
||||||
|
} else {
|
||||||
|
entity->type = ENTITY_TYPE_NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
||||||
|
|
||||||
|
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
|
||||||
|
// chunk asset entry (released above) is what actually holds the ref on
|
||||||
|
// each model, so nothing to unlock here, just drop our own copies.
|
||||||
|
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
||||||
|
chunk->modelEntries[m] = NULL;
|
||||||
|
}
|
||||||
|
chunk->meshCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t chunkLoad(chunk_t *chunk) {
|
||||||
|
if(!mapIsLoaded()) errorThrow("No map loaded");
|
||||||
|
|
||||||
|
chunkLoadQueueRemove(chunk);
|
||||||
|
if(CHUNK_LOADING == chunk) CHUNK_LOADING = 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));
|
||||||
|
chunk->meshCount = 0;
|
||||||
|
|
||||||
|
char_t path[MAP_FILE_PATH_MAX + 64];
|
||||||
|
stringFormat(
|
||||||
|
path, sizeof(path),
|
||||||
|
"map/%s/chunks/%d_%d_%d.json",
|
||||||
|
MAP.name,
|
||||||
|
(int32_t)chunk->position.x,
|
||||||
|
(int32_t)chunk->position.y,
|
||||||
|
(int32_t)chunk->position.z
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!assetFileExists(path)) {
|
||||||
|
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||||
|
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
CHUNK_LOAD_QUEUE_COUNT < MAP_CHUNK_COUNT,
|
||||||
|
"Chunk load queue overflow"
|
||||||
|
);
|
||||||
|
CHUNK_LOAD_QUEUE[CHUNK_LOAD_QUEUE_COUNT++] = chunk;
|
||||||
|
chunkLoadNext();
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunkLoadNext(void) {
|
||||||
|
if(CHUNK_LOADING != NULL) return;
|
||||||
|
if(CHUNK_LOAD_QUEUE_COUNT == 0) return;
|
||||||
|
|
||||||
|
chunk_t *chunk = CHUNK_LOAD_QUEUE[0];
|
||||||
|
for(uint32_t i = 1; i < CHUNK_LOAD_QUEUE_COUNT; i++) {
|
||||||
|
CHUNK_LOAD_QUEUE[i - 1] = CHUNK_LOAD_QUEUE[i];
|
||||||
|
}
|
||||||
|
CHUNK_LOAD_QUEUE_COUNT--;
|
||||||
|
CHUNK_LOADING = chunk;
|
||||||
|
|
||||||
|
char_t path[MAP_FILE_PATH_MAX + 64];
|
||||||
|
stringFormat(
|
||||||
|
path, sizeof(path),
|
||||||
|
"map/%s/chunks/%d_%d_%d.json",
|
||||||
|
MAP.name,
|
||||||
|
(int32_t)chunk->position.x,
|
||||||
|
(int32_t)chunk->position.y,
|
||||||
|
(int32_t)chunk->position.z
|
||||||
|
);
|
||||||
|
|
||||||
|
assetentry_t *entry = assetLock(path, ASSET_LOADER_TYPE_CHUNK, NULL);
|
||||||
|
assertNotNull(entry, "Failed to get chunk asset 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
|
||||||
|
// will again, so handle the terminal state directly instead of waiting on
|
||||||
|
// a subscription that would never trigger.
|
||||||
|
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
|
chunkLoaded(entry, chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||||
|
chunkLoadError(entry, chunk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSubscribe(&entry->onLoaded, chunkLoaded, chunk);
|
||||||
|
eventSubscribe(&entry->onError, chunkLoadError, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunkLoadQueueRemove(chunk_t *chunk) {
|
||||||
|
for(uint32_t i = 0; i < CHUNK_LOAD_QUEUE_COUNT; i++) {
|
||||||
|
if(CHUNK_LOAD_QUEUE[i] != chunk) continue;
|
||||||
|
for(uint32_t j = i + 1; j < CHUNK_LOAD_QUEUE_COUNT; j++) {
|
||||||
|
CHUNK_LOAD_QUEUE[j - 1] = CHUNK_LOAD_QUEUE[j];
|
||||||
|
}
|
||||||
|
CHUNK_LOAD_QUEUE_COUNT--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkindex_t chunkGetIndexAt(const chunkpos_t position) {
|
||||||
|
if(!mapIsLoaded()) return -1;
|
||||||
|
|
||||||
|
chunkpos_t relPos = {
|
||||||
|
position.x - CHUNK_POSITION.x,
|
||||||
|
position.y - CHUNK_POSITION.y,
|
||||||
|
position.z - CHUNK_POSITION.z
|
||||||
|
};
|
||||||
|
|
||||||
|
if(
|
||||||
|
relPos.x < 0 || relPos.y < 0 || relPos.z < 0 ||
|
||||||
|
relPos.x >= MAP_CHUNK_WIDTH ||
|
||||||
|
relPos.y >= MAP_CHUNK_HEIGHT ||
|
||||||
|
relPos.z >= MAP_CHUNK_DEPTH
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunkPosToIndex(&relPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk_t *chunkGet(const uint8_t index) {
|
||||||
|
if(index >= MAP_CHUNK_COUNT) return NULL;
|
||||||
|
if(!mapIsLoaded()) return NULL;
|
||||||
|
return CHUNK_ORDER[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
tile_t chunkGetTile(const worldpos_t position) {
|
||||||
|
if(!mapIsLoaded()) return TILE_NULL;
|
||||||
|
|
||||||
|
chunkpos_t chunkPos;
|
||||||
|
worldPosToChunkPos(&position, &chunkPos);
|
||||||
|
chunkindex_t chunkIndex = chunkGetIndexAt(chunkPos);
|
||||||
|
if(chunkIndex == -1) return TILE_NULL;
|
||||||
|
|
||||||
|
chunk_t *chunk = chunkGet(chunkIndex);
|
||||||
|
assertNotNull(chunk, "Chunk pointer cannot be NULL");
|
||||||
|
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
|
||||||
|
tile_t tile = chunk->tiles[tileIndex];
|
||||||
|
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t chunkGetWalkableZNear(
|
||||||
|
const worldunit_t x,
|
||||||
|
const worldunit_t y,
|
||||||
|
const worldunit_t nearZ,
|
||||||
|
worldunit_t *outZ
|
||||||
|
) {
|
||||||
|
assertNotNull(outZ, "Output Z pointer cannot be NULL");
|
||||||
|
|
||||||
|
const worldunit_t candidates[] = {
|
||||||
|
nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
|
||||||
|
};
|
||||||
|
for(uint8_t i = 0; i < 3; i++) {
|
||||||
|
const worldpos_t pos = { x, y, candidates[i] };
|
||||||
|
if(!tileShapeIsWalkable(chunkGetTile(pos).shape)) continue;
|
||||||
|
*outZ = candidates[i];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunkRebuildOrder(void) {
|
||||||
|
memoryZero(CHUNK_ORDER, sizeof(CHUNK_ORDER));
|
||||||
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
|
chunk_t *chunk = &CHUNKS[i];
|
||||||
|
const chunkpos_t rel = {
|
||||||
|
chunk->position.x - CHUNK_POSITION.x,
|
||||||
|
chunk->position.y - CHUNK_POSITION.y,
|
||||||
|
chunk->position.z - CHUNK_POSITION.z
|
||||||
|
};
|
||||||
|
if(
|
||||||
|
rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH ||
|
||||||
|
rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT ||
|
||||||
|
rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH
|
||||||
|
) continue;
|
||||||
|
CHUNK_ORDER[chunkPosToIndex(&rel)] = chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunkLoadError(void *params, void *user) {
|
||||||
|
assertNotNull(params, "chunkLoadError: params cannot be NULL");
|
||||||
|
assertNotNull(user, "chunkLoadError: user cannot be NULL");
|
||||||
|
assetentry_t *entry = (assetentry_t *)params;
|
||||||
|
chunk_t *chunk = (chunk_t *)user;
|
||||||
|
if(chunk->dataEntry != entry) return;
|
||||||
|
consolePrint(
|
||||||
|
"Chunk load error: %d %d %d",
|
||||||
|
(int32_t)chunk->position.x,
|
||||||
|
(int32_t)chunk->position.y,
|
||||||
|
(int32_t)chunk->position.z
|
||||||
|
);
|
||||||
|
eventUnsubscribe(&entry->onLoaded, chunkLoaded);
|
||||||
|
eventUnsubscribe(&entry->onError, chunkLoadError);
|
||||||
|
assetUnlockEntry(chunk->dataEntry);
|
||||||
|
chunk->dataEntry = NULL;
|
||||||
|
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||||
|
|
||||||
|
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||||
|
chunkLoadNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunkLoaded(void *params, void *user) {
|
||||||
|
assertNotNull(params, "chunkLoaded: params cannot be NULL");
|
||||||
|
assertNotNull(user, "chunkLoaded: user cannot be NULL");
|
||||||
|
assetentry_t *entry = (assetentry_t *)params;
|
||||||
|
chunk_t *chunk = (chunk_t *)user;
|
||||||
|
if(chunk->dataEntry != entry) return;
|
||||||
|
|
||||||
|
uint8_t meshCount = entry->data.chunk.meshCount;
|
||||||
|
memoryCopy(
|
||||||
|
chunk->tiles,
|
||||||
|
entry->data.chunk.tiles,
|
||||||
|
sizeof(chunk->tiles)
|
||||||
|
);
|
||||||
|
worldpos_t wp;
|
||||||
|
chunkPosToWorldPos(&chunk->position, &wp);
|
||||||
|
vec3 wpf = {
|
||||||
|
(float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT
|
||||||
|
};
|
||||||
|
for(uint8_t m = 0; m < meshCount; m++) {
|
||||||
|
stringCopy(
|
||||||
|
chunk->modelNames[m],
|
||||||
|
entry->data.chunk.modelNames[m],
|
||||||
|
CHUNK_MESH_NAME_MAX
|
||||||
|
);
|
||||||
|
glm_vec3_copy(
|
||||||
|
entry->data.chunk.meshOffsets[m],
|
||||||
|
chunk->meshOffsets[m]
|
||||||
|
);
|
||||||
|
vec3 scaledOffset = {
|
||||||
|
chunk->meshOffsets[m][0],
|
||||||
|
chunk->meshOffsets[m][1],
|
||||||
|
chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT
|
||||||
|
};
|
||||||
|
vec3 pos;
|
||||||
|
glm_vec3_add(wpf, scaledOffset, pos);
|
||||||
|
glm_translate_make(chunk->meshModels[m], pos);
|
||||||
|
// Borrow the pointer rather than stealing it - the chunk asset entry
|
||||||
|
// keeps its own lock on each model (taken once while it loaded) and we
|
||||||
|
// keep the chunk asset entry itself locked (see below), so the models
|
||||||
|
// stay valid for as long as this chunk_t is using them. The entry may
|
||||||
|
// now be reused by a later chunkLoad for a different chunk_t once we
|
||||||
|
// eventually unlock it in chunkUnload, at which point its modelEntries
|
||||||
|
// must still be intact for that next reuse to copy from.
|
||||||
|
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
||||||
|
}
|
||||||
|
eventUnsubscribe(&entry->onLoaded, chunkLoaded);
|
||||||
|
eventUnsubscribe(&entry->onError, chunkLoadError);
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if(CHUNK_LOADING == chunk) CHUNK_LOADING = NULL;
|
||||||
|
chunkLoadNext();
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ typedef struct chunk_s {
|
|||||||
chunkpos_t position;
|
chunkpos_t position;
|
||||||
tile_t tiles[CHUNK_TILE_COUNT];
|
tile_t tiles[CHUNK_TILE_COUNT];
|
||||||
|
|
||||||
assetentry_t *dcfEntry;
|
assetentry_t *dataEntry;
|
||||||
|
|
||||||
uint8_t meshCount;
|
uint8_t meshCount;
|
||||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||||
@@ -30,6 +30,16 @@ typedef struct chunk_s {
|
|||||||
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
uint8_t entities[CHUNK_ENTITY_COUNT_MAX];
|
||||||
} chunk_t;
|
} chunk_t;
|
||||||
|
|
||||||
|
/** Every chunk slot for the currently loaded map. */
|
||||||
|
extern chunk_t CHUNKS[MAP_CHUNK_COUNT];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk pointers arranged by position relative to the currently loaded
|
||||||
|
* window, indexed via chunkPosToIndex(). NULL where no chunk occupies
|
||||||
|
* that slot. Rebuilt by chunkRebuildOrder() whenever the window moves.
|
||||||
|
*/
|
||||||
|
extern chunk_t *CHUNK_ORDER[MAP_CHUNK_COUNT];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the tile index for a tile position within a chunk.
|
* Gets the tile index for a tile position within a chunk.
|
||||||
*
|
*
|
||||||
@@ -46,3 +56,126 @@ uint32_t chunkGetTileIndex(const chunkpos_t position);
|
|||||||
* @return true if equal, false otherwise.
|
* @return true if equal, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b);
|
bool_t chunkPositionIsEqual(const chunkpos_t a, const chunkpos_t b);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets and starts loading the initial MAP_CHUNK_WIDTH x HEIGHT x DEPTH
|
||||||
|
* grid of chunks (async, see chunkLoad), anchored at chunk position
|
||||||
|
* (0,0,0). Does not unload chunks already loaded - call chunksUnloadAll()
|
||||||
|
* first when switching to a different map.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t chunksLoadGrid(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloads every chunk slot in CHUNKS.
|
||||||
|
*/
|
||||||
|
void chunksUnloadAll(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the loaded chunk window to be centered around newPos, unloading
|
||||||
|
* chunks that fall outside the new window and loading any newly exposed
|
||||||
|
* ones. No-op if newPos matches the currently loaded window.
|
||||||
|
*
|
||||||
|
* @param newPos The new chunk position.
|
||||||
|
* @return An error code.
|
||||||
|
*/
|
||||||
|
errorret_t chunkPositionSet(const chunkpos_t newPos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloads a chunk.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk to unload.
|
||||||
|
*/
|
||||||
|
void chunkUnload(chunk_t *chunk);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a chunk. Starts async loading without blocking.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk to load.
|
||||||
|
* @return An error code.
|
||||||
|
*/
|
||||||
|
errorret_t chunkLoad(chunk_t *chunk);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts loading the next queued chunk, if no chunk is currently mid-load.
|
||||||
|
* Called after chunkLoad enqueues a chunk, and again after the
|
||||||
|
* currently-loading chunk finishes (or is unloaded) to advance the queue.
|
||||||
|
*/
|
||||||
|
void chunkLoadNext(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a chunk from the load queue if present. Used when a chunk is
|
||||||
|
* re-queued or unloaded before its turn to load has come up.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk to remove from the load queue.
|
||||||
|
*/
|
||||||
|
void chunkLoadQueueRemove(chunk_t *chunk);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param params The failed assetentry_t.
|
||||||
|
* @param user The chunk_t that owns the entry.
|
||||||
|
*/
|
||||||
|
void chunkLoadError(void *params, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when a chunk JSON asset finishes loading.
|
||||||
|
* Always invoked on the main thread.
|
||||||
|
*
|
||||||
|
* @param params The loaded assetentry_t.
|
||||||
|
* @param user The chunk_t that owns the entry.
|
||||||
|
*/
|
||||||
|
void chunkLoaded(void *params, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds CHUNK_ORDER from the loaded chunks that fall within the
|
||||||
|
* current render window. Called whenever the chunk position changes.
|
||||||
|
*/
|
||||||
|
void chunkRebuildOrder(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of a chunk, within the currently loaded window, at the
|
||||||
|
* given position.
|
||||||
|
*
|
||||||
|
* @param position The chunk position.
|
||||||
|
* @return The index of the chunk, or -1 if out of bounds.
|
||||||
|
*/
|
||||||
|
chunkindex_t chunkGetIndexAt(const chunkpos_t position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a chunk by its index in CHUNK_ORDER.
|
||||||
|
*
|
||||||
|
* @param index The index of the chunk.
|
||||||
|
* @return A pointer to the chunk.
|
||||||
|
*/
|
||||||
|
chunk_t *chunkGet(const uint8_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the tile at the given world position.
|
||||||
|
*
|
||||||
|
* @param position The world position.
|
||||||
|
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
|
||||||
|
*/
|
||||||
|
tile_t chunkGetTile(const worldpos_t position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
|
||||||
|
* nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
|
||||||
|
* change height by one Z layer between adjacent tiles.
|
||||||
|
*
|
||||||
|
* @param x The world X coordinate to check.
|
||||||
|
* @param y The world Y coordinate to check.
|
||||||
|
* @param nearZ The reference Z layer to search outward from.
|
||||||
|
* @param outZ Output pointer, set to the resolved Z layer on success.
|
||||||
|
* @return true if a walkable tile was found, false otherwise.
|
||||||
|
*/
|
||||||
|
bool_t chunkGetWalkableZNear(
|
||||||
|
const worldunit_t x,
|
||||||
|
const worldunit_t y,
|
||||||
|
const worldunit_t nearZ,
|
||||||
|
worldunit_t *outZ
|
||||||
|
);
|
||||||
|
|||||||
+87
-390
@@ -10,31 +10,64 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "event/event.h"
|
#include "event/event.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "rpg/entity/global/entityglobal.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
#include "rpg/entity/entity.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
map_t MAP;
|
map_t MAP;
|
||||||
|
|
||||||
errorret_t mapInit() {
|
errorret_t mapInit(const char_t *name) {
|
||||||
|
errorChain(mapSetMap(name));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t mapSetMap(const char_t *name) {
|
||||||
|
assertNotNull(name, "Map name cannot be NULL");
|
||||||
|
assertStrLenMin(name, 1, "Map name cannot be empty");
|
||||||
|
assertStrLenMax(name, MAP_FILE_PATH_MAX, "Map name too long");
|
||||||
|
|
||||||
|
if(mapIsLoaded() && stringEquals(MAP.name, name)) errorOk();
|
||||||
|
|
||||||
|
if(mapIsLoaded()) {
|
||||||
|
chunksUnloadAll();
|
||||||
|
if(MAP.defEntry != NULL) {
|
||||||
|
eventUnsubscribe(&MAP.defEntry->onLoaded, mapDefLoaded);
|
||||||
|
eventUnsubscribe(&MAP.defEntry->onError, mapDefLoadError);
|
||||||
|
assetUnlockEntry(MAP.defEntry);
|
||||||
|
MAP.defEntry = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
memoryZero(&MAP, sizeof(map_t));
|
memoryZero(&MAP, sizeof(map_t));
|
||||||
|
stringCopy(MAP.name, name, MAP_FILE_PATH_MAX);
|
||||||
MAP.loaded = true;
|
MAP.loaded = true;
|
||||||
|
|
||||||
chunkindex_t i = 0;
|
char_t defPath[MAP_FILE_PATH_MAX + 16];
|
||||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
stringFormat(defPath, sizeof(defPath), "map/%s/map.json", MAP.name);
|
||||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
|
||||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
assetentry_t *defEntry = assetLock(defPath, ASSET_LOADER_TYPE_JSON, NULL);
|
||||||
chunk_t *chunk = &MAP.chunks[i++];
|
assertNotNull(defEntry, "Failed to get map def asset entry");
|
||||||
chunk->position = (chunkpos_t){
|
MAP.defEntry = defEntry;
|
||||||
(chunkunit_t)x, (chunkunit_t)y, (chunkunit_t)z
|
|
||||||
};
|
// The entry may already be resident from an earlier load that hasn't been
|
||||||
errorChain(mapChunkLoad(chunk));
|
// reaped yet - in that case onLoaded/onError already fired once and never
|
||||||
}
|
// will again, so handle the terminal state directly instead of waiting on
|
||||||
}
|
// a subscription that would never trigger.
|
||||||
|
if(defEntry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
|
mapDefLoaded(defEntry, NULL);
|
||||||
|
} else if(defEntry->state == ASSET_ENTRY_STATE_ERROR) {
|
||||||
|
mapDefLoadError(defEntry, NULL);
|
||||||
|
} else {
|
||||||
|
eventSubscribe(&defEntry->onLoaded, mapDefLoaded, NULL);
|
||||||
|
eventSubscribe(&defEntry->onError, mapDefLoadError, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
mapRebuildChunkOrder();
|
errorChain(chunksLoadGrid());
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,397 +75,61 @@ bool_t mapIsLoaded() {
|
|||||||
return MAP.loaded;
|
return MAP.loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t mapPositionSet(const chunkpos_t newPos) {
|
|
||||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
|
||||||
if(chunkPositionIsEqual(newPos, MAP.chunkPosition)) errorOk();
|
|
||||||
|
|
||||||
// Separate loaded chunks into "keep" and "free" buckets.
|
|
||||||
chunkindex_t chunksFreed[MAP_CHUNK_COUNT];
|
|
||||||
uint32_t freedCount = 0;
|
|
||||||
|
|
||||||
// Use a boolean grid so the inner load loop can check O(1).
|
|
||||||
bool_t posLoaded[MAP_CHUNK_WIDTH][MAP_CHUNK_HEIGHT][MAP_CHUNK_DEPTH];
|
|
||||||
memoryZero(posLoaded, sizeof(posLoaded));
|
|
||||||
|
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
|
||||||
chunk_t *chunk = &MAP.chunks[i];
|
|
||||||
chunkunit_t rx = chunk->position.x - newPos.x;
|
|
||||||
chunkunit_t ry = chunk->position.y - newPos.y;
|
|
||||||
chunkunit_t rz = chunk->position.z - newPos.z;
|
|
||||||
if(
|
|
||||||
rx >= 0 && rx < MAP_CHUNK_WIDTH &&
|
|
||||||
ry >= 0 && ry < MAP_CHUNK_HEIGHT &&
|
|
||||||
rz >= 0 && rz < MAP_CHUNK_DEPTH
|
|
||||||
) {
|
|
||||||
posLoaded[rx][ry][rz] = true;
|
|
||||||
} else {
|
|
||||||
mapChunkUnload(chunk);
|
|
||||||
chunksFreed[freedCount++] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(chunkunit_t z = 0; z < MAP_CHUNK_DEPTH; z++) {
|
|
||||||
for(chunkunit_t y = 0; y < MAP_CHUNK_HEIGHT; y++) {
|
|
||||||
for(chunkunit_t x = 0; x < MAP_CHUNK_WIDTH; x++) {
|
|
||||||
if(posLoaded[x][y][z]) continue;
|
|
||||||
assertTrue(freedCount > 0, "No free chunk slot available.");
|
|
||||||
chunk_t *chunk = &MAP.chunks[chunksFreed[--freedCount]];
|
|
||||||
chunk->position = (chunkpos_t){
|
|
||||||
newPos.x + (chunkunit_t)x,
|
|
||||||
newPos.y + (chunkunit_t)y,
|
|
||||||
newPos.z + (chunkunit_t)z
|
|
||||||
};
|
|
||||||
errorChain(mapChunkLoad(chunk));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MAP.chunkPosition = newPos;
|
|
||||||
mapRebuildChunkOrder();
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t mapUpdate() {
|
errorret_t mapUpdate() {
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t mapDispose() {
|
errorret_t mapDispose() {
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
chunksUnloadAll();
|
||||||
mapChunkUnload(&MAP.chunks[i]);
|
|
||||||
|
if(MAP.defEntry != NULL) {
|
||||||
|
eventUnsubscribe(&MAP.defEntry->onLoaded, mapDefLoaded);
|
||||||
|
eventUnsubscribe(&MAP.defEntry->onError, mapDefLoadError);
|
||||||
|
assetUnlockEntry(MAP.defEntry);
|
||||||
|
MAP.defEntry = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void mapChunkUnload(chunk_t *chunk) {
|
void mapDefLoadError(void *params, void *user) {
|
||||||
mapChunkLoadQueueRemove(chunk);
|
assertNotNull(params, "mapDefLoadError: params cannot be NULL");
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
assetentry_t *entry = (assetentry_t *)params;
|
||||||
|
if(MAP.defEntry != entry) return;
|
||||||
|
consolePrint("Failed to load map.json for '%s'", MAP.name);
|
||||||
|
eventUnsubscribe(&entry->onLoaded, mapDefLoaded);
|
||||||
|
eventUnsubscribe(&entry->onError, mapDefLoadError);
|
||||||
|
}
|
||||||
|
|
||||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
void mapDefLoaded(void *params, void *user) {
|
||||||
if(chunk->entities[i] == 0xFF) continue;
|
assertNotNull(params, "mapDefLoaded: params cannot be NULL");
|
||||||
entity_t *entity = &ENTITIES[chunk->entities[i]];
|
assetentry_t *entry = (assetentry_t *)params;
|
||||||
if(!entityCanUnload(entity)) {
|
if(MAP.defEntry != entry) return;
|
||||||
entitySetChunk(entity, 0xFF);
|
|
||||||
|
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||||
|
yyjson_val *nameVal = yyjson_obj_get(root, "name");
|
||||||
|
if(!nameVal || !yyjson_is_str(nameVal)) {
|
||||||
|
consolePrint("map.json for '%s' missing 'name' string", MAP.name);
|
||||||
} else {
|
} else {
|
||||||
entity->type = ENTITY_TYPE_NULL;
|
const char_t *nameStr = yyjson_get_str(nameVal);
|
||||||
|
size_t nameLen = yyjson_get_len(nameVal);
|
||||||
|
if(nameLen >= MAP_DISPLAY_NAME_MAX) {
|
||||||
|
consolePrint("Map display name '%s' exceeds max length", nameStr);
|
||||||
|
} else {
|
||||||
|
memoryCopy(MAP.displayName, nameStr, nameLen + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
yyjson_val *entitiesVal = yyjson_obj_get(root, "entities");
|
||||||
|
if(entitiesVal && yyjson_is_arr(entitiesVal)) {
|
||||||
if(chunk->dcfEntry != NULL) {
|
size_t entIdx, entMax;
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
yyjson_val *entObj;
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
yyjson_arr_foreach(entitiesVal, entIdx, entMax, entObj) {
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
entity_t *spawned = NULL;
|
||||||
chunk->dcfEntry = NULL;
|
errorCatch(errorPrint(entityCreateFromJson(entObj, &spawned)));
|
||||||
}
|
|
||||||
|
|
||||||
// modelEntries are borrowed pointers, not independently locked - the
|
|
||||||
// chunk asset entry (released above) is what actually holds the ref on
|
|
||||||
// each model, so nothing to unlock here, just drop our own copies.
|
|
||||||
for(uint8_t m = 0; m < chunk->meshCount; m++) {
|
|
||||||
chunk->modelEntries[m] = NULL;
|
|
||||||
}
|
|
||||||
chunk->meshCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t mapChunkLoad(chunk_t *chunk) {
|
|
||||||
if(!mapIsLoaded()) errorThrow("No map loaded");
|
|
||||||
|
|
||||||
mapChunkLoadQueueRemove(chunk);
|
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
|
||||||
|
|
||||||
if(chunk->dcfEntry != NULL) {
|
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
|
||||||
chunk->dcfEntry = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
memorySet(chunk->entities, 0xFF, sizeof(chunk->entities));
|
|
||||||
chunk->meshCount = 0;
|
|
||||||
|
|
||||||
char_t name[64];
|
|
||||||
stringFormat(
|
|
||||||
name, sizeof(name),
|
|
||||||
"chunks/%d_%d_%d.dcf",
|
|
||||||
(int32_t)chunk->position.x,
|
|
||||||
(int32_t)chunk->position.y,
|
|
||||||
(int32_t)chunk->position.z
|
|
||||||
);
|
|
||||||
|
|
||||||
if(!assetFileExists(name)) {
|
|
||||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
|
||||||
// chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND, .z = 0 };
|
|
||||||
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(
|
|
||||||
MAP.loadQueueCount < MAP_CHUNK_COUNT,
|
|
||||||
"Chunk load queue overflow"
|
|
||||||
);
|
|
||||||
MAP.loadQueue[MAP.loadQueueCount++] = chunk;
|
|
||||||
mapChunkLoadNext();
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void mapChunkLoadNext() {
|
|
||||||
if(MAP.loadingChunk != NULL) return;
|
|
||||||
if(MAP.loadQueueCount == 0) return;
|
|
||||||
|
|
||||||
chunk_t *chunk = MAP.loadQueue[0];
|
|
||||||
for(uint32_t i = 1; i < MAP.loadQueueCount; i++) {
|
|
||||||
MAP.loadQueue[i - 1] = MAP.loadQueue[i];
|
|
||||||
}
|
|
||||||
MAP.loadQueueCount--;
|
|
||||||
MAP.loadingChunk = chunk;
|
|
||||||
|
|
||||||
char_t name[64];
|
|
||||||
stringFormat(
|
|
||||||
name, sizeof(name),
|
|
||||||
"chunks/%d_%d_%d.dcf",
|
|
||||||
(int32_t)chunk->position.x,
|
|
||||||
(int32_t)chunk->position.y,
|
|
||||||
(int32_t)chunk->position.z
|
|
||||||
);
|
|
||||||
|
|
||||||
assetentry_t *entry = assetLock(name, ASSET_LOADER_TYPE_CHUNK, NULL);
|
|
||||||
assertNotNull(entry, "Failed to get chunk asset entry");
|
|
||||||
chunk->dcfEntry = entry;
|
|
||||||
|
|
||||||
// The entry may already be resident from an earlier load that hasn't been
|
|
||||||
// reaped yet - in that case onLoaded/onError already fired once and never
|
|
||||||
// will again, so handle the terminal state directly instead of waiting on
|
|
||||||
// a subscription that would never trigger.
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
mapChunkLoaded(entry, chunk);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
mapChunkLoadError(entry, chunk);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
|
||||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
void mapChunkLoadQueueRemove(chunk_t *chunk) {
|
|
||||||
for(uint32_t i = 0; i < MAP.loadQueueCount; i++) {
|
|
||||||
if(MAP.loadQueue[i] != chunk) continue;
|
|
||||||
for(uint32_t j = i + 1; j < MAP.loadQueueCount; j++) {
|
|
||||||
MAP.loadQueue[j - 1] = MAP.loadQueue[j];
|
|
||||||
}
|
|
||||||
MAP.loadQueueCount--;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventUnsubscribe(&entry->onLoaded, mapDefLoaded);
|
||||||
chunkindex_t mapGetChunkIndexAt(const chunkpos_t position) {
|
eventUnsubscribe(&entry->onError, mapDefLoadError);
|
||||||
if(!mapIsLoaded()) return -1;
|
|
||||||
|
|
||||||
chunkpos_t relPos = {
|
|
||||||
position.x - MAP.chunkPosition.x,
|
|
||||||
position.y - MAP.chunkPosition.y,
|
|
||||||
position.z - MAP.chunkPosition.z
|
|
||||||
};
|
|
||||||
|
|
||||||
if(
|
|
||||||
relPos.x < 0 || relPos.y < 0 || relPos.z < 0 ||
|
|
||||||
relPos.x >= MAP_CHUNK_WIDTH ||
|
|
||||||
relPos.y >= MAP_CHUNK_HEIGHT ||
|
|
||||||
relPos.z >= MAP_CHUNK_DEPTH
|
|
||||||
) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chunkPosToIndex(&relPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk_t *mapGetChunk(const uint8_t index) {
|
|
||||||
if(index >= MAP_CHUNK_COUNT) return NULL;
|
|
||||||
if(!mapIsLoaded()) return NULL;
|
|
||||||
return MAP.chunkOrder[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
tile_t mapGetTile(const worldpos_t position) {
|
|
||||||
if(!mapIsLoaded()) return TILE_NULL;
|
|
||||||
|
|
||||||
chunkpos_t chunkPos;
|
|
||||||
worldPosToChunkPos(&position, &chunkPos);
|
|
||||||
chunkindex_t chunkIndex = mapGetChunkIndexAt(chunkPos);
|
|
||||||
if(chunkIndex == -1) return TILE_NULL;
|
|
||||||
|
|
||||||
chunk_t *chunk = mapGetChunk(chunkIndex);
|
|
||||||
assertNotNull(chunk, "Chunk pointer cannot be NULL");
|
|
||||||
chunktileindex_t tileIndex = worldPosToChunkTileIndex(&position);
|
|
||||||
tile_t tile = chunk->tiles[tileIndex];
|
|
||||||
if(tile.z != worldPosToChunkLocalZ(&position)) return TILE_NULL;
|
|
||||||
return tile;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t mapGetWalkableZNear(
|
|
||||||
const worldunit_t x,
|
|
||||||
const worldunit_t y,
|
|
||||||
const worldunit_t nearZ,
|
|
||||||
worldunit_t *outZ
|
|
||||||
) {
|
|
||||||
assertNotNull(outZ, "Output Z pointer cannot be NULL");
|
|
||||||
|
|
||||||
const worldunit_t candidates[] = {
|
|
||||||
nearZ, (worldunit_t)(nearZ + 1), (worldunit_t)(nearZ - 1)
|
|
||||||
};
|
|
||||||
for(uint8_t i = 0; i < 3; i++) {
|
|
||||||
const worldpos_t pos = { x, y, candidates[i] };
|
|
||||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) continue;
|
|
||||||
*outZ = candidates[i];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity_t * mapSpawnEntity(
|
|
||||||
const entityglobalid_t globalId,
|
|
||||||
const worldpos_t position
|
|
||||||
) {
|
|
||||||
assertTrue(
|
|
||||||
globalId > ENTITY_GLOBAL_ID_START,
|
|
||||||
"mapSpawnEntity requires a global ID greater than ENTITY_GLOBAL_ID_START"
|
|
||||||
);
|
|
||||||
assertTrue(
|
|
||||||
globalId < ENTITY_GLOBAL_LIST_COUNT,
|
|
||||||
"Global ID is out of range for entity global init callbacks"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Already spawned? Reuse the existing entity instead of making a
|
|
||||||
// duplicate - two entities must never share a global ID.
|
|
||||||
entity_t *existing = entityGetByGlobalId(globalId);
|
|
||||||
if(existing != NULL) return existing;
|
|
||||||
|
|
||||||
// See if there is a callback for this entity first.
|
|
||||||
const entityglobaldef_t *def = &ENTITY_GLOBAL_LIST[globalId];
|
|
||||||
assertNotNull(def, "No global entity definition for this ID");
|
|
||||||
assertNotNull(def->callback, "No callback registered for this global ID");
|
|
||||||
|
|
||||||
// Get available entity.
|
|
||||||
uint8_t index = entityGetAvailable();
|
|
||||||
assertTrue(index != 0xFF, "No available entity slots for mapSpawnEntity");
|
|
||||||
|
|
||||||
// Get the pointer and do the init.
|
|
||||||
entity_t *entity = &ENTITIES[index];
|
|
||||||
entityInit(entity, def->type);
|
|
||||||
entity->globalId = globalId;
|
|
||||||
entityPositionSet(entity, position);// Also assigns the entity's chunk.
|
|
||||||
|
|
||||||
// Invoke the callback to initialize the entity.
|
|
||||||
entityglobalcreate_t create = {
|
|
||||||
.entity = entity,
|
|
||||||
.position = position
|
|
||||||
};
|
|
||||||
def->callback(&create);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
void mapRebuildChunkOrder() {
|
|
||||||
memoryZero(MAP.chunkOrder, sizeof(MAP.chunkOrder));
|
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
|
||||||
chunk_t *chunk = &MAP.chunks[i];
|
|
||||||
const chunkpos_t rel = {
|
|
||||||
chunk->position.x - MAP.chunkPosition.x,
|
|
||||||
chunk->position.y - MAP.chunkPosition.y,
|
|
||||||
chunk->position.z - MAP.chunkPosition.z
|
|
||||||
};
|
|
||||||
if(
|
|
||||||
rel.x < 0 || rel.x >= MAP_CHUNK_WIDTH ||
|
|
||||||
rel.y < 0 || rel.y >= MAP_CHUNK_HEIGHT ||
|
|
||||||
rel.z < 0 || rel.z >= MAP_CHUNK_DEPTH
|
|
||||||
) continue;
|
|
||||||
MAP.chunkOrder[chunkPosToIndex(&rel)] = chunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void mapChunkLoadError(void *params, void *user) {
|
|
||||||
assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
|
|
||||||
assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
|
|
||||||
assetentry_t *entry = (assetentry_t *)params;
|
|
||||||
chunk_t *chunk = (chunk_t *)user;
|
|
||||||
if(chunk->dcfEntry != entry) return;
|
|
||||||
consolePrint(
|
|
||||||
"Chunk load error: %d %d %d",
|
|
||||||
(int32_t)chunk->position.x,
|
|
||||||
(int32_t)chunk->position.y,
|
|
||||||
(int32_t)chunk->position.z
|
|
||||||
);
|
|
||||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
|
||||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
|
||||||
chunk->dcfEntry = NULL;
|
|
||||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
|
||||||
|
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
|
||||||
mapChunkLoadNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
void mapChunkLoaded(void *params, void *user) {
|
|
||||||
assertNotNull(params, "mapChunkLoaded: params cannot be NULL");
|
|
||||||
assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
|
|
||||||
assetentry_t *entry = (assetentry_t *)params;
|
|
||||||
chunk_t *chunk = (chunk_t *)user;
|
|
||||||
if(chunk->dcfEntry != entry) return;
|
|
||||||
// consolePrint(
|
|
||||||
// "Chunk loaded: %d %d %d",
|
|
||||||
// (int32_t)chunk->position.x,
|
|
||||||
// (int32_t)chunk->position.y,
|
|
||||||
// (int32_t)chunk->position.z
|
|
||||||
// );
|
|
||||||
uint8_t meshCount = entry->data.chunk.meshCount;
|
|
||||||
memoryCopy(
|
|
||||||
chunk->tiles,
|
|
||||||
entry->data.chunk.tiles,
|
|
||||||
sizeof(chunk->tiles)
|
|
||||||
);
|
|
||||||
worldpos_t wp;
|
|
||||||
chunkPosToWorldPos(&chunk->position, &wp);
|
|
||||||
vec3 wpf = {
|
|
||||||
(float_t)wp.x, (float_t)wp.y, (float_t)wp.z * WORLD_LAYER_HEIGHT
|
|
||||||
};
|
|
||||||
for(uint8_t m = 0; m < meshCount; m++) {
|
|
||||||
stringCopy(
|
|
||||||
chunk->modelNames[m],
|
|
||||||
entry->data.chunk.modelNames[m],
|
|
||||||
CHUNK_MESH_NAME_MAX
|
|
||||||
);
|
|
||||||
glm_vec3_copy(
|
|
||||||
entry->data.chunk.meshOffsets[m],
|
|
||||||
chunk->meshOffsets[m]
|
|
||||||
);
|
|
||||||
vec3 scaledOffset = {
|
|
||||||
chunk->meshOffsets[m][0],
|
|
||||||
chunk->meshOffsets[m][1],
|
|
||||||
chunk->meshOffsets[m][2] * WORLD_LAYER_HEIGHT
|
|
||||||
};
|
|
||||||
vec3 pos;
|
|
||||||
glm_vec3_add(wpf, scaledOffset, pos);
|
|
||||||
glm_translate_make(chunk->meshModels[m], pos);
|
|
||||||
// Borrow the pointer rather than stealing it - the chunk asset entry
|
|
||||||
// keeps its own lock on each model (taken once while it loaded) and we
|
|
||||||
// keep the chunk asset entry itself locked (see below), so the models
|
|
||||||
// stay valid for as long as this chunk_t is using them. The entry may
|
|
||||||
// now be reused by a later mapChunkLoad for a different chunk_t once we
|
|
||||||
// eventually unlock it in mapChunkUnload, at which point its
|
|
||||||
// modelEntries must still be intact for that next reuse to copy from.
|
|
||||||
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
|
||||||
}
|
|
||||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
|
||||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
|
||||||
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
|
|
||||||
// chunk asset entry (and therefore its model locks) alive for as long as
|
|
||||||
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
|
||||||
chunk->meshCount = meshCount;
|
|
||||||
|
|
||||||
if(MAP.loadingChunk == chunk) MAP.loadingChunk = NULL;
|
|
||||||
mapChunkLoadNext();
|
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-122
@@ -7,33 +7,48 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "rpg/overworld/chunk.h"
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
|
|
||||||
#define MAP_FILE_PATH_MAX 128
|
#define MAP_FILE_PATH_MAX 32
|
||||||
|
#define MAP_DISPLAY_NAME_MAX 64
|
||||||
|
|
||||||
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
typedef struct map_s {
|
typedef struct map_s {
|
||||||
|
char_t name[MAP_FILE_PATH_MAX];
|
||||||
|
char_t displayName[MAP_DISPLAY_NAME_MAX];
|
||||||
bool_t loaded;
|
bool_t loaded;
|
||||||
|
|
||||||
chunk_t chunks[MAP_CHUNK_COUNT];
|
// Asset lock for the map's map.json, held while its async load is
|
||||||
chunk_t *chunkOrder[MAP_CHUNK_COUNT];
|
// pending and for as long as the map stays loaded.
|
||||||
chunkpos_t chunkPosition;
|
assetentry_t *defEntry;
|
||||||
|
|
||||||
// Only one chunk may be mid-load (asset locked & awaiting onLoaded/
|
|
||||||
// onError) at any given time - everything else waits here in FIFO order.
|
|
||||||
chunk_t *loadQueue[MAP_CHUNK_COUNT];
|
|
||||||
uint32_t loadQueueCount;
|
|
||||||
chunk_t *loadingChunk;
|
|
||||||
} map_t;
|
} map_t;
|
||||||
|
|
||||||
extern map_t MAP;
|
extern map_t MAP;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the map.
|
* Initializes the map, loading its chunks from beneath the given name's
|
||||||
|
* asset directory (e.g. "testmap" -> assets/map/testmap/chunks/X_Y_Z.dcf).
|
||||||
*
|
*
|
||||||
|
* @param name The map's directory name, under assets/map/.
|
||||||
* @return An error code.
|
* @return An error code.
|
||||||
*/
|
*/
|
||||||
errorret_t mapInit();
|
errorret_t mapInit(const char_t *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to a different map, unloading every currently loaded chunk and
|
||||||
|
* starting an async load of the initial chunk grid (at chunk position
|
||||||
|
* 0,0,0) plus map.json (-> MAP.displayName), both from beneath the new
|
||||||
|
* name's asset directory. Once map.json loads, its optional "entities"
|
||||||
|
* array (see entityCreateFromJson) is spawned; entries that fail to parse
|
||||||
|
* are logged and skipped rather than failing the whole map load. Returns
|
||||||
|
* before either finishes loading - callers must not assume
|
||||||
|
* MAP.displayName, spawned entities, or chunk data is populated yet.
|
||||||
|
* No-op if name matches the currently loaded map.
|
||||||
|
*
|
||||||
|
* @param name The map's directory name, under assets/map/.
|
||||||
|
* @return An error code.
|
||||||
|
*/
|
||||||
|
errorret_t mapSetMap(const char_t *name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a map is loaded.
|
* Checks if a map is loaded.
|
||||||
@@ -57,121 +72,22 @@ errorret_t mapUpdate();
|
|||||||
errorret_t mapDispose();
|
errorret_t mapDispose();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the map position and updates chunks accordingly.
|
* Callback invoked when a map's map.json asset fails to load. Leaves
|
||||||
*
|
* MAP.displayName empty and logs a console message.
|
||||||
* @param newPos The new chunk position.
|
|
||||||
* @return An error code.
|
|
||||||
*/
|
|
||||||
errorret_t mapPositionSet(const chunkpos_t newPos);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unloads a chunk.
|
|
||||||
*
|
|
||||||
* @param chunk The chunk to unload.
|
|
||||||
*/
|
|
||||||
void mapChunkUnload(chunk_t* chunk);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a chunk. Starts async loading without blocking.
|
|
||||||
*
|
|
||||||
* @param chunk The chunk to load.
|
|
||||||
* @return An error code.
|
|
||||||
*/
|
|
||||||
errorret_t mapChunkLoad(chunk_t* chunk);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts loading the next queued chunk, if no chunk is currently mid-load.
|
|
||||||
* Called after mapChunkLoad enqueues a chunk, and again after the
|
|
||||||
* currently-loading chunk finishes (or is unloaded) to advance the queue.
|
|
||||||
*/
|
|
||||||
void mapChunkLoadNext();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a chunk from the load queue if present. Used when a chunk is
|
|
||||||
* re-queued or unloaded before its turn to load has come up.
|
|
||||||
*
|
|
||||||
* @param chunk The chunk to remove from the load queue.
|
|
||||||
*/
|
|
||||||
void mapChunkLoadQueueRemove(chunk_t *chunk);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked when a chunk DCF asset fails to load. Fills the
|
|
||||||
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
|
|
||||||
* Always invoked on the main thread.
|
* Always invoked on the main thread.
|
||||||
*
|
*
|
||||||
* @param params The failed assetentry_t.
|
* @param params The failed assetentry_t.
|
||||||
* @param user The chunk_t that owns the entry.
|
* @param user Unused.
|
||||||
*/
|
*/
|
||||||
void mapChunkLoadError(void *params, void *user);
|
void mapDefLoadError(void *params, void *user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback invoked when a chunk DCF asset finishes loading.
|
* Callback invoked when a map's map.json asset finishes loading. Parses
|
||||||
|
* out the "name" string into MAP.displayName, and spawns each entry of
|
||||||
|
* the optional "entities" array via entityCreateFromJson.
|
||||||
* Always invoked on the main thread.
|
* Always invoked on the main thread.
|
||||||
*
|
*
|
||||||
* @param params The loaded assetentry_t.
|
* @param params The loaded assetentry_t.
|
||||||
* @param user The chunk_t that owns the entry.
|
* @param user Unused.
|
||||||
*/
|
*/
|
||||||
void mapChunkLoaded(void *params, void *user);
|
void mapDefLoaded(void *params, void *user);
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuilds chunkOrder from the loaded chunks that fall within the
|
|
||||||
* current render window. Called whenever chunkPosition changes.
|
|
||||||
*/
|
|
||||||
void mapRebuildChunkOrder();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the index of a chunk, within the world, at the given position.
|
|
||||||
*
|
|
||||||
* @param position The chunk position.
|
|
||||||
* @return The index of the chunk, or -1 if out of bounds.
|
|
||||||
*/
|
|
||||||
chunkindex_t mapGetChunkIndexAt(const chunkpos_t position);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a chunk by its index.
|
|
||||||
*
|
|
||||||
* @param chunkIndex The index of the chunk.
|
|
||||||
* @return A pointer to the chunk.
|
|
||||||
*/
|
|
||||||
chunk_t * mapGetChunk(const uint8_t chunkIndex);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the tile at the given world position.
|
|
||||||
*
|
|
||||||
* @param position The world position.
|
|
||||||
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
|
|
||||||
*/
|
|
||||||
tile_t mapGetTile(const worldpos_t position);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the closest walkable Z layer to nearZ at the given X/Y. Checks
|
|
||||||
* nearZ first, then nearZ + 1, then nearZ - 1, since ramps only ever
|
|
||||||
* change height by one Z layer between adjacent tiles.
|
|
||||||
*
|
|
||||||
* @param x The world X coordinate to check.
|
|
||||||
* @param y The world Y coordinate to check.
|
|
||||||
* @param nearZ The reference Z layer to search outward from.
|
|
||||||
* @param outZ Output pointer, set to the resolved Z layer on success.
|
|
||||||
* @return true if a walkable tile was found, false otherwise.
|
|
||||||
*/
|
|
||||||
bool_t mapGetWalkableZNear(
|
|
||||||
const worldunit_t x,
|
|
||||||
const worldunit_t y,
|
|
||||||
const worldunit_t nearZ,
|
|
||||||
worldunit_t *outZ
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spawns a global (persistent) entity into the world at the given position.
|
|
||||||
* Asserts globalId is greater than ENTITY_GLOBAL_ID_START - use entityInit
|
|
||||||
* directly for ephemeral, non-global entities.
|
|
||||||
*
|
|
||||||
* @param globalId The global entity ID to assign, must be greater than
|
|
||||||
* ENTITY_GLOBAL_ID_START.
|
|
||||||
* @param position The world position to spawn the entity at.
|
|
||||||
* @return Pointer to the spawned entity.
|
|
||||||
*/
|
|
||||||
entity_t * mapSpawnEntity(
|
|
||||||
const entityglobalid_t globalId,
|
|
||||||
const worldpos_t position
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
|
|
||||||
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
|
maparea_t MAP_AREAS[MAP_AREA_COUNT_MAX];
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ bool_t mapAreaCanUnload(const maparea_t *area) {
|
|||||||
assertNotNull(area, "Map area pointer cannot be NULL");
|
assertNotNull(area, "Map area pointer cannot be NULL");
|
||||||
|
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
if(mapAreaIsChunkOverlappingOrInside(area, &MAP.chunks[i])) return false;
|
if(mapAreaIsChunkOverlappingOrInside(area, &CHUNKS[i])) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+6
-27
@@ -7,12 +7,10 @@
|
|||||||
|
|
||||||
#include "rpg.h"
|
#include "rpg.h"
|
||||||
#include "entity/entity.h"
|
#include "entity/entity.h"
|
||||||
#include "rpg/entity/npc/npcpath.h"
|
|
||||||
#include "rpg/entity/item/entityitem.h"
|
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "rpg/overworld/maparea.h"
|
#include "rpg/overworld/maparea.h"
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
#include "rpg/cutscene/scene/testcutscene.h"
|
|
||||||
#include "rpg/item/backpack.h"
|
#include "rpg/item/backpack.h"
|
||||||
#include "rpg/battle/party.h"
|
#include "rpg/battle/party.h"
|
||||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||||
@@ -37,35 +35,16 @@ errorret_t rpgInit(void) {
|
|||||||
partyInit();
|
partyInit();
|
||||||
cutsceneSystemInit();
|
cutsceneSystemInit();
|
||||||
|
|
||||||
errorChain(mapInit());
|
errorChain(mapInit("testmap"));
|
||||||
|
|
||||||
rpgCameraInit();
|
rpgCameraInit();
|
||||||
// Init world
|
// Init world
|
||||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
errorChain(chunkPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||||
|
|
||||||
// TEST: Create some entities.
|
|
||||||
uint8_t entIndex = entityGetAvailable();
|
|
||||||
assertTrue(entIndex != 0xFF, "No available entity slots!.");
|
|
||||||
entity_t *ent = &ENTITIES[entIndex];
|
|
||||||
entityInit(ent, ENTITY_TYPE_PLAYER);
|
|
||||||
entityPositionSet(ent, (worldpos_t){ 10, 2, 0 });// Also assigns the chunk.
|
|
||||||
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
|
||||||
RPG_CAMERA.followEntity.followEntityId = ent->id;
|
|
||||||
|
|
||||||
mapSpawnEntity(3, (worldpos_t){ 8, 8, 1 });
|
|
||||||
|
|
||||||
// TEST: Place an item entity.
|
|
||||||
uint8_t itemEntIndex = entityGetAvailable();
|
|
||||||
assertTrue(itemEntIndex != 0xFF, "No available entity slots!.");
|
|
||||||
entity_t *itemEnt = &ENTITIES[itemEntIndex];
|
|
||||||
entityInit(itemEnt, ENTITY_TYPE_ITEM);
|
|
||||||
entityItemSet(itemEnt, ITEM_ID_POTION, 1);
|
|
||||||
entityPositionSet(itemEnt, (worldpos_t){ 12, 2, 0 });
|
|
||||||
|
|
||||||
// TEST: Give the player a starting assortment of items.
|
// TEST: Give the player a starting assortment of items.
|
||||||
backpackAdd(ITEM_ID_POTION, 5);
|
backpackAdd(itemGetIdByName("POTION"), 5);
|
||||||
backpackAdd(ITEM_ID_POTATO, 3);
|
backpackAdd(itemGetIdByName("POTATO"), 3);
|
||||||
backpackAdd(ITEM_ID_APPLE, 8);
|
backpackAdd(itemGetIdByName("APPLE"), 8);
|
||||||
|
|
||||||
// TEST: Create a test map area.
|
// TEST: Create a test map area.
|
||||||
uint8_t areaIndex = mapAreaAdd(
|
uint8_t areaIndex = mapAreaAdd(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "util/random.h"
|
#include "util/random.h"
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/map.h"
|
||||||
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
|
|
||||||
@@ -114,6 +115,17 @@ errorret_t rpgCameraUpdate(void) {
|
|||||||
RPG_CAMERA.shakeTime += TIME.delta;
|
RPG_CAMERA.shakeTime += TIME.delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The player entity may spawn asynchronously (e.g. via map.json), so
|
||||||
|
// start following it as soon as it shows up rather than requiring
|
||||||
|
// whoever creates it to also wire up the camera.
|
||||||
|
if(RPG_CAMERA.mode == RPG_CAMERA_MODE_FREE) {
|
||||||
|
entity_t *player = entityGetByGlobalId(ENTITY_GLOBAL_ID_PLAYER);
|
||||||
|
if(player != NULL) {
|
||||||
|
RPG_CAMERA.mode = RPG_CAMERA_MODE_FOLLOW_ENTITY;
|
||||||
|
RPG_CAMERA.followEntity.followEntityId = player->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!mapIsLoaded()) errorOk();
|
if(!mapIsLoaded()) errorOk();
|
||||||
|
|
||||||
vec3 pos;
|
vec3 pos;
|
||||||
@@ -125,7 +137,7 @@ errorret_t rpgCameraUpdate(void) {
|
|||||||
.z = (chunkunit_t)floorf(pos[2] / WORLD_LAYER_HEIGHT / CHUNK_DEPTH)
|
.z = (chunkunit_t)floorf(pos[2] / WORLD_LAYER_HEIGHT / CHUNK_DEPTH)
|
||||||
};
|
};
|
||||||
|
|
||||||
errorChain(mapPositionSet((chunkpos_t){
|
errorChain(chunkPositionSet((chunkpos_t){
|
||||||
.x = chunkPos.x - (MAP_CHUNK_WIDTH / 2),
|
.x = chunkPos.x - (MAP_CHUNK_WIDTH / 2),
|
||||||
.y = chunkPos.y - (MAP_CHUNK_HEIGHT / 2),
|
.y = chunkPos.y - (MAP_CHUNK_HEIGHT / 2),
|
||||||
.z = chunkPos.z - (MAP_CHUNK_DEPTH / 2)
|
.z = chunkPos.z - (MAP_CHUNK_DEPTH / 2)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
|
|
||||||
#include "rpg/overworld/map.h"
|
#include "rpg/overworld/chunk.h"
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
#include "rpg/rpgcamera.h"
|
#include "rpg/rpgcamera.h"
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ errorret_t sceneOverworldDrawEntity(
|
|||||||
|
|
||||||
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
chunk_t *chunk = MAP.chunkOrder[i];
|
chunk_t *chunk = CHUNK_ORDER[i];
|
||||||
if(chunk == NULL) continue;
|
if(chunk == NULL) continue;
|
||||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||||
if(chunk->meshCount == 0) continue;
|
if(chunk->meshCount == 0) continue;
|
||||||
@@ -220,7 +220,7 @@ errorret_t sceneOverworldDrawChunksBase(const sceneoverworld_t *overworld) {
|
|||||||
|
|
||||||
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
|
errorret_t sceneOverworldDrawChunksProps(const sceneoverworld_t *overworld) {
|
||||||
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
for(chunkindex_t i = 0; i < MAP_CHUNK_COUNT; i++) {
|
||||||
chunk_t *chunk = MAP.chunkOrder[i];
|
chunk_t *chunk = CHUNK_ORDER[i];
|
||||||
if(chunk == NULL) continue;
|
if(chunk == NULL) continue;
|
||||||
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
if(!sceneOverworldChunkShouldRender(overworld, chunk)) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ void uiBackpackTabChanged(
|
|||||||
const uint8_t index,
|
const uint8_t index,
|
||||||
const uimenuitem_t *item
|
const uimenuitem_t *item
|
||||||
) {
|
) {
|
||||||
const itemtype_t type = (itemtype_t)(index + 1);
|
const itemtypeid_t type = (itemtypeid_t)(index + 1);
|
||||||
const inventory_t *inventory = backpackGetInventory(type);
|
const inventory_t *inventory = backpackGetInventory(type);
|
||||||
|
|
||||||
errorCatch(uiItemListSetItemStacks(
|
errorCatch(uiItemListSetItemStacks(
|
||||||
@@ -41,11 +41,16 @@ void uiBackpackTabSelected(
|
|||||||
errorret_t uiBackpackInit(void) {
|
errorret_t uiBackpackInit(void) {
|
||||||
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
|
memoryZero(&UI_BACKPACK, sizeof(uibackpack_t));
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
ITEM_TYPE_COUNT - 1 <= UI_BACKPACK_TAB_COUNT,
|
||||||
|
"Item type count exceeds UI_BACKPACK_TAB_COUNT"
|
||||||
|
);
|
||||||
|
|
||||||
MENU_BEGIN(
|
MENU_BEGIN(
|
||||||
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
|
&UI_BACKPACK.tabsMenu, UI_BACKPACK.tabs,
|
||||||
uiBackpackTabSelected, NULL, uiBackpackTabChanged
|
uiBackpackTabSelected, NULL, uiBackpackTabChanged
|
||||||
);
|
);
|
||||||
for(uint8_t i = 0; i < UI_BACKPACK_TAB_COUNT; i++) {
|
for(uint32_t i = 0; i < ITEM_TYPE_COUNT - 1; i++) {
|
||||||
stringFormat(
|
stringFormat(
|
||||||
UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
|
UI_BACKPACK.tabLabels[i], UI_BACKPACK_TAB_LABEL_MAX - 1,
|
||||||
"Category %u", i + 1
|
"Category %u", i + 1
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
#include "ui/widget/uiitemlist.h"
|
#include "ui/widget/uiitemlist.h"
|
||||||
#include "rpg/item/item.h"
|
#include "rpg/item/item.h"
|
||||||
|
|
||||||
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT - 1)
|
#define UI_BACKPACK_TAB_COUNT (ITEM_TYPE_COUNT_MAX - 1)
|
||||||
#define UI_BACKPACK_TAB_LABEL_MAX 32
|
#define UI_BACKPACK_TAB_LABEL_MAX 32
|
||||||
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
|
#define UI_BACKPACK_ITEM_LIST_COLUMNS 4
|
||||||
#define UI_BACKPACK_ITEM_LIST_ROWS 5
|
#define UI_BACKPACK_ITEM_LIST_ROWS 5
|
||||||
|
|||||||
@@ -12,9 +12,12 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/color.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
#include "rpg/overworld/map.h"
|
||||||
|
|
||||||
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
#define UI_GAME_MENU_INDEX_CHARACTERS 0
|
||||||
#define UI_GAME_MENU_INDEX_ITEMS 1
|
#define UI_GAME_MENU_INDEX_ITEMS 1
|
||||||
@@ -77,12 +80,25 @@ errorret_t uiGameMenuDraw(void) {
|
|||||||
const float_t y = (float_t)SCREEN.scanY;
|
const float_t y = (float_t)SCREEN.scanY;
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
const float_t contentX = x + UI_FRAME_START_X;
|
||||||
|
const float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||||
|
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
|
||||||
|
|
||||||
|
// Map display name header - stopgap placement until this gets a proper
|
||||||
|
// HUD element of its own.
|
||||||
|
const float_t nameRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
errorChain(textDraw(
|
||||||
|
contentX, contentY, MAP.displayName, COLOR_WHITE, &FONT_DEFAULT
|
||||||
|
));
|
||||||
|
|
||||||
errorChain(uiMenuDraw(
|
errorChain(uiMenuDraw(
|
||||||
&UI_GAME_MENU.menu,
|
&UI_GAME_MENU.menu,
|
||||||
x + UI_FRAME_START_X,
|
contentX,
|
||||||
y + UI_FRAME_START_Y,
|
contentY + nameRowHeight + UI_FRAME_PADDING_Y,
|
||||||
width - (UI_FRAME_START_X * 2),
|
contentWidth,
|
||||||
height - (UI_FRAME_START_Y * 2)
|
contentHeight - nameRowHeight - UI_FRAME_PADDING_Y
|
||||||
));
|
));
|
||||||
|
|
||||||
errorChain(spriteBatchFlush());
|
errorChain(spriteBatchFlush());
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ void uiItemListSetItems(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(list, "Item list cannot be NULL");
|
assertNotNull(list, "Item list cannot be NULL");
|
||||||
assertTrue(
|
assertTrue(
|
||||||
itemCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
|
itemCount <= INVENTORY_CAPACITY_MAX, "Too many items for list"
|
||||||
);
|
);
|
||||||
|
|
||||||
memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount);
|
memoryCopy(list->items, items, sizeof(uiitem_t) * itemCount);
|
||||||
@@ -58,7 +58,7 @@ errorret_t uiItemListSetItemStacks(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(list, "Item list cannot be NULL");
|
assertNotNull(list, "Item list cannot be NULL");
|
||||||
assertTrue(
|
assertTrue(
|
||||||
stackCount <= UI_ITEM_LIST_CAPACITY_MAX, "Too many items for list"
|
stackCount <= INVENTORY_CAPACITY_MAX, "Too many items for list"
|
||||||
);
|
);
|
||||||
|
|
||||||
for(uint8_t i = 0; i < stackCount; i++) {
|
for(uint8_t i = 0; i < stackCount; i++) {
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
#include "ui/focus/uifocus.h"
|
#include "ui/focus/uifocus.h"
|
||||||
#include "rpg/item/inventory.h"
|
#include "rpg/item/inventory.h"
|
||||||
|
|
||||||
#define UI_ITEM_LIST_CAPACITY_MAX 40
|
|
||||||
|
|
||||||
typedef struct uiitemlist_s uiitemlist_t;
|
typedef struct uiitemlist_s uiitemlist_t;
|
||||||
|
|
||||||
typedef void (*uiitemlistselectedcallback_t)(
|
typedef void (*uiitemlistselectedcallback_t)(
|
||||||
@@ -51,7 +49,7 @@ typedef errorret_t (*uiitemlistcolumncallback_t)(
|
|||||||
);
|
);
|
||||||
|
|
||||||
struct uiitemlist_s {
|
struct uiitemlist_s {
|
||||||
uiitem_t items[UI_ITEM_LIST_CAPACITY_MAX];
|
uiitem_t items[INVENTORY_CAPACITY_MAX];
|
||||||
uint8_t itemCount;
|
uint8_t itemCount;
|
||||||
|
|
||||||
// Grid layout: how many item slots wide/tall the list displays.
|
// Grid layout: how many item slots wide/tall the list displays.
|
||||||
@@ -105,7 +103,7 @@ void uiItemListInit(
|
|||||||
* @param list The item list to update.
|
* @param list The item list to update.
|
||||||
* @param items The items to display.
|
* @param items The items to display.
|
||||||
* @param itemCount Number of entries in items. Must be <=
|
* @param itemCount Number of entries in items. Must be <=
|
||||||
* UI_ITEM_LIST_CAPACITY_MAX.
|
* INVENTORY_CAPACITY_MAX.
|
||||||
*/
|
*/
|
||||||
void uiItemListSetItems(
|
void uiItemListSetItems(
|
||||||
uiitemlist_t *list,
|
uiitemlist_t *list,
|
||||||
@@ -120,7 +118,7 @@ void uiItemListSetItems(
|
|||||||
* @param list The item list to update.
|
* @param list The item list to update.
|
||||||
* @param stacks The item stacks to display.
|
* @param stacks The item stacks to display.
|
||||||
* @param stackCount Number of entries in stacks. Must be <=
|
* @param stackCount Number of entries in stacks. Must be <=
|
||||||
* UI_ITEM_LIST_CAPACITY_MAX.
|
* INVENTORY_CAPACITY_MAX.
|
||||||
* @return Any error that occurs.
|
* @return Any error that occurs.
|
||||||
*/
|
*/
|
||||||
errorret_t uiItemListSetItemStacks(
|
errorret_t uiItemListSetItemStacks(
|
||||||
|
|||||||
+62
-159
@@ -4,10 +4,10 @@
|
|||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Generates DCF + companion DMF files from raw chunk JSON files, or upgrades
|
Generates chunk JSON + companion DMF mesh files from raw chunk JSON files.
|
||||||
legacy DCF files (version 1 or 2) to the current version.
|
|
||||||
|
|
||||||
JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
JSON input (assetsraw/<map>/chunks/chunk_X_Y_Z.json, one subdirectory per
|
||||||
|
map - e.g. assetsraw/overworld/chunks/):
|
||||||
{
|
{
|
||||||
"tiles": [
|
"tiles": [
|
||||||
{ "pos": [x, y, z], "type": <tile_shape_int>, "tile": <uv_tile_int> }
|
{ "pos": [x, y, z], "type": <tile_shape_int>, "tile": <uv_tile_int> }
|
||||||
@@ -20,22 +20,28 @@ JSON input (assetsraw/chunks/chunk_X_Y_Z.json):
|
|||||||
Tiles absent from the array default to TILE_SHAPE_NULL. Each (x, y)
|
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,
|
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
|
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.
|
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
|
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:
|
Output chunk JSON is derived automatically, preserving the map
|
||||||
assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf
|
subdirectory:
|
||||||
|
assetsraw/<map>/chunks/chunk_X_Y_Z.json -> assets/<map>/chunks/X_Y_Z.json
|
||||||
|
|
||||||
Version 4 DCF format (after 8-byte header):
|
Chunk JSON format (loaded at runtime by assetChunkLoaderSync):
|
||||||
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,
|
"tiles": [ [shape, z], ... ] // exactly CHUNK_WIDTH * CHUNK_HEIGHT
|
||||||
matching the C tile_t struct's layout: { tileshape_t shape; uint8_t z; })
|
// entries, one per (x, y) column,
|
||||||
uint8_t meshCount
|
// x-major (matches tile_t: { shape, z })
|
||||||
for each model:
|
"meshes": [
|
||||||
null-terminated string (relative asset path to .json model)
|
{ "model": "<path to .json model, relative to assets/>",
|
||||||
float32[3] (x, y, z offset)
|
"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:
|
DMF format:
|
||||||
Bytes 0-3: DMF\\x00
|
Bytes 0-3: DMF\\x00
|
||||||
@@ -45,9 +51,8 @@ DMF format:
|
|||||||
Each vertex: uv[2] + pos[3] = 5 x float32 LE = 20 bytes
|
Each vertex: uv[2] + pos[3] = 5 x float32 LE = 20 bytes
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 -m tools.asset.chunk # process all assetsraw/chunks/*.json
|
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.json> # process one JSON file
|
||||||
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -75,20 +80,13 @@ WORLD_LAYER_HEIGHT = 1.0 / math.sqrt(2)
|
|||||||
CHUNK_MESH_COUNT_MAX = 10
|
CHUNK_MESH_COUNT_MAX = 10
|
||||||
CHUNK_MESH_NAME_MAX = 64
|
CHUNK_MESH_NAME_MAX = 64
|
||||||
|
|
||||||
# Matches sizeof(tile_t) on the C side: uint32_t shape + uint8_t z, padded
|
# Internal working format for a column's (shape, z) pair, packed/unpacked
|
||||||
# to 8 bytes ({ tileshape_t shape; uint8_t z; } with 4-byte enum alignment).
|
# 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_STRUCT_FORMAT = '<IB3x'
|
||||||
TILE_SIZE = struct.calcsize(TILE_STRUCT_FORMAT)
|
TILE_SIZE = struct.calcsize(TILE_STRUCT_FORMAT)
|
||||||
VERTEX_SIZE = 20
|
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_NULL = 0
|
||||||
TILE_SHAPE_GROUND = 1
|
TILE_SHAPE_GROUND = 1
|
||||||
TILE_SHAPE_RAMP_NORTH = 2
|
TILE_SHAPE_RAMP_NORTH = 2
|
||||||
@@ -104,9 +102,7 @@ TILE_SHAPE_RAMP_NORTHWEST_INNER = 11
|
|||||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12
|
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12
|
||||||
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
|
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
|
||||||
|
|
||||||
FILE_MAGIC = b'DCF'
|
|
||||||
DMF_MAGIC = b'DMF\x00'
|
DMF_MAGIC = b'DMF\x00'
|
||||||
VERSION_OUT = 4
|
|
||||||
DMF_VERSION = 1
|
DMF_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
@@ -142,13 +138,13 @@ def write_model_json(path, mesh_rel, texture_rel, color):
|
|||||||
print(f' Wrote model JSON {path}')
|
print(f' Wrote model JSON {path}')
|
||||||
|
|
||||||
|
|
||||||
def derive_dcf_path(json_path):
|
def derive_chunk_json_path(json_path):
|
||||||
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
|
"""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]
|
base = os.path.splitext(os.path.basename(json_path))[0]
|
||||||
if base.startswith('chunk_'):
|
if base.startswith('chunk_'):
|
||||||
base = base[len('chunk_'):]
|
base = base[len('chunk_'):]
|
||||||
rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR)
|
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):
|
def write_dmf(path, vertex_bytes):
|
||||||
@@ -163,36 +159,36 @@ def write_dmf(path, vertex_bytes):
|
|||||||
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
||||||
|
|
||||||
|
|
||||||
def write_dcf(dcf_path, tiles, mesh_names, mesh_offsets=None):
|
def write_chunk_json(chunk_json_path, tiles, mesh_names, mesh_offsets=None):
|
||||||
"""Write a current-version DCF referencing the given DMF asset paths."""
|
"""Write a chunk JSON file referencing the given model asset paths."""
|
||||||
mesh_count = len(mesh_names)
|
mesh_count = len(mesh_names)
|
||||||
if mesh_offsets is None:
|
if mesh_offsets is None:
|
||||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||||
|
|
||||||
buf = bytearray()
|
tile_list = []
|
||||||
buf += FILE_MAGIC
|
for i in range(CHUNK_TILE_COUNT):
|
||||||
buf += b'\x00'
|
shape, z = struct.unpack_from(TILE_STRUCT_FORMAT, tiles, i * TILE_SIZE)
|
||||||
buf += struct.pack('<I', VERSION_OUT)
|
tile_list.append([shape, z])
|
||||||
buf += tiles
|
|
||||||
buf += struct.pack('<B', mesh_count)
|
meshes = []
|
||||||
for name, offset in zip(mesh_names, mesh_offsets):
|
for name, offset in zip(mesh_names, mesh_offsets):
|
||||||
encoded = name.encode('ascii')
|
if len(name) >= CHUNK_MESH_NAME_MAX:
|
||||||
if len(encoded) >= CHUNK_MESH_NAME_MAX:
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}"
|
f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}"
|
||||||
)
|
)
|
||||||
buf += encoded + b'\x00'
|
meshes.append({'model': name, 'offset': list(offset)})
|
||||||
buf += struct.pack('<3f', offset[0], offset[1], offset[2])
|
|
||||||
with open(dcf_path, 'wb') as f:
|
obj = {'tiles': tile_list, 'meshes': meshes}
|
||||||
f.write(buf)
|
os.makedirs(os.path.dirname(chunk_json_path), exist_ok=True)
|
||||||
|
with open(chunk_json_path, 'w') as f:
|
||||||
|
json.dump(obj, f)
|
||||||
print(
|
print(
|
||||||
f' Wrote DCF {dcf_path}: '
|
f' Wrote chunk JSON {chunk_json_path}: {mesh_count} mesh(es)'
|
||||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), {len(buf)} bytes'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# JSON -> DCF + DMF
|
# JSON -> chunk JSON + DMF
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
||||||
@@ -263,7 +259,7 @@ def build_terrain_verts(tiles_bytes):
|
|||||||
return bytes(buf)
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
def from_json(json_path, dcf_path):
|
def from_json(json_path, chunk_json_path):
|
||||||
with open(json_path) as f:
|
with open(json_path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
@@ -288,7 +284,7 @@ def from_json(json_path, dcf_path):
|
|||||||
model_names = []
|
model_names = []
|
||||||
mesh_offsets = []
|
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 mesh + model (only written if non-empty)
|
||||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||||
@@ -296,10 +292,10 @@ def from_json(json_path, dcf_path):
|
|||||||
os.makedirs(terrain_dir, exist_ok=True)
|
os.makedirs(terrain_dir, exist_ok=True)
|
||||||
os.makedirs(models_dir, exist_ok=True)
|
os.makedirs(models_dir, exist_ok=True)
|
||||||
if terrain_verts:
|
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)
|
write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts)
|
||||||
mesh_rel = f'meshes/chunks/{dmf_name}'
|
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(
|
write_model_json(
|
||||||
os.path.join(models_dir, json_name),
|
os.path.join(models_dir, json_name),
|
||||||
mesh_rel,
|
mesh_rel,
|
||||||
@@ -323,105 +319,14 @@ def from_json(json_path, dcf_path):
|
|||||||
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2])))
|
||||||
print(f' Resolved {filename} -> {rel}')
|
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):
|
def process_json(json_path):
|
||||||
dcf_path = derive_dcf_path(json_path)
|
chunk_json_path = derive_chunk_json_path(json_path)
|
||||||
os.makedirs(os.path.dirname(dcf_path), exist_ok=True)
|
os.makedirs(os.path.dirname(chunk_json_path), exist_ok=True)
|
||||||
print(f"{json_path} -> {dcf_path}")
|
print(f"{json_path} -> {chunk_json_path}")
|
||||||
from_json(json_path, dcf_path)
|
from_json(json_path, chunk_json_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)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -432,27 +337,25 @@ def main():
|
|||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
chunks_dir = os.path.join(ASSETSRAW_DIR, 'chunks')
|
|
||||||
if not os.path.isdir(chunks_dir):
|
|
||||||
print(f"No directory found: {chunks_dir}")
|
|
||||||
sys.exit(1)
|
|
||||||
json_files = sorted(
|
json_files = sorted(
|
||||||
os.path.join(chunks_dir, f)
|
os.path.join(dirpath, f)
|
||||||
for f in os.listdir(chunks_dir)
|
for dirpath, _, filenames in os.walk(ASSETSRAW_DIR)
|
||||||
|
if os.path.basename(dirpath) == 'chunks'
|
||||||
|
for f in filenames
|
||||||
if f.endswith('.json')
|
if f.endswith('.json')
|
||||||
)
|
)
|
||||||
if not json_files:
|
if not json_files:
|
||||||
print(f"No JSON files found in {chunks_dir}")
|
print(f"No chunk JSON files found under {ASSETSRAW_DIR}")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
for p in json_files:
|
for p in json_files:
|
||||||
process_json(p)
|
process_json(p)
|
||||||
return
|
return
|
||||||
|
|
||||||
src = args[0]
|
src = args[0]
|
||||||
if os.path.splitext(src)[1].lower() == '.json':
|
if os.path.splitext(src)[1].lower() != '.json':
|
||||||
|
print(f"Expected a .json input file, got: {src}")
|
||||||
|
sys.exit(1)
|
||||||
process_json(src)
|
process_json(src)
|
||||||
else:
|
|
||||||
upgrade_dcf(src)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Item JSON to .h defines")
|
|
||||||
parser.add_argument("--json", required=True, help="Path to item JSON file")
|
|
||||||
parser.add_argument("--output", required=True, help="Path to output .h file")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
def type_enum(name):
|
|
||||||
return "ITEM_TYPE_" + name.upper()
|
|
||||||
|
|
||||||
def id_enum(name):
|
|
||||||
return "ITEM_ID_" + name.upper()
|
|
||||||
|
|
||||||
# Load JSON
|
|
||||||
item_ids = []
|
|
||||||
item_types = []
|
|
||||||
rows = {}
|
|
||||||
|
|
||||||
with open(args.json, encoding="utf-8") as f:
|
|
||||||
entries = json.load(f)
|
|
||||||
|
|
||||||
if not all(
|
|
||||||
"id" in row and "type" in row and "name" in row for row in entries
|
|
||||||
):
|
|
||||||
raise ValueError("Each item must have 'id', 'type', and 'name' fields")
|
|
||||||
|
|
||||||
for row in entries:
|
|
||||||
item_id, item_type = row["id"], row["type"]
|
|
||||||
if item_id not in item_ids:
|
|
||||||
item_ids.append(item_id)
|
|
||||||
if item_type not in item_types:
|
|
||||||
item_types.append(item_type)
|
|
||||||
rows[item_id] = row
|
|
||||||
|
|
||||||
# Assign enum values: types and IDs each start from 1 with NULL = 0.
|
|
||||||
type_values = {}
|
|
||||||
type_count = 1
|
|
||||||
for t in item_types:
|
|
||||||
type_values[t] = type_count
|
|
||||||
type_count += 1
|
|
||||||
|
|
||||||
id_values = {}
|
|
||||||
id_count = 1
|
|
||||||
for i in item_ids:
|
|
||||||
id_values[i] = id_count
|
|
||||||
id_count += 1
|
|
||||||
|
|
||||||
# Count items per type
|
|
||||||
type_item_counts = { t: 0 for t in item_types }
|
|
||||||
for i in item_ids:
|
|
||||||
type_item_counts[rows[i]["type"]] += 1
|
|
||||||
|
|
||||||
# Build output
|
|
||||||
out = [
|
|
||||||
"#pragma once",
|
|
||||||
'#include "dusk.h"',
|
|
||||||
"",
|
|
||||||
"typedef enum {",
|
|
||||||
" ITEM_TYPE_NULL = 0,",
|
|
||||||
]
|
|
||||||
for t in item_types:
|
|
||||||
out.append(f" {type_enum(t)} = {type_values[t]},")
|
|
||||||
out += [
|
|
||||||
f" ITEM_TYPE_COUNT = {type_count}",
|
|
||||||
"} itemtype_t;",
|
|
||||||
"",
|
|
||||||
"typedef enum {",
|
|
||||||
" ITEM_ID_NULL = 0,",
|
|
||||||
]
|
|
||||||
for i in item_ids:
|
|
||||||
out.append(f" {id_enum(i)} = {id_values[i]},")
|
|
||||||
out += [
|
|
||||||
f" ITEM_ID_COUNT = {id_count}",
|
|
||||||
"} itemid_t;",
|
|
||||||
"",
|
|
||||||
"typedef struct {",
|
|
||||||
" itemid_t id;",
|
|
||||||
" itemtype_t type;",
|
|
||||||
" const char_t *name;",
|
|
||||||
"} item_t;",
|
|
||||||
"",
|
|
||||||
"static const item_t ITEMS[] = {",
|
|
||||||
]
|
|
||||||
for i in item_ids:
|
|
||||||
row = rows[i]
|
|
||||||
out += [
|
|
||||||
f" [{id_enum(i)}] = {{",
|
|
||||||
f" .id = {id_enum(i)},",
|
|
||||||
f" .type = {type_enum(row['type'])},",
|
|
||||||
f" .name = \"item.{row['name']}.name\",",
|
|
||||||
" },",
|
|
||||||
]
|
|
||||||
out += [
|
|
||||||
"};",
|
|
||||||
"",
|
|
||||||
"static const uint8_t ITEM_TYPE_COUNTS[] = {",
|
|
||||||
]
|
|
||||||
for t in item_types:
|
|
||||||
out.append(f" [{type_enum(t)}] = {type_item_counts[t]},")
|
|
||||||
out += [
|
|
||||||
"};",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
max_type_count = max(type_item_counts.values()) if type_item_counts else 0
|
|
||||||
out += [
|
|
||||||
f"#define ITEM_TYPE_COUNT_MAX {max_type_count}",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
|
||||||
with open(args.output, "w", encoding="utf-8") as f:
|
|
||||||
f.write("\n".join(out))
|
|
||||||
Reference in New Issue
Block a user