Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 |
@@ -124,18 +124,16 @@ Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||
|
||||
### Layer structure
|
||||
```
|
||||
src/dusk/ core, platform-agnostic game logic
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
||||
src/dusklinux/ Linux + Knulli platform impl
|
||||
src/duskpsp/ PSP platform impl
|
||||
src/duskvita/ Vita platform impl
|
||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||
```
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Dusk Roadmap
|
||||
|
||||
Tracking upcoming milestones for the engine.
|
||||
|
||||
## Upcoming milestones
|
||||
|
||||
1. Add a very basic physics engine, moving away from the current
|
||||
tile-based movement.
|
||||
2. Give entities full freedom of movement (no longer locked to tile
|
||||
grid positions).
|
||||
3. Update entity interaction, triggers, chunk management, and other
|
||||
systems that currently assume tile-based positioning so they work
|
||||
with the new 3D positioning/movement code.
|
||||
4. Investigate and fix poor UI rendering performance. Rendering the
|
||||
console alone tanks framerate despite the existing mesh
|
||||
optimizations, so there is likely more headroom to find in the
|
||||
vertex/text rendering path.
|
||||
5. Create UI elements for displaying status indicators, e.g. network
|
||||
connection state and save-in-progress.
|
||||
6. Fully test saving end-to-end on all supported platforms.
|
||||
7. Remove the tile system from chunks in favor of meshes, with
|
||||
dynamic hitboxes per chunk loaded in from the chunk file data.
|
||||
8. Create UI elements for network status: a connecting modal, an
|
||||
error state, and a connected flag. Retire the test HTTP request
|
||||
once these are in place.
|
||||
9. Build the socket server and client implementation, including
|
||||
handlers for the different packet types.
|
||||
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
||||
the existing `npc` and `player` types. Limit to 8 (defined
|
||||
constant) for now.
|
||||
11. Send and receive `clientplayer` position over the network.
|
||||
12. Create a UI menu for creating a server and joining a server. For
|
||||
now, join IPs are hard-coded (testing against a fixed IP of
|
||||
10.0.0.94).
|
||||
13. Create "handshake" packets. For now, just send the username,
|
||||
enforced to be under 10 characters long.
|
||||
14. Server tracks all players' positions and broadcasts them to all
|
||||
connected clients.
|
||||
15. Server sends disconnect packets for users who leave.
|
||||
16. Server assigns each client a UUID; all clients know every other
|
||||
client's UUID (used to reference them across position updates,
|
||||
disconnect packets, etc).
|
||||
17. Server notifies all clients (by UUID) when a user joins, leaves,
|
||||
or is disconnected, so clients can spawn or remove the
|
||||
corresponding `clientplayer` entity in the world.
|
||||
|
||||
## Principles
|
||||
|
||||
- Never trust the network implicitly. Neither side (server or client)
|
||||
should assume the other's packets are well-formed or benign --
|
||||
validate all incoming packet data defensively, since either side
|
||||
may send garbage or malicious data. Use `errorret_t` /
|
||||
`errorThrow()` for these runtime checks, not assert macros --
|
||||
asserts are debug-only and won't guard release builds against
|
||||
malformed or malicious packet data.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"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." }
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [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]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 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]}]}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,88 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
if(NOT DEFINED ENV{VITASDK})
|
||||
message(FATAL_ERROR "VITASDK environment variable is not set.")
|
||||
endif()
|
||||
|
||||
include("$ENV{VITASDK}/share/vita.cmake" REQUIRED)
|
||||
|
||||
set(VITA_APP_NAME "Dusk")
|
||||
set(VITA_TITLEID "DUSK00001")
|
||||
set(VITA_VERSION "01.00")
|
||||
|
||||
find_package(SDL2 REQUIRED)
|
||||
|
||||
# Custom flags for cglm
|
||||
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
|
||||
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
|
||||
find_package(cglm REQUIRED)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
${SDL2_LIBRARIES}
|
||||
cglm
|
||||
SDL2
|
||||
SDL2main
|
||||
zip
|
||||
bz2
|
||||
z
|
||||
zstd
|
||||
crypto
|
||||
lzma
|
||||
m
|
||||
pthread
|
||||
stdc++
|
||||
vitaGL
|
||||
mathneon
|
||||
vitashark
|
||||
kubridge_stub
|
||||
SceAppMgr_stub
|
||||
SceAudio_stub
|
||||
SceCtrl_stub
|
||||
SceCommonDialog_stub
|
||||
SceDisplay_stub
|
||||
SceKernelDmacMgr_stub
|
||||
SceGxm_stub
|
||||
SceShaccCg_stub
|
||||
SceSysmodule_stub
|
||||
ScePower_stub
|
||||
SceTouch_stub
|
||||
SceVshBridge_stub
|
||||
SceIofilemgr_stub
|
||||
SceShaccCgExt
|
||||
libtaihen_stub.a
|
||||
|
||||
|
||||
# SceKernel_stub
|
||||
SceAppUtil_stub
|
||||
SceHid_stub
|
||||
SceRtc_stub
|
||||
)
|
||||
|
||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||
${SDL2_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_SDL2
|
||||
DUSK_OPENGL
|
||||
DUSK_VITA
|
||||
DUSK_INPUT_GAMEPAD
|
||||
DUSK_PLATFORM_ENDIAN_LITTLE
|
||||
DUSK_OPENGL_LEGACY
|
||||
DUSK_DISPLAY_WIDTH=960
|
||||
DUSK_DISPLAY_HEIGHT=544
|
||||
)
|
||||
|
||||
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
|
||||
vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE)
|
||||
|
||||
# Post-build: package SELF + assets into a .vpk installable on the Vita
|
||||
vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self
|
||||
VERSION ${VITA_VERSION}
|
||||
NAME ${VITA_APP_NAME}
|
||||
FILE ${DUSK_ASSETS_ZIP} dusk.dsk
|
||||
)
|
||||
@@ -15,11 +15,6 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
||||
add_subdirectory(dusksdl2)
|
||||
add_subdirectory(duskgl)
|
||||
|
||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
||||
add_subdirectory(duskvita)
|
||||
add_subdirectory(dusksdl2)
|
||||
add_subdirectory(duskgl)
|
||||
|
||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||
add_subdirectory(duskdolphin)
|
||||
|
||||
|
||||
@@ -8,17 +8,40 @@
|
||||
#include "assetchunkloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "util/endian.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.chunk.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire chunk file."
|
||||
);
|
||||
|
||||
loading->loading.chunk.data = data;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -31,141 +54,12 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
|
||||
switch(loading->loading.chunk.state) {
|
||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_JSON;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: {
|
||||
// Lock the chunk's JSON as a sub-asset. The entry key is prefixed
|
||||
// with "json:" to avoid a type-collision with the chunk entry
|
||||
// itself (both share the same filename). The JSON loader reads
|
||||
// the real file path supplied in the input.
|
||||
char_t jsonKey[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(
|
||||
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
|
||||
);
|
||||
assetloaderinput_t jsonInput;
|
||||
memoryZero(&jsonInput, sizeof(jsonInput));
|
||||
stringCopy(
|
||||
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
|
||||
);
|
||||
assetentry_t *jsonEntry = assetLock(
|
||||
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(jsonEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||
|
||||
yyjson_val *tilesVal = yyjson_obj_get(root, "tiles");
|
||||
if(
|
||||
!tilesVal || !yyjson_is_arr(tilesVal) ||
|
||||
yyjson_arr_size(tilesVal) != CHUNK_TILE_COUNT
|
||||
) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow(
|
||||
"Chunk JSON 'tiles' must have exactly %d entries",
|
||||
CHUNK_TILE_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
|
||||
size_t tileIdx, tileMax;
|
||||
yyjson_val *tileVal;
|
||||
yyjson_arr_foreach(tilesVal, tileIdx, tileMax, tileVal) {
|
||||
yyjson_val *shapeVal = yyjson_arr_get(tileVal, 0);
|
||||
yyjson_val *zVal = yyjson_arr_get(tileVal, 1);
|
||||
if(
|
||||
!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) != 2 ||
|
||||
!yyjson_is_int(shapeVal) || !yyjson_is_int(zVal)
|
||||
) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Chunk JSON tile entries must be [shape, z] arrays");
|
||||
}
|
||||
out->tiles[tileIdx] = (tile_t){
|
||||
.shape = (tileshape_t)yyjson_get_int(shapeVal),
|
||||
.z = (uint8_t)yyjson_get_int(zVal)
|
||||
};
|
||||
}
|
||||
|
||||
yyjson_val *meshesVal = yyjson_obj_get(root, "meshes");
|
||||
out->meshCount = 0;
|
||||
if(meshesVal && yyjson_is_arr(meshesVal)) {
|
||||
size_t meshCount = yyjson_arr_size(meshesVal);
|
||||
if(meshCount > CHUNK_MESH_COUNT_MAX) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow(
|
||||
"Chunk JSON 'meshes' exceeds CHUNK_MESH_COUNT_MAX (%d)",
|
||||
CHUNK_MESH_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
size_t meshIdx, meshMax;
|
||||
yyjson_val *meshVal;
|
||||
yyjson_arr_foreach(meshesVal, meshIdx, meshMax, meshVal) {
|
||||
yyjson_val *modelVal = yyjson_obj_get(meshVal, "model");
|
||||
if(!modelVal || !yyjson_is_str(modelVal)) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Chunk JSON mesh entry missing 'model' string");
|
||||
}
|
||||
const char_t *modelStr = yyjson_get_str(modelVal);
|
||||
size_t modelLen = yyjson_get_len(modelVal);
|
||||
if(modelLen >= CHUNK_MESH_NAME_MAX) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow(
|
||||
"Chunk JSON model path '%s' exceeds max length", modelStr
|
||||
);
|
||||
}
|
||||
memoryCopy(out->modelNames[meshIdx], modelStr, modelLen + 1);
|
||||
|
||||
vec3 offset = { 0.0f, 0.0f, 0.0f };
|
||||
yyjson_val *offsetVal = yyjson_obj_get(meshVal, "offset");
|
||||
if(
|
||||
offsetVal && yyjson_is_arr(offsetVal) &&
|
||||
yyjson_arr_size(offsetVal) == 3
|
||||
) {
|
||||
size_t offIdx, offMax;
|
||||
yyjson_val *offElem;
|
||||
yyjson_arr_foreach(offsetVal, offIdx, offMax, offElem) {
|
||||
if(yyjson_is_num(offElem)) {
|
||||
offset[offIdx] = (float_t)yyjson_get_num(offElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
glm_vec3_copy(offset, out->meshOffsets[meshIdx]);
|
||||
}
|
||||
out->meshCount = (uint8_t)meshCount;
|
||||
}
|
||||
|
||||
assetUnlockEntry(jsonEntry);
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
||||
break;
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
||||
@@ -197,6 +91,73 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.chunk.data;
|
||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported chunk file version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
size_t offset = 8;
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
out->tiles = memoryAllocate(tileSize);
|
||||
memoryCopy(out->tiles, data + offset, tileSize);
|
||||
offset += tileSize;
|
||||
|
||||
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
|
||||
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
|
||||
*shape = endianLittleToHost32(*shape);
|
||||
}
|
||||
|
||||
out->meshCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
||||
"Chunk mesh count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
uint8_t nameLen = 0;
|
||||
while(
|
||||
data[offset + nameLen] != '\0' &&
|
||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
||||
) {
|
||||
nameLen++;
|
||||
}
|
||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
||||
out->modelNames[m][nameLen] = '\0';
|
||||
offset += nameLen + 1;
|
||||
|
||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
||||
offset += sizeof(vec3);
|
||||
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
|
||||
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
|
||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 4
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
@@ -18,12 +20,16 @@ typedef struct {
|
||||
|
||||
typedef enum {
|
||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_JSON,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS
|
||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
||||
ASSET_CHUNK_LOADING_STATE_DONE
|
||||
} assetchunkloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetchunkloadingstate_t state;
|
||||
uint8_t *data;
|
||||
uint8_t modelIndex;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
@@ -36,9 +42,9 @@ typedef struct {
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for chunk assets. No-op - the chunk's JSON file is
|
||||
* loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync),
|
||||
* which handles its own async file I/O.
|
||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
||||
* the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
@@ -46,22 +52,9 @@ typedef struct {
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for chunk assets. Locks and parses the chunk's JSON
|
||||
* file (tiles plus referenced model paths/offsets), then locks each
|
||||
* referenced model asset before marking the entry loaded.
|
||||
*
|
||||
* Expected JSON shape:
|
||||
* {
|
||||
* "tiles": [ [shape, z], ... ] // exactly CHUNK_TILE_COUNT entries,
|
||||
* // one per (x, y) column, x-major
|
||||
* "meshes": [
|
||||
* { "model": "models/chunks/chunk_0_0_0_0.json",
|
||||
* "offset": [0.0, 0.0, 0.0] }
|
||||
* ]
|
||||
* }
|
||||
* "meshes" is optional; each entry's "offset" defaults to [0, 0, 0] if
|
||||
* omitted. By convention mesh index 0 is the chunk's terrain and the
|
||||
* rest are props (see sceneOverworldDrawChunksBase/Props).
|
||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||
* read by the async phase and populates the output assetchunkoutput_t with
|
||||
* tile data and model paths.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
|
||||
@@ -42,6 +42,7 @@ void consolePrint(const char_t *message, ...) {
|
||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||
);
|
||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||
CONSOLE.dirty = true;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexUnlock(&CONSOLE.printMutex);
|
||||
|
||||
@@ -21,6 +21,11 @@ typedef struct {
|
||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||
bool_t visible;
|
||||
|
||||
// Set whenever the history changes; consumers rendering the console
|
||||
// (e.g. uiConsoleDraw) check and clear this to know when their own
|
||||
// cached representation of the history needs rebuilding.
|
||||
bool_t dirty;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadmutex_t printMutex;
|
||||
#endif
|
||||
|
||||
@@ -21,6 +21,7 @@ errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
|
||||
|
||||
errorret_t shaderBind(shader_t *shader) {
|
||||
assertNotNull(shader, "Shader cannot be null");
|
||||
if(bound == shader) errorOk();
|
||||
errorChain(shaderBindPlatform(shader));
|
||||
bound = shader;
|
||||
errorOk();
|
||||
|
||||
+96
-12
@@ -10,7 +10,6 @@
|
||||
#include "time/time.h"
|
||||
#include "input/input.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "rpg/item/item.h"
|
||||
#include "rpg/rpg.h"
|
||||
#include "display/display.h"
|
||||
#include "scene/scene.h"
|
||||
@@ -18,9 +17,11 @@
|
||||
#include "ui/ui.h"
|
||||
#include "assert/assert.h"
|
||||
#include "network/network.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
#include "save/save.h"
|
||||
#include "save/savesettings.h"
|
||||
|
||||
engine_t ENGINE;
|
||||
|
||||
@@ -38,15 +39,22 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
// errorChain(saveInit());
|
||||
errorChain(saveInit());
|
||||
errorChain(saveSettingsLoad());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(itemInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
errorChain(networkInit());
|
||||
errorChain(sceneInit());
|
||||
|
||||
networkRequestConnection(
|
||||
engineNetworkOnConnected,
|
||||
engineNetworkOnFailed,
|
||||
engineNetworkOnDisconnect,
|
||||
NULL
|
||||
);
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
|
||||
#ifdef DUSK_ASSERTIONS_FAKED
|
||||
@@ -56,7 +64,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
#endif
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -65,16 +72,33 @@ errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
errorChain(networkUpdate());
|
||||
timeUpdate();
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
errorChain(rpgUpdate());
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
|
||||
if(
|
||||
ENGINE.networkDisconnectTestPending &&
|
||||
TIME.time >= ENGINE.networkDisconnectTestAt
|
||||
) {
|
||||
ENGINE.networkDisconnectTestPending = false;
|
||||
networkRequestDisconnection(engineNetworkDisconnectTestOnComplete, NULL);
|
||||
}
|
||||
|
||||
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
||||
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
errorChain(rpgUpdate());
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
}
|
||||
|
||||
// Render
|
||||
errorChain(displayUpdate());
|
||||
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
||||
if(
|
||||
dialogType == SYSTEM_DIALOG_TYPE_NONE &&
|
||||
inputPressed(INPUT_ACTION_RAGEQUIT)
|
||||
) {
|
||||
ENGINE.running = false;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -90,8 +114,68 @@ errorret_t engineDispose(void) {
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
// errorChain(saveDispose());
|
||||
errorChain(saveDispose());
|
||||
errorChain(assetDispose());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void engineHttpTestOnComplete(void *params, void *user) {
|
||||
networkhttprequest_t *request = (networkhttprequest_t *)params;
|
||||
const networkhttpresponse_t *response = &request->response;
|
||||
|
||||
consolePrint("frogfind.com response status: %d", response->status);
|
||||
|
||||
for(uint32_t i = 0; i < response->headers.count; i++) {
|
||||
const networkhttpheader_t *header = &response->headers.headers[i];
|
||||
consolePrint("frogfind.com header: %s: %s", header->name, header->value);
|
||||
}
|
||||
|
||||
consolePrint("frogfind.com body length: %d", (int32_t)response->bodyLength);
|
||||
|
||||
const size_t chunkMax = CONSOLE_LINE_MAX - 32;
|
||||
for(size_t offset = 0; offset < response->bodyLength; offset += chunkMax) {
|
||||
size_t chunkLength = response->bodyLength - offset;
|
||||
if(chunkLength > chunkMax) chunkLength = chunkMax;
|
||||
|
||||
consolePrint("frogfind.com body: %.*s",
|
||||
(int32_t)chunkLength, (const char_t *)response->body + offset);
|
||||
}
|
||||
}
|
||||
|
||||
void engineHttpTestOnError(void *params, void *user) {
|
||||
consolePrint("frogfind.com request failed");
|
||||
}
|
||||
|
||||
void engineNetworkOnConnected(void *user) {
|
||||
consolePrint("Network connected");
|
||||
|
||||
errorret_t ret = networkHttpRequest(
|
||||
NETWORK_HTTP_METHOD_GET,
|
||||
"http://frogfind.com",
|
||||
NULL, 0,
|
||||
NULL, 0,
|
||||
NULL, 0,
|
||||
engineHttpTestOnComplete,
|
||||
engineHttpTestOnError,
|
||||
NULL
|
||||
);
|
||||
errorCatch(errorPrint(ret));
|
||||
|
||||
ENGINE.networkDisconnectTestAt = TIME.time + 10.0f;
|
||||
ENGINE.networkDisconnectTestPending = true;
|
||||
}
|
||||
|
||||
void engineNetworkOnFailed(errorret_t error, void *user) {
|
||||
consolePrint("Network connection failed");
|
||||
errorCatch(errorPrint(error));
|
||||
}
|
||||
|
||||
void engineNetworkOnDisconnect(errorret_t error, void *user) {
|
||||
consolePrint("Network disconnected");
|
||||
errorCatch(errorPrint(error));
|
||||
}
|
||||
|
||||
void engineNetworkDisconnectTestOnComplete(void *user) {
|
||||
consolePrint("Network disconnect test complete");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ typedef struct {
|
||||
int32_t argc;
|
||||
const char_t **argv;
|
||||
const char_t *version;
|
||||
|
||||
// Test: disconnects the network 10 seconds after it connects.
|
||||
bool_t networkDisconnectTestPending;
|
||||
float_t networkDisconnectTestAt;
|
||||
} engine_t;
|
||||
|
||||
extern engine_t ENGINE;
|
||||
@@ -38,3 +42,51 @@ errorret_t engineUpdate(void);
|
||||
*/
|
||||
errorret_t engineDispose(void);
|
||||
|
||||
/**
|
||||
* Logs the response status, headers and body of the frogfind.com test
|
||||
* request to the console.
|
||||
*
|
||||
* @param params The completed networkhttprequest_t.
|
||||
* @param user Unused.
|
||||
*/
|
||||
void engineHttpTestOnComplete(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Logs that the frogfind.com test request failed.
|
||||
*
|
||||
* @param params The failed networkhttprequest_t.
|
||||
* @param user Unused.
|
||||
*/
|
||||
void engineHttpTestOnError(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Fires the frogfind.com test request once the network connection is
|
||||
* up (some platforms, such as PSP, only bring the network stack up
|
||||
* asynchronously after networkRequestConnection is called).
|
||||
*
|
||||
* @param user Unused.
|
||||
*/
|
||||
void engineNetworkOnConnected(void *user);
|
||||
|
||||
/**
|
||||
* Logs that the network connection could not be established.
|
||||
*
|
||||
* @param error The error describing why the connection failed.
|
||||
* @param user Unused.
|
||||
*/
|
||||
void engineNetworkOnFailed(errorret_t error, void *user);
|
||||
|
||||
/**
|
||||
* Logs that the network connection was lost after having connected.
|
||||
*
|
||||
* @param error The error describing why the connection was lost.
|
||||
* @param user Unused.
|
||||
*/
|
||||
void engineNetworkOnDisconnect(errorret_t error, void *user);
|
||||
|
||||
/**
|
||||
* Logs that the 10-second test disconnect completed.
|
||||
*
|
||||
* @param user Unused.
|
||||
*/
|
||||
void engineNetworkDisconnectTestOnComplete(void *user);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "util/string.h"
|
||||
#include "util/math.h"
|
||||
#include "time/time.h"
|
||||
#include "event/event.h"
|
||||
|
||||
input_t INPUT;
|
||||
|
||||
@@ -23,20 +22,6 @@ errorret_t inputInit(void) {
|
||||
INPUT.actions[i].action = (inputaction_t)i;
|
||||
INPUT.actions[i].lastValue = 0.0f;
|
||||
INPUT.actions[i].currentValue = 0.0f;
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onPressed,
|
||||
INPUT.actions[i].onPressedCallbacks,
|
||||
INPUT.actions[i].onPressedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onReleased,
|
||||
INPUT.actions[i].onReleasedCallbacks,
|
||||
INPUT.actions[i].onReleasedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
#ifdef inputInitPlatform
|
||||
@@ -98,18 +83,6 @@ void inputUpdate(void) {
|
||||
|
||||
cur++;
|
||||
}
|
||||
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
if(TIME.dynamicUpdate) return;
|
||||
#endif
|
||||
|
||||
for(uint8_t i = INPUT_ACTION_NULL + 1; i < INPUT_ACTION_COUNT; i++) {
|
||||
inputactiondata_t *act = &INPUT.actions[i];
|
||||
bool_t isDown = act->currentValue > 0.0f;
|
||||
bool_t wasDown = act->lastValue > 0.0f;
|
||||
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
|
||||
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
|
||||
}
|
||||
}
|
||||
|
||||
float_t inputGetCurrentValue(const inputaction_t action) {
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
#pragma once
|
||||
#include "time/time.h"
|
||||
#include "input/inputactiondefs.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
inputaction_t action;
|
||||
@@ -21,13 +18,6 @@ typedef struct {
|
||||
float_t lastDynamicValue;
|
||||
float_t currentDynamicValue;
|
||||
#endif
|
||||
|
||||
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onPressed;
|
||||
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onReleased;
|
||||
} inputactiondata_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
network.c
|
||||
networkinfo.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(http)
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
entityanim.c
|
||||
entityanimidle.c
|
||||
entityanimturn.c
|
||||
entityanimwalk.c
|
||||
entityanimrun.c
|
||||
networkhttpheader.c
|
||||
networkhttpurl.c
|
||||
networkhttprequest.c
|
||||
networkhttpprocess.c
|
||||
networkhttpthread.c
|
||||
networkhttp.c
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttp.h"
|
||||
#include "networkhttprequest.h"
|
||||
#include "networkhttpthread.h"
|
||||
|
||||
networkhttp_t NETWORK_HTTP;
|
||||
|
||||
errorret_t networkHttpInit(void) {
|
||||
networkHttpRequestPoolInit();
|
||||
|
||||
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
|
||||
threadStart(&NETWORK_HTTP.thread);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUpdate(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
const networkhttprequeststate_t state = request->state;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
|
||||
eventInvoke(&request->onComplete, request);
|
||||
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
|
||||
eventInvoke(&request->onError, request);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
networkHttpRequestReset(request);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpDispose(void) {
|
||||
threadStop(&NETWORK_HTTP.thread);
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
typedef struct {
|
||||
thread_t thread;
|
||||
} networkhttp_t;
|
||||
|
||||
extern networkhttp_t NETWORK_HTTP;
|
||||
|
||||
/**
|
||||
* Initializes the HTTP client: prepares the request pool and starts
|
||||
* the background worker thread. Called once during engine startup.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpInit(void);
|
||||
|
||||
/**
|
||||
* Dispatches any requests that finished on the background thread since
|
||||
* the last call: fires onComplete/onError on the main thread, frees
|
||||
* the response, and returns the slot to the pool. Call once per frame.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpUpdate(void);
|
||||
|
||||
/**
|
||||
* Stops the background worker thread and disposes of the HTTP client.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpDispose(void);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpheader.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
|
||||
memoryZero(list, sizeof(networkhttpheaderlist_t));
|
||||
}
|
||||
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name) {
|
||||
return
|
||||
stringCompareInsensitive(name, "Host") == 0 ||
|
||||
stringCompareInsensitive(name, "Connection") == 0 ||
|
||||
stringCompareInsensitive(name, "Content-Length") == 0;
|
||||
}
|
||||
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
) {
|
||||
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
|
||||
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
|
||||
|
||||
networkhttpheader_t *header = &list->headers[list->count];
|
||||
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
|
||||
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
|
||||
list->count++;
|
||||
}
|
||||
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
) {
|
||||
for(uint32_t i = 0; i < list->count; i++) {
|
||||
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
|
||||
return &list->headers[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
|
||||
#define NETWORK_HTTP_HEADER_NAME_MAX 64
|
||||
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
} networkhttpheader_t;
|
||||
|
||||
typedef struct {
|
||||
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
|
||||
uint32_t count;
|
||||
} networkhttpheaderlist_t;
|
||||
|
||||
/**
|
||||
* Clears a header list, removing all entries.
|
||||
*
|
||||
* @param list The header list to clear.
|
||||
*/
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
|
||||
|
||||
/**
|
||||
* Determines whether a header name is reserved, meaning it is computed
|
||||
* internally by the request writer and must not be set by the caller.
|
||||
* This is Host, Connection and Content-Length.
|
||||
*
|
||||
* @param name The header name to check, compared case-insensitively.
|
||||
* @return true if the header name is reserved.
|
||||
*/
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name);
|
||||
|
||||
/**
|
||||
* Appends a header to the list, silently doing nothing if the list is
|
||||
* already full or if skipReserved is true and the name is reserved (see
|
||||
* networkHttpHeaderIsReserved).
|
||||
*
|
||||
* @param list The header list to append to.
|
||||
* @param name The header name.
|
||||
* @param value The header value.
|
||||
* @param skipReserved If true, reserved header names are ignored.
|
||||
*/
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the first header in the list matching the given name, compared
|
||||
* case-insensitively.
|
||||
*
|
||||
* @param list The header list to search.
|
||||
* @param name The header name to search for.
|
||||
* @return Pointer to the matching header, or NULL if not found.
|
||||
*/
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
);
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
) {
|
||||
switch(method) {
|
||||
case NETWORK_HTTP_METHOD_POST: return "POST";
|
||||
case NETWORK_HTTP_METHOD_PUT: return "PUT";
|
||||
case NETWORK_HTTP_METHOD_GET:
|
||||
default:
|
||||
return "GET";
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
) {
|
||||
size_t cursor = 0;
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Connection: close\r\n"
|
||||
));
|
||||
|
||||
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
|
||||
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
char_t lengthStr[24];
|
||||
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Content-Length: "
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
*outLength = cursor;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
) {
|
||||
size_t sentTotal = 0;
|
||||
while(sentTotal < length) {
|
||||
size_t sent = 0;
|
||||
errorChain(networkSocketPlatformSend(
|
||||
sock, data + sentTotal, length - sentTotal, &sent
|
||||
));
|
||||
if(sent == 0) errorThrow("Connection closed while sending data");
|
||||
sentTotal += sent;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
) {
|
||||
size_t headerLength = 0;
|
||||
headerBuf[0] = '\0';
|
||||
|
||||
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
|
||||
for(;;) {
|
||||
size_t received = 0;
|
||||
errorChain(networkSocketPlatformReceive(
|
||||
sock, chunk, sizeof(chunk), &received
|
||||
));
|
||||
if(received == 0) {
|
||||
errorThrow("Connection closed before response headers completed");
|
||||
}
|
||||
|
||||
if(headerLength + received >= headerBufSize) {
|
||||
errorThrow("Response headers too large");
|
||||
}
|
||||
|
||||
memoryCopy(headerBuf + headerLength, chunk, received);
|
||||
headerLength += received;
|
||||
headerBuf[headerLength] = '\0';
|
||||
|
||||
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
|
||||
if(terminator == NULL) continue;
|
||||
|
||||
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
|
||||
const size_t bodyStart = terminatorOffset + 4;
|
||||
const size_t extra = headerLength - bodyStart;
|
||||
|
||||
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
|
||||
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
|
||||
*outLeftoverLength = extra;
|
||||
|
||||
// Keep the trailing "\r\n" of the last header line, drop the blank
|
||||
// line so callers can split on "\r\n" without a special case.
|
||||
headerBuf[terminatorOffset + 2] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
) {
|
||||
const char_t *space = strchr(line, ' ');
|
||||
if(space == NULL) errorThrow("Malformed status line: %s", line);
|
||||
|
||||
char_t statusStr[4];
|
||||
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
|
||||
memoryCopy(statusStr, space + 1, 3);
|
||||
statusStr[3] = '\0';
|
||||
|
||||
if(!stringToU16(statusStr, outStatus)) {
|
||||
errorThrow("Malformed status code: %s", line);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
const char_t *colon = strchr(line, ':');
|
||||
if(colon == NULL) return;
|
||||
|
||||
const size_t nameLength = (size_t)(colon - line);
|
||||
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
|
||||
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
memoryCopy(name, line, nameLength);
|
||||
name[nameLength] = '\0';
|
||||
|
||||
const char_t *valueStart = colon + 1;
|
||||
while(*valueStart == ' ') valueStart++;
|
||||
|
||||
const size_t valueLength = strlen(valueStart);
|
||||
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
|
||||
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
memoryCopy(value, valueStart, valueLength + 1);
|
||||
|
||||
networkHttpHeaderListAdd(outHeaders, name, value, false);
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
char_t *cursor = headerBuf;
|
||||
bool_t first = true;
|
||||
|
||||
for(;;) {
|
||||
char_t *lineEnd = strstr(cursor, "\r\n");
|
||||
const bool_t isLast = lineEnd == NULL;
|
||||
if(lineEnd != NULL) *lineEnd = '\0';
|
||||
|
||||
if(first) {
|
||||
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
|
||||
first = false;
|
||||
} else if(cursor[0] != '\0') {
|
||||
networkHttpProcessParseHeaderLine(cursor, outHeaders);
|
||||
}
|
||||
|
||||
if(isLast) break;
|
||||
cursor = lineEnd + 2;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
) {
|
||||
const networkhttpheader_t *contentLength =
|
||||
networkHttpHeaderListFind(headers, "Content-Length");
|
||||
|
||||
if(contentLength != NULL) {
|
||||
int64_t expected = 0;
|
||||
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
|
||||
errorThrow("Malformed Content-Length header: %s", contentLength->value);
|
||||
}
|
||||
|
||||
if(expected == 0) {
|
||||
*outBody = NULL;
|
||||
*outBodyLength = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *body = memoryAllocate((size_t)expected);
|
||||
const size_t initialCopy =
|
||||
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
|
||||
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
|
||||
size_t received = initialCopy;
|
||||
|
||||
while(received < (size_t)expected) {
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + received, (size_t)expected - received, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) {
|
||||
memoryFree(body);
|
||||
errorThrow("Connection closed before full body was received");
|
||||
}
|
||||
received += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = (size_t)expected;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// No Content-Length: read until the connection closes.
|
||||
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
|
||||
if(leftoverLength > capacity) capacity = leftoverLength;
|
||||
|
||||
uint8_t *body = memoryAllocate(capacity);
|
||||
size_t length = 0;
|
||||
if(leftoverLength > 0) {
|
||||
memoryCopy(body, leftover, leftoverLength);
|
||||
length = leftoverLength;
|
||||
}
|
||||
|
||||
for(;;) {
|
||||
if(length == capacity) {
|
||||
const size_t newCapacity = capacity * 2;
|
||||
memoryResize((void **)&body, capacity, newCapacity);
|
||||
capacity = newCapacity;
|
||||
}
|
||||
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + length, capacity - length, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) break;
|
||||
length += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = length;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
) {
|
||||
memoryZero(outResponse, sizeof(networkhttpresponse_t));
|
||||
|
||||
networksocketplatform_t sock;
|
||||
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
|
||||
|
||||
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
|
||||
size_t headLength = 0;
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
|
||||
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, (const uint8_t *)head, headLength
|
||||
));
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, request->body, request->bodyLength
|
||||
));
|
||||
}
|
||||
|
||||
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
|
||||
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
size_t leftoverLength = 0;
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
|
||||
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
|
||||
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
|
||||
headerBuf, &outResponse->status, &outResponse->headers
|
||||
));
|
||||
|
||||
errorret_t bodyRet = networkHttpProcessReadBody(
|
||||
&sock, &outResponse->headers, leftover, leftoverLength,
|
||||
&outResponse->body, &outResponse->bodyLength
|
||||
);
|
||||
networkSocketPlatformClose(&sock);
|
||||
errorChain(bodyRet);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
#include "network/networksocketplatform.h"
|
||||
|
||||
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
|
||||
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
|
||||
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
|
||||
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
|
||||
|
||||
/**
|
||||
* Shorthand to chain an error, closing sock first if expr failed. Used
|
||||
* for every step of an attempt after the socket has connected, since
|
||||
* failing partway through must not leak the socket.
|
||||
*/
|
||||
#define networkHttpProcessErrorChain(sock, _expr) { \
|
||||
errorret_t _nhpErr = (_expr); \
|
||||
if(errorIsNotOk(_nhpErr)) { \
|
||||
networkSocketPlatformClose(sock); \
|
||||
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wire method name ("GET", "POST", "PUT") for method.
|
||||
*
|
||||
* @param method The method to name.
|
||||
* @return A static string naming the method.
|
||||
*/
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds the request line, headers and blank line terminator (but not
|
||||
* the body) for request, targeting target, into dest.
|
||||
*
|
||||
* @param request The request being sent.
|
||||
* @param target The resolved host/port/path for this attempt.
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param outLength The number of bytes written is written here.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends length bytes of data over sock, looping until all of it has
|
||||
* been written.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @return An error if the send failed or the connection closed early.
|
||||
*/
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads from sock until the blank line ("\r\n\r\n") terminating the
|
||||
* response headers has been seen, writing the status line and header
|
||||
* lines (without the blank line) into headerBuf as a null-terminated
|
||||
* string. Any body bytes read past the terminator in the same receive
|
||||
* are written to leftover.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headerBuf The destination buffer for the header text.
|
||||
* @param headerBufSize The size of headerBuf, including the null
|
||||
* terminator.
|
||||
* @param leftover Destination buffer for any body bytes read early.
|
||||
* @param leftoverBufSize The size of leftover.
|
||||
* @param outLeftoverLength The number of bytes written to leftover is
|
||||
* written here.
|
||||
* @return An error if the headers could not be read, were malformed,
|
||||
* or did not fit within the given buffers.
|
||||
*/
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a "HTTP/1.1 <status> <reason>" status line.
|
||||
*
|
||||
* @param line The status line, null-terminated.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a single "Name: Value" response header line and appends it to
|
||||
* outHeaders. Silently does nothing if the line has no colon or the
|
||||
* name/value are too long to fit a networkhttpheader_t.
|
||||
*
|
||||
* @param line The header line, null-terminated.
|
||||
* @param outHeaders The header list to append to.
|
||||
*/
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses the status line and every header line out of headerBuf (as
|
||||
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
|
||||
* place to split it into lines.
|
||||
*
|
||||
* @param headerBuf The header text to parse.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @param outHeaders The header list to populate.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the response body from sock. If headers contains a
|
||||
* Content-Length, reads exactly that many bytes; otherwise reads until
|
||||
* the connection closes. Either way, leftover/leftoverLength (body
|
||||
* bytes already read while looking for the header terminator) are
|
||||
* included at the start of the result.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headers The parsed response headers.
|
||||
* @param leftover Body bytes already read past the header terminator.
|
||||
* @param leftoverLength The number of bytes in leftover.
|
||||
* @param outBody A memoryAllocate'd buffer holding the body is written
|
||||
* here, or NULL if the body is empty. Owned by the caller.
|
||||
* @param outBodyLength The number of bytes in *outBody is written here.
|
||||
* @return An error if the body could not be fully read.
|
||||
*/
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Performs a single blocking HTTP exchange: connects to target, writes
|
||||
* the request head + body, and reads back one parsed response. Does
|
||||
* not follow redirects -- that is the caller's responsibility.
|
||||
*
|
||||
* @param request The request to send (method, headers, body).
|
||||
* @param target The resolved host/port/path to connect to for this
|
||||
* attempt (may differ from the request's own URL if this is a
|
||||
* redirected attempt).
|
||||
* @param outResponse The parsed response is written here on success.
|
||||
* @return An error if the exchange could not be completed.
|
||||
*/
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttprequest.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
void networkHttpRequestPoolInit(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
memoryZero(request, sizeof(networkhttprequest_t));
|
||||
threadMutexInit(&request->mutex);
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
}
|
||||
}
|
||||
|
||||
networkhttprequest_t * networkHttpRequestClaim(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
threadMutexUnlock(&request->mutex);
|
||||
return request;
|
||||
}
|
||||
|
||||
assertUnreachable("No available HTTP request slots.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void networkHttpRequestReset(networkhttprequest_t *request) {
|
||||
assertNotNull(request, "request must not be NULL");
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
|
||||
if(request->body != NULL) memoryFree(request->body);
|
||||
request->body = NULL;
|
||||
request->bodyLength = 0;
|
||||
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
memoryZero(&request->response, sizeof(networkhttpresponse_t));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
|
||||
networkhttprequest_t *request = networkHttpRequestClaim();
|
||||
request->method = method;
|
||||
|
||||
errorChain(networkHttpUrlBuild(
|
||||
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
|
||||
));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
for(uint32_t i = 0; i < headerCount; i++) {
|
||||
networkHttpHeaderListAdd(
|
||||
&request->requestHeaders, headers[i].name, headers[i].value, true
|
||||
);
|
||||
}
|
||||
|
||||
request->bodyLength = bodyLength;
|
||||
if(bodyLength > 0) {
|
||||
assertNotNull(body, "body must not be NULL when bodyLength > 0");
|
||||
request->body = memoryAllocate(bodyLength);
|
||||
memoryCopy(request->body, body, bodyLength);
|
||||
}
|
||||
|
||||
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
|
||||
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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 "event/event.h"
|
||||
#include "thread/threadmutex.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
|
||||
#define NETWORK_HTTP_REQUEST_COUNT_MAX 4
|
||||
#define NETWORK_HTTP_REQUEST_EVENT_MAX 1
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_METHOD_GET,
|
||||
NETWORK_HTTP_METHOD_POST,
|
||||
NETWORK_HTTP_METHOD_PUT
|
||||
} networkhttpmethod_t;
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_REQUEST_STATE_FREE,
|
||||
NETWORK_HTTP_REQUEST_STATE_PENDING,
|
||||
NETWORK_HTTP_REQUEST_STATE_ACTIVE,
|
||||
NETWORK_HTTP_REQUEST_STATE_DONE,
|
||||
NETWORK_HTTP_REQUEST_STATE_ERROR
|
||||
} networkhttprequeststate_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t status;
|
||||
networkhttpheaderlist_t headers;
|
||||
|
||||
// Owned, memoryAllocate'd. Only valid until the onComplete/onError
|
||||
// event has finished firing, at which point it is freed.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
} networkhttpresponse_t;
|
||||
|
||||
typedef struct {
|
||||
threadmutex_t mutex;
|
||||
networkhttprequeststate_t state;
|
||||
|
||||
networkhttpmethod_t method;
|
||||
|
||||
// Fully built, including any query params, by networkHttpRequest.
|
||||
char_t url[NETWORK_HTTP_URL_MAX];
|
||||
networkhttpheaderlist_t requestHeaders;
|
||||
|
||||
// Owned copy, memoryAllocate'd.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
|
||||
/** Fired on the main thread once a response has been received. */
|
||||
event_t onComplete;
|
||||
eventcallback_t onCompleteCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onCompleteUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
/** Fired on the main thread if the request could not be completed. */
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onErrorUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
networkhttpresponse_t response;
|
||||
} networkhttprequest_t;
|
||||
|
||||
extern networkhttprequest_t
|
||||
NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the request pool: prepares every slot's mutex and events
|
||||
* and marks them all FREE. Called once by networkHttpInit.
|
||||
*/
|
||||
void networkHttpRequestPoolInit(void);
|
||||
|
||||
/**
|
||||
* Finds and returns a FREE slot in the request pool. The slot is not
|
||||
* marked as in-use by this call; the caller must populate it and then
|
||||
* set its state to PENDING.
|
||||
*
|
||||
* @return A free request slot.
|
||||
*/
|
||||
networkhttprequest_t * networkHttpRequestClaim(void);
|
||||
|
||||
/**
|
||||
* Resets a request slot back to FREE, freeing any owned buffers
|
||||
* (request body, response body) and clearing its event subscribers.
|
||||
*
|
||||
* @param request The request slot to reset.
|
||||
*/
|
||||
void networkHttpRequestReset(networkhttprequest_t *request);
|
||||
|
||||
/**
|
||||
* Sends an HTTP request asynchronously. The request runs on a
|
||||
* background thread; onComplete or onError is invoked on the main
|
||||
* thread (from networkHttpUpdate) once it finishes. 301 redirects are
|
||||
* followed automatically.
|
||||
*
|
||||
* @param method The HTTP method to use.
|
||||
* @param url The "http://host[:port]/path" URL to request. Must not
|
||||
* use any scheme other than http.
|
||||
* @param headers Request headers to send, may be NULL if headerCount
|
||||
* is 0. The Host, Connection and Content-Length headers are
|
||||
* reserved and computed internally; any of those passed here
|
||||
* are ignored.
|
||||
* @param headerCount Number of entries in headers.
|
||||
* @param queryParams Query string parameters to append to url, may be
|
||||
* NULL if queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @param body Request body to send for POST/PUT, may be NULL if
|
||||
* bodyLength is 0. Ignored for GET.
|
||||
* @param bodyLength Number of bytes in body.
|
||||
* @param onComplete Invoked with (networkhttprequest_t *, user) once a
|
||||
* response has been received, whatever its status code. May be
|
||||
* NULL to ignore.
|
||||
* @param onError Invoked with (networkhttprequest_t *, user) if the
|
||||
* request could not be completed (DNS/connect/send/receive
|
||||
* failure, or a malformed response). May be NULL to ignore.
|
||||
* @param user Arbitrary pointer forwarded to onComplete/onError.
|
||||
* @return An error if the request could not be queued (e.g. a
|
||||
* malformed url).
|
||||
*/
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
);
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpthread.h"
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include <unistd.h>
|
||||
|
||||
void networkHttpThreadRun(thread_t *thread) {
|
||||
assertNotMainThread("networkHttpThreadRun must not run on the main thread.");
|
||||
|
||||
while(!threadShouldStop(thread)) {
|
||||
bool_t didWork = false;
|
||||
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_PENDING) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_ACTIVE;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
didWork = true;
|
||||
networkHttpThreadProcessRequest(request);
|
||||
}
|
||||
|
||||
if(threadShouldStop(thread)) break;
|
||||
if(!didWork) usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request) {
|
||||
networkhttpurl_t target;
|
||||
errorret_t parseRet = networkHttpUrlParse(request->url, &target);
|
||||
|
||||
networkhttprequeststate_t finalState = NETWORK_HTTP_REQUEST_STATE_DONE;
|
||||
|
||||
if(errorIsNotOk(parseRet)) {
|
||||
errorCatch(errorPrint(parseRet));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
} else {
|
||||
for(uint32_t redirect = 0; ; redirect++) {
|
||||
errorret_t ret = networkHttpProcessAttempt(
|
||||
request, &target, &request->response
|
||||
);
|
||||
|
||||
if(errorIsNotOk(ret)) {
|
||||
errorCatch(errorPrint(ret));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
const bool_t isRedirect = request->response.status == 301;
|
||||
const networkhttpheader_t *location = isRedirect ?
|
||||
networkHttpHeaderListFind(&request->response.headers, "Location") :
|
||||
NULL;
|
||||
|
||||
if(location == NULL || redirect >= NETWORK_HTTP_REDIRECT_COUNT_MAX) {
|
||||
break;
|
||||
}
|
||||
|
||||
networkhttpurl_t nextTarget;
|
||||
errorret_t nextRet = networkHttpUrlParse(location->value, &nextTarget);
|
||||
if(errorIsNotOk(nextRet)) {
|
||||
// Not an absolute URL we can follow -- treat the 301 itself as
|
||||
// the final response instead of failing the whole request.
|
||||
errorCatch(errorPrint(nextRet));
|
||||
break;
|
||||
}
|
||||
|
||||
// Free the intermediate response body before following further.
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
target = nextTarget;
|
||||
}
|
||||
}
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = finalState;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "thread/thread.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
|
||||
#define NETWORK_HTTP_REDIRECT_COUNT_MAX 5
|
||||
|
||||
/**
|
||||
* The background worker thread body: repeatedly scans the request
|
||||
* pool for PENDING slots, processes each to completion, and marks it
|
||||
* DONE or ERROR. Idles briefly when there is nothing to do. Runs
|
||||
* until the thread is stopped.
|
||||
*
|
||||
* @param thread The thread runner.
|
||||
*/
|
||||
void networkHttpThreadRun(thread_t *thread);
|
||||
|
||||
/**
|
||||
* Processes a single ACTIVE request slot to completion, following up
|
||||
* to NETWORK_HTTP_REDIRECT_COUNT_MAX HTTP 301 redirects, and leaves it
|
||||
* in the DONE or ERROR state. Only called from the background thread.
|
||||
*
|
||||
* @param request The request slot to process.
|
||||
*/
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request);
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpurl.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
assertNotNull(out, "out must not be NULL");
|
||||
|
||||
if(strlen(url) <= 7 || strncasecmp(url, "http://", 7) != 0) {
|
||||
errorThrow("Unsupported URL scheme (only http:// is supported): %s", url);
|
||||
}
|
||||
|
||||
const char_t *rest = url + 7;
|
||||
|
||||
const size_t hostLen = strcspn(rest, ":/");
|
||||
if(hostLen == 0 || hostLen >= NETWORK_HTTP_HOST_MAX) {
|
||||
errorThrow("Invalid host in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->host, rest, hostLen);
|
||||
out->host[hostLen] = '\0';
|
||||
rest += hostLen;
|
||||
|
||||
if(*rest == ':') {
|
||||
rest++;
|
||||
|
||||
const size_t portLen = strcspn(rest, "/");
|
||||
char_t portStr[8];
|
||||
if(portLen == 0 || portLen >= sizeof(portStr)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
memoryCopy(portStr, rest, portLen);
|
||||
portStr[portLen] = '\0';
|
||||
|
||||
if(!stringToU16(portStr, &out->port)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
rest += portLen;
|
||||
} else {
|
||||
out->port = NETWORK_HTTP_PORT_DEFAULT;
|
||||
}
|
||||
|
||||
const char_t *path = *rest == '\0' ? "/" : rest;
|
||||
const size_t pathLen = strlen(path);
|
||||
if(pathLen >= NETWORK_HTTP_PATH_MAX) {
|
||||
errorThrow("Path too long in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->path, path, pathLen + 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
) {
|
||||
assertNotNull(dest, "dest must not be NULL");
|
||||
assertNotNull(baseUrl, "baseUrl must not be NULL");
|
||||
|
||||
const size_t baseLen = strlen(baseUrl);
|
||||
if(baseLen >= destSize) errorThrow("URL too long: %s", baseUrl);
|
||||
memoryCopy(dest, baseUrl, baseLen + 1);
|
||||
|
||||
size_t cursor = baseLen;
|
||||
bool_t hasQuery = stringIncludesString(baseUrl, "?");
|
||||
|
||||
for(uint32_t i = 0; i < queryParamCount; i++) {
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, hasQuery ? "&" : "?"
|
||||
));
|
||||
hasQuery = true;
|
||||
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].name
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "="));
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].value
|
||||
));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
) {
|
||||
const size_t len = strlen(str);
|
||||
if(*cursor + len >= destSize) errorThrow("URL buffer too small");
|
||||
|
||||
memoryCopy(dest + *cursor, str, len + 1);
|
||||
*cursor += len;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
) {
|
||||
const char_t *hex = "0123456789ABCDEF";
|
||||
|
||||
for(const char_t *p = src; *p != '\0'; p++) {
|
||||
const uint8_t c = (uint8_t)*p;
|
||||
const bool_t unreserved =
|
||||
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
|
||||
|
||||
if(unreserved) {
|
||||
if(*cursor + 1 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = (char_t)c;
|
||||
} else {
|
||||
if(*cursor + 3 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = '%';
|
||||
dest[(*cursor)++] = hex[(c >> 4) & 0xF];
|
||||
dest[(*cursor)++] = hex[c & 0xF];
|
||||
}
|
||||
}
|
||||
|
||||
dest[*cursor] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
|
||||
#define NETWORK_HTTP_URL_MAX 1024
|
||||
#define NETWORK_HTTP_HOST_MAX 256
|
||||
#define NETWORK_HTTP_PATH_MAX 768
|
||||
#define NETWORK_HTTP_PORT_DEFAULT 80
|
||||
|
||||
typedef struct {
|
||||
char_t host[NETWORK_HTTP_HOST_MAX];
|
||||
uint16_t port;
|
||||
|
||||
// Includes the leading slash and, if present, the query string.
|
||||
char_t path[NETWORK_HTTP_PATH_MAX];
|
||||
} networkhttpurl_t;
|
||||
|
||||
/**
|
||||
* Parses a "http://host[:port]/path[?query]" URL. Any other scheme is
|
||||
* rejected, since this client does not support TLS.
|
||||
*
|
||||
* @param url The URL to parse.
|
||||
* @param out The parsed URL is written here.
|
||||
* @return An error if the URL is malformed or uses an unsupported scheme.
|
||||
*/
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out);
|
||||
|
||||
/**
|
||||
* Copies baseUrl into dest, then appends each queryParam as a
|
||||
* percent-encoded "key=value" pair, joined with "&" (or "?" for the
|
||||
* first one, unless baseUrl already contains a "?").
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param baseUrl The URL to copy before appending query params.
|
||||
* @param queryParams Array of query parameters to append, may be NULL if
|
||||
* queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Appends a raw (not percent-encoded) string to dest at *cursor,
|
||||
* advancing *cursor and keeping dest null-terminated.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param str The string to append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
);
|
||||
|
||||
/**
|
||||
* Percent-encodes src and appends it to dest at *cursor, advancing
|
||||
* *cursor and keeping dest null-terminated. Unreserved characters
|
||||
* (letters, digits, "-", "_", ".", "~") are copied as-is.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param src The string to percent-encode and append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
);
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "network.h"
|
||||
#include "network/http/networkhttp.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "log/log.h"
|
||||
@@ -18,11 +19,13 @@ errorret_t networkInit() {
|
||||
NETWORK.errorState.code = ERROR_OK;
|
||||
NETWORK.onDisconnect = NULL;
|
||||
|
||||
return networkPlatformInit();
|
||||
errorChain(networkPlatformInit());
|
||||
return networkHttpInit();
|
||||
}
|
||||
|
||||
errorret_t networkUpdate() {
|
||||
errorChain(networkPlatformUpdate());
|
||||
errorChain(networkHttpUpdate());
|
||||
|
||||
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
|
||||
NETWORK.state = NETWORK_STATE_DISCONNECTED;
|
||||
@@ -112,5 +115,5 @@ errorret_t networkDispose() {
|
||||
}
|
||||
|
||||
errorChain(networkPlatformDispose());
|
||||
errorOk();
|
||||
return networkHttpDispose();
|
||||
}
|
||||
|
||||
@@ -11,10 +11,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(cutscene)
|
||||
add_subdirectory(entity)
|
||||
add_subdirectory(overworld)
|
||||
|
||||
add_subdirectory(story)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(physics)
|
||||
@@ -1,12 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battle.c
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battle.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
void battleInit(void) {
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
BATTLE.fighters[i].id = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t battleGetAvailableFighter(void) {
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = battleGetAvailableFighter();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[index];
|
||||
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
|
||||
return fighter;
|
||||
}
|
||||
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
) {
|
||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||
|
||||
BATTLE.encounterType = encounterType;
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(true);
|
||||
|
||||
BATTLE.active = true;
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
battleInit();
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(!BATTLE.active) return NULL;
|
||||
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].team != team) continue;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
) {
|
||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
||||
assertNotNull(defender, "Defender cannot be NULL");
|
||||
|
||||
const int32_t rawDamage =
|
||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
||||
|
||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleNextTurn(void) {
|
||||
BATTLE.turnIndex++;
|
||||
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
||||
|
||||
BATTLE.round++;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(false);
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_LOSS;
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_WIN;
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *attacker = battleGetCurrentFighter();
|
||||
if(attacker == NULL) return;
|
||||
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
|
||||
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
||||
if(!battleFighterIsAlive(defender)) return;
|
||||
|
||||
battleResolveAttack(attacker, defender);
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(!BATTLE.active) return;
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
|
||||
if(!battleFighterIsAlive(current)) {
|
||||
battleNextTurn();
|
||||
return;
|
||||
}
|
||||
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||
|
||||
battlefighter_t *target = battleAIChooseTarget(current);
|
||||
if(target != NULL) battleResolveAttack(current, target);
|
||||
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.turnCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
||||
const uint8_t key = BATTLE.turnOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
||||
j--;
|
||||
}
|
||||
BATTLE.turnOrder[j + 1] = key;
|
||||
}
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
|
||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
||||
}
|
||||
}
|
||||
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
}
|
||||
|
||||
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
battlefighter_t *weakest = NULL;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
if(weakest == NULL || candidate->health < weakest->health) {
|
||||
weakest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return weakest;
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
|
||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ENCOUNTER_REGULAR,
|
||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
||||
|
||||
BATTLE_ENCOUNTER_COUNT
|
||||
} battleencountertype_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_RESULT_NONE,
|
||||
BATTLE_RESULT_WIN,
|
||||
BATTLE_RESULT_LOSS,
|
||||
BATTLE_RESULT_FLED,
|
||||
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
typedef struct {
|
||||
bool_t active;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t turnCount;
|
||||
uint8_t turnIndex;
|
||||
uint16_t round;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
|
||||
/**
|
||||
* Initializes the battle system. Marks it as inactive with no fighters.
|
||||
*/
|
||||
void battleInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) fighter slot index.
|
||||
*
|
||||
* @return The index of an available fighter slot, or 0xFF if none are
|
||||
* available.
|
||||
*/
|
||||
uint8_t battleGetAvailableFighter(void);
|
||||
|
||||
/**
|
||||
* Adds a fighter to the battle in the next available slot.
|
||||
*
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
* @return Pointer to the newly added fighter, or NULL if the battle is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: builds the opening turn order (biased by
|
||||
* encounterType for the first round only) and marks the battle active.
|
||||
* Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's turn order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
*/
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter whose turn it currently is.
|
||||
*
|
||||
* @return Pointer to the active fighter, or NULL if the battle isn't
|
||||
* active or has no living fighters left to act.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Returns the number of living fighters on a team.
|
||||
*
|
||||
* @param team The team to count.
|
||||
* @return Count of living fighters on that team.
|
||||
*/
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Resolves a physical attack from attacker onto defender: damage is the
|
||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
||||
* subtracted from the defender's health. The defender is marked dead
|
||||
* once health reaches 0.
|
||||
*
|
||||
* @param attacker The attacking fighter.
|
||||
* @param defender The defending fighter.
|
||||
*/
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
);
|
||||
|
||||
/**
|
||||
* Ends the current fighter's turn and advances to the next fighter in
|
||||
* the turn order, starting a new round (rebuilding turn order purely by
|
||||
* speed) once every fighter in the current round has acted.
|
||||
*/
|
||||
void battleNextTurn(void);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, updating and
|
||||
* returning BATTLE.result. Does nothing if a result has already been
|
||||
* set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Submits the current fighter's attack against a target, if it is
|
||||
* currently a player-controlled fighter's turn. Resolves the attack,
|
||||
* checks for a battle result, and advances the turn.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the current fighter's turn, if it is
|
||||
* currently a player-controlled fighter's turn and fleeing is
|
||||
* available for this battle. Always succeeds, ending the battle with
|
||||
* BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame: resolves the current
|
||||
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
||||
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
||||
* battle isn't active or already has a result.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.turnOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the turn order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
||||
* the opposing team.
|
||||
*
|
||||
* @param fighter The AI-controlled fighter choosing a target.
|
||||
* @return The chosen target, or NULL if the opposing team has no
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->healthMax = healthMax;
|
||||
fighter->health = healthMax;
|
||||
fighter->mpMax = mpMax;
|
||||
fighter->mp = mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
@@ -1,87 +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"
|
||||
|
||||
// An empty status means the slot in BATTLE.fighters is unused.
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
|
||||
// Base combat stats, kept separate from the resource pools (health/mp) on
|
||||
// battlefighter_t so that equipment/buffs can later modify them without
|
||||
// touching current health/mp state.
|
||||
typedef struct {
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
uint16_t health;
|
||||
uint16_t healthMax;
|
||||
uint16_t mp;
|
||||
uint16_t mpMax;
|
||||
|
||||
battlefighterstats_t stats;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to their maximum
|
||||
* values and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "party.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
party_t PARTY;
|
||||
|
||||
void partyInit(void) {
|
||||
memoryZero(&PARTY, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
PARTY.members[i].id = i;
|
||||
}
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
PARTY.order[i] = PARTY_ORDER_EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t partyGetAvailableMember(void) {
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = partyGetAvailableMember();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *member = &PARTY.members[index];
|
||||
battleFighterInit(
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
stats, healthMax, mpMax
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
|
||||
PARTY.order[i] = index;
|
||||
break;
|
||||
}
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
|
||||
const uint8_t index = PARTY.order[slot];
|
||||
if(index == PARTY_ORDER_EMPTY) return NULL;
|
||||
return &PARTY.members[index];
|
||||
}
|
||||
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
assertTrue(
|
||||
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
|
||||
"Invalid party member index"
|
||||
);
|
||||
|
||||
PARTY.order[slot] = memberIndex;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
|
||||
#define PARTY_MEMBER_COUNT_MAX 4
|
||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||
#define PARTY_ORDER_EMPTY 0xFF
|
||||
|
||||
typedef struct {
|
||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
||||
|
||||
// Maps an active battle slot to the roster member filling it, or
|
||||
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
|
||||
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
|
||||
// can be in the active lineup at once.
|
||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
||||
} party_t;
|
||||
|
||||
extern party_t PARTY;
|
||||
|
||||
/**
|
||||
* Initializes the party system with an empty roster and order.
|
||||
*/
|
||||
void partyInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) party member slot index.
|
||||
*
|
||||
* @return The index of an available slot, or 0xFF if the party is full.
|
||||
*/
|
||||
uint8_t partyGetAvailableMember(void);
|
||||
|
||||
/**
|
||||
* Adds a member to the party roster in the next available slot. Party
|
||||
* members are always allies controlled by the player. If there is a
|
||||
* free active order slot, the new member is placed into it.
|
||||
*
|
||||
* @param stats The member's base combat stats.
|
||||
* @param healthMax The member's maximum health.
|
||||
* @param mpMax The member's maximum mp.
|
||||
* @return Pointer to the newly added party member, or NULL if the party is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot.
|
||||
*
|
||||
* @param slot The active order slot to query.
|
||||
* @return Pointer to the member in that slot, or NULL if the slot is
|
||||
* empty.
|
||||
*/
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Assigns a roster member to an active order slot, replacing whatever
|
||||
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
|
||||
*
|
||||
* @param slot The active order slot to assign.
|
||||
* @param memberIndex The roster member index to place there, or
|
||||
* PARTY_ORDER_EMPTY to clear the slot.
|
||||
*/
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
||||
@@ -6,7 +6,6 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscene.c
|
||||
cutscenesystem.c
|
||||
)
|
||||
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
/**
|
||||
* 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,8 +9,6 @@
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenepause.h"
|
||||
|
||||
typedef struct yyjson_val yyjson_val;
|
||||
|
||||
typedef struct cutscene_s {
|
||||
const cutsceneitem_t *items;
|
||||
uint8_t itemCount;
|
||||
@@ -167,10 +165,10 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||
|
||||
#define CUTSCENE_ITEM_GIVE(ITEM_NAME, QUANTITY) \
|
||||
#define CUTSCENE_ITEM_GIVE(ITEM_ID, QUANTITY) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ITEM_GIVE, \
|
||||
.itemGive = { .itemName = ITEM_NAME, .quantity = QUANTITY } \
|
||||
.itemGive = { .item = ITEM_ID, .quantity = QUANTITY } \
|
||||
}
|
||||
|
||||
// Runs all listed items simultaneously and waits until all are done.
|
||||
@@ -232,118 +230,3 @@ typedef struct cutscene_s {
|
||||
), \
|
||||
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
|
||||
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
|
||||
|
||||
#define CUTSCENE_JSON_NAME_MAX 32
|
||||
#define CUTSCENE_JSON_ITEM_COUNT_MAX 32
|
||||
#define CUTSCENE_JSON_WAYPOINT_COUNT_MAX 8
|
||||
#define CUTSCENE_JSON_CACHE_MAX 16
|
||||
|
||||
// One slot of the cutsceneGetByName cache: a parsed cutscene plus the
|
||||
// backing storage its items point into (items themselves, and the
|
||||
// waypoint lists any "entityWalkTo" items reference).
|
||||
typedef struct {
|
||||
char_t name[CUTSCENE_JSON_NAME_MAX];
|
||||
cutscene_t cutscene;
|
||||
cutsceneitem_t items[CUTSCENE_JSON_ITEM_COUNT_MAX];
|
||||
worldpos_t waypoints
|
||||
[CUTSCENE_JSON_ITEM_COUNT_MAX][CUTSCENE_JSON_WAYPOINT_COUNT_MAX];
|
||||
} cutscenejsoncacheentry_t;
|
||||
|
||||
/**
|
||||
* Parses a cutscene pause value ("NONE", "DEFAULT" or "ALL") from a
|
||||
* yyjson string value.
|
||||
*
|
||||
* @param val The yyjson value to parse.
|
||||
* @param outPause Output pointer, set to the parsed pause flags.
|
||||
* @return Any error that occurs (missing/invalid/unknown value).
|
||||
*/
|
||||
errorret_t cutsceneJsonParsePause(yyjson_val *val, cutscenepause_t *outPause);
|
||||
|
||||
/**
|
||||
* Parses an entityIndex-shaped yyjson value - a raw number, or one of
|
||||
* "interact", "interacted", "lastCreated" - into a raw uint8_t index or
|
||||
* the matching CUTSCENE_ENTITY_* sentinel (see cutscenesystem.h).
|
||||
*
|
||||
* @param val The yyjson value to parse.
|
||||
* @param outIndex Output pointer, set to the parsed index.
|
||||
* @return Any error that occurs (missing/invalid/unknown value).
|
||||
*/
|
||||
errorret_t cutsceneJsonParseEntityIndex(yyjson_val *val, uint8_t *outIndex);
|
||||
|
||||
/**
|
||||
* Parses a [x, y, z] yyjson array into a worldpos_t.
|
||||
*
|
||||
* @param val The yyjson value to parse.
|
||||
* @param outPos Output pointer, set to the parsed position.
|
||||
* @return Any error that occurs (missing/invalid value).
|
||||
*/
|
||||
errorret_t cutsceneJsonParseWorldPos(yyjson_val *val, worldpos_t *outPos);
|
||||
|
||||
/**
|
||||
* Parses a single cutscene item from a yyjson object into outItem.
|
||||
* "type" selects the item shape (required):
|
||||
* { "type": "text", "text": "Hello!" }
|
||||
* { "type": "textMini", "text": "Hi", "position": [x, y, z],
|
||||
* "duration": 3.0 }
|
||||
* { "type": "textMiniHide", "index": 0 }
|
||||
* { "type": "wait", "duration": 1.5 }
|
||||
* { "type": "entityAdd", "entityType": "npc", "position": [x, y, z] }
|
||||
* { "type": "entityRemove", "entityIndex": 0 }
|
||||
* { "type": "entityTurn", "entityIndex": 0, "direction": "south" }
|
||||
* { "type": "entityWalkTo", "entityIndex": 0,
|
||||
* "positions": [[x, y, z], ...], "walkAround": true }
|
||||
* { "type": "entityWalkToEntity", "entityIndex": 0,
|
||||
* "targetEntityIndex": 1, "offsetX": 1, "offsetY": 0 }
|
||||
* { "type": "entityTeleport", "entityIndex": 0, "position": [x, y, z] }
|
||||
* { "type": "emoji", "entityIndex": 0, "emojiType": "exclamation",
|
||||
* "duration": 2.0 }
|
||||
* { "type": "shake", "amount": 2, "duration": 0.5 }
|
||||
* { "type": "setPause", "pause": "ALL" }
|
||||
* "entityIndex"/"targetEntityIndex" accept a raw number, or one of
|
||||
* "interact", "interacted", "lastCreated" for the matching
|
||||
* CUTSCENE_ENTITY_* sentinel. "entityType" uses the same strings as
|
||||
* entityCreateFromJson ("player", "npc", "item"); "direction" is one of
|
||||
* "north"/"east"/"south"/"west"; "emojiType" is "question" or
|
||||
* "exclamation"; "pause" is "NONE", "DEFAULT" or "ALL".
|
||||
*
|
||||
* Not supported (would need persistent string/array storage or function
|
||||
* pointers this parser doesn't provide): itemGive, concurrent, fade,
|
||||
* map area items, callbacks, nested cutscene references.
|
||||
*
|
||||
* @param obj The yyjson object describing a single cutscene item.
|
||||
* @param outItem Output pointer, filled with the parsed item.
|
||||
* @param waypoints Backing storage for an "entityWalkTo" item's waypoint
|
||||
* list; must remain valid for as long as the parsed item is used.
|
||||
* Unused (may be NULL) for any other item type.
|
||||
* @param waypointsMax Capacity of waypoints.
|
||||
* @return Any error that occurs (missing/invalid/unknown fields).
|
||||
*/
|
||||
errorret_t cutsceneItemCreateFromJson(
|
||||
yyjson_val *obj,
|
||||
cutsceneitem_t *outItem,
|
||||
worldpos_t *waypoints,
|
||||
const uint8_t waypointsMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Loads (or returns the cached result of an earlier load of) the named
|
||||
* cutscene from assets/cutscene/<name>.json, parsing each entry of its
|
||||
* "items" array with cutsceneItemCreateFromJson. Parsed once per name
|
||||
* and cached for the lifetime of the process; holds at most
|
||||
* CUTSCENE_JSON_CACHE_MAX distinct names, each with at most
|
||||
* CUTSCENE_JSON_ITEM_COUNT_MAX items.
|
||||
*
|
||||
* File shape:
|
||||
* { "pause": "DEFAULT", "items": [ { "type": "text", ... }, ... ] }
|
||||
* "pause" is optional (one of "NONE", "DEFAULT", "ALL"; defaults to
|
||||
* "DEFAULT" when absent).
|
||||
*
|
||||
* @param name The cutscene's file name (without extension), under
|
||||
* assets/cutscene/.
|
||||
* @param outCutscene Output pointer, set to the loaded cutscene.
|
||||
* @return Any error that occurs (missing/malformed file, cache full,
|
||||
* unsupported/invalid item).
|
||||
*/
|
||||
errorret_t cutsceneGetByName(
|
||||
const char_t *name, const cutscene_t **outCutscene
|
||||
);
|
||||
|
||||
@@ -14,4 +14,3 @@ add_subdirectory(entity)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(maparea)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(battle)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenestartbattle_t *config = &item->startBattle;
|
||||
|
||||
battleInit();
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
battlefighter_t *member = partyGetOrderMember(i);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
member->stats, member->healthMax, member->mpMax
|
||||
);
|
||||
if(fighter == NULL) continue;
|
||||
|
||||
fighter->health = member->health;
|
||||
fighter->mp = member->mp;
|
||||
fighter->status = member->status;
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
||||
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemy->stats, enemy->healthMax, enemy->mpMax
|
||||
);
|
||||
}
|
||||
|
||||
battleStart(config->encounterType, config->fleeAvailable);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
|
||||
|
||||
// Sync ally HP/MP back to the persistent party roster. Relies on
|
||||
// ally fighters having been added to BATTLE.fighters in the same
|
||||
// order partyGetOrderMember() iterates, starting at index 0 (see
|
||||
// cutsceneStartBattleStart).
|
||||
uint8_t allySlot = 0;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
battlefighter_t *member = partyGetOrderMember(i);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
||||
member->health = fighter->health;
|
||||
member->mp = fighter->mp;
|
||||
member->status = fighter->status;
|
||||
}
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
battleDispose();
|
||||
return true;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
battlefighterstats_t stats;
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
} cutscenestartbattleenemy_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
uint8_t enemyCount;
|
||||
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
|
||||
} cutscenestartbattle_t;
|
||||
|
||||
/**
|
||||
* Starts a battle: seeds BATTLE with the party's active order members
|
||||
* and the item's configured enemies, then switches to the battle
|
||||
* scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Waits for the battle to produce a result, syncs ally HP/MP back to
|
||||
* the party roster, then returns to the overworld scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once the battle has ended.
|
||||
*/
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -105,11 +105,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.update = cutsceneMapAreaWaitUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
|
||||
.init = cutsceneStartBattleStart,
|
||||
.update = cutsceneStartBattleUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
||||
.init = cutsceneEmojiStart,
|
||||
.update = cutsceneEmojiUpdate
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
@@ -52,7 +51,6 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
|
||||
@@ -82,7 +80,6 @@ struct cutsceneitem_s {
|
||||
cutscenemapareaadd_t mapAreaAdd;
|
||||
cutscenemaparearemove_t mapAreaRemove;
|
||||
cutscenemapareawait_t mapAreaWait;
|
||||
cutscenestartbattle_t startBattle;
|
||||
cutsceneemoji_t emoji;
|
||||
cutsceneshake_t shake;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/entity/entitypathstep.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
|
||||
void cutsceneEntityWalkToEntityStart(
|
||||
const cutsceneitem_t *item,
|
||||
@@ -35,7 +35,7 @@ bool_t cutsceneEntityWalkToEntityUpdate(
|
||||
};
|
||||
|
||||
worldunit_t z;
|
||||
if(chunkGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
||||
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
|
||||
|
||||
return entityPathStep(entity, dest, true);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/item/item.h"
|
||||
#include "rpg/item/itemgive.h"
|
||||
#include "ui/rpg/textbox/uitextboxmain.h"
|
||||
|
||||
@@ -14,8 +13,7 @@ void cutsceneItemGiveStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
itemid_t itemId = itemGetIdByName(item->itemGive.itemName);
|
||||
itemGive(itemId, item->itemGive.quantity);
|
||||
itemGive(item->itemGive.item, item->itemGive.quantity);
|
||||
}
|
||||
|
||||
bool_t cutsceneItemGiveUpdate(
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
const char_t *itemName;
|
||||
itemid_t item;
|
||||
uint8_t quantity;
|
||||
} cutsceneitemgive_t;
|
||||
|
||||
|
||||
@@ -11,4 +11,21 @@
|
||||
|
||||
CUTSCENE(TEST_ONE, 0, DEFAULT,
|
||||
CUTSCENE_TEXT("Test One."),
|
||||
);
|
||||
|
||||
CUTSCENE(TEST_TWO, 0, DEFAULT,
|
||||
CUTSCENE_TEXT("Test Two."),
|
||||
CUTSCENE_ENTITY_ADD(ENTITY_TYPE_NPC, 4, 4, 0),
|
||||
CUTSCENE_TEXT_MINI("Hello!", 4, 4, 0, 3.0f),
|
||||
CUTSCENE_EMOJI(
|
||||
CUTSCENE_ENTITY_LAST_CREATED, UI_EMOJI_EXCLAMATION_MARK, 2.0f
|
||||
),
|
||||
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_LAST_CREATED, 8, 2, 0),
|
||||
// CUTSCENE_CONCURRENT(
|
||||
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
|
||||
// CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0),
|
||||
// ),
|
||||
// CUTSCENE_ITEM_GIVE(ITEM_ID_POTATO, 3),
|
||||
// CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
|
||||
CUTSCENE_TEXT("Done."),
|
||||
);
|
||||
@@ -12,7 +12,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
player.c
|
||||
)
|
||||
|
||||
add_subdirectory(anim)
|
||||
add_subdirectory(interact)
|
||||
add_subdirectory(npc)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(global)
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "time/time.h"
|
||||
#include "entityanimwalk.h"
|
||||
|
||||
const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
|
||||
[ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
|
||||
[ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
|
||||
[ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
|
||||
[ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
|
||||
};
|
||||
|
||||
float_t entityAnimTileZOffset(const worldpos_t pos) {
|
||||
return tileShapeIsRamp(chunkGetTile(pos).shape) ? 0.5f : 0.0f;
|
||||
}
|
||||
|
||||
void entityAnimUpdate(entity_t *entity) {
|
||||
if(entity->animation != ENTITY_ANIM_IDLE) {
|
||||
entity->animTime -= TIME.delta;
|
||||
if(entity->animTime <= 0) {
|
||||
if(
|
||||
entity->animation == ENTITY_ANIM_WALK ||
|
||||
entity->animation == ENTITY_ANIM_RUN
|
||||
) {
|
||||
entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
|
||||
}
|
||||
entity->animation = ENTITY_ANIM_IDLE;
|
||||
entity->animTime = 0;
|
||||
}
|
||||
}
|
||||
if(entity->walkEndCooldown > 0) {
|
||||
entity->walkEndCooldown -= TIME.delta;
|
||||
if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0;
|
||||
}
|
||||
ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "entityanimidle.h"
|
||||
#include "entityanimturn.h"
|
||||
#include "entityanimwalk.h"
|
||||
#include "entityanimrun.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
typedef enum {
|
||||
ENTITY_ANIM_IDLE,
|
||||
ENTITY_ANIM_TURN,
|
||||
ENTITY_ANIM_WALK,
|
||||
ENTITY_ANIM_RUN,
|
||||
ENTITY_ANIM_COUNT
|
||||
} entityanim_t;
|
||||
|
||||
typedef struct {
|
||||
/** Updates the render position for this animation state. */
|
||||
void (*update)(entity_t *entity);
|
||||
} entityanimcallback_t;
|
||||
|
||||
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
|
||||
|
||||
/**
|
||||
* Updates the entity animation timer and render position.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimUpdate(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
|
||||
* Used to lift entity render position to mid-ramp height.
|
||||
*
|
||||
* @param pos World position to sample.
|
||||
* @returns float_t The Z offset to apply.
|
||||
*/
|
||||
float_t entityAnimTileZOffset(const worldpos_t pos);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimIdleUpdate(entity_t *entity) {
|
||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
||||
entity->renderPosition[2] = (
|
||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
||||
) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
/**
|
||||
* Updates render position for the idle animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimIdleUpdate(entity_t *entity);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimRunUpdate(entity_t *entity) {
|
||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
|
||||
float_t zFrom = (float_t)entity->lastPosition.z
|
||||
+ entityAnimTileZOffset(entity->lastPosition);
|
||||
float_t zTo = (float_t)entity->position.z
|
||||
+ entityAnimTileZOffset(entity->position);
|
||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
||||
);
|
||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
||||
);
|
||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
|
||||
|
||||
/**
|
||||
* Updates render position for the run animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimRunUpdate(entity_t *entity);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimTurnUpdate(entity_t *entity) {
|
||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
||||
entity->renderPosition[2] = (
|
||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
||||
) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
|
||||
|
||||
/**
|
||||
* Updates render position for the turn animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimTurnUpdate(entity_t *entity);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimWalkUpdate(entity_t *entity) {
|
||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
|
||||
float_t zFrom = (float_t)entity->lastPosition.z
|
||||
+ entityAnimTileZOffset(entity->lastPosition);
|
||||
float_t zTo = (float_t)entity->position.z
|
||||
+ entityAnimTileZOffset(entity->position);
|
||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
||||
);
|
||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
||||
);
|
||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
|
||||
#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
|
||||
|
||||
/**
|
||||
* Updates render position for the walk animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimWalkUpdate(entity_t *entity);
|
||||
+117
-344
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Dominic Masters
|
||||
*
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
@@ -8,16 +8,14 @@
|
||||
#include "entity.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "time/time.h"
|
||||
#include "util/math.h"
|
||||
#include "time/time.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/maparea.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
entity_t ENTITIES[ENTITY_COUNT];
|
||||
physicsworld_t ENTITY_PHYSICS_WORLD;
|
||||
|
||||
void entityInit(entity_t *entity, const entitytype_t type) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
@@ -34,6 +32,9 @@ void entityInit(entity_t *entity, const entitytype_t type) {
|
||||
entity->type = type;
|
||||
entity->chunkIndex = 0xFF;
|
||||
|
||||
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||
physicsBodyInit(&entity->body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
|
||||
|
||||
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
|
||||
}
|
||||
|
||||
@@ -42,212 +43,115 @@ void entityUpdate(entity_t *entity) {
|
||||
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
|
||||
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
|
||||
|
||||
// What state is the entity in?
|
||||
entityAnimUpdate(entity);
|
||||
|
||||
// Movement code.
|
||||
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
|
||||
ENTITY_CALLBACKS[entity->type].movement(entity);
|
||||
}
|
||||
}
|
||||
|
||||
bool_t entityCanTurn(entity_t *entity) {
|
||||
return entity->animation == ENTITY_ANIM_IDLE &&
|
||||
entity->walkEndCooldown <= 0;
|
||||
}
|
||||
physicsbody_t *others[ENTITY_COUNT];
|
||||
uint32_t othersCount = 0;
|
||||
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||
if(ENTITIES[i].type == ENTITY_TYPE_NULL) continue;
|
||||
if(&ENTITIES[i] == entity) continue;
|
||||
others[othersCount++] = &ENTITIES[i].body;
|
||||
}
|
||||
|
||||
bool_t entityCanWalk(entity_t *entity) {
|
||||
return entity->animation == ENTITY_ANIM_IDLE;
|
||||
}
|
||||
|
||||
bool_t entityCanRun(entity_t *entity) {
|
||||
return entity->animation == ENTITY_ANIM_IDLE;
|
||||
physicsWorldStep(
|
||||
&ENTITY_PHYSICS_WORLD, &entity->body, TIME.delta, others, othersCount
|
||||
);
|
||||
entitySyncFromPhysics(entity);
|
||||
}
|
||||
|
||||
bool_t entityCanUnload(entity_t *entity) {
|
||||
return entity->globalId < ENTITY_GLOBAL_ID_START;
|
||||
}
|
||||
|
||||
void entityMove(
|
||||
entity_t *entity, const vec2 direction, const bool_t running
|
||||
) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
const float_t magSq =
|
||||
direction[0] * direction[0] + direction[1] * direction[1];
|
||||
if(magSq <= ENTITY_MOVE_DEADZONE * ENTITY_MOVE_DEADZONE) {
|
||||
entity->body.velocity[0] = 0.0f;
|
||||
entity->body.velocity[1] = 0.0f;
|
||||
entity->animation = ENTITY_ANIM_IDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
const float_t mag = sqrtf(magSq);
|
||||
const float_t clampedMag = mathMin(mag, 1.0f);
|
||||
const float_t speed = running ? ENTITY_MOVE_RUN_SPEED : ENTITY_MOVE_WALK_SPEED;
|
||||
|
||||
entity->body.velocity[0] = (direction[0] / mag) * clampedMag * speed;
|
||||
entity->body.velocity[1] = (direction[1] / mag) * clampedMag * speed;
|
||||
entity->direction = entityDirFromVec2(direction);
|
||||
entity->animation = running ? ENTITY_ANIM_RUN : ENTITY_ANIM_WALK;
|
||||
}
|
||||
|
||||
void entityStop(entity_t *entity) {
|
||||
const vec2 zero = { 0.0f, 0.0f };
|
||||
entityMove(entity, zero, false);
|
||||
}
|
||||
|
||||
void entityTurn(entity_t *entity, const entitydir_t direction) {
|
||||
if(!entityCanTurn(entity)) return;
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
entity->direction = direction;
|
||||
entity->animation = ENTITY_ANIM_TURN;
|
||||
entity->animTime = ENTITY_ANIM_TURN_DURATION;
|
||||
}
|
||||
|
||||
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||
if(!entityCanWalk(entity)) return;
|
||||
// TODO: Animation, delay, etc.
|
||||
entity->direction = direction;
|
||||
|
||||
// Where are we moving?
|
||||
worldpos_t newPos = entity->position;
|
||||
worldunits_t relX, relY;
|
||||
{
|
||||
entityDirGetRelative(direction, &relX, &relY);
|
||||
newPos.x += relX;
|
||||
newPos.y += relY;
|
||||
}
|
||||
|
||||
// Get tile under foot
|
||||
tile_t tileCurrent = chunkGetTile(entity->position);
|
||||
tile_t tileNew = chunkGetTile(newPos);
|
||||
bool_t fall = false;
|
||||
bool_t raise = false;
|
||||
|
||||
// Are we walking up a ramp?
|
||||
if(
|
||||
tileShapeIsRamp(tileCurrent.shape) &&
|
||||
(
|
||||
// Can only walk UP the direction the ramp faces.
|
||||
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
|
||||
// If diagonal ramp, can go up one of two ways only. Inner ramps
|
||||
// share the same allowed directions as their outer counterparts.
|
||||
(
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
||||
)
|
||||
)
|
||||
// Must be able to walk up.
|
||||
)
|
||||
) {
|
||||
tile_t tileNewSaved = tileNew;
|
||||
tileNew = TILE_NULL;
|
||||
worldpos_t abovePos = newPos;
|
||||
abovePos.z += 1;
|
||||
tile_t tileAbove = chunkGetTile(abovePos);
|
||||
|
||||
if(
|
||||
tileAbove.shape != TILE_SHAPE_NULL &&
|
||||
tileShapeIsWalkable(tileAbove.shape)
|
||||
) {
|
||||
raise = true;
|
||||
} else {
|
||||
tileNew = tileNewSaved;
|
||||
}
|
||||
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
|
||||
// Falling down?
|
||||
worldpos_t belowPos = newPos;
|
||||
belowPos.z -= 1;
|
||||
tile_t tileBelow = chunkGetTile(belowPos);
|
||||
if(
|
||||
tileBelow.shape != TILE_SHAPE_NULL &&
|
||||
tileShapeIsRamp(tileBelow.shape) &&
|
||||
(
|
||||
// This handles regular cardinal ramps
|
||||
(
|
||||
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
|
||||
) == tileBelow.shape ||
|
||||
// This handles diagonal ramps. Inner ramps share the same
|
||||
// allowed directions as their outer counterparts.
|
||||
(
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
// We will fall to this tile.
|
||||
fall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Can we walk here?
|
||||
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
|
||||
|
||||
// Raise/fall must be applied before checking for blocking entities,
|
||||
// otherwise the check compares against the wrong z-level.
|
||||
if(raise) {
|
||||
newPos.z += 1;
|
||||
} else if(fall) {
|
||||
newPos.z -= 1;
|
||||
}
|
||||
|
||||
// Entity in way?
|
||||
entity_t *other = ENTITIES;
|
||||
do {
|
||||
if(other == entity) continue;
|
||||
if(other->type == ENTITY_TYPE_NULL) continue;
|
||||
if(!worldPosIsEqual(other->position, newPos)) continue;
|
||||
return;// Blocked
|
||||
} while(++other, other < &ENTITIES[ENTITY_COUNT]);
|
||||
|
||||
entity->lastPosition = entity->position;
|
||||
entity->position = newPos;
|
||||
entity->animation = ENTITY_ANIM_WALK;
|
||||
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
|
||||
entityUpdateChunk(entity);
|
||||
mapAreaCheckEntity(entity);
|
||||
vec2 dirVec;
|
||||
entityDirToVec2(direction, dirVec);
|
||||
entityMove(entity, dirVec, false);
|
||||
}
|
||||
|
||||
void entityRun(entity_t *entity, const entitydir_t direction) {
|
||||
if(!entityCanRun(entity)) return;
|
||||
entityWalk(entity, direction);
|
||||
if(entity->animation == ENTITY_ANIM_WALK) {
|
||||
entity->animation = ENTITY_ANIM_RUN;
|
||||
entity->animTime = ENTITY_ANIM_RUN_DURATION;
|
||||
}
|
||||
vec2 dirVec;
|
||||
entityDirToVec2(direction, dirVec);
|
||||
entityMove(entity, dirVec, true);
|
||||
}
|
||||
|
||||
entity_t * entityGetAt(const worldpos_t position) {
|
||||
entity_t * entityGetFacing(entity_t *entity, const float_t range) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
vec2 dir;
|
||||
entityDirToVec2(entity->direction, dir);
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(&entity->body, min, max);
|
||||
|
||||
const vec3 probeMin = {
|
||||
min[0] + dir[0] * range, min[1] + dir[1] * range, min[2]
|
||||
};
|
||||
const vec3 probeMax = {
|
||||
max[0] + dir[0] * range, max[1] + dir[1] * range, max[2]
|
||||
};
|
||||
|
||||
entity_t *best = NULL;
|
||||
float_t bestDistSq = 0.0f;
|
||||
|
||||
entity_t *ent = ENTITIES;
|
||||
do {
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
if(!worldPosIsEqual(ent->position, position)) continue;
|
||||
return ent;
|
||||
if(ent == entity) continue;
|
||||
|
||||
vec3 oMin, oMax;
|
||||
physicsBodyGetBounds(&ent->body, oMin, oMax);
|
||||
|
||||
if(probeMin[0] >= oMax[0] || probeMax[0] <= oMin[0]) continue;
|
||||
if(probeMin[1] >= oMax[1] || probeMax[1] <= oMin[1]) continue;
|
||||
if(probeMin[2] >= oMax[2] || probeMax[2] <= oMin[2]) continue;
|
||||
|
||||
const float_t dx = ent->body.position[0] - entity->body.position[0];
|
||||
const float_t dy = ent->body.position[1] - entity->body.position[1];
|
||||
const float_t distSq = dx * dx + dy * dy;
|
||||
|
||||
if(best != NULL && distSq >= bestDistSq) continue;
|
||||
best = ent;
|
||||
bestDistSq = distSq;
|
||||
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
|
||||
|
||||
return NULL;
|
||||
return best;
|
||||
}
|
||||
|
||||
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
|
||||
@@ -272,19 +176,21 @@ uint8_t entityGetAvailable() {
|
||||
|
||||
void entityPositionSet(entity_t *entity, const worldpos_t pos) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
entity->lastPosition = pos;
|
||||
entity->position = pos;
|
||||
|
||||
const vec3 floatPos = {
|
||||
(float_t)pos.x, (float_t)pos.y, (float_t)pos.z
|
||||
};
|
||||
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||
physicsBodyInit(&entity->body, floatPos, extents);
|
||||
entity->animation = ENTITY_ANIM_IDLE;
|
||||
entity->animTime = 0;
|
||||
entity->walkEndCooldown = 0;
|
||||
entityUpdateChunk(entity);
|
||||
entitySyncFromPhysics(entity);
|
||||
}
|
||||
|
||||
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
if(entity->chunkIndex != 0xFF) {
|
||||
chunk_t *old = chunkGet(entity->chunkIndex);
|
||||
chunk_t *old = mapGetChunk(entity->chunkIndex);
|
||||
if(old != NULL) {
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(old->entities[i] != entity->id) continue;
|
||||
@@ -297,13 +203,16 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||
entity->chunkIndex = chunkIndex;
|
||||
|
||||
if(chunkIndex != 0xFF) {
|
||||
chunk_t *next = chunkGet(chunkIndex);
|
||||
chunk_t *next = mapGetChunk(chunkIndex);
|
||||
if(next != NULL) {
|
||||
bool_t inserted = false;
|
||||
for(uint8_t i = 0; i < CHUNK_ENTITY_COUNT_MAX; i++) {
|
||||
if(next->entities[i] != 0xFF) continue;
|
||||
next->entities[i] = entity->id;
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
assertTrue(inserted, "Chunk entity slot overflow");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,159 +222,23 @@ void entityUpdateChunk(entity_t *entity) {
|
||||
|
||||
chunkpos_t cp;
|
||||
worldPosToChunkPos(&entity->position, &cp);
|
||||
chunkindex_t ci = chunkGetIndexAt(cp);
|
||||
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
||||
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
||||
if(ci == -1 || ci == entity->chunkIndex) return;
|
||||
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");
|
||||
void entitySyncFromPhysics(entity_t *entity) {
|
||||
assertNotNull(entity, "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);
|
||||
entity->position = (worldpos_t){
|
||||
(worldunit_t)floorf(entity->body.position[0]),
|
||||
(worldunit_t)floorf(entity->body.position[1]),
|
||||
(worldunit_t)floorf(entity->body.position[2])
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
glm_vec3_copy(entity->body.position, entity->renderPosition);
|
||||
entity->renderPosition[2] *= WORLD_LAYER_HEIGHT;
|
||||
|
||||
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();
|
||||
}
|
||||
entityUpdateChunk(entity);
|
||||
mapAreaCheckEntity(entity);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
|
||||
#pragma once
|
||||
#include "entitydir.h"
|
||||
#include "anim/entityanim.h"
|
||||
#include "interact/entityinteract.h"
|
||||
#include "entitytype.h"
|
||||
#include "npc/npc.h"
|
||||
#include "rpg/physics/physicsbody.h"
|
||||
#include "rpg/physics/physicsworld.h"
|
||||
|
||||
typedef struct map_s map_t;
|
||||
typedef struct yyjson_val yyjson_val;
|
||||
|
||||
typedef uint16_t entityglobalid_t;
|
||||
|
||||
@@ -21,6 +21,29 @@ typedef uint16_t entityglobalid_t;
|
||||
#define ENTITY_GLOBAL_ID_START 1
|
||||
#define ENTITY_GLOBAL_ID_PLAYER 1
|
||||
|
||||
// Default collision box for every entity's physics body - matches the old
|
||||
// system's exact one-tile footprint.
|
||||
#define ENTITY_PHYSICS_EXTENTS_DEFAULT { 1.0f, 1.0f, 1.0f }
|
||||
|
||||
// Movement speeds, in grid units per second - chosen to match the feel of
|
||||
// the old fixed-duration one-tile-per-step system (12 ticks/tile walking,
|
||||
// 6 ticks/tile running, at DUSK_TIME_STEP = 16ms).
|
||||
#define ENTITY_MOVE_WALK_SPEED 5.2083f
|
||||
#define ENTITY_MOVE_RUN_SPEED 10.4167f
|
||||
|
||||
// Movement vectors below this magnitude are treated as no movement.
|
||||
#define ENTITY_MOVE_DEADZONE 0.1f
|
||||
|
||||
// How far in front of an entity entityGetFacing probes for a target.
|
||||
#define ENTITY_INTERACT_RANGE 1.0f
|
||||
|
||||
typedef enum {
|
||||
ENTITY_ANIM_IDLE,
|
||||
ENTITY_ANIM_WALK,
|
||||
ENTITY_ANIM_RUN,
|
||||
ENTITY_ANIM_COUNT
|
||||
} entityanim_t;
|
||||
|
||||
typedef struct entity_s {
|
||||
uint8_t id;
|
||||
entityglobalid_t globalId;
|
||||
@@ -29,13 +52,18 @@ typedef struct entity_s {
|
||||
|
||||
// Movement
|
||||
entitydir_t direction;
|
||||
physicsbody_t body;
|
||||
|
||||
// Derived each frame from body.position (floored) - kept for systems
|
||||
// that still assume an integer grid position (chunk membership, map
|
||||
// area triggers, entity-at-position queries).
|
||||
worldpos_t position;
|
||||
worldpos_t lastPosition;
|
||||
|
||||
// Derived each frame from body.position - mirrors the physics position
|
||||
// into render/world-float space (z scaled by WORLD_LAYER_HEIGHT).
|
||||
vec3 renderPosition;
|
||||
|
||||
entityanim_t animation;
|
||||
float_t animTime;
|
||||
float_t walkEndCooldown;
|
||||
|
||||
entityinteract_t interact;
|
||||
|
||||
@@ -44,6 +72,9 @@ typedef struct entity_s {
|
||||
|
||||
extern entity_t ENTITIES[ENTITY_COUNT];
|
||||
|
||||
// Shared physics world every entity's body steps against.
|
||||
extern physicsworld_t ENTITY_PHYSICS_WORLD;
|
||||
|
||||
/**
|
||||
* Initializes an entity structure.
|
||||
*
|
||||
@@ -59,30 +90,6 @@ void entityInit(entity_t *entity, const entitytype_t type);
|
||||
*/
|
||||
void entityUpdate(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is in a state where it can turn.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
* @returns True if the entity can turn.
|
||||
*/
|
||||
bool_t entityCanTurn(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is in a state where it can walk.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
* @returns True if the entity can walk.
|
||||
*/
|
||||
bool_t entityCanWalk(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is in a state where it can run.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
* @returns True if the entity can run.
|
||||
*/
|
||||
bool_t entityCanRun(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is allowed to be unloaded. By default this is
|
||||
* true for entities whose global ID falls within the randomly assigned
|
||||
@@ -94,7 +101,29 @@ bool_t entityCanRun(entity_t *entity);
|
||||
bool_t entityCanUnload(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Turn an entity to face a new direction.
|
||||
* Moves an entity continuously in a direction, at walking or running
|
||||
* speed. Sets the entity's facing to the nearest cardinal direction that
|
||||
* matches the movement vector. Must be called every tick the entity
|
||||
* should keep moving - unlike the old tile-stepping system this does not
|
||||
* complete a move on its own; call entityStop to stop.
|
||||
*
|
||||
* @param entity Pointer to the entity to move.
|
||||
* @param direction Movement vector, magnitude 0-1 (values longer than 1
|
||||
* are clamped to 1, so diagonals aren't faster than cardinals).
|
||||
* @param running Whether to move at running speed instead of walking.
|
||||
*/
|
||||
void entityMove(entity_t *entity, const vec2 direction, const bool_t running);
|
||||
|
||||
/**
|
||||
* Stops an entity's horizontal movement (equivalent to
|
||||
* entityMove(entity, {0, 0}, false)).
|
||||
*
|
||||
* @param entity Pointer to the entity to stop.
|
||||
*/
|
||||
void entityStop(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Turn an entity to face a new direction, instantly, without moving it.
|
||||
*
|
||||
* @param entity Pointer to the entity to turn.
|
||||
* @param direction The direction to face.
|
||||
@@ -102,7 +131,10 @@ bool_t entityCanUnload(entity_t *entity);
|
||||
void entityTurn(entity_t *entity, const entitydir_t direction);
|
||||
|
||||
/**
|
||||
* Make an entity walk in a direction.
|
||||
* Makes an entity walk continuously in a cardinal direction, at walking
|
||||
* speed. Convenience wrapper over entityMove for callers that only think
|
||||
* in terms of the 4 cardinal directions. Must be called every tick the
|
||||
* entity should keep moving.
|
||||
*
|
||||
* @param entity Pointer to the entity to make walk.
|
||||
* @param direction The direction to walk in.
|
||||
@@ -110,7 +142,8 @@ void entityTurn(entity_t *entity, const entitydir_t direction);
|
||||
void entityWalk(entity_t *entity, const entitydir_t direction);
|
||||
|
||||
/**
|
||||
* Make an entity run in a direction.
|
||||
* Makes an entity walk continuously in a cardinal direction, at running
|
||||
* speed. See entityWalk.
|
||||
*
|
||||
* @param entity Pointer to the entity to make run.
|
||||
* @param direction The direction to run in.
|
||||
@@ -118,13 +151,16 @@ void entityWalk(entity_t *entity, const entitydir_t direction);
|
||||
void entityRun(entity_t *entity, const entitydir_t direction);
|
||||
|
||||
/**
|
||||
* Gets the entity at a specific world position.
|
||||
* Finds the closest other entity whose bounds overlap a probe box
|
||||
* projected out from the given entity's own bounds, along its current
|
||||
* facing direction. Used for interaction targeting - continuous-position
|
||||
* aware, unlike an exact tile match.
|
||||
*
|
||||
* @param map Pointer to the map to check.
|
||||
* @param pos The world position to check.
|
||||
* @return Pointer to the entity at the position, or NULL if none.
|
||||
* @param entity Pointer to the entity to probe from.
|
||||
* @param range Distance, in grid units, to project the probe box.
|
||||
* @return Pointer to the closest overlapping entity, or NULL if none.
|
||||
*/
|
||||
entity_t *entityGetAt(const worldpos_t pos);
|
||||
entity_t *entityGetFacing(entity_t *entity, const float_t range);
|
||||
|
||||
/**
|
||||
* Gets the entity with the given global ID, if one is currently loaded.
|
||||
@@ -168,39 +204,10 @@ void entityUpdateChunk(entity_t *entity);
|
||||
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.
|
||||
* Derives position and renderPosition from the entity's physics body, and
|
||||
* refreshes chunk membership and map area triggers to match. Called once
|
||||
* per entity per frame, after the physics step.
|
||||
*
|
||||
* 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).
|
||||
* @param entity Pointer to the entity to sync.
|
||||
*/
|
||||
errorret_t entityCreateFromJson(yyjson_val *obj, entity_t **outEntity);
|
||||
void entitySyncFromPhysics(entity_t *entity);
|
||||
@@ -48,4 +48,27 @@ void entityDirGetRelative(
|
||||
*outY = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void entityDirToVec2(const entitydir_t dir, vec2 out) {
|
||||
assertValidEntityDir(dir, "Invalid direction provided");
|
||||
assertNotNull(out, "Output vector cannot be NULL");
|
||||
|
||||
worldunits_t relX, relY;
|
||||
entityDirGetRelative(dir, &relX, &relY);
|
||||
out[0] = (float_t)relX;
|
||||
out[1] = (float_t)relY;
|
||||
}
|
||||
|
||||
entitydir_t entityDirFromVec2(const vec2 direction) {
|
||||
assertNotNull(direction, "Direction vector cannot be NULL");
|
||||
assertTrue(
|
||||
direction[0] != 0.0f || direction[1] != 0.0f,
|
||||
"Direction vector cannot be zero"
|
||||
);
|
||||
|
||||
if(fabsf(direction[0]) > fabsf(direction[1])) {
|
||||
return direction[0] > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
}
|
||||
return direction[1] > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
}
|
||||
@@ -44,11 +44,30 @@ entitydir_t entityDirGetOpposite(const entitydir_t dir);
|
||||
|
||||
/**
|
||||
* Gets the relative x and y offsets for a given direction.
|
||||
*
|
||||
*
|
||||
* @param dir The direction to get offsets for.
|
||||
* @param relX Pointer to store the relative x offset.
|
||||
* @param relY Pointer to store the relative y offset.
|
||||
*/
|
||||
void entityDirGetRelative(
|
||||
const entitydir_t dir, worldunits_t *relX, worldunits_t *relY
|
||||
);
|
||||
);
|
||||
|
||||
/**
|
||||
* Converts a cardinal direction to a unit 2D vector, using the same axis
|
||||
* convention as entityDirGetRelative (north = +y, east = +x).
|
||||
*
|
||||
* @param dir The direction to convert.
|
||||
* @param out Output unit vector for the direction.
|
||||
*/
|
||||
void entityDirToVec2(const entitydir_t dir, vec2 out);
|
||||
|
||||
/**
|
||||
* Quantizes a 2D direction vector to the nearest cardinal direction, by
|
||||
* comparing the magnitude of its x and y components.
|
||||
*
|
||||
* @param direction The direction vector to quantize. Must not be a zero
|
||||
* vector.
|
||||
* @return The nearest cardinal direction.
|
||||
*/
|
||||
entitydir_t entityDirFromVec2(const vec2 direction);
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "entitydir.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
// Distance, in grid units, within which the entity is considered to have
|
||||
// arrived at its path target.
|
||||
#define ENTITY_PATH_ARRIVE_EPSILON 0.05f
|
||||
|
||||
bool_t entityPathStep(
|
||||
entity_t *entity,
|
||||
const worldpos_t target,
|
||||
@@ -24,36 +28,44 @@ bool_t entityPathStep(
|
||||
"Entity pointer is out of bounds"
|
||||
);
|
||||
|
||||
if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
|
||||
const float_t dx = (float_t)target.x - entity->body.position[0];
|
||||
const float_t dy = (float_t)target.y - entity->body.position[1];
|
||||
|
||||
if(
|
||||
fabsf(dx) <= ENTITY_PATH_ARRIVE_EPSILON &&
|
||||
fabsf(dy) <= ENTITY_PATH_ARRIVE_EPSILON
|
||||
) {
|
||||
entityStop(entity);
|
||||
return true;
|
||||
}
|
||||
if(!entityCanWalk(entity)) return false;
|
||||
|
||||
entitydir_t dir;
|
||||
if(entity->position.x != target.x) {
|
||||
dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
} else if(entity->position.y != target.y) {
|
||||
dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
bool_t horizontal;
|
||||
if(fabsf(dx) > fabsf(dy)) {
|
||||
dir = dx > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
horizontal = true;
|
||||
} else {
|
||||
dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
dir = dy > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
horizontal = false;
|
||||
}
|
||||
|
||||
// Was the entity trying to walk on this axis last tick, but ended up not
|
||||
// moving (velocity clamped by a collision)? If so, treat it as blocked.
|
||||
if(walkAround) {
|
||||
const uint8_t axis = horizontal ? 0 : 1;
|
||||
const bool_t blockedLastTick =
|
||||
entity->animation != ENTITY_ANIM_IDLE &&
|
||||
fabsf(entity->body.velocity[axis]) <= ENTITY_MOVE_DEADZONE;
|
||||
|
||||
if(blockedLastTick) {
|
||||
const entitydir_t alt = horizontal
|
||||
? (dy >= 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH)
|
||||
: (dx >= 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST);
|
||||
entityWalk(entity, alt);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
entityWalk(entity, dir);
|
||||
|
||||
if(walkAround && entity->animation == ENTITY_ANIM_IDLE) {
|
||||
// Primary direction blocked - try perpendicular axes.
|
||||
entitydir_t alt, altOpp;
|
||||
if(dir == ENTITY_DIR_EAST || dir == ENTITY_DIR_WEST) {
|
||||
alt = entity->position.y <= target.y
|
||||
? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
} else {
|
||||
alt = entity->position.x <= target.x
|
||||
? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
}
|
||||
altOpp = entityDirGetOpposite(alt);
|
||||
entityWalk(entity, alt);
|
||||
if(entity->animation == ENTITY_ANIM_IDLE) entityWalk(entity, altOpp);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
#include "entity.h"
|
||||
|
||||
/**
|
||||
* Attempts to walk the entity one tile toward target. Prefers resolving X
|
||||
* first, then Y, then Z. Does nothing if the entity cannot currently walk.
|
||||
* When walkAround is true and the preferred direction is blocked by an entity,
|
||||
* tries perpendicular directions to navigate around it.
|
||||
* Continuously walks the entity toward target's X/Y position (Z is left to
|
||||
* gravity/ground collision, not deliberate path-stepping). Prefers closing
|
||||
* whichever of X/Y currently differs most. Must be called every tick until
|
||||
* it returns true. When walkAround is true and the last tick's movement on
|
||||
* the current axis was blocked (its velocity on that axis reads as zero
|
||||
* despite currently walking), tries a perpendicular direction instead.
|
||||
*
|
||||
* @param entity Pointer to the entity to move.
|
||||
* @param target The world position to move toward.
|
||||
* @param walkAround Whether to try perpendicular directions when blocked.
|
||||
* @returns true if the entity is already at target, false otherwise.
|
||||
* @param target The world position to move toward (Z is ignored).
|
||||
* @param walkAround Whether to try a perpendicular direction when blocked.
|
||||
* @returns true once the entity is within arrival distance of target.
|
||||
*/
|
||||
bool_t entityPathStep(
|
||||
entity_t *entity,
|
||||
|
||||
@@ -6,5 +6,4 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
log.c
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -29,15 +29,9 @@ void npcPathAddNode(npc_t *npc, const worldpos_t pos) {
|
||||
void npcPathMovement(entity_t *entity) {
|
||||
npcpath_t *path = &entity->data.npc.moveData.path;
|
||||
if(path->count == 0) return;
|
||||
if(!entityCanWalk(entity)) return;
|
||||
|
||||
// Advance past any waypoints already reached (including the current one).
|
||||
worldpos_t *target = &path->positions[path->index];
|
||||
if(entityPathStep(entity, *target, false)) {
|
||||
const worldpos_t target = path->positions[path->index];
|
||||
if(entityPathStep(entity, target, false)) {
|
||||
path->index = (path->index + 1) % path->count;
|
||||
target = &path->positions[path->index];
|
||||
// New target is the same tile - nothing to do this tick
|
||||
if(worldPosIsEqual(entity->position, *target)) return;
|
||||
entityPathStep(entity, *target, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ void npcRandomTurnMovement(entity_t *entity) {
|
||||
turn->timer -= TIME.delta;
|
||||
if(turn->timer > 0.0f) return;
|
||||
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
|
||||
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
}
|
||||
|
||||
@@ -16,14 +16,24 @@ void npcRandomWalkInit(npc_t *npc) {
|
||||
walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
||||
walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
||||
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
||||
walk->moveDuration = 0.0f;
|
||||
}
|
||||
|
||||
void npcRandomWalkMovement(entity_t *entity) {
|
||||
npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
|
||||
|
||||
if(walk->moveDuration > 0.0f) {
|
||||
walk->moveDuration -= TIME.delta;
|
||||
entityWalk(entity, walk->direction);
|
||||
if(walk->moveDuration <= 0.0f) entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
walk->timer -= TIME.delta;
|
||||
if(walk->timer > 0.0f) return;
|
||||
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
||||
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
|
||||
walk->direction = (entitydir_t)(rand() % 4);
|
||||
walk->moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
|
||||
}
|
||||
|
||||
void npcRandomTurnAndWalkInit(npc_t *npc) {
|
||||
@@ -32,21 +42,30 @@ void npcRandomTurnAndWalkInit(npc_t *npc) {
|
||||
tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
||||
tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
||||
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
||||
tw->walk.moveDuration = 0.0f;
|
||||
}
|
||||
|
||||
void npcRandomTurnAndWalkMovement(entity_t *entity) {
|
||||
npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
|
||||
|
||||
if(tw->walk.moveDuration > 0.0f) {
|
||||
tw->walk.moveDuration -= TIME.delta;
|
||||
entityWalk(entity, tw->walk.direction);
|
||||
if(tw->walk.moveDuration <= 0.0f) entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
tw->turn.timer -= TIME.delta;
|
||||
if(tw->turn.timer <= 0.0f) {
|
||||
tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
|
||||
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
}
|
||||
|
||||
tw->walk.timer -= TIME.delta;
|
||||
if(tw->walk.timer <= 0.0f) {
|
||||
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
||||
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
|
||||
tw->walk.direction = (entitydir_t)(rand() % 4);
|
||||
tw->walk.moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,25 @@
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "npcturn.h"
|
||||
#include "rpg/entity/entitydir.h"
|
||||
|
||||
/** Default min/max seconds between NPC random-walk ticks. */
|
||||
#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
|
||||
#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
|
||||
|
||||
/** How long a random-walk move lasts once triggered, roughly one tile's
|
||||
* worth of travel at walking speed. */
|
||||
#define NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT 0.2f
|
||||
|
||||
typedef struct {
|
||||
float_t frequencyMin;
|
||||
float_t frequencyMax;
|
||||
float_t timer;
|
||||
|
||||
// In-progress move state - while moveDuration > 0, movement keeps
|
||||
// walking in direction every tick.
|
||||
float_t moveDuration;
|
||||
entitydir_t direction;
|
||||
} npcrandomwalk_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -25,8 +25,11 @@ bool_t playerCanInteract(entity_t *entity) {
|
||||
|
||||
void playerInput(entity_t *entity) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
|
||||
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) {
|
||||
entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle game menu on pause
|
||||
if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
|
||||
uiGameMenuClose();
|
||||
@@ -38,63 +41,27 @@ void playerInput(entity_t *entity) {
|
||||
}
|
||||
|
||||
// Can player act?
|
||||
if(UI_FOCUS.count > 0) return;
|
||||
|
||||
// Determine direction player may be trying to face/go.
|
||||
const playerinputdirmap_t *dirMap;
|
||||
|
||||
// First pass, we want to prefer a button relative to what the player is
|
||||
// already facing, e.g. if they hold DOWN and start moving down, then tap
|
||||
// right we want to prefer down over right.
|
||||
|
||||
// Determine current dirmap based on facing direction.
|
||||
dirMap = PLAYER_INPUT_DIR_MAP;
|
||||
do {
|
||||
if(dirMap->direction != entity->direction) continue;
|
||||
break;
|
||||
} while((++dirMap)->action != 0xFF);
|
||||
|
||||
// Are we holding that button?
|
||||
if(!inputIsDown(dirMap->action)) {
|
||||
// No, so we need to check all directions.
|
||||
dirMap = PLAYER_INPUT_DIR_MAP;
|
||||
do {
|
||||
if(!inputIsDown(dirMap->action)) continue;
|
||||
break;
|
||||
} while((++dirMap)->action != 0xFF);
|
||||
if(UI_FOCUS.count > 0) {
|
||||
entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trying to direct?
|
||||
if(dirMap->action != 0xFF) {
|
||||
// Is the player attempting to turn? That is, is the player idle and has the
|
||||
// turn lockout passed?
|
||||
if(entityCanTurn(entity)) {
|
||||
if(entity->direction != dirMap->direction) {
|
||||
entityTurn(entity, dirMap->direction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Analog/free-angle movement vector from the 4 directional actions -
|
||||
// already normalized so diagonals aren't faster than cardinals, and
|
||||
// preserves real analog magnitude on platforms that bind an actual
|
||||
// gamepad stick to these actions.
|
||||
vec2 moveDir;
|
||||
inputAngle2D(
|
||||
INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT,
|
||||
INPUT_ACTION_DOWN, INPUT_ACTION_UP,
|
||||
moveDir
|
||||
);
|
||||
|
||||
bool_t running = inputIsDown(INPUT_ACTION_CANCEL);
|
||||
if(running && entityCanRun(entity)) {
|
||||
entityRun(entity, dirMap->direction);
|
||||
} else if(entityCanWalk(entity)) {
|
||||
entityWalk(entity, dirMap->direction);
|
||||
}
|
||||
}
|
||||
entityMove(entity, moveDir, inputIsDown(INPUT_ACTION_CANCEL));
|
||||
|
||||
// Interaction
|
||||
if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) {
|
||||
worldunit_t x, y, z;
|
||||
{
|
||||
worldunits_t relX, relY;
|
||||
entityDirGetRelative(entity->direction, &relX, &relY);
|
||||
x = entity->position.x + relX;
|
||||
y = entity->position.y + relY;
|
||||
z = entity->position.z;
|
||||
}
|
||||
|
||||
entity_t *target = entityGetAt((worldpos_t){ x, y, z });
|
||||
entity_t *target = entityGetFacing(entity, ENTITY_INTERACT_RANGE);
|
||||
if(target == NULL) return;
|
||||
entityInteractWith(entity, target);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,6 @@ typedef struct {
|
||||
void *nothing;
|
||||
} player_t;
|
||||
|
||||
typedef struct {
|
||||
inputaction_t action;
|
||||
entitydir_t direction;
|
||||
} playerinputdirmap_t;
|
||||
|
||||
static const playerinputdirmap_t PLAYER_INPUT_DIR_MAP[] = {
|
||||
{ INPUT_ACTION_UP, ENTITY_DIR_NORTH },
|
||||
{ INPUT_ACTION_DOWN, ENTITY_DIR_SOUTH },
|
||||
{ INPUT_ACTION_LEFT, ENTITY_DIR_WEST },
|
||||
{ INPUT_ACTION_RIGHT, ENTITY_DIR_EAST },
|
||||
|
||||
{ 0xFF, 0xFF }
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a player entity.
|
||||
*
|
||||
|
||||
@@ -10,4 +10,13 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
inventory.c
|
||||
backpack.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;
|
||||
|
||||
void backpackInit() {
|
||||
for(uint32_t i = 0; i < ITEM_TYPE_COUNT_MAX; i++) {
|
||||
for(uint8_t i = 0; i < ITEM_TYPE_COUNT; i++) {
|
||||
inventoryInit(
|
||||
&BACKPACK.inventories[i],
|
||||
BACKPACK.storage[i],
|
||||
INVENTORY_CAPACITY_MAX
|
||||
ITEM_TYPE_COUNT_MAX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
inventory_t *backpackGetInventory(const itemtypeid_t type) {
|
||||
inventory_t *backpackGetInventory(const itemtype_t type) {
|
||||
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];
|
||||
}
|
||||
|
||||
void backpackAdd(const itemid_t item, const uint8_t quantity) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
inventoryAdd(backpackGetInventory(ITEMS[item].type), item, quantity);
|
||||
}
|
||||
|
||||
void backpackRemove(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
inventoryRemove(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
void backpackSet(const itemid_t item, const uint8_t quantity) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
inventorySet(backpackGetInventory(ITEMS[item].type), item, quantity);
|
||||
}
|
||||
|
||||
uint8_t backpackGetCount(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
return inventoryGetCount(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
bool_t backpackItemExists(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
return inventoryItemExists(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
bool_t backpackIsFull(const itemtypeid_t type) {
|
||||
bool_t backpackIsFull(const itemtype_t type) {
|
||||
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));
|
||||
}
|
||||
|
||||
bool_t backpackItemFull(const itemid_t item) {
|
||||
assertTrue(item > ITEM_ID_NULL, "Item ID must not be null");
|
||||
assertTrue(item <= ITEM_COUNT, "Item ID out of range");
|
||||
assertTrue(item < ITEM_ID_COUNT, "Item ID out of range");
|
||||
return inventoryItemFull(backpackGetInventory(ITEMS[item].type), item);
|
||||
}
|
||||
|
||||
void backpackSort(
|
||||
const itemtypeid_t type,
|
||||
const itemtype_t type,
|
||||
const inventorysort_t sortBy,
|
||||
const bool_t reverse
|
||||
) {
|
||||
assertTrue(type > ITEM_TYPE_NULL, "Item type must not be null");
|
||||
assertTrue(type <= ITEM_TYPE_COUNT, "Item type out of range");
|
||||
assertTrue(type < ITEM_TYPE_COUNT, "Item type out of range");
|
||||
inventorySort(backpackGetInventory(type), sortBy, reverse);
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
#include "inventory.h"
|
||||
|
||||
typedef struct {
|
||||
inventorystack_t storage[ITEM_TYPE_COUNT_MAX][INVENTORY_CAPACITY_MAX];
|
||||
inventory_t inventories[ITEM_TYPE_COUNT_MAX];
|
||||
inventorystack_t storage[ITEM_TYPE_COUNT][ITEM_TYPE_COUNT_MAX];
|
||||
inventory_t inventories[ITEM_TYPE_COUNT];
|
||||
} backpack_t;
|
||||
|
||||
extern backpack_t BACKPACK;
|
||||
@@ -26,7 +26,7 @@ void backpackInit();
|
||||
* @param type The item type.
|
||||
* @returns Pointer to the inventory for that type.
|
||||
*/
|
||||
inventory_t *backpackGetInventory(const itemtypeid_t type);
|
||||
inventory_t *backpackGetInventory(const itemtype_t type);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns true if the type's inventory is full.
|
||||
*/
|
||||
bool_t backpackIsFull(const itemtypeid_t type);
|
||||
bool_t backpackIsFull(const itemtype_t type);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
void backpackSort(
|
||||
const itemtypeid_t type,
|
||||
const itemtype_t type,
|
||||
const inventorysort_t sortBy,
|
||||
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) {
|
||||
const inventorystack_t *stackA = (const inventorystack_t*)a;
|
||||
const inventorystack_t *stackB = (const inventorystack_t*)b;
|
||||
const itemtypeid_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtypeid_t typeB = ITEMS[stackB->item].type;
|
||||
const itemtype_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtype_t typeB = ITEMS[stackB->item].type;
|
||||
if(typeA < typeB) return -1;
|
||||
if(typeA > typeB) return 1;
|
||||
return 0;
|
||||
@@ -198,8 +198,8 @@ int_t inventorySortByType(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 *stackB = (const inventorystack_t*)b;
|
||||
const itemtypeid_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtypeid_t typeB = ITEMS[stackB->item].type;
|
||||
const itemtype_t typeA = ITEMS[stackA->item].type;
|
||||
const itemtype_t typeB = ITEMS[stackB->item].type;
|
||||
if(typeA < typeB) return 1;
|
||||
if(typeA > typeB) return -1;
|
||||
return 0;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "rpg/item/item.h"
|
||||
|
||||
#define ITEM_STACK_QUANTITY_MAX 99
|
||||
#define INVENTORY_CAPACITY_MAX 250
|
||||
|
||||
typedef enum {
|
||||
INVENTORY_SORT_BY_ID,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user