Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 | |||
| ca02ee0352 | |||
| fbaa54145e | |||
| 28754ffbf2 | |||
| 470c0eba7a | |||
| 7098dcec43 | |||
| 07137f57af | |||
| 8b7491a3d3 | |||
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 | |||
| a73f55beb0 | |||
| 0bd2491ab7 | |||
| 860c797c9c | |||
| 38c5080f9f | |||
| fae191d8fe | |||
| d83a953e2d | |||
| 2dcf0d0f0d | |||
| 6dee37d8c1 | |||
| 3bad03afb3 | |||
| 6a6d8448f7 | |||
| 189babd2cf | |||
| 988a0f2294 | |||
| 85bf455731 | |||
| 589e4224f3 | |||
| bd7fa154b4 | |||
| a292f1992b | |||
| 88ddf429b7 | |||
| 7a1f6662df | |||
| afc68bccc6 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -124,18 +124,16 @@ Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||
|
||||
### Layer structure
|
||||
```
|
||||
src/dusk/ core, platform-agnostic game logic
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
||||
src/dusklinux/ Linux + Knulli platform impl
|
||||
src/duskpsp/ PSP platform impl
|
||||
src/duskvita/ Vita platform impl
|
||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||
```
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Dusk Roadmap
|
||||
|
||||
Tracking upcoming milestones for the engine.
|
||||
|
||||
## Upcoming milestones
|
||||
|
||||
1. Add a very basic physics engine, moving away from the current
|
||||
tile-based movement.
|
||||
2. Give entities full freedom of movement (no longer locked to tile
|
||||
grid positions).
|
||||
3. Update entity interaction, triggers, chunk management, and other
|
||||
systems that currently assume tile-based positioning so they work
|
||||
with the new 3D positioning/movement code.
|
||||
4. Investigate and fix poor UI rendering performance. Rendering the
|
||||
console alone tanks framerate despite the existing mesh
|
||||
optimizations, so there is likely more headroom to find in the
|
||||
vertex/text rendering path.
|
||||
5. Create UI elements for displaying status indicators, e.g. network
|
||||
connection state and save-in-progress.
|
||||
6. Fully test saving end-to-end on all supported platforms.
|
||||
7. Remove the tile system from chunks in favor of meshes, with
|
||||
dynamic hitboxes per chunk loaded in from the chunk file data.
|
||||
8. Create UI elements for network status: a connecting modal, an
|
||||
error state, and a connected flag. Retire the test HTTP request
|
||||
once these are in place.
|
||||
9. Build the socket server and client implementation, including
|
||||
handlers for the different packet types.
|
||||
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
||||
the existing `npc` and `player` types. Limit to 8 (defined
|
||||
constant) for now.
|
||||
11. Send and receive `clientplayer` position over the network.
|
||||
12. Create a UI menu for creating a server and joining a server. For
|
||||
now, join IPs are hard-coded (testing against a fixed IP of
|
||||
10.0.0.94).
|
||||
13. Create "handshake" packets. For now, just send the username,
|
||||
enforced to be under 10 characters long.
|
||||
14. Server tracks all players' positions and broadcasts them to all
|
||||
connected clients.
|
||||
15. Server sends disconnect packets for users who leave.
|
||||
16. Server assigns each client a UUID; all clients know every other
|
||||
client's UUID (used to reference them across position updates,
|
||||
disconnect packets, etc).
|
||||
17. Server notifies all clients (by UUID) when a user joins, leaves,
|
||||
or is disconnected, so clients can spawn or remove the
|
||||
corresponding `clientplayer` entity in the world.
|
||||
|
||||
## Principles
|
||||
|
||||
- Never trust the network implicitly. Neither side (server or client)
|
||||
should assume the other's packets are well-formed or benign --
|
||||
validate all incoming packet data defensively, since either side
|
||||
may send garbage or malicious data. Use `errorret_t` /
|
||||
`errorThrow()` for these runtime checks, not assert macros --
|
||||
asserts are debug-only and won't guard release builds against
|
||||
malformed or malicious packet data.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+49
-43
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
if(NOT DEFINED ENV{VITASDK})
|
||||
message(FATAL_ERROR "VITASDK environment variable is not set.")
|
||||
endif()
|
||||
|
||||
include("$ENV{VITASDK}/share/vita.cmake" REQUIRED)
|
||||
|
||||
set(VITA_APP_NAME "Dusk")
|
||||
set(VITA_TITLEID "DUSK00001")
|
||||
set(VITA_VERSION "01.00")
|
||||
|
||||
find_package(SDL2 REQUIRED)
|
||||
|
||||
# Custom flags for cglm
|
||||
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
|
||||
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
|
||||
find_package(cglm REQUIRED)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
${SDL2_LIBRARIES}
|
||||
cglm
|
||||
SDL2
|
||||
SDL2main
|
||||
zip
|
||||
bz2
|
||||
z
|
||||
zstd
|
||||
crypto
|
||||
lzma
|
||||
m
|
||||
pthread
|
||||
stdc++
|
||||
vitaGL
|
||||
mathneon
|
||||
vitashark
|
||||
kubridge_stub
|
||||
SceAppMgr_stub
|
||||
SceAudio_stub
|
||||
SceCtrl_stub
|
||||
SceCommonDialog_stub
|
||||
SceDisplay_stub
|
||||
SceKernelDmacMgr_stub
|
||||
SceGxm_stub
|
||||
SceShaccCg_stub
|
||||
SceSysmodule_stub
|
||||
ScePower_stub
|
||||
SceTouch_stub
|
||||
SceVshBridge_stub
|
||||
SceIofilemgr_stub
|
||||
SceShaccCgExt
|
||||
libtaihen_stub.a
|
||||
|
||||
|
||||
# SceKernel_stub
|
||||
SceAppUtil_stub
|
||||
SceHid_stub
|
||||
SceRtc_stub
|
||||
)
|
||||
|
||||
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||
${SDL2_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_SDL2
|
||||
DUSK_OPENGL
|
||||
DUSK_VITA
|
||||
DUSK_INPUT_GAMEPAD
|
||||
DUSK_PLATFORM_ENDIAN_LITTLE
|
||||
DUSK_OPENGL_LEGACY
|
||||
DUSK_DISPLAY_WIDTH=960
|
||||
DUSK_DISPLAY_HEIGHT=544
|
||||
)
|
||||
|
||||
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
|
||||
vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE)
|
||||
|
||||
# Post-build: package SELF + assets into a .vpk installable on the Vita
|
||||
vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self
|
||||
VERSION ${VITA_VERSION}
|
||||
NAME ${VITA_APP_NAME}
|
||||
FILE ${DUSK_ASSETS_ZIP} dusk.dsk
|
||||
)
|
||||
@@ -15,11 +15,6 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
||||
add_subdirectory(dusksdl2)
|
||||
add_subdirectory(duskgl)
|
||||
|
||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
||||
add_subdirectory(duskvita)
|
||||
add_subdirectory(dusksdl2)
|
||||
add_subdirectory(duskgl)
|
||||
|
||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||
add_subdirectory(duskdolphin)
|
||||
|
||||
|
||||
+48
-57
@@ -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
@@ -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];
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -21,6 +21,11 @@ typedef struct {
|
||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||
bool_t visible;
|
||||
|
||||
// Set whenever the history changes; consumers rendering the console
|
||||
// (e.g. uiConsoleDraw) check and clear this to know when their own
|
||||
// cached representation of the history needs rebuilding.
|
||||
bool_t dirty;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadmutex_t printMutex;
|
||||
#endif
|
||||
|
||||
@@ -21,6 +21,7 @@ errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
|
||||
|
||||
errorret_t shaderBind(shader_t *shader) {
|
||||
assertNotNull(shader, "Shader cannot be null");
|
||||
if(bound == shader) errorOk();
|
||||
errorChain(shaderBindPlatform(shader));
|
||||
bound = shader;
|
||||
errorOk();
|
||||
|
||||
+96
-10
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
network.c
|
||||
networkinfo.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(http)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttp.h"
|
||||
#include "networkhttprequest.h"
|
||||
#include "networkhttpthread.h"
|
||||
|
||||
networkhttp_t NETWORK_HTTP;
|
||||
|
||||
errorret_t networkHttpInit(void) {
|
||||
networkHttpRequestPoolInit();
|
||||
|
||||
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
|
||||
threadStart(&NETWORK_HTTP.thread);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUpdate(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
const networkhttprequeststate_t state = request->state;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
|
||||
eventInvoke(&request->onComplete, request);
|
||||
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
|
||||
eventInvoke(&request->onError, request);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
networkHttpRequestReset(request);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpDispose(void) {
|
||||
threadStop(&NETWORK_HTTP.thread);
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
typedef struct {
|
||||
thread_t thread;
|
||||
} networkhttp_t;
|
||||
|
||||
extern networkhttp_t NETWORK_HTTP;
|
||||
|
||||
/**
|
||||
* Initializes the HTTP client: prepares the request pool and starts
|
||||
* the background worker thread. Called once during engine startup.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpInit(void);
|
||||
|
||||
/**
|
||||
* Dispatches any requests that finished on the background thread since
|
||||
* the last call: fires onComplete/onError on the main thread, frees
|
||||
* the response, and returns the slot to the pool. Call once per frame.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpUpdate(void);
|
||||
|
||||
/**
|
||||
* Stops the background worker thread and disposes of the HTTP client.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpDispose(void);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpheader.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
|
||||
memoryZero(list, sizeof(networkhttpheaderlist_t));
|
||||
}
|
||||
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name) {
|
||||
return
|
||||
stringCompareInsensitive(name, "Host") == 0 ||
|
||||
stringCompareInsensitive(name, "Connection") == 0 ||
|
||||
stringCompareInsensitive(name, "Content-Length") == 0;
|
||||
}
|
||||
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
) {
|
||||
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
|
||||
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
|
||||
|
||||
networkhttpheader_t *header = &list->headers[list->count];
|
||||
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
|
||||
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
|
||||
list->count++;
|
||||
}
|
||||
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
) {
|
||||
for(uint32_t i = 0; i < list->count; i++) {
|
||||
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
|
||||
return &list->headers[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
|
||||
#define NETWORK_HTTP_HEADER_NAME_MAX 64
|
||||
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
} networkhttpheader_t;
|
||||
|
||||
typedef struct {
|
||||
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
|
||||
uint32_t count;
|
||||
} networkhttpheaderlist_t;
|
||||
|
||||
/**
|
||||
* Clears a header list, removing all entries.
|
||||
*
|
||||
* @param list The header list to clear.
|
||||
*/
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
|
||||
|
||||
/**
|
||||
* Determines whether a header name is reserved, meaning it is computed
|
||||
* internally by the request writer and must not be set by the caller.
|
||||
* This is Host, Connection and Content-Length.
|
||||
*
|
||||
* @param name The header name to check, compared case-insensitively.
|
||||
* @return true if the header name is reserved.
|
||||
*/
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name);
|
||||
|
||||
/**
|
||||
* Appends a header to the list, silently doing nothing if the list is
|
||||
* already full or if skipReserved is true and the name is reserved (see
|
||||
* networkHttpHeaderIsReserved).
|
||||
*
|
||||
* @param list The header list to append to.
|
||||
* @param name The header name.
|
||||
* @param value The header value.
|
||||
* @param skipReserved If true, reserved header names are ignored.
|
||||
*/
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the first header in the list matching the given name, compared
|
||||
* case-insensitively.
|
||||
*
|
||||
* @param list The header list to search.
|
||||
* @param name The header name to search for.
|
||||
* @return Pointer to the matching header, or NULL if not found.
|
||||
*/
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
);
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
) {
|
||||
switch(method) {
|
||||
case NETWORK_HTTP_METHOD_POST: return "POST";
|
||||
case NETWORK_HTTP_METHOD_PUT: return "PUT";
|
||||
case NETWORK_HTTP_METHOD_GET:
|
||||
default:
|
||||
return "GET";
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
) {
|
||||
size_t cursor = 0;
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Connection: close\r\n"
|
||||
));
|
||||
|
||||
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
|
||||
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
char_t lengthStr[24];
|
||||
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Content-Length: "
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
*outLength = cursor;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
) {
|
||||
size_t sentTotal = 0;
|
||||
while(sentTotal < length) {
|
||||
size_t sent = 0;
|
||||
errorChain(networkSocketPlatformSend(
|
||||
sock, data + sentTotal, length - sentTotal, &sent
|
||||
));
|
||||
if(sent == 0) errorThrow("Connection closed while sending data");
|
||||
sentTotal += sent;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
) {
|
||||
size_t headerLength = 0;
|
||||
headerBuf[0] = '\0';
|
||||
|
||||
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
|
||||
for(;;) {
|
||||
size_t received = 0;
|
||||
errorChain(networkSocketPlatformReceive(
|
||||
sock, chunk, sizeof(chunk), &received
|
||||
));
|
||||
if(received == 0) {
|
||||
errorThrow("Connection closed before response headers completed");
|
||||
}
|
||||
|
||||
if(headerLength + received >= headerBufSize) {
|
||||
errorThrow("Response headers too large");
|
||||
}
|
||||
|
||||
memoryCopy(headerBuf + headerLength, chunk, received);
|
||||
headerLength += received;
|
||||
headerBuf[headerLength] = '\0';
|
||||
|
||||
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
|
||||
if(terminator == NULL) continue;
|
||||
|
||||
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
|
||||
const size_t bodyStart = terminatorOffset + 4;
|
||||
const size_t extra = headerLength - bodyStart;
|
||||
|
||||
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
|
||||
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
|
||||
*outLeftoverLength = extra;
|
||||
|
||||
// Keep the trailing "\r\n" of the last header line, drop the blank
|
||||
// line so callers can split on "\r\n" without a special case.
|
||||
headerBuf[terminatorOffset + 2] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
) {
|
||||
const char_t *space = strchr(line, ' ');
|
||||
if(space == NULL) errorThrow("Malformed status line: %s", line);
|
||||
|
||||
char_t statusStr[4];
|
||||
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
|
||||
memoryCopy(statusStr, space + 1, 3);
|
||||
statusStr[3] = '\0';
|
||||
|
||||
if(!stringToU16(statusStr, outStatus)) {
|
||||
errorThrow("Malformed status code: %s", line);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
const char_t *colon = strchr(line, ':');
|
||||
if(colon == NULL) return;
|
||||
|
||||
const size_t nameLength = (size_t)(colon - line);
|
||||
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
|
||||
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
memoryCopy(name, line, nameLength);
|
||||
name[nameLength] = '\0';
|
||||
|
||||
const char_t *valueStart = colon + 1;
|
||||
while(*valueStart == ' ') valueStart++;
|
||||
|
||||
const size_t valueLength = strlen(valueStart);
|
||||
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
|
||||
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
memoryCopy(value, valueStart, valueLength + 1);
|
||||
|
||||
networkHttpHeaderListAdd(outHeaders, name, value, false);
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
char_t *cursor = headerBuf;
|
||||
bool_t first = true;
|
||||
|
||||
for(;;) {
|
||||
char_t *lineEnd = strstr(cursor, "\r\n");
|
||||
const bool_t isLast = lineEnd == NULL;
|
||||
if(lineEnd != NULL) *lineEnd = '\0';
|
||||
|
||||
if(first) {
|
||||
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
|
||||
first = false;
|
||||
} else if(cursor[0] != '\0') {
|
||||
networkHttpProcessParseHeaderLine(cursor, outHeaders);
|
||||
}
|
||||
|
||||
if(isLast) break;
|
||||
cursor = lineEnd + 2;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
) {
|
||||
const networkhttpheader_t *contentLength =
|
||||
networkHttpHeaderListFind(headers, "Content-Length");
|
||||
|
||||
if(contentLength != NULL) {
|
||||
int64_t expected = 0;
|
||||
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
|
||||
errorThrow("Malformed Content-Length header: %s", contentLength->value);
|
||||
}
|
||||
|
||||
if(expected == 0) {
|
||||
*outBody = NULL;
|
||||
*outBodyLength = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *body = memoryAllocate((size_t)expected);
|
||||
const size_t initialCopy =
|
||||
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
|
||||
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
|
||||
size_t received = initialCopy;
|
||||
|
||||
while(received < (size_t)expected) {
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + received, (size_t)expected - received, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) {
|
||||
memoryFree(body);
|
||||
errorThrow("Connection closed before full body was received");
|
||||
}
|
||||
received += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = (size_t)expected;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// No Content-Length: read until the connection closes.
|
||||
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
|
||||
if(leftoverLength > capacity) capacity = leftoverLength;
|
||||
|
||||
uint8_t *body = memoryAllocate(capacity);
|
||||
size_t length = 0;
|
||||
if(leftoverLength > 0) {
|
||||
memoryCopy(body, leftover, leftoverLength);
|
||||
length = leftoverLength;
|
||||
}
|
||||
|
||||
for(;;) {
|
||||
if(length == capacity) {
|
||||
const size_t newCapacity = capacity * 2;
|
||||
memoryResize((void **)&body, capacity, newCapacity);
|
||||
capacity = newCapacity;
|
||||
}
|
||||
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + length, capacity - length, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) break;
|
||||
length += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = length;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
) {
|
||||
memoryZero(outResponse, sizeof(networkhttpresponse_t));
|
||||
|
||||
networksocketplatform_t sock;
|
||||
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
|
||||
|
||||
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
|
||||
size_t headLength = 0;
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
|
||||
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, (const uint8_t *)head, headLength
|
||||
));
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, request->body, request->bodyLength
|
||||
));
|
||||
}
|
||||
|
||||
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
|
||||
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
size_t leftoverLength = 0;
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
|
||||
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
|
||||
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
|
||||
headerBuf, &outResponse->status, &outResponse->headers
|
||||
));
|
||||
|
||||
errorret_t bodyRet = networkHttpProcessReadBody(
|
||||
&sock, &outResponse->headers, leftover, leftoverLength,
|
||||
&outResponse->body, &outResponse->bodyLength
|
||||
);
|
||||
networkSocketPlatformClose(&sock);
|
||||
errorChain(bodyRet);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
#include "network/networksocketplatform.h"
|
||||
|
||||
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
|
||||
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
|
||||
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
|
||||
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
|
||||
|
||||
/**
|
||||
* Shorthand to chain an error, closing sock first if expr failed. Used
|
||||
* for every step of an attempt after the socket has connected, since
|
||||
* failing partway through must not leak the socket.
|
||||
*/
|
||||
#define networkHttpProcessErrorChain(sock, _expr) { \
|
||||
errorret_t _nhpErr = (_expr); \
|
||||
if(errorIsNotOk(_nhpErr)) { \
|
||||
networkSocketPlatformClose(sock); \
|
||||
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wire method name ("GET", "POST", "PUT") for method.
|
||||
*
|
||||
* @param method The method to name.
|
||||
* @return A static string naming the method.
|
||||
*/
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds the request line, headers and blank line terminator (but not
|
||||
* the body) for request, targeting target, into dest.
|
||||
*
|
||||
* @param request The request being sent.
|
||||
* @param target The resolved host/port/path for this attempt.
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param outLength The number of bytes written is written here.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends length bytes of data over sock, looping until all of it has
|
||||
* been written.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @return An error if the send failed or the connection closed early.
|
||||
*/
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads from sock until the blank line ("\r\n\r\n") terminating the
|
||||
* response headers has been seen, writing the status line and header
|
||||
* lines (without the blank line) into headerBuf as a null-terminated
|
||||
* string. Any body bytes read past the terminator in the same receive
|
||||
* are written to leftover.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headerBuf The destination buffer for the header text.
|
||||
* @param headerBufSize The size of headerBuf, including the null
|
||||
* terminator.
|
||||
* @param leftover Destination buffer for any body bytes read early.
|
||||
* @param leftoverBufSize The size of leftover.
|
||||
* @param outLeftoverLength The number of bytes written to leftover is
|
||||
* written here.
|
||||
* @return An error if the headers could not be read, were malformed,
|
||||
* or did not fit within the given buffers.
|
||||
*/
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a "HTTP/1.1 <status> <reason>" status line.
|
||||
*
|
||||
* @param line The status line, null-terminated.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a single "Name: Value" response header line and appends it to
|
||||
* outHeaders. Silently does nothing if the line has no colon or the
|
||||
* name/value are too long to fit a networkhttpheader_t.
|
||||
*
|
||||
* @param line The header line, null-terminated.
|
||||
* @param outHeaders The header list to append to.
|
||||
*/
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses the status line and every header line out of headerBuf (as
|
||||
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
|
||||
* place to split it into lines.
|
||||
*
|
||||
* @param headerBuf The header text to parse.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @param outHeaders The header list to populate.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the response body from sock. If headers contains a
|
||||
* Content-Length, reads exactly that many bytes; otherwise reads until
|
||||
* the connection closes. Either way, leftover/leftoverLength (body
|
||||
* bytes already read while looking for the header terminator) are
|
||||
* included at the start of the result.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headers The parsed response headers.
|
||||
* @param leftover Body bytes already read past the header terminator.
|
||||
* @param leftoverLength The number of bytes in leftover.
|
||||
* @param outBody A memoryAllocate'd buffer holding the body is written
|
||||
* here, or NULL if the body is empty. Owned by the caller.
|
||||
* @param outBodyLength The number of bytes in *outBody is written here.
|
||||
* @return An error if the body could not be fully read.
|
||||
*/
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Performs a single blocking HTTP exchange: connects to target, writes
|
||||
* the request head + body, and reads back one parsed response. Does
|
||||
* not follow redirects -- that is the caller's responsibility.
|
||||
*
|
||||
* @param request The request to send (method, headers, body).
|
||||
* @param target The resolved host/port/path to connect to for this
|
||||
* attempt (may differ from the request's own URL if this is a
|
||||
* redirected attempt).
|
||||
* @param outResponse The parsed response is written here on success.
|
||||
* @return An error if the exchange could not be completed.
|
||||
*/
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttprequest.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
void networkHttpRequestPoolInit(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
memoryZero(request, sizeof(networkhttprequest_t));
|
||||
threadMutexInit(&request->mutex);
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
}
|
||||
}
|
||||
|
||||
networkhttprequest_t * networkHttpRequestClaim(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
threadMutexUnlock(&request->mutex);
|
||||
return request;
|
||||
}
|
||||
|
||||
assertUnreachable("No available HTTP request slots.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void networkHttpRequestReset(networkhttprequest_t *request) {
|
||||
assertNotNull(request, "request must not be NULL");
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
|
||||
if(request->body != NULL) memoryFree(request->body);
|
||||
request->body = NULL;
|
||||
request->bodyLength = 0;
|
||||
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
memoryZero(&request->response, sizeof(networkhttpresponse_t));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
|
||||
networkhttprequest_t *request = networkHttpRequestClaim();
|
||||
request->method = method;
|
||||
|
||||
errorChain(networkHttpUrlBuild(
|
||||
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
|
||||
));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
for(uint32_t i = 0; i < headerCount; i++) {
|
||||
networkHttpHeaderListAdd(
|
||||
&request->requestHeaders, headers[i].name, headers[i].value, true
|
||||
);
|
||||
}
|
||||
|
||||
request->bodyLength = bodyLength;
|
||||
if(bodyLength > 0) {
|
||||
assertNotNull(body, "body must not be NULL when bodyLength > 0");
|
||||
request->body = memoryAllocate(bodyLength);
|
||||
memoryCopy(request->body, body, bodyLength);
|
||||
}
|
||||
|
||||
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
|
||||
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "event/event.h"
|
||||
#include "thread/threadmutex.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
|
||||
#define NETWORK_HTTP_REQUEST_COUNT_MAX 4
|
||||
#define NETWORK_HTTP_REQUEST_EVENT_MAX 1
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_METHOD_GET,
|
||||
NETWORK_HTTP_METHOD_POST,
|
||||
NETWORK_HTTP_METHOD_PUT
|
||||
} networkhttpmethod_t;
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_REQUEST_STATE_FREE,
|
||||
NETWORK_HTTP_REQUEST_STATE_PENDING,
|
||||
NETWORK_HTTP_REQUEST_STATE_ACTIVE,
|
||||
NETWORK_HTTP_REQUEST_STATE_DONE,
|
||||
NETWORK_HTTP_REQUEST_STATE_ERROR
|
||||
} networkhttprequeststate_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t status;
|
||||
networkhttpheaderlist_t headers;
|
||||
|
||||
// Owned, memoryAllocate'd. Only valid until the onComplete/onError
|
||||
// event has finished firing, at which point it is freed.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
} networkhttpresponse_t;
|
||||
|
||||
typedef struct {
|
||||
threadmutex_t mutex;
|
||||
networkhttprequeststate_t state;
|
||||
|
||||
networkhttpmethod_t method;
|
||||
|
||||
// Fully built, including any query params, by networkHttpRequest.
|
||||
char_t url[NETWORK_HTTP_URL_MAX];
|
||||
networkhttpheaderlist_t requestHeaders;
|
||||
|
||||
// Owned copy, memoryAllocate'd.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
|
||||
/** Fired on the main thread once a response has been received. */
|
||||
event_t onComplete;
|
||||
eventcallback_t onCompleteCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onCompleteUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
/** Fired on the main thread if the request could not be completed. */
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onErrorUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
networkhttpresponse_t response;
|
||||
} networkhttprequest_t;
|
||||
|
||||
extern networkhttprequest_t
|
||||
NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the request pool: prepares every slot's mutex and events
|
||||
* and marks them all FREE. Called once by networkHttpInit.
|
||||
*/
|
||||
void networkHttpRequestPoolInit(void);
|
||||
|
||||
/**
|
||||
* Finds and returns a FREE slot in the request pool. The slot is not
|
||||
* marked as in-use by this call; the caller must populate it and then
|
||||
* set its state to PENDING.
|
||||
*
|
||||
* @return A free request slot.
|
||||
*/
|
||||
networkhttprequest_t * networkHttpRequestClaim(void);
|
||||
|
||||
/**
|
||||
* Resets a request slot back to FREE, freeing any owned buffers
|
||||
* (request body, response body) and clearing its event subscribers.
|
||||
*
|
||||
* @param request The request slot to reset.
|
||||
*/
|
||||
void networkHttpRequestReset(networkhttprequest_t *request);
|
||||
|
||||
/**
|
||||
* Sends an HTTP request asynchronously. The request runs on a
|
||||
* background thread; onComplete or onError is invoked on the main
|
||||
* thread (from networkHttpUpdate) once it finishes. 301 redirects are
|
||||
* followed automatically.
|
||||
*
|
||||
* @param method The HTTP method to use.
|
||||
* @param url The "http://host[:port]/path" URL to request. Must not
|
||||
* use any scheme other than http.
|
||||
* @param headers Request headers to send, may be NULL if headerCount
|
||||
* is 0. The Host, Connection and Content-Length headers are
|
||||
* reserved and computed internally; any of those passed here
|
||||
* are ignored.
|
||||
* @param headerCount Number of entries in headers.
|
||||
* @param queryParams Query string parameters to append to url, may be
|
||||
* NULL if queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @param body Request body to send for POST/PUT, may be NULL if
|
||||
* bodyLength is 0. Ignored for GET.
|
||||
* @param bodyLength Number of bytes in body.
|
||||
* @param onComplete Invoked with (networkhttprequest_t *, user) once a
|
||||
* response has been received, whatever its status code. May be
|
||||
* NULL to ignore.
|
||||
* @param onError Invoked with (networkhttprequest_t *, user) if the
|
||||
* request could not be completed (DNS/connect/send/receive
|
||||
* failure, or a malformed response). May be NULL to ignore.
|
||||
* @param user Arbitrary pointer forwarded to onComplete/onError.
|
||||
* @return An error if the request could not be queued (e.g. a
|
||||
* malformed url).
|
||||
*/
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
);
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpthread.h"
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include <unistd.h>
|
||||
|
||||
void networkHttpThreadRun(thread_t *thread) {
|
||||
assertNotMainThread("networkHttpThreadRun must not run on the main thread.");
|
||||
|
||||
while(!threadShouldStop(thread)) {
|
||||
bool_t didWork = false;
|
||||
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_PENDING) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_ACTIVE;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
didWork = true;
|
||||
networkHttpThreadProcessRequest(request);
|
||||
}
|
||||
|
||||
if(threadShouldStop(thread)) break;
|
||||
if(!didWork) usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request) {
|
||||
networkhttpurl_t target;
|
||||
errorret_t parseRet = networkHttpUrlParse(request->url, &target);
|
||||
|
||||
networkhttprequeststate_t finalState = NETWORK_HTTP_REQUEST_STATE_DONE;
|
||||
|
||||
if(errorIsNotOk(parseRet)) {
|
||||
errorCatch(errorPrint(parseRet));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
} else {
|
||||
for(uint32_t redirect = 0; ; redirect++) {
|
||||
errorret_t ret = networkHttpProcessAttempt(
|
||||
request, &target, &request->response
|
||||
);
|
||||
|
||||
if(errorIsNotOk(ret)) {
|
||||
errorCatch(errorPrint(ret));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
const bool_t isRedirect = request->response.status == 301;
|
||||
const networkhttpheader_t *location = isRedirect ?
|
||||
networkHttpHeaderListFind(&request->response.headers, "Location") :
|
||||
NULL;
|
||||
|
||||
if(location == NULL || redirect >= NETWORK_HTTP_REDIRECT_COUNT_MAX) {
|
||||
break;
|
||||
}
|
||||
|
||||
networkhttpurl_t nextTarget;
|
||||
errorret_t nextRet = networkHttpUrlParse(location->value, &nextTarget);
|
||||
if(errorIsNotOk(nextRet)) {
|
||||
// Not an absolute URL we can follow -- treat the 301 itself as
|
||||
// the final response instead of failing the whole request.
|
||||
errorCatch(errorPrint(nextRet));
|
||||
break;
|
||||
}
|
||||
|
||||
// Free the intermediate response body before following further.
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
target = nextTarget;
|
||||
}
|
||||
}
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = finalState;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "thread/thread.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
|
||||
#define NETWORK_HTTP_REDIRECT_COUNT_MAX 5
|
||||
|
||||
/**
|
||||
* The background worker thread body: repeatedly scans the request
|
||||
* pool for PENDING slots, processes each to completion, and marks it
|
||||
* DONE or ERROR. Idles briefly when there is nothing to do. Runs
|
||||
* until the thread is stopped.
|
||||
*
|
||||
* @param thread The thread runner.
|
||||
*/
|
||||
void networkHttpThreadRun(thread_t *thread);
|
||||
|
||||
/**
|
||||
* Processes a single ACTIVE request slot to completion, following up
|
||||
* to NETWORK_HTTP_REDIRECT_COUNT_MAX HTTP 301 redirects, and leaves it
|
||||
* in the DONE or ERROR state. Only called from the background thread.
|
||||
*
|
||||
* @param request The request slot to process.
|
||||
*/
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request);
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpurl.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
assertNotNull(out, "out must not be NULL");
|
||||
|
||||
if(strlen(url) <= 7 || strncasecmp(url, "http://", 7) != 0) {
|
||||
errorThrow("Unsupported URL scheme (only http:// is supported): %s", url);
|
||||
}
|
||||
|
||||
const char_t *rest = url + 7;
|
||||
|
||||
const size_t hostLen = strcspn(rest, ":/");
|
||||
if(hostLen == 0 || hostLen >= NETWORK_HTTP_HOST_MAX) {
|
||||
errorThrow("Invalid host in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->host, rest, hostLen);
|
||||
out->host[hostLen] = '\0';
|
||||
rest += hostLen;
|
||||
|
||||
if(*rest == ':') {
|
||||
rest++;
|
||||
|
||||
const size_t portLen = strcspn(rest, "/");
|
||||
char_t portStr[8];
|
||||
if(portLen == 0 || portLen >= sizeof(portStr)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
memoryCopy(portStr, rest, portLen);
|
||||
portStr[portLen] = '\0';
|
||||
|
||||
if(!stringToU16(portStr, &out->port)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
rest += portLen;
|
||||
} else {
|
||||
out->port = NETWORK_HTTP_PORT_DEFAULT;
|
||||
}
|
||||
|
||||
const char_t *path = *rest == '\0' ? "/" : rest;
|
||||
const size_t pathLen = strlen(path);
|
||||
if(pathLen >= NETWORK_HTTP_PATH_MAX) {
|
||||
errorThrow("Path too long in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->path, path, pathLen + 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
) {
|
||||
assertNotNull(dest, "dest must not be NULL");
|
||||
assertNotNull(baseUrl, "baseUrl must not be NULL");
|
||||
|
||||
const size_t baseLen = strlen(baseUrl);
|
||||
if(baseLen >= destSize) errorThrow("URL too long: %s", baseUrl);
|
||||
memoryCopy(dest, baseUrl, baseLen + 1);
|
||||
|
||||
size_t cursor = baseLen;
|
||||
bool_t hasQuery = stringIncludesString(baseUrl, "?");
|
||||
|
||||
for(uint32_t i = 0; i < queryParamCount; i++) {
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, hasQuery ? "&" : "?"
|
||||
));
|
||||
hasQuery = true;
|
||||
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].name
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "="));
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].value
|
||||
));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
) {
|
||||
const size_t len = strlen(str);
|
||||
if(*cursor + len >= destSize) errorThrow("URL buffer too small");
|
||||
|
||||
memoryCopy(dest + *cursor, str, len + 1);
|
||||
*cursor += len;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
) {
|
||||
const char_t *hex = "0123456789ABCDEF";
|
||||
|
||||
for(const char_t *p = src; *p != '\0'; p++) {
|
||||
const uint8_t c = (uint8_t)*p;
|
||||
const bool_t unreserved =
|
||||
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
|
||||
|
||||
if(unreserved) {
|
||||
if(*cursor + 1 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = (char_t)c;
|
||||
} else {
|
||||
if(*cursor + 3 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = '%';
|
||||
dest[(*cursor)++] = hex[(c >> 4) & 0xF];
|
||||
dest[(*cursor)++] = hex[c & 0xF];
|
||||
}
|
||||
}
|
||||
|
||||
dest[*cursor] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
|
||||
#define NETWORK_HTTP_URL_MAX 1024
|
||||
#define NETWORK_HTTP_HOST_MAX 256
|
||||
#define NETWORK_HTTP_PATH_MAX 768
|
||||
#define NETWORK_HTTP_PORT_DEFAULT 80
|
||||
|
||||
typedef struct {
|
||||
char_t host[NETWORK_HTTP_HOST_MAX];
|
||||
uint16_t port;
|
||||
|
||||
// Includes the leading slash and, if present, the query string.
|
||||
char_t path[NETWORK_HTTP_PATH_MAX];
|
||||
} networkhttpurl_t;
|
||||
|
||||
/**
|
||||
* Parses a "http://host[:port]/path[?query]" URL. Any other scheme is
|
||||
* rejected, since this client does not support TLS.
|
||||
*
|
||||
* @param url The URL to parse.
|
||||
* @param out The parsed URL is written here.
|
||||
* @return An error if the URL is malformed or uses an unsupported scheme.
|
||||
*/
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out);
|
||||
|
||||
/**
|
||||
* Copies baseUrl into dest, then appends each queryParam as a
|
||||
* percent-encoded "key=value" pair, joined with "&" (or "?" for the
|
||||
* first one, unless baseUrl already contains a "?").
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param baseUrl The URL to copy before appending query params.
|
||||
* @param queryParams Array of query parameters to append, may be NULL if
|
||||
* queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Appends a raw (not percent-encoded) string to dest at *cursor,
|
||||
* advancing *cursor and keeping dest null-terminated.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param str The string to append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
);
|
||||
|
||||
/**
|
||||
* Percent-encodes src and appends it to dest at *cursor, advancing
|
||||
* *cursor and keeping dest null-terminated. Unreserved characters
|
||||
* (letters, digits, "-", "_", ".", "~") are copied as-is.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param src The string to percent-encode and append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
);
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "network.h"
|
||||
#include "network/http/networkhttp.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "log/log.h"
|
||||
@@ -18,11 +19,13 @@ errorret_t networkInit() {
|
||||
NETWORK.errorState.code = ERROR_OK;
|
||||
NETWORK.onDisconnect = NULL;
|
||||
|
||||
return networkPlatformInit();
|
||||
errorChain(networkPlatformInit());
|
||||
return networkHttpInit();
|
||||
}
|
||||
|
||||
errorret_t networkUpdate() {
|
||||
errorChain(networkPlatformUpdate());
|
||||
errorChain(networkHttpUpdate());
|
||||
|
||||
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
|
||||
NETWORK.state = NETWORK_STATE_DISCONNECTED;
|
||||
@@ -112,5 +115,5 @@ errorret_t networkDispose() {
|
||||
}
|
||||
|
||||
errorChain(networkPlatformDispose());
|
||||
errorOk();
|
||||
return networkHttpDispose();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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."),
|
||||
);
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "entityanimidle.h"
|
||||
#include "entityanimturn.h"
|
||||
#include "entityanimwalk.h"
|
||||
#include "entityanimrun.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
typedef enum {
|
||||
ENTITY_ANIM_IDLE,
|
||||
ENTITY_ANIM_TURN,
|
||||
ENTITY_ANIM_WALK,
|
||||
ENTITY_ANIM_RUN,
|
||||
ENTITY_ANIM_COUNT
|
||||
} entityanim_t;
|
||||
|
||||
typedef struct {
|
||||
/** Updates the render position for this animation state. */
|
||||
void (*update)(entity_t *entity);
|
||||
} entityanimcallback_t;
|
||||
|
||||
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
|
||||
|
||||
/**
|
||||
* Updates the entity animation timer and render position.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimUpdate(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
|
||||
* Used to lift entity render position to mid-ramp height.
|
||||
*
|
||||
* @param pos World position to sample.
|
||||
* @returns float_t The Z offset to apply.
|
||||
*/
|
||||
float_t entityAnimTileZOffset(const worldpos_t pos);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimIdleUpdate(entity_t *entity) {
|
||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
||||
entity->renderPosition[2] = (
|
||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
||||
) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
/**
|
||||
* Updates render position for the idle animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimIdleUpdate(entity_t *entity);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimRunUpdate(entity_t *entity) {
|
||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
|
||||
float_t zFrom = (float_t)entity->lastPosition.z
|
||||
+ entityAnimTileZOffset(entity->lastPosition);
|
||||
float_t zTo = (float_t)entity->position.z
|
||||
+ entityAnimTileZOffset(entity->position);
|
||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
||||
);
|
||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
||||
);
|
||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
|
||||
|
||||
/**
|
||||
* Updates render position for the run animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimRunUpdate(entity_t *entity);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimTurnUpdate(entity_t *entity) {
|
||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
||||
entity->renderPosition[2] = (
|
||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
||||
) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
|
||||
|
||||
/**
|
||||
* Updates render position for the turn animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimTurnUpdate(entity_t *entity);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimWalkUpdate(entity_t *entity) {
|
||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
|
||||
float_t zFrom = (float_t)entity->lastPosition.z
|
||||
+ entityAnimTileZOffset(entity->lastPosition);
|
||||
float_t zTo = (float_t)entity->position.z
|
||||
+ entityAnimTileZOffset(entity->position);
|
||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
||||
);
|
||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
||||
);
|
||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
|
||||
#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
|
||||
|
||||
/**
|
||||
* Updates render position for the walk animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimWalkUpdate(entity_t *entity);
|
||||
+137
-175
@@ -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
@@ -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);
|
||||
@@ -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
Reference in New Issue
Block a user