44 Commits

Author SHA1 Message Date
YourWishes 8fb1c9fb42 physics and rendering fixes 2026-07-17 21:06:21 -05:00
YourWishes 5f08337726 physics stuff 2026-07-17 19:07:32 -05:00
YourWishes 0bc80d5df3 Roadmap and physics 2026-07-17 13:09:58 -05:00
YourWishes 2432443ea6 Cleaned settings a bit 2026-07-17 09:11:19 -05:00
YourWishes fa138971e7 Console optimized slightly for rendering 2026-07-16 23:52:47 -05:00
YourWishes d7982599d1 Simplified PSP network 2026-07-16 22:55:47 -05:00
YourWishes f16c80aa0f Fixed PSP rendering 2026-07-16 21:20:49 -05:00
YourWishes ef5adf8274 PSP Network connect 2026-07-16 20:09:39 -05:00
YourWishes 2ffc65b1b1 Network conn and disconn 2026-07-16 19:32:44 -05:00
YourWishes d4a17bd98d Net 2026-07-16 19:08:32 -05:00
YourWishes dcf5f434c5 Remove vita for now 2026-07-16 15:50:19 -05:00
YourWishes 78cd973a33 Remove story and battle stuff 2026-07-16 15:37:55 -05:00
YourWishes ca02ee0352 Add camera shake 2026-07-11 11:02:09 -05:00
YourWishes fbaa54145e Fixed dolphin rendering. 2026-07-10 20:15:26 -05:00
YourWishes 28754ffbf2 Removed old scripted types 2026-07-10 13:24:21 -05:00
YourWishes 470c0eba7a Whatever, some minor map chunking improvements 2026-07-10 12:57:31 -05:00
YourWishes 7098dcec43 Cleaned some log 2026-07-09 23:25:43 -05:00
YourWishes 07137f57af Assets are slightly optimized 2026-07-09 23:25:26 -05:00
YourWishes 8b7491a3d3 Emoji support to characters 2026-07-09 13:18:48 -05:00
YourWishes 8cfa8ddfeb Weaather baseline 2026-07-08 12:57:13 -05:00
YourWishes ef284a15a1 pre-cache shader matrices 2026-07-08 12:36:40 -05:00
YourWishes 3723921573 Render culling on sceneoverworld.h 2026-07-08 12:02:41 -05:00
YourWishes 195399635e Updating mini textbox 2026-07-08 11:45:10 -05:00
YourWishes 46e2a924d3 Mini textboxes 2026-07-08 10:53:53 -05:00
YourWishes b693ea4102 Starting item and battle stuff 2026-07-08 10:05:21 -05:00
YourWishes a73f55beb0 Battle stuff
Build Dusk / build-linux (push) Successful in 7m5s
Build Dusk / build-psp (push) Successful in 1m33s
Build Dusk / build-knulli (push) Successful in 4m0s
Build Dusk / build-gamecube (push) Successful in 3m31s
Build Dusk / build-gamecube-iso (push) Successful in 3m39s
Build Dusk / build-wii (push) Successful in 3m5s
Build Dusk / build-wii-iso (push) Successful in 3m25s
2026-07-08 07:55:15 -05:00
YourWishes 0bd2491ab7 Dropdown test. 2026-07-07 15:18:33 -05:00
YourWishes 860c797c9c Last round of cleanup for settings for now. 2026-07-07 14:56:08 -05:00
YourWishes 38c5080f9f Collapsing more ui settings code 2026-07-07 14:36:19 -05:00
YourWishes fae191d8fe Settings improvements 2026-07-07 14:00:37 -05:00
YourWishes d83a953e2d First pass of language 2026-07-07 11:14:02 -05:00
YourWishes 2dcf0d0f0d UI Confirm 2026-07-07 10:12:22 -05:00
YourWishes 6dee37d8c1 Slider widget and some cleanup 2026-07-07 09:58:10 -05:00
YourWishes 3bad03afb3 Item entity 2026-07-07 07:52:22 -05:00
YourWishes 6a6d8448f7 Area cutscene controls 2026-07-07 07:28:12 -05:00
YourWishes 189babd2cf Trigger types on areas 2026-07-07 07:07:32 -05:00
YourWishes 988a0f2294 Area basics 2026-07-06 23:19:05 -05:00
YourWishes 85bf455731 entity pos sets chunk now 2026-07-04 18:43:09 -05:00
YourWishes 589e4224f3 Example global ent 2026-07-04 13:03:24 -05:00
YourWishes bd7fa154b4 Add user data to callbacks 2026-07-04 09:13:37 -05:00
YourWishes a292f1992b Change to use the Z tile system 2026-07-04 08:40:53 -05:00
YourWishes 88ddf429b7 add some entity management in prep for global entities. 2026-07-03 12:25:24 -05:00
YourWishes 7a1f6662df More cutscene tools 2026-07-03 09:17:49 -05:00
YourWishes afc68bccc6 Update test runner
Build Dusk / build-linux (push) Successful in 5m36s
Build Dusk / build-psp (push) Successful in 1m15s
Build Dusk / build-knulli (push) Successful in 4m53s
Build Dusk / build-gamecube (push) Successful in 3m51s
Build Dusk / build-gamecube-iso (push) Successful in 3m52s
Build Dusk / build-wii (push) Successful in 3m17s
Build Dusk / build-wii-iso (push) Successful in 4m54s
2026-07-02 19:20:17 -05:00
320 changed files with 12309 additions and 4547 deletions
-30
View File
@@ -4,36 +4,6 @@ on:
tags:
- '*'
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
python3 \
python3-pip \
python3-polib \
python3-pil \
libsdl2-dev \
libgl1-mesa-dev \
libzip-dev \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
git \
libssl-dev
- name: Run tests
run: ./scripts/test-linux.sh
build-linux:
runs-on: ubuntu-latest
steps:
+35
View File
@@ -0,0 +1,35 @@
name: Test Dusk
on:
pull_request:
branches:
- main
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
python3 \
python3-pip \
python3-polib \
python3-pil \
libsdl2-dev \
libgl1-mesa-dev \
libzip-dev \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
git \
libssl-dev
- name: Run tests
run: ./scripts/test-linux.sh
+2 -4
View File
@@ -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
View File
@@ -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.
+49 -43
View File
@@ -10,51 +10,57 @@ msgid "ui.title"
msgstr ""
"Welcome"
#: ui/user.c:22
msgid "ui.greeting"
msgstr "Hello, %s!"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: ui/files.c:40
msgid "ui.file_status"
msgstr "%s has %d files."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Input"
#: ui/cart.c:55
msgid "cart.item_count"
msgid_plural "cart.item_count"
msgstr[0] "%d item"
msgstr[1] "%d items (dual)"
msgstr[2] "%d items (few)"
msgstr[3] "%d items (many)"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Display"
#: ui/notifications.c:71
msgid ""
"ui.multiline_help"
msgstr ""
"Line one of the help text.\n"
"Line two continues here.\n"
"Line three ends here."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
#: ui/errors.c:90
msgid ""
"error.upload_failed.long"
msgstr ""
"Upload failed for file \"%s\".\n"
"Please try again later or contact support."
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: ui/messages.c:110
msgid ""
"user.invite_status"
msgid_plural ""
"user.invite_status"
msgstr[0] ""
"%s invited %d user.\n"
"Please review the request."
msgstr[1] ""
"%s invited %d users (dual).\n"
"Please review the requests."
msgstr[2] ""
"%s invited %d users (few).\n"
"Please review the requests."
msgstr[3] ""
"%s invited %d users (many).\n"
"Please review the requests."
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Language"
msgid "ui.settings.general.language_detail"
msgstr "Takes effect after restarting the application."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Apply"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "Discard unsaved changes?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Characters"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Items"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Settings"
msgid "item.potion.name"
msgstr "Potion"
msgid "item.potato.name"
msgstr "Potato"
msgid "item.apple.name"
msgstr "Apple"
+70
View File
@@ -0,0 +1,70 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: es\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Bienvenido"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "Entrada"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "Pantalla"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "Audio"
msgid "ui.settings.input.deadzone"
msgstr "Deadzone"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "Idioma"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "Se aplica después de reiniciar la aplicación."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "Aplicar"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "¿Descartar los cambios no guardados?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "Personajes"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "Objetos"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "Configuración"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "Poción"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "Papa"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "Manzana"
+70
View File
@@ -0,0 +1,70 @@
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: ja\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=1; plural=(0);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"歓迎"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "一般"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.input"
msgstr "入力"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.display"
msgstr "表示"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.audio"
msgstr "オーディオ"
msgid "ui.settings.input.deadzone"
msgstr "デッドゾーン"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language"
msgstr "言語"
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
msgid "ui.settings.general.language_detail"
msgstr "アプリケーションを再起動すると適用されます。"
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.apply"
msgstr "適用"
#: src/dusk/ui/frame/uiconfirm.c
msgid "ui.confirm.discard_changes"
msgstr "未保存の変更を破棄しますか?"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.characters"
msgstr "キャラクター"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.items"
msgstr "アイテム"
#: src/dusk/ui/frame/game/uigamemenu.c
msgid "ui.game_menu.settings"
msgstr "設定"
#: src/dusk/rpg/item/item.json
msgid "item.potion.name"
msgstr "ポーション"
#: src/dusk/rpg/item/item.json
msgid "item.potato.name"
msgstr "ジャガイモ"
#: src/dusk/rpg/item/item.json
msgid "item.apple.name"
msgstr "リンゴ"
Binary file not shown.
Binary file not shown.
+3
View File
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
# like every other target instead of relying on endian.h's runtime probe.
DUSK_PLATFORM_ENDIAN_BIG
)
# Custom compiler flags
+1 -1
View File
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_WIDTH_DEFAULT=854
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
+1
View File
@@ -56,6 +56,7 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD
DUSK_TIME_DYNAMIC
DUSK_DISPLAY_OVERSCAN=6
)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
-88
View File
@@ -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
)
-5
View File
@@ -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)
+48 -57
View File
@@ -59,20 +59,28 @@ assetentry_t * assetGetEntry(
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing, Find first available slot.
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
// We did not find one existing. Find first available slot, reaping
// zero-ref entries to make room if none are immediately available.
bool_t reaped = false;
for(;;) {
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(reaped) break;
reaped = true;
errorCatch(assetReapUnused());
}
assertUnreachable("No available asset entry slots.");
return NULL;
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
assetEntryUnlock(entry);
}
errorret_t assetReapUnused(void) {
assertIsMainThread("assetReapUnused must be called from the main thread.");
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
// This handles dependency chains where an entry (e.g. a model) holds refs
// on child entries (mesh, texture): dispose parents first so child ref
// counts drop to zero, then pick up the children on the next pass. Without
// this, a forward-only scan fails when a shared child entry appears before
// a parent that still holds a ref to it.
bool_t any;
do {
any = false;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
if(entry->refs.count > 0) { entry++; continue; }
errorChain(assetEntryDispose(entry));
any = true;
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorOk();
}
errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread.");
@@ -324,30 +358,6 @@ errorret_t assetUpdate(void) {
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
// consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk();
}
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries
// until none remain. This handles dependency chains where an entry
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
// parents first so child ref counts drop to zero, then pick up the
// children on the next pass. Without this, a forward-only scan fails
// when a shared child entry appears before a parent that still holds a
// ref to it.
bool_t any;
do {
any = false;
assetentry_t *e = ASSET.entries;
do {
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
if(e->refs.count > 0) { e++; continue; }
errorChain(assetEntryDispose(e));
any = true;
e++;
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorChain(assetReapUnused());
// Cleanup zip file.
if(ASSET.zip != NULL) {
+12 -2
View File
@@ -23,8 +23,8 @@
#define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 20
#define ASSET_ENTRY_COUNT_MAX 128
#define ASSET_LOADING_COUNT_MAX 10
#define ASSET_ENTRY_COUNT_MAX 64
typedef struct asset_s {
zip_t *zip;
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
* until a full pass frees nothing further, since disposing a parent entry
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
* making it eligible for reaping too.
*
* @return An error code if any entry could not be disposed properly.
*/
errorret_t assetReapUnused(void);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
@@ -111,9 +111,15 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
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(
@@ -135,6 +141,9 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
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);
@@ -157,6 +166,12 @@ errorret_t assetChunkDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread.");
assetchunkoutput_t *out = &entry->data.chunk;
if(out->tiles != NULL) {
memoryFree(out->tiles);
out->tiles = NULL;
}
for(uint8_t m = 0; m < out->meshCount; m++) {
if(out->modelEntries[m] == NULL) continue;
assetUnlockEntry(out->modelEntries[m]);
@@ -9,7 +9,7 @@
#include "asset/assetfile.h"
#include "rpg/overworld/chunk.h"
#define ASSET_CHUNK_FILE_VERSION 3
#define ASSET_CHUNK_FILE_VERSION 4
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
@@ -34,7 +34,7 @@ typedef struct {
} assetchunkloaderloading_t;
typedef struct {
tile_t tiles[CHUNK_TILE_COUNT];
tile_t *tiles;
uint8_t meshCount;
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
+23 -4
View File
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
meshvertex_t *vertices = NULL;
if(vertCount > 0) {
vertices = memoryAllocate(vertCount * sizeof(meshvertex_t));
// 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
// require this for the DMA'd vertex data to actually reach the GPU
// coherently. Static compiled-in vertex arrays happen to get this from
// the linker; a plain memoryAllocate here would not.
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
for(uint32_t v = 0; v < vertCount; v++) {
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
}
}
memoryFree(raw);
@@ -103,18 +115,22 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
out->vertices = NULL;
errorChain(ret);
}
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
#ifndef DUSK_OPENGL_LEGACY
// VBO owns the data now; CPU copy is no longer needed.
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
// VBO owns the data now; CPU copy is no longer needed. The platform
// mesh object itself still needs meshDispose later - tracked via
// out->meshInitialized, independent of the CPU buffer's lifetime.
memoryFree(out->vertices);
out->vertices = NULL;
#endif
@@ -129,8 +145,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
assertIsMainThread("Must be called from the main thread.");
assetmeshoutput_t *out = &entry->data.mesh;
if(out->vertices != NULL) {
if(out->meshInitialized) {
errorChain(meshDispose(&out->mesh));
out->meshInitialized = false;
}
if(out->vertices != NULL) {
memoryFree(out->vertices);
out->vertices = NULL;
}
@@ -30,6 +30,7 @@ typedef struct {
typedef struct {
mesh_t mesh;
bool_t meshInitialized;
meshvertex_t *vertices;
} assetmeshoutput_t;
@@ -506,12 +506,17 @@ errorret_t assetLocaleGetString(
sizeof(lineBuffer)
);
// Prime the reader with the first line before scanning; outBuffer holds
// uninitialized memory until the first Next() call fills it.
errorChain(assetFileLineReaderNext(&reader));
// Skip blanks, comments, etc and start looking for msgid's
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
while(!reader.eof) {
while(true) {
// Is this msgid?
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
if(reader.eof) break;
errorChain(assetFileLineReaderNext(&reader));
msgidBuffer[0] = '\0';
continue;
@@ -535,7 +540,7 @@ errorret_t assetLocaleGetString(
}
// We are either going to see a msgstr or a msgid_plural
while(!reader.eof) {
while(true) {
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
// Is msgid_plural?
+2 -1
View File
@@ -17,7 +17,7 @@ console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = true;
CONSOLE.visible = false;
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
@@ -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);
+5
View File
@@ -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
+1
View File
@@ -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 -10
View File
@@ -17,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;
@@ -37,7 +39,8 @@ 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(displayInit());
errorChain(uiInit());
@@ -45,6 +48,13 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(networkInit());
errorChain(sceneInit());
networkRequestConnection(
engineNetworkOnConnected,
engineNetworkOnFailed,
engineNetworkOnDisconnect,
NULL
);
consolePrint("Engine initialized");
#ifdef DUSK_ASSERTIONS_FAKED
@@ -54,7 +64,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
#endif
sceneSet(SCENE_TYPE_OVERWORLD);
errorOk();
}
@@ -63,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();
}
@@ -88,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");
}
+52
View File
@@ -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);
+1 -27
View File
@@ -11,31 +11,17 @@
#include "util/string.h"
#include "util/math.h"
#include "time/time.h"
#include "event/event.h"
input_t INPUT;
errorret_t inputInit(void) {
memoryZero(&INPUT, sizeof(input_t));
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
INPUT.actions[i].action = (inputaction_t)i;
INPUT.actions[i].lastValue = 0.0f;
INPUT.actions[i].currentValue = 0.0f;
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
@@ -97,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) {
+4
View File
@@ -12,11 +12,15 @@
#define INPUT_LISTENER_PRESSED_MAX 16
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
#define INPUT_DEADZONE_DEFAULT 0.1f
typedef struct {
inputactiondata_t actions[INPUT_ACTION_COUNT];
inputplatform_t platform;
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
float_t deadzone;
} input_t;
extern input_t INPUT;
-10
View File
@@ -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;
/**
+10
View File
@@ -16,4 +16,14 @@ typedef struct {
static const localeinfo_t LOCALE_EN_US = {
.name = "en-US",
.file = "locale/en_US.po",
};
static const localeinfo_t LOCALE_JP_JP = {
.name = "ja-JP",
.file = "locale/jp_JP.po",
};
static const localeinfo_t LOCALE_ES_MX = {
.name = "es-MX",
.file = "locale/es_MX.po",
};
+3
View File
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
network.c
networkinfo.c
)
# Subdirs
add_subdirectory(http)
+14
View File
@@ -0,0 +1,14 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networkhttpheader.c
networkhttpurl.c
networkhttprequest.c
networkhttpprocess.c
networkhttpthread.c
networkhttp.c
)
+48
View File
@@ -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();
}
+40
View File
@@ -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);
+49
View File
@@ -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;
}
+70
View File
@@ -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
);
+346
View File
@@ -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();
}
+185
View File
@@ -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
);
+121
View File
@@ -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();
}
+137
View File
@@ -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
);
+88
View File
@@ -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);
}
+31
View File
@@ -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);
+136
View File
@@ -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();
}
+89
View File
@@ -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
);
+5 -2
View File
@@ -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();
}
+2 -3
View File
@@ -14,6 +14,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(cutscene)
add_subdirectory(entity)
add_subdirectory(overworld)
add_subdirectory(story)
add_subdirectory(item)
add_subdirectory(item)
add_subdirectory(physics)
+116 -2
View File
@@ -13,14 +13,19 @@ typedef struct cutscene_s {
const cutsceneitem_t *items;
uint8_t itemCount;
cutscenepause_t pause;
// Size in bytes of this cutscene's custom user data, carved out of
// CUTSCENE_SYSTEM.data while the cutscene is running.
size_t dataSize;
} cutscene_t;
#define CUTSCENE(NAME, PAUSE_TYPE, ...) \
#define CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...) \
static const cutsceneitem_t CUTSCENE_##NAME##_ITEMS[] = { __VA_ARGS__ }; \
static const cutscene_t CUTSCENE_##NAME = { \
.items = CUTSCENE_##NAME##_ITEMS, \
.itemCount = sizeof(CUTSCENE_##NAME##_ITEMS) / sizeof(cutsceneitem_t), \
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE \
.pause = CUTSCENE_PAUSE_##PAUSE_TYPE, \
.dataSize = SIZE \
};
#define CUTSCENE_REFERENCE(CUTSCENE) \
@@ -29,6 +34,22 @@ typedef struct cutscene_s {
#define CUTSCENE_TEXT(TEXT) \
{ .type = CUTSCENE_ITEM_TYPE_TEXT, .text = { .text = TEXT } }
#define CUTSCENE_TEXT_MINI(TEXT, X, Y, Z, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI, \
.textMini = { \
.text = TEXT, \
.position = { X, Y, Z }, \
.duration = DURATION \
} \
}
#define CUTSCENE_TEXT_MINI_HIDE(INDEX) \
{ \
.type = CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE, \
.textMiniHide = { .index = INDEX } \
}
#define CUTSCENE_WAIT(WAIT) \
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
@@ -70,6 +91,35 @@ typedef struct cutscene_s {
.entityRemove = { .entityIndex = ENTITY_INDEX } \
}
#define CUTSCENE_ENTITY_ADD(TYPE, X, Y, Z) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_ADD, \
.entityAdd = { .entityType = TYPE, .position = { X, Y, Z } } \
}
#define CUTSCENE_ENTITY_TURN(ENTITY_INDEX, DIRECTION) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TURN, \
.entityTurn = { .entityIndex = ENTITY_INDEX, .direction = DIRECTION } \
}
// Walks ENTITY_INDEX to stand beside TARGET_ENTITY_INDEX, offset by
// (OFFSET_X, OFFSET_Y) on the 2D plane. The destination Z is resolved
// from nearby terrain each frame, so ramps between the two entities are
// accounted for automatically.
#define CUTSCENE_ENTITY_WALK_TO_ENTITY( \
ENTITY_INDEX, TARGET_ENTITY_INDEX, OFFSET_X, OFFSET_Y \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY, \
.entityWalkToEntity = { \
.entityIndex = ENTITY_INDEX, \
.targetEntityIndex = TARGET_ENTITY_INDEX, \
.offsetX = OFFSET_X, \
.offsetY = OFFSET_Y \
} \
}
#define CUTSCENE_ENTITY_TELEPORT(ENTITY_INDEX, X, Y, Z) \
{ \
.type = CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT, \
@@ -94,6 +144,24 @@ typedef struct cutscene_s {
#define CUTSCENE_FADE_FROM_WHITE(DURATION) \
CUTSCENE_FADE(COLOR_WHITE, COLOR_TRANSPARENT_WHITE, DURATION, EASING_LINEAR)
#define CUTSCENE_EMOJI(ENTITY_INDEX, EMOJI_TYPE, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_EMOJI, \
.emoji = { \
.entityIndex = ENTITY_INDEX, \
.emojiType = EMOJI_TYPE, \
.duration = DURATION \
} \
}
// AMOUNT ranges 0 (no shake) to 4 (three tiles): 1 is half a tile, 2 is
// a full tile, 3 is two tiles, and 4 is three tiles.
#define CUTSCENE_SHAKE(AMOUNT, DURATION) \
{ \
.type = CUTSCENE_ITEM_TYPE_SHAKE, \
.shake = { .amount = AMOUNT, .duration = DURATION } \
}
#define CUTSCENE_SET_PAUSE(FLAGS) \
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
@@ -116,3 +184,49 @@ typedef struct cutscene_s {
) \
} \
}
#define CUTSCENE_MAP_AREA_ADD( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, CALLBACK, NOTIFY, TRIGGER \
) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_ADD, \
.mapAreaAdd = { \
.min = { MIN_X, MIN_Y, MIN_Z }, \
.max = { MAX_X, MAX_Y, MAX_Z }, \
.callback = CALLBACK, \
.notify = NOTIFY, \
.trigger = TRIGGER \
} \
}
#define CUTSCENE_MAP_AREA_REMOVE(AREA_ID) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE, \
.mapAreaRemove = { .areaId = AREA_ID } \
}
// Waits until any one of the given map area IDs has its callback invoked.
// Accepts CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
#define CUTSCENE_MAP_AREA_WAIT(...) \
{ \
.type = CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT, \
.mapAreaWait = { \
.areaIds = (const uint8_t[]){ __VA_ARGS__ }, \
.count = (uint8_t)( \
sizeof((const uint8_t[]){ __VA_ARGS__ }) / sizeof(uint8_t) \
) \
} \
}
// Adds a map area, waits for it to be triggered once, then removes it
// before the cutscene continues. Uses a no-op callback since the wait is
// driven by the area's trigger count rather than callback logic.
#define CUTSCENE_MAP_AREA_TRIGGER_ONCE( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, NOTIFY, TRIGGER \
) \
CUTSCENE_MAP_AREA_ADD( \
MIN_X, MIN_Y, MIN_Z, MAX_X, MAX_Y, MAX_Z, \
mapAreaNoopCallback, NOTIFY, TRIGGER \
), \
CUTSCENE_MAP_AREA_WAIT(CUTSCENE_AREA_LAST_CREATED), \
CUTSCENE_MAP_AREA_REMOVE(CUTSCENE_AREA_LAST_CREATED)
+66 -9
View File
@@ -25,10 +25,19 @@ void cutsceneSystemStartCutsceneWith(
entity_t *interact,
entity_t *interacted
) {
assertTrue(
cutscene->dataSize < CUTSCENE_SYSTEM_SIZE_MAX,
"Cutscene data size exceeds CUTSCENE_SYSTEM_SIZE_MAX"
);
CUTSCENE_SYSTEM.scene = cutscene;
CUTSCENE_SYSTEM.pause = cutscene->pause;
CUTSCENE_SYSTEM.entityInteract = interact;
CUTSCENE_SYSTEM.entityInteracted = interacted;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
cutsceneSystemNext();
}
@@ -54,6 +63,10 @@ void cutsceneSystemNext() {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
CUTSCENE_SYSTEM.entityInteract = NULL;
CUTSCENE_SYSTEM.entityInteracted = NULL;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
return;
}
@@ -70,25 +83,65 @@ const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
}
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex) {
entity_t *entity;
if(entityIndex == CUTSCENE_ENTITY_INTERACT) {
assertNotNull(
CUTSCENE_SYSTEM.entityInteract,
"CUTSCENE_ENTITY_INTERACT used but no interact entity is set"
);
return CUTSCENE_SYSTEM.entityInteract;
}
if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
entity = CUTSCENE_SYSTEM.entityInteract;
} else if(entityIndex == CUTSCENE_ENTITY_INTERACTED) {
assertNotNull(
CUTSCENE_SYSTEM.entityInteracted,
"CUTSCENE_ENTITY_INTERACTED used but no interacted entity is set"
);
return CUTSCENE_SYSTEM.entityInteracted;
entity = CUTSCENE_SYSTEM.entityInteracted;
} else if(entityIndex == CUTSCENE_ENTITY_LAST_CREATED) {
assertNotNull(
CUTSCENE_SYSTEM.entityLastCreated,
"CUTSCENE_ENTITY_LAST_CREATED used but no entity has been created"
);
entity = CUTSCENE_SYSTEM.entityLastCreated;
} else if(entityIndex == CUTSCENE_ENTITY_LAST_REF) {
assertNotNull(
CUTSCENE_SYSTEM.entityLastRef,
"CUTSCENE_ENTITY_LAST_REF used but no entity has been referenced"
);
entity = CUTSCENE_SYSTEM.entityLastRef;
} else {
assertTrue(
entityIndex < ENTITY_COUNT,
"Entity index is out of range"
);
entity = &ENTITIES[entityIndex];
}
assertTrue(
entityIndex < ENTITY_COUNT,
"Entity index is out of range"
);
return &ENTITIES[entityIndex];
CUTSCENE_SYSTEM.entityLastRef = entity;
return entity;
}
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId) {
if(areaId == CUTSCENE_AREA_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.areaLastCreated != CUTSCENE_AREA_LAST_CREATED,
"CUTSCENE_AREA_LAST_CREATED used but no map area has been created"
);
return CUTSCENE_SYSTEM.areaLastCreated;
}
return areaId;
}
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
if(index == CUTSCENE_TEXT_MINI_LAST_CREATED) {
assertTrue(
CUTSCENE_SYSTEM.textMiniLastCreated != CUTSCENE_TEXT_MINI_LAST_CREATED,
"CUTSCENE_TEXT_MINI_LAST_CREATED used but no mini textbox has been "
"shown"
);
return CUTSCENE_SYSTEM.textMiniLastCreated;
}
return index;
}
void cutsceneSystemDispose() {
@@ -97,4 +150,8 @@ void cutsceneSystemDispose() {
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
CUTSCENE_SYSTEM.entityInteract = NULL;
CUTSCENE_SYSTEM.entityInteracted = NULL;
CUTSCENE_SYSTEM.entityLastCreated = NULL;
CUTSCENE_SYSTEM.entityLastRef = NULL;
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
}
+39 -3
View File
@@ -10,8 +10,16 @@
typedef struct entity_s entity_t;
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
#define CUTSCENE_ENTITY_INTERACT ((uint8_t)0xFE)
#define CUTSCENE_ENTITY_INTERACTED ((uint8_t)0xFD)
#define CUTSCENE_ENTITY_LAST_CREATED ((uint8_t)0xFC)
#define CUTSCENE_ENTITY_LAST_REF ((uint8_t)0xFB)
#define CUTSCENE_AREA_LAST_CREATED ((uint8_t)0xFF)
#define CUTSCENE_TEXT_MINI_LAST_CREATED ((uint8_t)0xFA)
// Maximum number of bytes a running cutscene may request via
// cutscene_t.dataSize.
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
typedef struct {
const cutscene_t *scene;
@@ -19,9 +27,17 @@ typedef struct {
cutscenepause_t pause;
entity_t *entityInteract;
entity_t *entityInteracted;
entity_t *entityLastCreated;
entity_t *entityLastRef;
uint8_t areaLastCreated;
uint8_t textMiniLastCreated;
// Data (used by the current item).
cutsceneitemdata_t data;
// Custom user data for the running cutscene, sized per-scene by
// cutscene_t.dataSize.
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
} cutscenesystem_t;
extern cutscenesystem_t CUTSCENE_SYSTEM;
@@ -53,7 +69,9 @@ void cutsceneSystemStartCutsceneWith(
/**
* Resolves a raw entity index (or sentinel) to an entity pointer.
* Handles CUTSCENE_ENTITY_INTERACT and CUTSCENE_ENTITY_INTERACTED.
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
* CUTSCENE_ENTITY_LAST_CREATED and CUTSCENE_ENTITY_LAST_REF.
* Updates CUTSCENE_SYSTEM.entityLastRef to the resolved entity.
* Asserts the resolved entity is within bounds.
*
* @param entityIndex Raw entity index or sentinel value.
@@ -61,6 +79,24 @@ void cutsceneSystemStartCutsceneWith(
*/
entity_t * cutsceneSystemGetEntity(const uint8_t entityIndex);
/**
* Resolves a raw map area ID (or CUTSCENE_AREA_LAST_CREATED sentinel) to
* a concrete map area ID.
*
* @param areaId Raw map area ID or sentinel value.
* @returns The resolved map area ID.
*/
uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
/**
* Resolves a raw mini textbox slot index (or CUTSCENE_TEXT_MINI_LAST_CREATED
* sentinel) to a concrete UI_TEXTBOX_MINI_LIST slot index.
*
* @param index Raw slot index or sentinel value.
* @returns The resolved slot index.
*/
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
/**
* Advance to the next item in the cutscene.
*/
@@ -12,4 +12,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(control)
add_subdirectory(entity)
add_subdirectory(item)
add_subdirectory(maparea)
add_subdirectory(ui)
@@ -6,12 +6,13 @@
*/
#include "cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void cutsceneCallbackStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(item->callback != NULL) item->callback();
if(item->callback != NULL) item->callback(CUTSCENE_SYSTEM.userData);
}
bool_t cutsceneCallbackUpdate(
@@ -11,7 +11,7 @@
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef void (*cutscenecallback_t)(void);
typedef void (*cutscenecallback_t)(void *userData);
/**
* Starts a callback item (invokes the callback immediately).
+133 -82
View File
@@ -7,90 +7,141 @@
#include "rpg/cutscene/cutscenesystem.h"
cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
[CUTSCENE_ITEM_TYPE_NULL] = { 0 },
[CUTSCENE_ITEM_TYPE_TEXT] = {
.init = cutsceneTextStart,
.update = cutsceneTextUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI] = {
.init = cutsceneTextMiniStart,
.update = cutsceneTextMiniUpdate
},
[CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE] = {
.init = cutsceneTextMiniHideStart,
.update = cutsceneTextMiniHideUpdate
},
[CUTSCENE_ITEM_TYPE_CALLBACK] = {
.init = cutsceneCallbackStart,
.update = cutsceneCallbackUpdate
},
[CUTSCENE_ITEM_TYPE_WAIT] = {
.init = cutsceneWaitStart,
.update = cutsceneWaitUpdate
},
[CUTSCENE_ITEM_TYPE_CUTSCENE] = {
.init = cutsceneCutsceneStart,
.update = cutsceneCutsceneUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT] = {
.init = cutsceneEntityTeleportStart,
.update = cutsceneEntityTeleportUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO] = {
.init = cutsceneEntityWalkToStart,
.update = cutsceneEntityWalkToUpdate
},
[CUTSCENE_ITEM_TYPE_FADE] = {
.init = cutsceneFadeStart,
.update = cutsceneFadeUpdate
},
[CUTSCENE_ITEM_TYPE_SET_PAUSE] = {
.init = cutsceneSetPauseStart,
.update = cutsceneSetPauseUpdate
},
[CUTSCENE_ITEM_TYPE_CONCURRENT] = {
.init = cutsceneConcurrentStart,
.update = cutsceneConcurrentUpdate
},
[CUTSCENE_ITEM_TYPE_ITEM_GIVE] = {
.init = cutsceneItemGiveStart,
.update = cutsceneItemGiveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_REMOVE] = {
.init = cutsceneEntityRemoveStart,
.update = cutsceneEntityRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_ADD] = {
.init = cutsceneEntityAddStart,
.update = cutsceneEntityAddUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_TURN] = {
.init = cutsceneEntityTurnStart,
.update = cutsceneEntityTurnUpdate
},
[CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY] = {
.init = cutsceneEntityWalkToEntityStart,
.update = cutsceneEntityWalkToEntityUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_ADD] = {
.init = cutsceneMapAreaAddStart,
.update = cutsceneMapAreaAddUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE] = {
.init = cutsceneMapAreaRemoveStart,
.update = cutsceneMapAreaRemoveUpdate
},
[CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT] = {
.init = cutsceneMapAreaWaitStart,
.update = cutsceneMapAreaWaitUpdate
},
[CUTSCENE_ITEM_TYPE_EMOJI] = {
.init = cutsceneEmojiStart,
.update = cutsceneEmojiUpdate
},
[CUTSCENE_ITEM_TYPE_SHAKE] = {
.init = cutsceneShakeStart,
.update = cutsceneShakeUpdate
}
};
void cutsceneItemStart(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) {
case CUTSCENE_ITEM_TYPE_TEXT:
cutsceneTextStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CALLBACK:
cutsceneCallbackStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_WAIT:
cutsceneWaitStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CUTSCENE:
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
cutsceneEntityTeleportStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
cutsceneEntityWalkToStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_FADE:
cutsceneFadeStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
cutsceneSetPauseStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_CONCURRENT:
cutsceneConcurrentStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
cutsceneItemGiveStart(item, data);
break;
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
cutsceneEntityRemoveStart(item, data);
break;
default:
break;
}
cutsceneiteminitcallback_t *init = CUTSCENE_ITEM_CALLBACKS[item->type].init;
if(init != NULL) init(item, data);
}
bool_t cutsceneItemUpdate(const cutsceneitem_t *item, cutsceneitemdata_t *data) {
switch(item->type) {
case CUTSCENE_ITEM_TYPE_TEXT:
return cutsceneTextUpdate(item, data);
bool_t cutsceneItemUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
cutsceneitemupdatecallback_t *update =
CUTSCENE_ITEM_CALLBACKS[item->type].update;
if(update == NULL) return false;
case CUTSCENE_ITEM_TYPE_CALLBACK:
return cutsceneCallbackUpdate(item, data);
case CUTSCENE_ITEM_TYPE_WAIT:
return cutsceneWaitUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
return cutsceneEntityTeleportUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO:
return cutsceneEntityWalkToUpdate(item, data);
case CUTSCENE_ITEM_TYPE_FADE:
return cutsceneFadeUpdate(item, data);
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
return cutsceneSetPauseUpdate(item, data);
case CUTSCENE_ITEM_TYPE_CONCURRENT:
return cutsceneConcurrentUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
return cutsceneItemGiveUpdate(item, data);
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
return cutsceneEntityRemoveUpdate(item, data);
default:
return false;
}
return update(item, data);
}
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
if(item->cutscene != NULL) cutsceneSystemStartCutscene(item->cutscene);
}
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return false;
}
+79 -1
View File
@@ -13,15 +13,28 @@
#include "entity/cutsceneentityteleport.h"
#include "entity/cutsceneentitywalkto.h"
#include "entity/cutsceneentityremove.h"
#include "entity/cutsceneentityadd.h"
#include "entity/cutsceneentityturn.h"
#include "entity/cutsceneentitywalktoentity.h"
#include "ui/cutscenetext.h"
#include "ui/cutscenetextmini.h"
#include "ui/cutscenetextminihide.h"
#include "ui/cutscenefade.h"
#include "ui/cutsceneemoji.h"
#include "ui/cutsceneshake.h"
#include "item/cutsceneitemgive.h"
#include "maparea/cutscenemapareaadd.h"
#include "maparea/cutscenemaparearemove.h"
#include "maparea/cutscenemapareawait.h"
typedef struct cutscene_s cutscene_t;
typedef enum {
CUTSCENE_ITEM_TYPE_NULL,
CUTSCENE_ITEM_TYPE_TEXT,
CUTSCENE_ITEM_TYPE_TEXT_MINI,
CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE,
CUTSCENE_ITEM_TYPE_CALLBACK,
CUTSCENE_ITEM_TYPE_WAIT,
CUTSCENE_ITEM_TYPE_CUTSCENE,
@@ -31,7 +44,17 @@ typedef enum {
CUTSCENE_ITEM_TYPE_SET_PAUSE,
CUTSCENE_ITEM_TYPE_CONCURRENT,
CUTSCENE_ITEM_TYPE_ITEM_GIVE,
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE
CUTSCENE_ITEM_TYPE_ENTITY_REMOVE,
CUTSCENE_ITEM_TYPE_ENTITY_ADD,
CUTSCENE_ITEM_TYPE_ENTITY_TURN,
CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY,
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
CUTSCENE_ITEM_TYPE_EMOJI,
CUTSCENE_ITEM_TYPE_SHAKE,
CUTSCENE_ITEM_TYPE_COUNT
} cutsceneitemtype_t;
struct cutsceneitem_s {
@@ -39,6 +62,8 @@ struct cutsceneitem_s {
union {
cutscenetext_t text;
cutscenetextmini_t textMini;
cutscenetextminihide_t textMiniHide;
cutscenecallback_t callback;
cutscenewait_t wait;
const cutscene_t *cutscene;
@@ -49,6 +74,14 @@ struct cutsceneitem_s {
cutsceneconcurrent_t concurrent;
cutsceneitemgive_t itemGive;
cutsceneentityremove_t entityRemove;
cutsceneentityadd_t entityAdd;
cutsceneentityturn_t entityTurn;
cutsceneentitywalktoentity_t entityWalkToEntity;
cutscenemapareaadd_t mapAreaAdd;
cutscenemaparearemove_t mapAreaRemove;
cutscenemapareawait_t mapAreaWait;
cutsceneemoji_t emoji;
cutsceneshake_t shake;
};
};
@@ -56,8 +89,27 @@ typedef union cutsceneitemdata_u {
cutscenewaitdata_t wait;
cutsceneentitywalktodata_t entityWalkTo;
cutsceneconcurrentdata_t concurrent;
cutscenemapareawaitdata_t mapAreaWait;
} cutsceneitemdata_t;
typedef void (cutsceneiteminitcallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef bool_t (cutsceneitemupdatecallback_t)(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
typedef struct {
cutsceneiteminitcallback_t *init;
cutsceneitemupdatecallback_t *update;
} cutsceneitemcallbacks_t;
extern cutsceneitemcallbacks_t
CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT];
/**
* Start the given cutscene item.
*
@@ -80,3 +132,29 @@ bool_t cutsceneItemUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Starts a nested-cutscene item, handing control over to the
* referenced cutscene.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneCutsceneStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a nested-cutscene item. By the time this would run, control
* has already moved on to the referenced cutscene, so this always
* reports incomplete.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns false always.
*/
bool_t cutsceneCutsceneUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutsceneentityteleport.c
cutsceneentitywalkto.c
cutsceneentityremove.c
cutsceneentityadd.c
cutsceneentityturn.c
cutsceneentitywalktoentity.c
)
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
#include "assert/assert.h"
void cutsceneEntityAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t entIndex = entityGetAvailable();
assertTrue(entIndex != 0xFF, "No available entity slots for CUTSCENE_ENTITY_ADD");
entity_t *entity = &ENTITIES[entIndex];
entityInit(entity, item->entityAdd.entityType);
entityPositionSet(entity, item->entityAdd.position);// Also assigns chunk.
CUTSCENE_SYSTEM.entityLastCreated = entity;
CUTSCENE_SYSTEM.entityLastRef = entity;
}
bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitytype.h"
#include "rpg/overworld/worldpos.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
entitytype_t entityType;
worldpos_t position;
} cutsceneentityadd_t;
/**
* Starts an entity add step (spawns the entity into the world immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity add step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEntityAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
void cutsceneEntityTurnStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->entityTurn.entityIndex);
if(
entity->direction == item->entityTurn.direction &&
entity->animation == ENTITY_ANIM_IDLE
) return true;
entityTurn(entity, item->entityTurn.direction);
return false;
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/entity/entitydir.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
entitydir_t direction;
} cutsceneentityturn_t;
/**
* Starts an entity turn step. The turn itself is driven from Update, since
* the entity may still be finishing a previous action.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityTurnStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity turn step, retrying entityTurn until it takes effect
* and its animation completes.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the entity is idle and facing the target direction.
*/
bool_t cutsceneEntityTurnUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
#include "rpg/entity/entitypathstep.h"
#include "rpg/overworld/map.h"
void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
}
bool_t cutsceneEntityWalkToEntityUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(
item->entityWalkToEntity.entityIndex
);
entity_t *target = cutsceneSystemGetEntity(
item->entityWalkToEntity.targetEntityIndex
);
worldpos_t dest = {
.x = (worldunit_t)(target->position.x + item->entityWalkToEntity.offsetX),
.y = (worldunit_t)(target->position.y + item->entityWalkToEntity.offsetY),
.z = target->position.z
};
worldunit_t z;
if(mapGetWalkableZNear(dest.x, dest.y, target->position.z, &z)) dest.z = z;
return entityPathStep(entity, dest, true);
}
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "rpg/overworld/worldpos.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
uint8_t targetEntityIndex;
worldunit_t offsetX;
worldunit_t offsetY;
} cutsceneentitywalktoentity_t;
/**
* Starts an entity walk-to-entity item. No setup is needed, the destination
* is recomputed from the target's live position every Update.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEntityWalkToEntityStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an entity walk-to-entity item. Re-reads the target entity's
* current position each frame, applies the X/Y offset, resolves the
* destination Z from nearby terrain (to account for ramps), and steps
* the entity toward it.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once the entity has reached the target's side.
*/
bool_t cutsceneEntityWalkToEntityUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -6,25 +6,14 @@
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/item/backpack.h"
#include "ui/rpg/uitextboxmain.h"
#include "util/string.h"
#include "rpg/item/itemgive.h"
#include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneItemGiveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
backpackAdd(item->itemGive.item, item->itemGive.quantity);
char_t msg[CUTSCENE_TEXT_MAX_CHARS];
stringFormat(
msg,
CUTSCENE_TEXT_MAX_CHARS - 1,
"Received %s x%u",
ITEMS[item->itemGive.item].name,
(uint32_t)item->itemGive.quantity
);
uiTextboxMainSetText(msg);
itemGive(item->itemGive.item, item->itemGive.quantity);
}
bool_t cutsceneItemGiveUpdate(
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenemapareaadd.c
cutscenemaparearemove.c
cutscenemapareawait.c
)
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
void cutsceneMapAreaAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
CUTSCENE_SYSTEM.areaLastCreated = mapAreaAdd(
item->mapAreaAdd.min,
item->mapAreaAdd.max,
item->mapAreaAdd.callback,
item->mapAreaAdd.notify,
item->mapAreaAdd.trigger
);
}
bool_t cutsceneMapAreaAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -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 "rpg/overworld/maparea.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
worldpos_t min;
worldpos_t max;
mapareacallback_t callback;
uint8_t notify;
uint8_t trigger;
} cutscenemapareaadd_t;
/**
* Starts a map area add step (adds the area immediately, storing its ID
* in CUTSCENE_SYSTEM.areaLastCreated).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaAddStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area add step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMapAreaAddUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/overworld/maparea.h"
void cutsceneMapAreaRemoveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
mapAreaRemove(cutsceneSystemGetAreaId(item->mapAreaRemove.areaId));
}
bool_t cutsceneMapAreaRemoveUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -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 "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t areaId;
} cutscenemaparearemove_t;
/**
* Starts a map area remove step (removes the area immediately). Accepts
* CUTSCENE_AREA_LAST_CREATED in place of a literal area ID.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaRemoveStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area remove step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneMapAreaRemoveUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/overworld/maparea.h"
#include "assert/assert.h"
void cutsceneMapAreaWaitStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
assertTrue(
item->mapAreaWait.count <= CUTSCENE_MAP_AREA_WAIT_MAX,
"Too many areas in CUTSCENE_MAP_AREA_WAIT"
);
for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
data->mapAreaWait.baseline[i] = MAP_AREAS[areaId].triggerCount;
}
}
bool_t cutsceneMapAreaWaitUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
for(uint8_t i = 0; i < item->mapAreaWait.count; i++) {
uint8_t areaId = cutsceneSystemGetAreaId(item->mapAreaWait.areaIds[i]);
if(MAP_AREAS[areaId].triggerCount != data->mapAreaWait.baseline[i]) {
return true;
}
}
return false;
}
@@ -0,0 +1,49 @@
/**
* 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 cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
/** Maximum number of areas a single CUTSCENE_MAP_AREA_WAIT may watch. */
#define CUTSCENE_MAP_AREA_WAIT_MAX 4
typedef struct {
const uint8_t *areaIds;
uint8_t count;
} cutscenemapareawait_t;
typedef struct {
uint32_t baseline[CUTSCENE_MAP_AREA_WAIT_MAX];
} cutscenemapareawaitdata_t;
/**
* Starts a map area wait step, snapshotting each watched area's current
* trigger count.
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneMapAreaWaitStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a map area wait step, completing once any watched area's
* trigger count has changed since Start (i.e. its callback fired).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true once any watched area has been triggered.
*/
bool_t cutsceneMapAreaWaitUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -6,5 +6,9 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
cutscenetext.c
cutscenetextmini.c
cutscenetextminihide.c
cutscenefade.c
cutsceneemoji.c
cutsceneshake.c
)
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "rpg/entity/entity.h"
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
entity_t *entity = cutsceneSystemGetEntity(item->emoji.entityIndex);
uiEmojiAdd(entity->id, item->emoji.duration, item->emoji.emojiType);
}
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "ui/rpg/uiemoji.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t entityIndex;
float_t duration;
uiemojitype_t emojiType;
} cutsceneemoji_t;
/**
* Starts an emoji step (shows an emoji above the entity for the given
* duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneEmojiStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates an emoji step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneEmojiUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/rpgcamera.h"
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
rpgCameraShake(item->shake.amount, item->shake.duration);
}
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,41 @@
/**
* 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 cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t amount;
float_t duration;
} cutsceneshake_t;
/**
* Starts a camera shake item (kicks off the shake on the RPG camera).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneShakeStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a camera shake item. The shake itself runs asynchronously on
* the RPG camera, so this always completes immediately.
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneShakeUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+1 -1
View File
@@ -6,7 +6,7 @@
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "ui/rpg/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxmain.h"
void cutsceneTextStart(
const cutsceneitem_t *item,
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = uiTextboxMiniListGetNext();
uiTextboxMiniShow(
&UI_TEXTBOX_MINI_LIST[index],
item->textMini.text,
item->textMini.position,
item->textMini.duration,
NULL,
NULL
);
CUTSCENE_SYSTEM.textMiniLastCreated = index;
}
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -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 "dusk.h"
typedef struct cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
#define CUTSCENE_TEXT_MINI_MAX_CHARS 128
typedef struct {
char_t text[CUTSCENE_TEXT_MINI_MAX_CHARS];
vec3 position;
float_t duration;
} cutscenetextmini_t;
/**
* Starts a mini text item (shows a mini textbox at the given world
* position for the given duration, then completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text item (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/cutscene/item/cutsceneitem.h"
#include "rpg/cutscene/cutscenesystem.h"
#include "ui/rpg/textbox/uitextboxminilist.h"
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
uint8_t index = cutsceneSystemGetTextMiniId(item->textMiniHide.index);
uiTextboxMiniClose(&UI_TEXTBOX_MINI_LIST[index]);
}
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
) {
return true;
}
@@ -0,0 +1,39 @@
/**
* 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 cutsceneitem_s cutsceneitem_t;
typedef union cutsceneitemdata_u cutsceneitemdata_t;
typedef struct {
uint8_t index;
} cutscenetextminihide_t;
/**
* Starts a mini text hide step (closes the mini textbox immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
*/
void cutsceneTextMiniHideStart(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
/**
* Updates a mini text hide step (always completes immediately).
*
* @param item The cutscene item.
* @param data Runtime data storage.
* @returns true always.
*/
bool_t cutsceneTextMiniHideUpdate(
const cutsceneitem_t *item,
cutsceneitemdata_t *data
);
+13 -7
View File
@@ -9,17 +9,23 @@
#include "rpg/cutscene/cutscene.h"
#include "rpg/cutscene/cutscenesystem.h"
CUTSCENE(TEST_ONE, DEFAULT,
CUTSCENE(TEST_ONE, 0, DEFAULT,
CUTSCENE_TEXT("Test One."),
);
CUTSCENE(TEST_TWO, DEFAULT,
CUTSCENE(TEST_TWO, 0, DEFAULT,
CUTSCENE_TEXT("Test Two."),
CUTSCENE_CONCURRENT(
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACT, 4, 4, 0),
CUTSCENE_ENTITY_WALK_TO(CUTSCENE_ENTITY_INTERACTED, 8, 2, 0),
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_ITEM_GIVE(ITEM_ID_POTATO, 3),
CUTSCENE_ENTITY_REMOVE(CUTSCENE_ENTITY_INTERACT),
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."),
);
+3 -2
View File
@@ -12,6 +12,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
player.c
)
add_subdirectory(anim)
add_subdirectory(interact)
add_subdirectory(npc)
add_subdirectory(npc)
add_subdirectory(item)
add_subdirectory(global)
-44
View File
@@ -1,44 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "rpg/entity/entity.h"
#include "rpg/overworld/map.h"
#include "rpg/overworld/tile.h"
#include "time/time.h"
#include "entityanimwalk.h"
const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT] = {
[ENTITY_ANIM_IDLE] = { entityAnimIdleUpdate },
[ENTITY_ANIM_TURN] = { entityAnimTurnUpdate },
[ENTITY_ANIM_WALK] = { entityAnimWalkUpdate },
[ENTITY_ANIM_RUN] = { entityAnimRunUpdate },
};
float_t entityAnimTileZOffset(const worldpos_t pos) {
return tileIsRamp(mapGetTile(pos)) ? 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);
}
-46
View File
@@ -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);
-17
View File
@@ -1,17 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#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;
}
-18
View File
@@ -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);
-24
View File
@@ -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;
}
-21
View File
@@ -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);
-17
View File
@@ -1,17 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#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;
}
-21
View File
@@ -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);
-24
View File
@@ -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;
}
-22
View File
@@ -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);
+137 -175
View File
@@ -1,6 +1,6 @@
/**
* Copyright (c) 2025 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
@@ -8,13 +8,14 @@
#include "entity.h"
#include "assert/assert.h"
#include "util/memory.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"
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");
@@ -27,9 +28,13 @@ void entityInit(entity_t *entity, const entitytype_t type) {
memoryZero(entity, sizeof(entity_t));
entity->id = (uint8_t)(entity - ENTITIES);
entity->globalId = ENTITY_GLOBAL_ID_NULL;
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);
}
@@ -38,197 +43,122 @@ 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);
}
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;
}
physicsWorldStep(
&ENTITY_PHYSICS_WORLD, &entity->body, TIME.delta, others, othersCount
);
entitySyncFromPhysics(entity);
}
bool_t entityCanTurn(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE &&
entity->walkEndCooldown <= 0;
bool_t entityCanUnload(entity_t *entity) {
return entity->globalId < ENTITY_GLOBAL_ID_START;
}
bool_t entityCanWalk(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE;
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;
}
bool_t entityCanRun(entity_t *entity) {
return entity->animation == ENTITY_ANIM_IDLE;
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 = mapGetTile(entity->position);
tile_t tileNew = mapGetTile(newPos);
bool_t fall = false;
bool_t raise = false;
// Are we walking up a ramp?
if(
tileIsRamp(tileCurrent) &&
(
// Can only walk UP the direction the ramp faces.
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent ||
// If diagonal ramp, can go up one of two ways only. Inner ramps
// share the same allowed directions as their outer counterparts.
(
(
(
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent == 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_SHAPE_NULL;
worldpos_t abovePos = newPos;
abovePos.z += 1;
tile_t tileAbove = mapGetTile(abovePos);
if(tileAbove != TILE_SHAPE_NULL && tileIsWalkable(tileAbove)) {
raise = true;
} else {
tileNew = tileNewSaved;
}
} else if(tileNew == TILE_SHAPE_NULL && newPos.z > 0) {
// Falling down?
worldpos_t belowPos = newPos;
belowPos.z -= 1;
tile_t tileBelow = mapGetTile(belowPos);
if(
tileBelow != TILE_SHAPE_NULL &&
tileIsRamp(tileBelow) &&
(
// This handles regular cardinal ramps
(entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow ||
// This handles diagonal ramps. Inner ramps share the same
// allowed directions as their outer counterparts.
(
(
(
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) ||
(
(
tileBelow == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) ||
(
(
tileBelow == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow == 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 && !tileIsWalkable(tileNew)) 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
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;
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 best;
}
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) continue;
if(ent->globalId != globalId) continue;
return ent;
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
@@ -246,11 +176,14 @@ 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;
entitySyncFromPhysics(entity);
}
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
@@ -272,11 +205,40 @@ void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
if(chunkIndex != 0xFF) {
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");
}
}
}
}
void entityUpdateChunk(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
chunkpos_t cp;
worldPosToChunkPos(&entity->position, &cp);
chunkindex_t ci = mapGetChunkIndexAt(cp);
if(ci == -1 || ci == entity->chunkIndex) return;
entitySetChunk(entity, (uint8_t)ci);
}
void entitySyncFromPhysics(entity_t *entity) {
assertNotNull(entity, "Entity pointer cannot be NULL");
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])
};
glm_vec3_copy(entity->body.position, entity->renderPosition);
entity->renderPosition[2] *= WORLD_LAYER_HEIGHT;
entityUpdateChunk(entity);
mapAreaCheckEntity(entity);
}
+105 -25
View File
@@ -7,27 +7,63 @@
#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 uint16_t entityglobalid_t;
#define ENTITY_GLOBAL_ID_NULL 0
#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;
entitytype_t type;
entitytypedata_t data;
// 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;
@@ -36,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.
*
@@ -52,31 +91,39 @@ 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.
* Returns true if the entity is allowed to be unloaded. By default this is
* true for entities whose global ID falls within the randomly assigned
* range below ENTITY_GLOBAL_ID_START.
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can turn.
* @returns True if the entity can be unloaded.
*/
bool_t entityCanTurn(entity_t *entity);
bool_t entityCanUnload(entity_t *entity);
/**
* Returns true if the entity is in a state where it can walk.
* 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 check.
* @returns True if the entity can walk.
* @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.
*/
bool_t entityCanWalk(entity_t *entity);
void entityMove(entity_t *entity, const vec2 direction, const bool_t running);
/**
* Returns true if the entity is in a state where it can run.
* Stops an entity's horizontal movement (equivalent to
* entityMove(entity, {0, 0}, false)).
*
* @param entity Pointer to the entity to check.
* @returns True if the entity can run.
* @param entity Pointer to the entity to stop.
*/
bool_t entityCanRun(entity_t *entity);
void entityStop(entity_t *entity);
/**
* Turn an entity to face a new direction.
* 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.
@@ -84,7 +131,10 @@ bool_t entityCanRun(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.
@@ -92,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.
@@ -100,13 +151,24 @@ 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.
*
* @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.
* 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 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.
*
* @param globalId The global ID to search for.
* @return Pointer to the matching entity, or NULL if none is loaded.
*/
entity_t *entityGetByGlobalId(const entityglobalid_t globalId);
/**
* Gets an available entity index.
@@ -124,10 +186,28 @@ uint8_t entityGetAvailable();
*/
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex);
/**
* Resolves the chunk that an entity's current position falls into and
* assigns the entity to it via entitySetChunk. Leaves the entity's chunk
* unchanged if its position doesn't fall within any loaded chunk.
*
* @param entity Pointer to the entity to update.
*/
void entityUpdateChunk(entity_t *entity);
/**
* Instantly moves an entity to a world position, resetting movement state.
*
* @param entity Pointer to the entity to move.
* @param pos The world position to place the entity at.
*/
void entityPositionSet(entity_t *entity, const worldpos_t pos);
void entityPositionSet(entity_t *entity, const worldpos_t pos);
/**
* 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.
*
* @param entity Pointer to the entity to sync.
*/
void entitySyncFromPhysics(entity_t *entity);
+23
View File
@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More