Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 |
@@ -124,18 +124,16 @@ Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||
|
||||
### Layer structure
|
||||
```
|
||||
src/dusk/ core, platform-agnostic game logic
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
||||
src/dusklinux/ Linux + Knulli platform impl
|
||||
src/duskpsp/ PSP platform impl
|
||||
src/duskvita/ Vita platform impl
|
||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||
```
|
||||
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Dusk Roadmap
|
||||
|
||||
Tracking upcoming milestones for the engine.
|
||||
|
||||
## Upcoming milestones
|
||||
|
||||
1. Add a very basic physics engine, moving away from the current
|
||||
tile-based movement.
|
||||
2. Give entities full freedom of movement (no longer locked to tile
|
||||
grid positions).
|
||||
3. Update entity interaction, triggers, chunk management, and other
|
||||
systems that currently assume tile-based positioning so they work
|
||||
with the new 3D positioning/movement code.
|
||||
4. Investigate and fix poor UI rendering performance. Rendering the
|
||||
console alone tanks framerate despite the existing mesh
|
||||
optimizations, so there is likely more headroom to find in the
|
||||
vertex/text rendering path.
|
||||
5. Create UI elements for displaying status indicators, e.g. network
|
||||
connection state and save-in-progress.
|
||||
6. Fully test saving end-to-end on all supported platforms.
|
||||
7. Remove the tile system from chunks in favor of meshes, with
|
||||
dynamic hitboxes per chunk loaded in from the chunk file data.
|
||||
8. Create UI elements for network status: a connecting modal, an
|
||||
error state, and a connected flag. Retire the test HTTP request
|
||||
once these are in place.
|
||||
9. Build the socket server and client implementation, including
|
||||
handlers for the different packet types.
|
||||
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
||||
the existing `npc` and `player` types. Limit to 8 (defined
|
||||
constant) for now.
|
||||
11. Send and receive `clientplayer` position over the network.
|
||||
12. Create a UI menu for creating a server and joining a server. For
|
||||
now, join IPs are hard-coded (testing against a fixed IP of
|
||||
10.0.0.94).
|
||||
13. Create "handshake" packets. For now, just send the username,
|
||||
enforced to be under 10 characters long.
|
||||
14. Server tracks all players' positions and broadcasts them to all
|
||||
connected clients.
|
||||
15. Server sends disconnect packets for users who leave.
|
||||
16. Server assigns each client a UUID; all clients know every other
|
||||
client's UUID (used to reference them across position updates,
|
||||
disconnect packets, etc).
|
||||
17. Server notifies all clients (by UUID) when a user joins, leaves,
|
||||
or is disconnected, so clients can spawn or remove the
|
||||
corresponding `clientplayer` entity in the world.
|
||||
|
||||
## Principles
|
||||
|
||||
- Never trust the network implicitly. Neither side (server or client)
|
||||
should assume the other's packets are well-formed or benign --
|
||||
validate all incoming packet data defensively, since either side
|
||||
may send garbage or malicious data. Use `errorret_t` /
|
||||
`errorThrow()` for these runtime checks, not assert macros --
|
||||
asserts are debug-only and won't guard release builds against
|
||||
malformed or malicious packet data.
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -55,7 +65,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -63,16 +72,33 @@ errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
errorChain(networkUpdate());
|
||||
timeUpdate();
|
||||
|
||||
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);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "util/string.h"
|
||||
#include "util/math.h"
|
||||
#include "time/time.h"
|
||||
#include "event/event.h"
|
||||
|
||||
input_t INPUT;
|
||||
|
||||
@@ -23,20 +22,6 @@ errorret_t inputInit(void) {
|
||||
INPUT.actions[i].action = (inputaction_t)i;
|
||||
INPUT.actions[i].lastValue = 0.0f;
|
||||
INPUT.actions[i].currentValue = 0.0f;
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onPressed,
|
||||
INPUT.actions[i].onPressedCallbacks,
|
||||
INPUT.actions[i].onPressedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onReleased,
|
||||
INPUT.actions[i].onReleasedCallbacks,
|
||||
INPUT.actions[i].onReleasedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
#ifdef inputInitPlatform
|
||||
@@ -98,18 +83,6 @@ void inputUpdate(void) {
|
||||
|
||||
cur++;
|
||||
}
|
||||
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
if(TIME.dynamicUpdate) return;
|
||||
#endif
|
||||
|
||||
for(uint8_t i = INPUT_ACTION_NULL + 1; i < INPUT_ACTION_COUNT; i++) {
|
||||
inputactiondata_t *act = &INPUT.actions[i];
|
||||
bool_t isDown = act->currentValue > 0.0f;
|
||||
bool_t wasDown = act->lastValue > 0.0f;
|
||||
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
|
||||
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
|
||||
}
|
||||
}
|
||||
|
||||
float_t inputGetCurrentValue(const inputaction_t action) {
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
#pragma once
|
||||
#include "time/time.h"
|
||||
#include "input/inputactiondefs.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
inputaction_t action;
|
||||
@@ -21,13 +18,6 @@ typedef struct {
|
||||
float_t lastDynamicValue;
|
||||
float_t currentDynamicValue;
|
||||
#endif
|
||||
|
||||
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onPressed;
|
||||
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onReleased;
|
||||
} inputactiondata_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
network.c
|
||||
networkinfo.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(http)
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
battle.c
|
||||
battlefighter.c
|
||||
party.c
|
||||
networkhttpheader.c
|
||||
networkhttpurl.c
|
||||
networkhttprequest.c
|
||||
networkhttpprocess.c
|
||||
networkhttpthread.c
|
||||
networkhttp.c
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttp.h"
|
||||
#include "networkhttprequest.h"
|
||||
#include "networkhttpthread.h"
|
||||
|
||||
networkhttp_t NETWORK_HTTP;
|
||||
|
||||
errorret_t networkHttpInit(void) {
|
||||
networkHttpRequestPoolInit();
|
||||
|
||||
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
|
||||
threadStart(&NETWORK_HTTP.thread);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUpdate(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
const networkhttprequeststate_t state = request->state;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
|
||||
eventInvoke(&request->onComplete, request);
|
||||
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
|
||||
eventInvoke(&request->onError, request);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
networkHttpRequestReset(request);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpDispose(void) {
|
||||
threadStop(&NETWORK_HTTP.thread);
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
typedef struct {
|
||||
thread_t thread;
|
||||
} networkhttp_t;
|
||||
|
||||
extern networkhttp_t NETWORK_HTTP;
|
||||
|
||||
/**
|
||||
* Initializes the HTTP client: prepares the request pool and starts
|
||||
* the background worker thread. Called once during engine startup.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpInit(void);
|
||||
|
||||
/**
|
||||
* Dispatches any requests that finished on the background thread since
|
||||
* the last call: fires onComplete/onError on the main thread, frees
|
||||
* the response, and returns the slot to the pool. Call once per frame.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpUpdate(void);
|
||||
|
||||
/**
|
||||
* Stops the background worker thread and disposes of the HTTP client.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t networkHttpDispose(void);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpheader.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
|
||||
memoryZero(list, sizeof(networkhttpheaderlist_t));
|
||||
}
|
||||
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name) {
|
||||
return
|
||||
stringCompareInsensitive(name, "Host") == 0 ||
|
||||
stringCompareInsensitive(name, "Connection") == 0 ||
|
||||
stringCompareInsensitive(name, "Content-Length") == 0;
|
||||
}
|
||||
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
) {
|
||||
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
|
||||
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
|
||||
|
||||
networkhttpheader_t *header = &list->headers[list->count];
|
||||
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
|
||||
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
|
||||
list->count++;
|
||||
}
|
||||
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
) {
|
||||
for(uint32_t i = 0; i < list->count; i++) {
|
||||
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
|
||||
return &list->headers[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
|
||||
#define NETWORK_HTTP_HEADER_NAME_MAX 64
|
||||
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
|
||||
|
||||
typedef struct {
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
} networkhttpheader_t;
|
||||
|
||||
typedef struct {
|
||||
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
|
||||
uint32_t count;
|
||||
} networkhttpheaderlist_t;
|
||||
|
||||
/**
|
||||
* Clears a header list, removing all entries.
|
||||
*
|
||||
* @param list The header list to clear.
|
||||
*/
|
||||
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
|
||||
|
||||
/**
|
||||
* Determines whether a header name is reserved, meaning it is computed
|
||||
* internally by the request writer and must not be set by the caller.
|
||||
* This is Host, Connection and Content-Length.
|
||||
*
|
||||
* @param name The header name to check, compared case-insensitively.
|
||||
* @return true if the header name is reserved.
|
||||
*/
|
||||
bool_t networkHttpHeaderIsReserved(const char_t *name);
|
||||
|
||||
/**
|
||||
* Appends a header to the list, silently doing nothing if the list is
|
||||
* already full or if skipReserved is true and the name is reserved (see
|
||||
* networkHttpHeaderIsReserved).
|
||||
*
|
||||
* @param list The header list to append to.
|
||||
* @param name The header name.
|
||||
* @param value The header value.
|
||||
* @param skipReserved If true, reserved header names are ignored.
|
||||
*/
|
||||
void networkHttpHeaderListAdd(
|
||||
networkhttpheaderlist_t *list,
|
||||
const char_t *name,
|
||||
const char_t *value,
|
||||
const bool_t skipReserved
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the first header in the list matching the given name, compared
|
||||
* case-insensitively.
|
||||
*
|
||||
* @param list The header list to search.
|
||||
* @param name The header name to search for.
|
||||
* @return Pointer to the matching header, or NULL if not found.
|
||||
*/
|
||||
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||
const networkhttpheaderlist_t *list,
|
||||
const char_t *name
|
||||
);
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
) {
|
||||
switch(method) {
|
||||
case NETWORK_HTTP_METHOD_POST: return "POST";
|
||||
case NETWORK_HTTP_METHOD_PUT: return "PUT";
|
||||
case NETWORK_HTTP_METHOD_GET:
|
||||
default:
|
||||
return "GET";
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
) {
|
||||
size_t cursor = 0;
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Connection: close\r\n"
|
||||
));
|
||||
|
||||
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
|
||||
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
char_t lengthStr[24];
|
||||
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
|
||||
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, "Content-Length: "
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
}
|
||||
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||
|
||||
*outLength = cursor;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
) {
|
||||
size_t sentTotal = 0;
|
||||
while(sentTotal < length) {
|
||||
size_t sent = 0;
|
||||
errorChain(networkSocketPlatformSend(
|
||||
sock, data + sentTotal, length - sentTotal, &sent
|
||||
));
|
||||
if(sent == 0) errorThrow("Connection closed while sending data");
|
||||
sentTotal += sent;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
) {
|
||||
size_t headerLength = 0;
|
||||
headerBuf[0] = '\0';
|
||||
|
||||
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
|
||||
for(;;) {
|
||||
size_t received = 0;
|
||||
errorChain(networkSocketPlatformReceive(
|
||||
sock, chunk, sizeof(chunk), &received
|
||||
));
|
||||
if(received == 0) {
|
||||
errorThrow("Connection closed before response headers completed");
|
||||
}
|
||||
|
||||
if(headerLength + received >= headerBufSize) {
|
||||
errorThrow("Response headers too large");
|
||||
}
|
||||
|
||||
memoryCopy(headerBuf + headerLength, chunk, received);
|
||||
headerLength += received;
|
||||
headerBuf[headerLength] = '\0';
|
||||
|
||||
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
|
||||
if(terminator == NULL) continue;
|
||||
|
||||
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
|
||||
const size_t bodyStart = terminatorOffset + 4;
|
||||
const size_t extra = headerLength - bodyStart;
|
||||
|
||||
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
|
||||
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
|
||||
*outLeftoverLength = extra;
|
||||
|
||||
// Keep the trailing "\r\n" of the last header line, drop the blank
|
||||
// line so callers can split on "\r\n" without a special case.
|
||||
headerBuf[terminatorOffset + 2] = '\0';
|
||||
break;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
) {
|
||||
const char_t *space = strchr(line, ' ');
|
||||
if(space == NULL) errorThrow("Malformed status line: %s", line);
|
||||
|
||||
char_t statusStr[4];
|
||||
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
|
||||
memoryCopy(statusStr, space + 1, 3);
|
||||
statusStr[3] = '\0';
|
||||
|
||||
if(!stringToU16(statusStr, outStatus)) {
|
||||
errorThrow("Malformed status code: %s", line);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
const char_t *colon = strchr(line, ':');
|
||||
if(colon == NULL) return;
|
||||
|
||||
const size_t nameLength = (size_t)(colon - line);
|
||||
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
|
||||
|
||||
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||
memoryCopy(name, line, nameLength);
|
||||
name[nameLength] = '\0';
|
||||
|
||||
const char_t *valueStart = colon + 1;
|
||||
while(*valueStart == ' ') valueStart++;
|
||||
|
||||
const size_t valueLength = strlen(valueStart);
|
||||
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
|
||||
|
||||
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||
memoryCopy(value, valueStart, valueLength + 1);
|
||||
|
||||
networkHttpHeaderListAdd(outHeaders, name, value, false);
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
) {
|
||||
char_t *cursor = headerBuf;
|
||||
bool_t first = true;
|
||||
|
||||
for(;;) {
|
||||
char_t *lineEnd = strstr(cursor, "\r\n");
|
||||
const bool_t isLast = lineEnd == NULL;
|
||||
if(lineEnd != NULL) *lineEnd = '\0';
|
||||
|
||||
if(first) {
|
||||
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
|
||||
first = false;
|
||||
} else if(cursor[0] != '\0') {
|
||||
networkHttpProcessParseHeaderLine(cursor, outHeaders);
|
||||
}
|
||||
|
||||
if(isLast) break;
|
||||
cursor = lineEnd + 2;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
) {
|
||||
const networkhttpheader_t *contentLength =
|
||||
networkHttpHeaderListFind(headers, "Content-Length");
|
||||
|
||||
if(contentLength != NULL) {
|
||||
int64_t expected = 0;
|
||||
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
|
||||
errorThrow("Malformed Content-Length header: %s", contentLength->value);
|
||||
}
|
||||
|
||||
if(expected == 0) {
|
||||
*outBody = NULL;
|
||||
*outBodyLength = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *body = memoryAllocate((size_t)expected);
|
||||
const size_t initialCopy =
|
||||
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
|
||||
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
|
||||
size_t received = initialCopy;
|
||||
|
||||
while(received < (size_t)expected) {
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + received, (size_t)expected - received, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) {
|
||||
memoryFree(body);
|
||||
errorThrow("Connection closed before full body was received");
|
||||
}
|
||||
received += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = (size_t)expected;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// No Content-Length: read until the connection closes.
|
||||
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
|
||||
if(leftoverLength > capacity) capacity = leftoverLength;
|
||||
|
||||
uint8_t *body = memoryAllocate(capacity);
|
||||
size_t length = 0;
|
||||
if(leftoverLength > 0) {
|
||||
memoryCopy(body, leftover, leftoverLength);
|
||||
length = leftoverLength;
|
||||
}
|
||||
|
||||
for(;;) {
|
||||
if(length == capacity) {
|
||||
const size_t newCapacity = capacity * 2;
|
||||
memoryResize((void **)&body, capacity, newCapacity);
|
||||
capacity = newCapacity;
|
||||
}
|
||||
|
||||
size_t got = 0;
|
||||
errorret_t ret = networkSocketPlatformReceive(
|
||||
sock, body + length, capacity - length, &got
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(body);
|
||||
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||
}
|
||||
if(got == 0) break;
|
||||
length += got;
|
||||
}
|
||||
|
||||
*outBody = body;
|
||||
*outBodyLength = length;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
) {
|
||||
memoryZero(outResponse, sizeof(networkhttpresponse_t));
|
||||
|
||||
networksocketplatform_t sock;
|
||||
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
|
||||
|
||||
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
|
||||
size_t headLength = 0;
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
|
||||
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, (const uint8_t *)head, headLength
|
||||
));
|
||||
|
||||
if(request->bodyLength > 0) {
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||
&sock, request->body, request->bodyLength
|
||||
));
|
||||
}
|
||||
|
||||
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
|
||||
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||
size_t leftoverLength = 0;
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
|
||||
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
|
||||
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
|
||||
));
|
||||
|
||||
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
|
||||
headerBuf, &outResponse->status, &outResponse->headers
|
||||
));
|
||||
|
||||
errorret_t bodyRet = networkHttpProcessReadBody(
|
||||
&sock, &outResponse->headers, leftover, leftoverLength,
|
||||
&outResponse->body, &outResponse->bodyLength
|
||||
);
|
||||
networkSocketPlatformClose(&sock);
|
||||
errorChain(bodyRet);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
#include "network/networksocketplatform.h"
|
||||
|
||||
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
|
||||
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
|
||||
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
|
||||
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
|
||||
|
||||
/**
|
||||
* Shorthand to chain an error, closing sock first if expr failed. Used
|
||||
* for every step of an attempt after the socket has connected, since
|
||||
* failing partway through must not leak the socket.
|
||||
*/
|
||||
#define networkHttpProcessErrorChain(sock, _expr) { \
|
||||
errorret_t _nhpErr = (_expr); \
|
||||
if(errorIsNotOk(_nhpErr)) { \
|
||||
networkSocketPlatformClose(sock); \
|
||||
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wire method name ("GET", "POST", "PUT") for method.
|
||||
*
|
||||
* @param method The method to name.
|
||||
* @return A static string naming the method.
|
||||
*/
|
||||
const char_t * networkHttpProcessMethodName(
|
||||
const networkhttpmethod_t method
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds the request line, headers and blank line terminator (but not
|
||||
* the body) for request, targeting target, into dest.
|
||||
*
|
||||
* @param request The request being sent.
|
||||
* @param target The resolved host/port/path for this attempt.
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param outLength The number of bytes written is written here.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpProcessBuildHead(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *outLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends length bytes of data over sock, looping until all of it has
|
||||
* been written.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param data The data to send.
|
||||
* @param length The number of bytes in data.
|
||||
* @return An error if the send failed or the connection closed early.
|
||||
*/
|
||||
errorret_t networkHttpProcessSendAll(
|
||||
networksocketplatform_t *sock,
|
||||
const uint8_t *data,
|
||||
const size_t length
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads from sock until the blank line ("\r\n\r\n") terminating the
|
||||
* response headers has been seen, writing the status line and header
|
||||
* lines (without the blank line) into headerBuf as a null-terminated
|
||||
* string. Any body bytes read past the terminator in the same receive
|
||||
* are written to leftover.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headerBuf The destination buffer for the header text.
|
||||
* @param headerBufSize The size of headerBuf, including the null
|
||||
* terminator.
|
||||
* @param leftover Destination buffer for any body bytes read early.
|
||||
* @param leftoverBufSize The size of leftover.
|
||||
* @param outLeftoverLength The number of bytes written to leftover is
|
||||
* written here.
|
||||
* @return An error if the headers could not be read, were malformed,
|
||||
* or did not fit within the given buffers.
|
||||
*/
|
||||
errorret_t networkHttpProcessReceiveHeaders(
|
||||
networksocketplatform_t *sock,
|
||||
char_t *headerBuf,
|
||||
const size_t headerBufSize,
|
||||
uint8_t *leftover,
|
||||
const size_t leftoverBufSize,
|
||||
size_t *outLeftoverLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a "HTTP/1.1 <status> <reason>" status line.
|
||||
*
|
||||
* @param line The status line, null-terminated.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseStatusLine(
|
||||
const char_t *line,
|
||||
uint16_t *outStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a single "Name: Value" response header line and appends it to
|
||||
* outHeaders. Silently does nothing if the line has no colon or the
|
||||
* name/value are too long to fit a networkhttpheader_t.
|
||||
*
|
||||
* @param line The header line, null-terminated.
|
||||
* @param outHeaders The header list to append to.
|
||||
*/
|
||||
void networkHttpProcessParseHeaderLine(
|
||||
const char_t *line,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses the status line and every header line out of headerBuf (as
|
||||
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
|
||||
* place to split it into lines.
|
||||
*
|
||||
* @param headerBuf The header text to parse.
|
||||
* @param outStatus The parsed status code is written here.
|
||||
* @param outHeaders The header list to populate.
|
||||
* @return An error if the status line is malformed.
|
||||
*/
|
||||
errorret_t networkHttpProcessParseHeaders(
|
||||
char_t *headerBuf,
|
||||
uint16_t *outStatus,
|
||||
networkhttpheaderlist_t *outHeaders
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the response body from sock. If headers contains a
|
||||
* Content-Length, reads exactly that many bytes; otherwise reads until
|
||||
* the connection closes. Either way, leftover/leftoverLength (body
|
||||
* bytes already read while looking for the header terminator) are
|
||||
* included at the start of the result.
|
||||
*
|
||||
* @param sock The connected socket.
|
||||
* @param headers The parsed response headers.
|
||||
* @param leftover Body bytes already read past the header terminator.
|
||||
* @param leftoverLength The number of bytes in leftover.
|
||||
* @param outBody A memoryAllocate'd buffer holding the body is written
|
||||
* here, or NULL if the body is empty. Owned by the caller.
|
||||
* @param outBodyLength The number of bytes in *outBody is written here.
|
||||
* @return An error if the body could not be fully read.
|
||||
*/
|
||||
errorret_t networkHttpProcessReadBody(
|
||||
networksocketplatform_t *sock,
|
||||
const networkhttpheaderlist_t *headers,
|
||||
const uint8_t *leftover,
|
||||
const size_t leftoverLength,
|
||||
uint8_t **outBody,
|
||||
size_t *outBodyLength
|
||||
);
|
||||
|
||||
/**
|
||||
* Performs a single blocking HTTP exchange: connects to target, writes
|
||||
* the request head + body, and reads back one parsed response. Does
|
||||
* not follow redirects -- that is the caller's responsibility.
|
||||
*
|
||||
* @param request The request to send (method, headers, body).
|
||||
* @param target The resolved host/port/path to connect to for this
|
||||
* attempt (may differ from the request's own URL if this is a
|
||||
* redirected attempt).
|
||||
* @param outResponse The parsed response is written here on success.
|
||||
* @return An error if the exchange could not be completed.
|
||||
*/
|
||||
errorret_t networkHttpProcessAttempt(
|
||||
const networkhttprequest_t *request,
|
||||
const networkhttpurl_t *target,
|
||||
networkhttpresponse_t *outResponse
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttprequest.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
void networkHttpRequestPoolInit(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
memoryZero(request, sizeof(networkhttprequest_t));
|
||||
threadMutexInit(&request->mutex);
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
}
|
||||
}
|
||||
|
||||
networkhttprequest_t * networkHttpRequestClaim(void) {
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
threadMutexUnlock(&request->mutex);
|
||||
return request;
|
||||
}
|
||||
|
||||
assertUnreachable("No available HTTP request slots.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void networkHttpRequestReset(networkhttprequest_t *request) {
|
||||
assertNotNull(request, "request must not be NULL");
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
|
||||
if(request->body != NULL) memoryFree(request->body);
|
||||
request->body = NULL;
|
||||
request->bodyLength = 0;
|
||||
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
memoryZero(&request->response, sizeof(networkhttpresponse_t));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
|
||||
eventInit(
|
||||
&request->onComplete, request->onCompleteCallbacks,
|
||||
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&request->onError, request->onErrorCallbacks,
|
||||
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||
);
|
||||
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
|
||||
networkhttprequest_t *request = networkHttpRequestClaim();
|
||||
request->method = method;
|
||||
|
||||
errorChain(networkHttpUrlBuild(
|
||||
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
|
||||
));
|
||||
|
||||
networkHttpHeaderListClear(&request->requestHeaders);
|
||||
for(uint32_t i = 0; i < headerCount; i++) {
|
||||
networkHttpHeaderListAdd(
|
||||
&request->requestHeaders, headers[i].name, headers[i].value, true
|
||||
);
|
||||
}
|
||||
|
||||
request->bodyLength = bodyLength;
|
||||
if(bodyLength > 0) {
|
||||
assertNotNull(body, "body must not be NULL when bodyLength > 0");
|
||||
request->body = memoryAllocate(bodyLength);
|
||||
memoryCopy(request->body, body, bodyLength);
|
||||
}
|
||||
|
||||
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
|
||||
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "event/event.h"
|
||||
#include "thread/threadmutex.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
#include "network/http/networkhttpurl.h"
|
||||
|
||||
#define NETWORK_HTTP_REQUEST_COUNT_MAX 4
|
||||
#define NETWORK_HTTP_REQUEST_EVENT_MAX 1
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_METHOD_GET,
|
||||
NETWORK_HTTP_METHOD_POST,
|
||||
NETWORK_HTTP_METHOD_PUT
|
||||
} networkhttpmethod_t;
|
||||
|
||||
typedef enum {
|
||||
NETWORK_HTTP_REQUEST_STATE_FREE,
|
||||
NETWORK_HTTP_REQUEST_STATE_PENDING,
|
||||
NETWORK_HTTP_REQUEST_STATE_ACTIVE,
|
||||
NETWORK_HTTP_REQUEST_STATE_DONE,
|
||||
NETWORK_HTTP_REQUEST_STATE_ERROR
|
||||
} networkhttprequeststate_t;
|
||||
|
||||
typedef struct {
|
||||
uint16_t status;
|
||||
networkhttpheaderlist_t headers;
|
||||
|
||||
// Owned, memoryAllocate'd. Only valid until the onComplete/onError
|
||||
// event has finished firing, at which point it is freed.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
} networkhttpresponse_t;
|
||||
|
||||
typedef struct {
|
||||
threadmutex_t mutex;
|
||||
networkhttprequeststate_t state;
|
||||
|
||||
networkhttpmethod_t method;
|
||||
|
||||
// Fully built, including any query params, by networkHttpRequest.
|
||||
char_t url[NETWORK_HTTP_URL_MAX];
|
||||
networkhttpheaderlist_t requestHeaders;
|
||||
|
||||
// Owned copy, memoryAllocate'd.
|
||||
uint8_t *body;
|
||||
size_t bodyLength;
|
||||
|
||||
/** Fired on the main thread once a response has been received. */
|
||||
event_t onComplete;
|
||||
eventcallback_t onCompleteCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onCompleteUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
/** Fired on the main thread if the request could not be completed. */
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
void *onErrorUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
|
||||
|
||||
networkhttpresponse_t response;
|
||||
} networkhttprequest_t;
|
||||
|
||||
extern networkhttprequest_t
|
||||
NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||
|
||||
/**
|
||||
* Initializes the request pool: prepares every slot's mutex and events
|
||||
* and marks them all FREE. Called once by networkHttpInit.
|
||||
*/
|
||||
void networkHttpRequestPoolInit(void);
|
||||
|
||||
/**
|
||||
* Finds and returns a FREE slot in the request pool. The slot is not
|
||||
* marked as in-use by this call; the caller must populate it and then
|
||||
* set its state to PENDING.
|
||||
*
|
||||
* @return A free request slot.
|
||||
*/
|
||||
networkhttprequest_t * networkHttpRequestClaim(void);
|
||||
|
||||
/**
|
||||
* Resets a request slot back to FREE, freeing any owned buffers
|
||||
* (request body, response body) and clearing its event subscribers.
|
||||
*
|
||||
* @param request The request slot to reset.
|
||||
*/
|
||||
void networkHttpRequestReset(networkhttprequest_t *request);
|
||||
|
||||
/**
|
||||
* Sends an HTTP request asynchronously. The request runs on a
|
||||
* background thread; onComplete or onError is invoked on the main
|
||||
* thread (from networkHttpUpdate) once it finishes. 301 redirects are
|
||||
* followed automatically.
|
||||
*
|
||||
* @param method The HTTP method to use.
|
||||
* @param url The "http://host[:port]/path" URL to request. Must not
|
||||
* use any scheme other than http.
|
||||
* @param headers Request headers to send, may be NULL if headerCount
|
||||
* is 0. The Host, Connection and Content-Length headers are
|
||||
* reserved and computed internally; any of those passed here
|
||||
* are ignored.
|
||||
* @param headerCount Number of entries in headers.
|
||||
* @param queryParams Query string parameters to append to url, may be
|
||||
* NULL if queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @param body Request body to send for POST/PUT, may be NULL if
|
||||
* bodyLength is 0. Ignored for GET.
|
||||
* @param bodyLength Number of bytes in body.
|
||||
* @param onComplete Invoked with (networkhttprequest_t *, user) once a
|
||||
* response has been received, whatever its status code. May be
|
||||
* NULL to ignore.
|
||||
* @param onError Invoked with (networkhttprequest_t *, user) if the
|
||||
* request could not be completed (DNS/connect/send/receive
|
||||
* failure, or a malformed response). May be NULL to ignore.
|
||||
* @param user Arbitrary pointer forwarded to onComplete/onError.
|
||||
* @return An error if the request could not be queued (e.g. a
|
||||
* malformed url).
|
||||
*/
|
||||
errorret_t networkHttpRequest(
|
||||
const networkhttpmethod_t method,
|
||||
const char_t *url,
|
||||
const networkhttpheader_t *headers,
|
||||
const uint32_t headerCount,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount,
|
||||
const uint8_t *body,
|
||||
const size_t bodyLength,
|
||||
const eventcallback_t onComplete,
|
||||
const eventcallback_t onError,
|
||||
void *user
|
||||
);
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpthread.h"
|
||||
#include "networkhttpprocess.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include <unistd.h>
|
||||
|
||||
void networkHttpThreadRun(thread_t *thread) {
|
||||
assertNotMainThread("networkHttpThreadRun must not run on the main thread.");
|
||||
|
||||
while(!threadShouldStop(thread)) {
|
||||
bool_t didWork = false;
|
||||
|
||||
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
if(request->state != NETWORK_HTTP_REQUEST_STATE_PENDING) {
|
||||
threadMutexUnlock(&request->mutex);
|
||||
continue;
|
||||
}
|
||||
request->state = NETWORK_HTTP_REQUEST_STATE_ACTIVE;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
|
||||
didWork = true;
|
||||
networkHttpThreadProcessRequest(request);
|
||||
}
|
||||
|
||||
if(threadShouldStop(thread)) break;
|
||||
if(!didWork) usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request) {
|
||||
networkhttpurl_t target;
|
||||
errorret_t parseRet = networkHttpUrlParse(request->url, &target);
|
||||
|
||||
networkhttprequeststate_t finalState = NETWORK_HTTP_REQUEST_STATE_DONE;
|
||||
|
||||
if(errorIsNotOk(parseRet)) {
|
||||
errorCatch(errorPrint(parseRet));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
} else {
|
||||
for(uint32_t redirect = 0; ; redirect++) {
|
||||
errorret_t ret = networkHttpProcessAttempt(
|
||||
request, &target, &request->response
|
||||
);
|
||||
|
||||
if(errorIsNotOk(ret)) {
|
||||
errorCatch(errorPrint(ret));
|
||||
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
const bool_t isRedirect = request->response.status == 301;
|
||||
const networkhttpheader_t *location = isRedirect ?
|
||||
networkHttpHeaderListFind(&request->response.headers, "Location") :
|
||||
NULL;
|
||||
|
||||
if(location == NULL || redirect >= NETWORK_HTTP_REDIRECT_COUNT_MAX) {
|
||||
break;
|
||||
}
|
||||
|
||||
networkhttpurl_t nextTarget;
|
||||
errorret_t nextRet = networkHttpUrlParse(location->value, &nextTarget);
|
||||
if(errorIsNotOk(nextRet)) {
|
||||
// Not an absolute URL we can follow -- treat the 301 itself as
|
||||
// the final response instead of failing the whole request.
|
||||
errorCatch(errorPrint(nextRet));
|
||||
break;
|
||||
}
|
||||
|
||||
// Free the intermediate response body before following further.
|
||||
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||
target = nextTarget;
|
||||
}
|
||||
}
|
||||
|
||||
threadMutexLock(&request->mutex);
|
||||
request->state = finalState;
|
||||
threadMutexUnlock(&request->mutex);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "thread/thread.h"
|
||||
#include "network/http/networkhttprequest.h"
|
||||
|
||||
#define NETWORK_HTTP_REDIRECT_COUNT_MAX 5
|
||||
|
||||
/**
|
||||
* The background worker thread body: repeatedly scans the request
|
||||
* pool for PENDING slots, processes each to completion, and marks it
|
||||
* DONE or ERROR. Idles briefly when there is nothing to do. Runs
|
||||
* until the thread is stopped.
|
||||
*
|
||||
* @param thread The thread runner.
|
||||
*/
|
||||
void networkHttpThreadRun(thread_t *thread);
|
||||
|
||||
/**
|
||||
* Processes a single ACTIVE request slot to completion, following up
|
||||
* to NETWORK_HTTP_REDIRECT_COUNT_MAX HTTP 301 redirects, and leaves it
|
||||
* in the DONE or ERROR state. Only called from the background thread.
|
||||
*
|
||||
* @param request The request slot to process.
|
||||
*/
|
||||
void networkHttpThreadProcessRequest(networkhttprequest_t *request);
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "networkhttpurl.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out) {
|
||||
assertNotNull(url, "url must not be NULL");
|
||||
assertNotNull(out, "out must not be NULL");
|
||||
|
||||
if(strlen(url) <= 7 || strncasecmp(url, "http://", 7) != 0) {
|
||||
errorThrow("Unsupported URL scheme (only http:// is supported): %s", url);
|
||||
}
|
||||
|
||||
const char_t *rest = url + 7;
|
||||
|
||||
const size_t hostLen = strcspn(rest, ":/");
|
||||
if(hostLen == 0 || hostLen >= NETWORK_HTTP_HOST_MAX) {
|
||||
errorThrow("Invalid host in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->host, rest, hostLen);
|
||||
out->host[hostLen] = '\0';
|
||||
rest += hostLen;
|
||||
|
||||
if(*rest == ':') {
|
||||
rest++;
|
||||
|
||||
const size_t portLen = strcspn(rest, "/");
|
||||
char_t portStr[8];
|
||||
if(portLen == 0 || portLen >= sizeof(portStr)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
memoryCopy(portStr, rest, portLen);
|
||||
portStr[portLen] = '\0';
|
||||
|
||||
if(!stringToU16(portStr, &out->port)) {
|
||||
errorThrow("Invalid port in URL: %s", url);
|
||||
}
|
||||
rest += portLen;
|
||||
} else {
|
||||
out->port = NETWORK_HTTP_PORT_DEFAULT;
|
||||
}
|
||||
|
||||
const char_t *path = *rest == '\0' ? "/" : rest;
|
||||
const size_t pathLen = strlen(path);
|
||||
if(pathLen >= NETWORK_HTTP_PATH_MAX) {
|
||||
errorThrow("Path too long in URL: %s", url);
|
||||
}
|
||||
memoryCopy(out->path, path, pathLen + 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
) {
|
||||
assertNotNull(dest, "dest must not be NULL");
|
||||
assertNotNull(baseUrl, "baseUrl must not be NULL");
|
||||
|
||||
const size_t baseLen = strlen(baseUrl);
|
||||
if(baseLen >= destSize) errorThrow("URL too long: %s", baseUrl);
|
||||
memoryCopy(dest, baseUrl, baseLen + 1);
|
||||
|
||||
size_t cursor = baseLen;
|
||||
bool_t hasQuery = stringIncludesString(baseUrl, "?");
|
||||
|
||||
for(uint32_t i = 0; i < queryParamCount; i++) {
|
||||
errorChain(networkHttpUrlAppend(
|
||||
dest, destSize, &cursor, hasQuery ? "&" : "?"
|
||||
));
|
||||
hasQuery = true;
|
||||
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].name
|
||||
));
|
||||
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "="));
|
||||
errorChain(networkHttpUrlEncodeComponent(
|
||||
dest, destSize, &cursor, queryParams[i].value
|
||||
));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
) {
|
||||
const size_t len = strlen(str);
|
||||
if(*cursor + len >= destSize) errorThrow("URL buffer too small");
|
||||
|
||||
memoryCopy(dest + *cursor, str, len + 1);
|
||||
*cursor += len;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
) {
|
||||
const char_t *hex = "0123456789ABCDEF";
|
||||
|
||||
for(const char_t *p = src; *p != '\0'; p++) {
|
||||
const uint8_t c = (uint8_t)*p;
|
||||
const bool_t unreserved =
|
||||
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
|
||||
|
||||
if(unreserved) {
|
||||
if(*cursor + 1 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = (char_t)c;
|
||||
} else {
|
||||
if(*cursor + 3 >= destSize) errorThrow("URL buffer too small");
|
||||
dest[(*cursor)++] = '%';
|
||||
dest[(*cursor)++] = hex[(c >> 4) & 0xF];
|
||||
dest[(*cursor)++] = hex[c & 0xF];
|
||||
}
|
||||
}
|
||||
|
||||
dest[*cursor] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "network/http/networkhttpheader.h"
|
||||
|
||||
#define NETWORK_HTTP_URL_MAX 1024
|
||||
#define NETWORK_HTTP_HOST_MAX 256
|
||||
#define NETWORK_HTTP_PATH_MAX 768
|
||||
#define NETWORK_HTTP_PORT_DEFAULT 80
|
||||
|
||||
typedef struct {
|
||||
char_t host[NETWORK_HTTP_HOST_MAX];
|
||||
uint16_t port;
|
||||
|
||||
// Includes the leading slash and, if present, the query string.
|
||||
char_t path[NETWORK_HTTP_PATH_MAX];
|
||||
} networkhttpurl_t;
|
||||
|
||||
/**
|
||||
* Parses a "http://host[:port]/path[?query]" URL. Any other scheme is
|
||||
* rejected, since this client does not support TLS.
|
||||
*
|
||||
* @param url The URL to parse.
|
||||
* @param out The parsed URL is written here.
|
||||
* @return An error if the URL is malformed or uses an unsupported scheme.
|
||||
*/
|
||||
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out);
|
||||
|
||||
/**
|
||||
* Copies baseUrl into dest, then appends each queryParam as a
|
||||
* percent-encoded "key=value" pair, joined with "&" (or "?" for the
|
||||
* first one, unless baseUrl already contains a "?").
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param baseUrl The URL to copy before appending query params.
|
||||
* @param queryParams Array of query parameters to append, may be NULL if
|
||||
* queryParamCount is 0.
|
||||
* @param queryParamCount Number of entries in queryParams.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlBuild(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
const char_t *baseUrl,
|
||||
const networkhttpheader_t *queryParams,
|
||||
const uint32_t queryParamCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Appends a raw (not percent-encoded) string to dest at *cursor,
|
||||
* advancing *cursor and keeping dest null-terminated.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param str The string to append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlAppend(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *str
|
||||
);
|
||||
|
||||
/**
|
||||
* Percent-encodes src and appends it to dest at *cursor, advancing
|
||||
* *cursor and keeping dest null-terminated. Unreserved characters
|
||||
* (letters, digits, "-", "_", ".", "~") are copied as-is.
|
||||
*
|
||||
* @param dest The destination buffer.
|
||||
* @param destSize The size of dest, including the null terminator.
|
||||
* @param cursor The current write offset into dest, updated in place.
|
||||
* @param src The string to percent-encode and append.
|
||||
* @return An error if the result would not fit in dest.
|
||||
*/
|
||||
errorret_t networkHttpUrlEncodeComponent(
|
||||
char_t *dest,
|
||||
const size_t destSize,
|
||||
size_t *cursor,
|
||||
const char_t *src
|
||||
);
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "network.h"
|
||||
#include "network/http/networkhttp.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "log/log.h"
|
||||
@@ -18,11 +19,13 @@ errorret_t networkInit() {
|
||||
NETWORK.errorState.code = ERROR_OK;
|
||||
NETWORK.onDisconnect = NULL;
|
||||
|
||||
return networkPlatformInit();
|
||||
errorChain(networkPlatformInit());
|
||||
return networkHttpInit();
|
||||
}
|
||||
|
||||
errorret_t networkUpdate() {
|
||||
errorChain(networkPlatformUpdate());
|
||||
errorChain(networkHttpUpdate());
|
||||
|
||||
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
|
||||
NETWORK.state = NETWORK_STATE_DISCONNECTED;
|
||||
@@ -112,5 +115,5 @@ errorret_t networkDispose() {
|
||||
}
|
||||
|
||||
errorChain(networkPlatformDispose());
|
||||
errorOk();
|
||||
return networkHttpDispose();
|
||||
}
|
||||
|
||||
@@ -11,10 +11,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(cutscene)
|
||||
add_subdirectory(entity)
|
||||
add_subdirectory(overworld)
|
||||
|
||||
add_subdirectory(story)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(physics)
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battle.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
void battleInit(void) {
|
||||
memoryZero(&BATTLE, sizeof(battle_t));
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
BATTLE.fighters[i].id = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t battleGetAvailableFighter(void) {
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = battleGetAvailableFighter();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[index];
|
||||
battleFighterInit(fighter, team, controller, stats, healthMax, mpMax);
|
||||
return fighter;
|
||||
}
|
||||
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
) {
|
||||
assertTrue(encounterType < BATTLE_ENCOUNTER_COUNT, "Invalid encounter type");
|
||||
|
||||
BATTLE.encounterType = encounterType;
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(true);
|
||||
|
||||
BATTLE.active = true;
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
battleInit();
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(!BATTLE.active) return NULL;
|
||||
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
uint8_t count = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(BATTLE.fighters[i].team != team) continue;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
) {
|
||||
assertNotNull(attacker, "Attacker cannot be NULL");
|
||||
assertNotNull(defender, "Defender cannot be NULL");
|
||||
|
||||
const int32_t rawDamage =
|
||||
(int32_t)attacker->stats.attack - (int32_t)defender->stats.defense;
|
||||
const uint16_t damage = rawDamage > 0 ? (uint16_t)rawDamage : 1;
|
||||
|
||||
defender->health = damage >= defender->health ? 0 : defender->health - damage;
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleNextTurn(void) {
|
||||
BATTLE.turnIndex++;
|
||||
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
||||
|
||||
BATTLE.round++;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(false);
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_LOSS;
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_WIN;
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *attacker = battleGetCurrentFighter();
|
||||
if(attacker == NULL) return;
|
||||
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
|
||||
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
||||
if(!battleFighterIsAlive(defender)) return;
|
||||
|
||||
battleResolveAttack(attacker, defender);
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(!BATTLE.active) return;
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
|
||||
if(!battleFighterIsAlive(current)) {
|
||||
battleNextTurn();
|
||||
return;
|
||||
}
|
||||
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||
|
||||
battlefighter_t *target = battleAIChooseTarget(current);
|
||||
if(target != NULL) battleResolveAttack(current, target);
|
||||
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.turnCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
||||
const uint8_t key = BATTLE.turnOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
||||
j--;
|
||||
}
|
||||
BATTLE.turnOrder[j + 1] = key;
|
||||
}
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
|
||||
if(BATTLE.encounterType == BATTLE_ENCOUNTER_PLAYER_ADVANTAGE) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ALLY);
|
||||
} else if(BATTLE.encounterType == BATTLE_ENCOUNTER_BACK_ATTACK) {
|
||||
battleMoveTeamFirst(BATTLE_FIGHTER_TEAM_ENEMY);
|
||||
}
|
||||
}
|
||||
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
}
|
||||
|
||||
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
const battlefighterteam_t enemyTeam =
|
||||
fighter->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
battlefighter_t *weakest = NULL;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
if(weakest == NULL || candidate->health < weakest->health) {
|
||||
weakest = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return weakest;
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
|
||||
#define BATTLE_FIGHTER_COUNT_MAX 8
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ENCOUNTER_REGULAR,
|
||||
BATTLE_ENCOUNTER_PLAYER_ADVANTAGE,
|
||||
BATTLE_ENCOUNTER_BACK_ATTACK,
|
||||
|
||||
BATTLE_ENCOUNTER_COUNT
|
||||
} battleencountertype_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_RESULT_NONE,
|
||||
BATTLE_RESULT_WIN,
|
||||
BATTLE_RESULT_LOSS,
|
||||
BATTLE_RESULT_FLED,
|
||||
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
typedef struct {
|
||||
bool_t active;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t turnCount;
|
||||
uint8_t turnIndex;
|
||||
uint16_t round;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
|
||||
/**
|
||||
* Initializes the battle system. Marks it as inactive with no fighters.
|
||||
*/
|
||||
void battleInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) fighter slot index.
|
||||
*
|
||||
* @return The index of an available fighter slot, or 0xFF if none are
|
||||
* available.
|
||||
*/
|
||||
uint8_t battleGetAvailableFighter(void);
|
||||
|
||||
/**
|
||||
* Adds a fighter to the battle in the next available slot.
|
||||
*
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
* @return Pointer to the newly added fighter, or NULL if the battle is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *battleAddFighter(
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: builds the opening turn order (biased by
|
||||
* encounterType for the first round only) and marks the battle active.
|
||||
* Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's turn order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
*/
|
||||
void battleStart(
|
||||
const battleencountertype_t encounterType,
|
||||
const bool_t fleeAvailable
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the battle, clearing all fighters and marking it inactive.
|
||||
*/
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter whose turn it currently is.
|
||||
*
|
||||
* @return Pointer to the active fighter, or NULL if the battle isn't
|
||||
* active or has no living fighters left to act.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
/**
|
||||
* Returns the number of living fighters on a team.
|
||||
*
|
||||
* @param team The team to count.
|
||||
* @return Count of living fighters on that team.
|
||||
*/
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Resolves a physical attack from attacker onto defender: damage is the
|
||||
* attacker's attack stat minus the defender's defense stat (minimum 1),
|
||||
* subtracted from the defender's health. The defender is marked dead
|
||||
* once health reaches 0.
|
||||
*
|
||||
* @param attacker The attacking fighter.
|
||||
* @param defender The defending fighter.
|
||||
*/
|
||||
void battleResolveAttack(
|
||||
battlefighter_t *attacker,
|
||||
battlefighter_t *defender
|
||||
);
|
||||
|
||||
/**
|
||||
* Ends the current fighter's turn and advances to the next fighter in
|
||||
* the turn order, starting a new round (rebuilding turn order purely by
|
||||
* speed) once every fighter in the current round has acted.
|
||||
*/
|
||||
void battleNextTurn(void);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, updating and
|
||||
* returning BATTLE.result. Does nothing if a result has already been
|
||||
* set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Submits the current fighter's attack against a target, if it is
|
||||
* currently a player-controlled fighter's turn. Resolves the attack,
|
||||
* checks for a battle result, and advances the turn.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the current fighter's turn, if it is
|
||||
* currently a player-controlled fighter's turn and fleeing is
|
||||
* available for this battle. Always succeeds, ending the battle with
|
||||
* BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame: resolves the current
|
||||
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
||||
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
||||
* battle isn't active or already has a result.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.turnOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the turn order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
/**
|
||||
* Picks an AI target for fighter: the lowest-health living fighter on
|
||||
* the opposing team.
|
||||
*
|
||||
* @param fighter The AI-controlled fighter choosing a target.
|
||||
* @return The chosen target, or NULL if the opposing team has no
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "battlefighter.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team");
|
||||
assertTrue(
|
||||
controller < BATTLE_FIGHTER_CONTROLLER_COUNT,
|
||||
"Invalid fighter controller"
|
||||
);
|
||||
|
||||
const uint8_t id = fighter->id;
|
||||
memoryZero(fighter, sizeof(battlefighter_t));
|
||||
fighter->id = id;
|
||||
fighter->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
fighter->team = team;
|
||||
fighter->controller = controller;
|
||||
fighter->stats = stats;
|
||||
fighter->healthMax = healthMax;
|
||||
fighter->health = healthMax;
|
||||
fighter->mpMax = mpMax;
|
||||
fighter->mp = mpMax;
|
||||
}
|
||||
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter) {
|
||||
assertNotNull(fighter, "Fighter pointer cannot be NULL");
|
||||
return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
// An empty status means the slot in BATTLE.fighters is unused.
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_STATUS_NULL,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_NORMAL,
|
||||
BATTLE_FIGHTER_STATUS_DEAD,
|
||||
|
||||
BATTLE_FIGHTER_STATUS_COUNT
|
||||
} battlefighterstatus_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_TEAM_ALLY,
|
||||
BATTLE_FIGHTER_TEAM_ENEMY,
|
||||
|
||||
BATTLE_FIGHTER_TEAM_COUNT
|
||||
} battlefighterteam_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
|
||||
BATTLE_FIGHTER_CONTROLLER_COUNT
|
||||
} battlefightercontroller_t;
|
||||
|
||||
// Base combat stats, kept separate from the resource pools (health/mp) on
|
||||
// battlefighter_t so that equipment/buffs can later modify them without
|
||||
// touching current health/mp state.
|
||||
typedef struct {
|
||||
uint16_t attack;
|
||||
uint16_t defense;
|
||||
uint16_t magic;
|
||||
uint16_t speed;
|
||||
uint16_t luck;
|
||||
} battlefighterstats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t id;
|
||||
battlefighterstatus_t status;
|
||||
battlefighterteam_t team;
|
||||
battlefightercontroller_t controller;
|
||||
|
||||
uint16_t health;
|
||||
uint16_t healthMax;
|
||||
uint16_t mp;
|
||||
uint16_t mpMax;
|
||||
|
||||
battlefighterstats_t stats;
|
||||
} battlefighter_t;
|
||||
|
||||
/**
|
||||
* Initializes a battle fighter in place, filling health/mp to their maximum
|
||||
* values and setting its status to normal.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to initialize.
|
||||
* @param team The team the fighter belongs to.
|
||||
* @param controller Who makes decisions for the fighter.
|
||||
* @param stats The fighter's base combat stats.
|
||||
* @param healthMax The fighter's maximum health.
|
||||
* @param mpMax The fighter's maximum mp.
|
||||
*/
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
const battlefightercontroller_t controller,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
*
|
||||
* @param fighter Pointer to the fighter to check.
|
||||
* @returns True if the fighter can act.
|
||||
*/
|
||||
bool_t battleFighterIsAlive(const battlefighter_t *fighter);
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "party.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
party_t PARTY;
|
||||
|
||||
void partyInit(void) {
|
||||
memoryZero(&PARTY, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
PARTY.members[i].id = i;
|
||||
}
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
PARTY.order[i] = PARTY_ORDER_EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t partyGetAvailableMember(void) {
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
}
|
||||
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
const uint8_t index = partyGetAvailableMember();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *member = &PARTY.members[index];
|
||||
battleFighterInit(
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
stats, healthMax, mpMax
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue;
|
||||
PARTY.order[i] = index;
|
||||
break;
|
||||
}
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
|
||||
const uint8_t index = PARTY.order[slot];
|
||||
if(index == PARTY_ORDER_EMPTY) return NULL;
|
||||
return &PARTY.members[index];
|
||||
}
|
||||
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
assertTrue(
|
||||
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
|
||||
"Invalid party member index"
|
||||
);
|
||||
|
||||
PARTY.order[slot] = memberIndex;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "battlefighter.h"
|
||||
|
||||
#define PARTY_MEMBER_COUNT_MAX 4
|
||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||
#define PARTY_ORDER_EMPTY 0xFF
|
||||
|
||||
typedef struct {
|
||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
||||
|
||||
// Maps an active battle slot to the roster member filling it, or
|
||||
// PARTY_ORDER_EMPTY if the slot is unfilled. Only the first
|
||||
// PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members
|
||||
// can be in the active lineup at once.
|
||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
||||
} party_t;
|
||||
|
||||
extern party_t PARTY;
|
||||
|
||||
/**
|
||||
* Initializes the party system with an empty roster and order.
|
||||
*/
|
||||
void partyInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) party member slot index.
|
||||
*
|
||||
* @return The index of an available slot, or 0xFF if the party is full.
|
||||
*/
|
||||
uint8_t partyGetAvailableMember(void);
|
||||
|
||||
/**
|
||||
* Adds a member to the party roster in the next available slot. Party
|
||||
* members are always allies controlled by the player. If there is a
|
||||
* free active order slot, the new member is placed into it.
|
||||
*
|
||||
* @param stats The member's base combat stats.
|
||||
* @param healthMax The member's maximum health.
|
||||
* @param mpMax The member's maximum mp.
|
||||
* @return Pointer to the newly added party member, or NULL if the party is
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *partyAddMember(
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot.
|
||||
*
|
||||
* @param slot The active order slot to query.
|
||||
* @return Pointer to the member in that slot, or NULL if the slot is
|
||||
* empty.
|
||||
*/
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Assigns a roster member to an active order slot, replacing whatever
|
||||
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
|
||||
*
|
||||
* @param slot The active order slot to assign.
|
||||
* @param memberIndex The roster member index to place there, or
|
||||
* PARTY_ORDER_EMPTY to clear the slot.
|
||||
*/
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
||||
@@ -14,4 +14,3 @@ add_subdirectory(entity)
|
||||
add_subdirectory(item)
|
||||
add_subdirectory(maparea)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(battle)
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenestartbattle_t *config = &item->startBattle;
|
||||
|
||||
battleInit();
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
battlefighter_t *member = partyGetOrderMember(i);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
member->stats, member->healthMax, member->mpMax
|
||||
);
|
||||
if(fighter == NULL) continue;
|
||||
|
||||
fighter->health = member->health;
|
||||
fighter->mp = member->mp;
|
||||
fighter->status = member->status;
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
||||
const cutscenestartbattleenemy_t *enemy = &config->enemies[i];
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemy->stats, enemy->healthMax, enemy->mpMax
|
||||
);
|
||||
}
|
||||
|
||||
battleStart(config->encounterType, config->fleeAvailable);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) return false;
|
||||
|
||||
// Sync ally HP/MP back to the persistent party roster. Relies on
|
||||
// ally fighters having been added to BATTLE.fighters in the same
|
||||
// order partyGetOrderMember() iterates, starting at index 0 (see
|
||||
// cutsceneStartBattleStart).
|
||||
uint8_t allySlot = 0;
|
||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||
battlefighter_t *member = partyGetOrderMember(i);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
||||
member->health = fighter->health;
|
||||
member->mp = fighter->mp;
|
||||
member->status = fighter->status;
|
||||
}
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
battleDispose();
|
||||
return true;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
#define CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
battlefighterstats_t stats;
|
||||
uint16_t healthMax;
|
||||
uint16_t mpMax;
|
||||
} cutscenestartbattleenemy_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
uint8_t enemyCount;
|
||||
cutscenestartbattleenemy_t enemies[CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX];
|
||||
} cutscenestartbattle_t;
|
||||
|
||||
/**
|
||||
* Starts a battle: seeds BATTLE with the party's active order members
|
||||
* and the item's configured enemies, then switches to the battle
|
||||
* scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneStartBattleStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Waits for the battle to produce a result, syncs ally HP/MP back to
|
||||
* the party roster, then returns to the overworld scene.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once the battle has ended.
|
||||
*/
|
||||
bool_t cutsceneStartBattleUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -105,11 +105,6 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
.update = cutsceneMapAreaWaitUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_START_BATTLE] = {
|
||||
.init = cutsceneStartBattleStart,
|
||||
.update = cutsceneStartBattleUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_EMOJI] = {
|
||||
.init = cutsceneEmojiStart,
|
||||
.update = cutsceneEmojiUpdate
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
@@ -52,7 +51,6 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_ADD,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE,
|
||||
CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT,
|
||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
|
||||
@@ -82,7 +80,6 @@ struct cutsceneitem_s {
|
||||
cutscenemapareaadd_t mapAreaAdd;
|
||||
cutscenemaparearemove_t mapAreaRemove;
|
||||
cutscenemapareawait_t mapAreaWait;
|
||||
cutscenestartbattle_t startBattle;
|
||||
cutsceneemoji_t emoji;
|
||||
cutsceneshake_t shake;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
player.c
|
||||
)
|
||||
|
||||
add_subdirectory(anim)
|
||||
add_subdirectory(interact)
|
||||
add_subdirectory(npc)
|
||||
add_subdirectory(item)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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
|
||||
entityanim.c
|
||||
entityanimidle.c
|
||||
entityanimturn.c
|
||||
entityanimwalk.c
|
||||
entityanimrun.c
|
||||
)
|
||||
@@ -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 tileShapeIsRamp(mapGetTile(pos).shape) ? 0.5f : 0.0f;
|
||||
}
|
||||
|
||||
void entityAnimUpdate(entity_t *entity) {
|
||||
if(entity->animation != ENTITY_ANIM_IDLE) {
|
||||
entity->animTime -= TIME.delta;
|
||||
if(entity->animTime <= 0) {
|
||||
if(
|
||||
entity->animation == ENTITY_ANIM_WALK ||
|
||||
entity->animation == ENTITY_ANIM_RUN
|
||||
) {
|
||||
entity->walkEndCooldown = ENTITY_ANIM_WALK_TURN_COOLDOWN;
|
||||
}
|
||||
entity->animation = ENTITY_ANIM_IDLE;
|
||||
entity->animTime = 0;
|
||||
}
|
||||
}
|
||||
if(entity->walkEndCooldown > 0) {
|
||||
entity->walkEndCooldown -= TIME.delta;
|
||||
if(entity->walkEndCooldown < 0) entity->walkEndCooldown = 0;
|
||||
}
|
||||
ENTITY_ANIM_CALLBACKS[entity->animation].update(entity);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "entityanimidle.h"
|
||||
#include "entityanimturn.h"
|
||||
#include "entityanimwalk.h"
|
||||
#include "entityanimrun.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
typedef enum {
|
||||
ENTITY_ANIM_IDLE,
|
||||
ENTITY_ANIM_TURN,
|
||||
ENTITY_ANIM_WALK,
|
||||
ENTITY_ANIM_RUN,
|
||||
ENTITY_ANIM_COUNT
|
||||
} entityanim_t;
|
||||
|
||||
typedef struct {
|
||||
/** Updates the render position for this animation state. */
|
||||
void (*update)(entity_t *entity);
|
||||
} entityanimcallback_t;
|
||||
|
||||
extern const entityanimcallback_t ENTITY_ANIM_CALLBACKS[ENTITY_ANIM_COUNT];
|
||||
|
||||
/**
|
||||
* Updates the entity animation timer and render position.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimUpdate(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns 0.5 if the tile at pos is a ramp, 0.0 otherwise.
|
||||
* Used to lift entity render position to mid-ramp height.
|
||||
*
|
||||
* @param pos World position to sample.
|
||||
* @returns float_t The Z offset to apply.
|
||||
*/
|
||||
float_t entityAnimTileZOffset(const worldpos_t pos);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimIdleUpdate(entity_t *entity) {
|
||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
||||
entity->renderPosition[2] = (
|
||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
||||
) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
/**
|
||||
* Updates render position for the idle animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimIdleUpdate(entity_t *entity);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimRunUpdate(entity_t *entity) {
|
||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_RUN_DURATION);
|
||||
float_t zFrom = (float_t)entity->lastPosition.z
|
||||
+ entityAnimTileZOffset(entity->lastPosition);
|
||||
float_t zTo = (float_t)entity->position.z
|
||||
+ entityAnimTileZOffset(entity->position);
|
||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
||||
);
|
||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
||||
);
|
||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_RUN_DURATION TIME_TICKS_TO_TIME(6)
|
||||
|
||||
/**
|
||||
* Updates render position for the run animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimRunUpdate(entity_t *entity);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimTurnUpdate(entity_t *entity) {
|
||||
entity->renderPosition[0] = (float_t)entity->position.x;
|
||||
entity->renderPosition[1] = (float_t)entity->position.y;
|
||||
entity->renderPosition[2] = (
|
||||
(float_t)entity->position.z + entityAnimTileZOffset(entity->position)
|
||||
) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_TURN_DURATION TIME_TICKS_TO_TIME(4)
|
||||
|
||||
/**
|
||||
* Updates render position for the turn animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimTurnUpdate(entity_t *entity);
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "entityanim.h"
|
||||
|
||||
void entityAnimWalkUpdate(entity_t *entity) {
|
||||
float_t t = 1.0f - (entity->animTime / ENTITY_ANIM_WALK_DURATION);
|
||||
float_t zFrom = (float_t)entity->lastPosition.z
|
||||
+ entityAnimTileZOffset(entity->lastPosition);
|
||||
float_t zTo = (float_t)entity->position.z
|
||||
+ entityAnimTileZOffset(entity->position);
|
||||
entity->renderPosition[0] = (float_t)entity->lastPosition.x + t * (
|
||||
(float_t)entity->position.x - (float_t)entity->lastPosition.x
|
||||
);
|
||||
entity->renderPosition[1] = (float_t)entity->lastPosition.y + t * (
|
||||
(float_t)entity->position.y - (float_t)entity->lastPosition.y
|
||||
);
|
||||
entity->renderPosition[2] = (zFrom + t * (zTo - zFrom)) * WORLD_LAYER_HEIGHT;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/time.h"
|
||||
|
||||
typedef struct entity_s entity_t;
|
||||
|
||||
#define ENTITY_ANIM_WALK_DURATION TIME_TICKS_TO_TIME(12)
|
||||
#define ENTITY_ANIM_WALK_TURN_COOLDOWN TIME_TICKS_TO_TIME(4)
|
||||
|
||||
/**
|
||||
* Updates render position for the walk animation state.
|
||||
*
|
||||
* @param entity Pointer to the entity to update.
|
||||
*/
|
||||
void entityAnimWalkUpdate(entity_t *entity);
|
||||
+116
-188
@@ -8,14 +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");
|
||||
@@ -32,6 +32,9 @@ void entityInit(entity_t *entity, const entitytype_t type) {
|
||||
entity->type = type;
|
||||
entity->chunkIndex = 0xFF;
|
||||
|
||||
const vec3 extents = ENTITY_PHYSICS_EXTENTS_DEFAULT;
|
||||
physicsBodyInit(&entity->body, (vec3){ 0.0f, 0.0f, 0.0f }, extents);
|
||||
|
||||
if(ENTITY_CALLBACKS[type].init != NULL) ENTITY_CALLBACKS[type].init(entity);
|
||||
}
|
||||
|
||||
@@ -40,212 +43,115 @@ void entityUpdate(entity_t *entity) {
|
||||
assertTrue(entity->type < ENTITY_TYPE_COUNT, "Invalid entity type");
|
||||
assertTrue(entity->type != ENTITY_TYPE_NULL, "Cannot have NULL entity type");
|
||||
|
||||
// What state is the entity in?
|
||||
entityAnimUpdate(entity);
|
||||
|
||||
// Movement code.
|
||||
if(ENTITY_CALLBACKS[entity->type].movement != NULL) {
|
||||
ENTITY_CALLBACKS[entity->type].movement(entity);
|
||||
}
|
||||
}
|
||||
|
||||
bool_t entityCanTurn(entity_t *entity) {
|
||||
return entity->animation == ENTITY_ANIM_IDLE &&
|
||||
entity->walkEndCooldown <= 0;
|
||||
}
|
||||
physicsbody_t *others[ENTITY_COUNT];
|
||||
uint32_t othersCount = 0;
|
||||
for(uint8_t i = 0; i < ENTITY_COUNT; i++) {
|
||||
if(ENTITIES[i].type == ENTITY_TYPE_NULL) continue;
|
||||
if(&ENTITIES[i] == entity) continue;
|
||||
others[othersCount++] = &ENTITIES[i].body;
|
||||
}
|
||||
|
||||
bool_t entityCanWalk(entity_t *entity) {
|
||||
return entity->animation == ENTITY_ANIM_IDLE;
|
||||
}
|
||||
|
||||
bool_t entityCanRun(entity_t *entity) {
|
||||
return entity->animation == ENTITY_ANIM_IDLE;
|
||||
physicsWorldStep(
|
||||
&ENTITY_PHYSICS_WORLD, &entity->body, TIME.delta, others, othersCount
|
||||
);
|
||||
entitySyncFromPhysics(entity);
|
||||
}
|
||||
|
||||
bool_t entityCanUnload(entity_t *entity) {
|
||||
return entity->globalId < ENTITY_GLOBAL_ID_START;
|
||||
}
|
||||
|
||||
void entityMove(
|
||||
entity_t *entity, const vec2 direction, const bool_t running
|
||||
) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
const float_t magSq =
|
||||
direction[0] * direction[0] + direction[1] * direction[1];
|
||||
if(magSq <= ENTITY_MOVE_DEADZONE * ENTITY_MOVE_DEADZONE) {
|
||||
entity->body.velocity[0] = 0.0f;
|
||||
entity->body.velocity[1] = 0.0f;
|
||||
entity->animation = ENTITY_ANIM_IDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
const float_t mag = sqrtf(magSq);
|
||||
const float_t clampedMag = mathMin(mag, 1.0f);
|
||||
const float_t speed = running ? ENTITY_MOVE_RUN_SPEED : ENTITY_MOVE_WALK_SPEED;
|
||||
|
||||
entity->body.velocity[0] = (direction[0] / mag) * clampedMag * speed;
|
||||
entity->body.velocity[1] = (direction[1] / mag) * clampedMag * speed;
|
||||
entity->direction = entityDirFromVec2(direction);
|
||||
entity->animation = running ? ENTITY_ANIM_RUN : ENTITY_ANIM_WALK;
|
||||
}
|
||||
|
||||
void entityStop(entity_t *entity) {
|
||||
const vec2 zero = { 0.0f, 0.0f };
|
||||
entityMove(entity, zero, false);
|
||||
}
|
||||
|
||||
void entityTurn(entity_t *entity, const entitydir_t direction) {
|
||||
if(!entityCanTurn(entity)) return;
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
entity->direction = direction;
|
||||
entity->animation = ENTITY_ANIM_TURN;
|
||||
entity->animTime = ENTITY_ANIM_TURN_DURATION;
|
||||
}
|
||||
|
||||
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||
if(!entityCanWalk(entity)) return;
|
||||
// TODO: Animation, delay, etc.
|
||||
entity->direction = direction;
|
||||
|
||||
// Where are we moving?
|
||||
worldpos_t newPos = entity->position;
|
||||
worldunits_t relX, relY;
|
||||
{
|
||||
entityDirGetRelative(direction, &relX, &relY);
|
||||
newPos.x += relX;
|
||||
newPos.y += relY;
|
||||
}
|
||||
|
||||
// Get tile under foot
|
||||
tile_t tileCurrent = mapGetTile(entity->position);
|
||||
tile_t tileNew = mapGetTile(newPos);
|
||||
bool_t fall = false;
|
||||
bool_t raise = false;
|
||||
|
||||
// Are we walking up a ramp?
|
||||
if(
|
||||
tileShapeIsRamp(tileCurrent.shape) &&
|
||||
(
|
||||
// Can only walk UP the direction the ramp faces.
|
||||
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape ||
|
||||
// If diagonal ramp, can go up one of two ways only. Inner ramps
|
||||
// share the same allowed directions as their outer counterparts.
|
||||
(
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
||||
tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
||||
)
|
||||
)
|
||||
// Must be able to walk up.
|
||||
)
|
||||
) {
|
||||
tile_t tileNewSaved = tileNew;
|
||||
tileNew = TILE_NULL;
|
||||
worldpos_t abovePos = newPos;
|
||||
abovePos.z += 1;
|
||||
tile_t tileAbove = mapGetTile(abovePos);
|
||||
|
||||
if(
|
||||
tileAbove.shape != TILE_SHAPE_NULL &&
|
||||
tileShapeIsWalkable(tileAbove.shape)
|
||||
) {
|
||||
raise = true;
|
||||
} else {
|
||||
tileNew = tileNewSaved;
|
||||
}
|
||||
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
|
||||
// Falling down?
|
||||
worldpos_t belowPos = newPos;
|
||||
belowPos.z -= 1;
|
||||
tile_t tileBelow = mapGetTile(belowPos);
|
||||
if(
|
||||
tileBelow.shape != TILE_SHAPE_NULL &&
|
||||
tileShapeIsRamp(tileBelow.shape) &&
|
||||
(
|
||||
// This handles regular cardinal ramps
|
||||
(
|
||||
entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH
|
||||
) == tileBelow.shape ||
|
||||
// This handles diagonal ramps. Inner ramps share the same
|
||||
// allowed directions as their outer counterparts.
|
||||
(
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
|
||||
) ||
|
||||
(
|
||||
(
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST ||
|
||||
tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER
|
||||
) &&
|
||||
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
// We will fall to this tile.
|
||||
fall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Can we walk here?
|
||||
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
|
||||
|
||||
// Raise/fall must be applied before checking for blocking entities,
|
||||
// otherwise the check compares against the wrong z-level.
|
||||
if(raise) {
|
||||
newPos.z += 1;
|
||||
} else if(fall) {
|
||||
newPos.z -= 1;
|
||||
}
|
||||
|
||||
// Entity in way?
|
||||
entity_t *other = ENTITIES;
|
||||
do {
|
||||
if(other == entity) continue;
|
||||
if(other->type == ENTITY_TYPE_NULL) continue;
|
||||
if(!worldPosIsEqual(other->position, newPos)) continue;
|
||||
return;// Blocked
|
||||
} while(++other, other < &ENTITIES[ENTITY_COUNT]);
|
||||
|
||||
entity->lastPosition = entity->position;
|
||||
entity->position = newPos;
|
||||
entity->animation = ENTITY_ANIM_WALK;
|
||||
entity->animTime = ENTITY_ANIM_WALK_DURATION;// TODO: Running vs walking
|
||||
entityUpdateChunk(entity);
|
||||
mapAreaCheckEntity(entity);
|
||||
vec2 dirVec;
|
||||
entityDirToVec2(direction, dirVec);
|
||||
entityMove(entity, dirVec, false);
|
||||
}
|
||||
|
||||
void entityRun(entity_t *entity, const entitydir_t direction) {
|
||||
if(!entityCanRun(entity)) return;
|
||||
entityWalk(entity, direction);
|
||||
if(entity->animation == ENTITY_ANIM_WALK) {
|
||||
entity->animation = ENTITY_ANIM_RUN;
|
||||
entity->animTime = ENTITY_ANIM_RUN_DURATION;
|
||||
}
|
||||
vec2 dirVec;
|
||||
entityDirToVec2(direction, dirVec);
|
||||
entityMove(entity, dirVec, true);
|
||||
}
|
||||
|
||||
entity_t * entityGetAt(const worldpos_t position) {
|
||||
entity_t * entityGetFacing(entity_t *entity, const float_t range) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
|
||||
vec2 dir;
|
||||
entityDirToVec2(entity->direction, dir);
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(&entity->body, min, max);
|
||||
|
||||
const vec3 probeMin = {
|
||||
min[0] + dir[0] * range, min[1] + dir[1] * range, min[2]
|
||||
};
|
||||
const vec3 probeMax = {
|
||||
max[0] + dir[0] * range, max[1] + dir[1] * range, max[2]
|
||||
};
|
||||
|
||||
entity_t *best = NULL;
|
||||
float_t bestDistSq = 0.0f;
|
||||
|
||||
entity_t *ent = ENTITIES;
|
||||
do {
|
||||
if(ent->type == ENTITY_TYPE_NULL) continue;
|
||||
if(!worldPosIsEqual(ent->position, position)) continue;
|
||||
return ent;
|
||||
if(ent == entity) continue;
|
||||
|
||||
vec3 oMin, oMax;
|
||||
physicsBodyGetBounds(&ent->body, oMin, oMax);
|
||||
|
||||
if(probeMin[0] >= oMax[0] || probeMax[0] <= oMin[0]) continue;
|
||||
if(probeMin[1] >= oMax[1] || probeMax[1] <= oMin[1]) continue;
|
||||
if(probeMin[2] >= oMax[2] || probeMax[2] <= oMin[2]) continue;
|
||||
|
||||
const float_t dx = ent->body.position[0] - entity->body.position[0];
|
||||
const float_t dy = ent->body.position[1] - entity->body.position[1];
|
||||
const float_t distSq = dx * dx + dy * dy;
|
||||
|
||||
if(best != NULL && distSq >= bestDistSq) continue;
|
||||
best = ent;
|
||||
bestDistSq = distSq;
|
||||
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
|
||||
|
||||
return NULL;
|
||||
return best;
|
||||
}
|
||||
|
||||
entity_t * entityGetByGlobalId(const entityglobalid_t globalId) {
|
||||
@@ -270,12 +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;
|
||||
entityUpdateChunk(entity);
|
||||
entitySyncFromPhysics(entity);
|
||||
}
|
||||
|
||||
void entitySetChunk(entity_t *entity, const uint8_t chunkIndex) {
|
||||
@@ -297,11 +205,14 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,5 +223,22 @@ void entityUpdateChunk(entity_t *entity) {
|
||||
chunkpos_t cp;
|
||||
worldPosToChunkPos(&entity->position, &cp);
|
||||
chunkindex_t ci = mapGetChunkIndexAt(cp);
|
||||
if(ci != -1) entitySetChunk(entity, (uint8_t)ci);
|
||||
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);
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
|
||||
#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;
|
||||
|
||||
@@ -20,6 +21,29 @@ typedef uint16_t entityglobalid_t;
|
||||
#define ENTITY_GLOBAL_ID_START 1
|
||||
#define ENTITY_GLOBAL_ID_PLAYER 1
|
||||
|
||||
// Default collision box for every entity's physics body - matches the old
|
||||
// system's exact one-tile footprint.
|
||||
#define ENTITY_PHYSICS_EXTENTS_DEFAULT { 1.0f, 1.0f, 1.0f }
|
||||
|
||||
// Movement speeds, in grid units per second - chosen to match the feel of
|
||||
// the old fixed-duration one-tile-per-step system (12 ticks/tile walking,
|
||||
// 6 ticks/tile running, at DUSK_TIME_STEP = 16ms).
|
||||
#define ENTITY_MOVE_WALK_SPEED 5.2083f
|
||||
#define ENTITY_MOVE_RUN_SPEED 10.4167f
|
||||
|
||||
// Movement vectors below this magnitude are treated as no movement.
|
||||
#define ENTITY_MOVE_DEADZONE 0.1f
|
||||
|
||||
// How far in front of an entity entityGetFacing probes for a target.
|
||||
#define ENTITY_INTERACT_RANGE 1.0f
|
||||
|
||||
typedef enum {
|
||||
ENTITY_ANIM_IDLE,
|
||||
ENTITY_ANIM_WALK,
|
||||
ENTITY_ANIM_RUN,
|
||||
ENTITY_ANIM_COUNT
|
||||
} entityanim_t;
|
||||
|
||||
typedef struct entity_s {
|
||||
uint8_t id;
|
||||
entityglobalid_t globalId;
|
||||
@@ -28,13 +52,18 @@ typedef struct entity_s {
|
||||
|
||||
// Movement
|
||||
entitydir_t direction;
|
||||
physicsbody_t body;
|
||||
|
||||
// Derived each frame from body.position (floored) - kept for systems
|
||||
// that still assume an integer grid position (chunk membership, map
|
||||
// area triggers, entity-at-position queries).
|
||||
worldpos_t position;
|
||||
worldpos_t lastPosition;
|
||||
|
||||
// Derived each frame from body.position - mirrors the physics position
|
||||
// into render/world-float space (z scaled by WORLD_LAYER_HEIGHT).
|
||||
vec3 renderPosition;
|
||||
|
||||
entityanim_t animation;
|
||||
float_t animTime;
|
||||
float_t walkEndCooldown;
|
||||
|
||||
entityinteract_t interact;
|
||||
|
||||
@@ -43,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.
|
||||
*
|
||||
@@ -58,30 +90,6 @@ void entityInit(entity_t *entity, const entitytype_t type);
|
||||
*/
|
||||
void entityUpdate(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is in a state where it can turn.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
* @returns True if the entity can turn.
|
||||
*/
|
||||
bool_t entityCanTurn(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is in a state where it can walk.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
* @returns True if the entity can walk.
|
||||
*/
|
||||
bool_t entityCanWalk(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is in a state where it can run.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
* @returns True if the entity can run.
|
||||
*/
|
||||
bool_t entityCanRun(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Returns true if the entity is allowed to be unloaded. By default this is
|
||||
* true for entities whose global ID falls within the randomly assigned
|
||||
@@ -93,7 +101,29 @@ bool_t entityCanRun(entity_t *entity);
|
||||
bool_t entityCanUnload(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Turn an entity to face a new direction.
|
||||
* Moves an entity continuously in a direction, at walking or running
|
||||
* speed. Sets the entity's facing to the nearest cardinal direction that
|
||||
* matches the movement vector. Must be called every tick the entity
|
||||
* should keep moving - unlike the old tile-stepping system this does not
|
||||
* complete a move on its own; call entityStop to stop.
|
||||
*
|
||||
* @param entity Pointer to the entity to move.
|
||||
* @param direction Movement vector, magnitude 0-1 (values longer than 1
|
||||
* are clamped to 1, so diagonals aren't faster than cardinals).
|
||||
* @param running Whether to move at running speed instead of walking.
|
||||
*/
|
||||
void entityMove(entity_t *entity, const vec2 direction, const bool_t running);
|
||||
|
||||
/**
|
||||
* Stops an entity's horizontal movement (equivalent to
|
||||
* entityMove(entity, {0, 0}, false)).
|
||||
*
|
||||
* @param entity Pointer to the entity to stop.
|
||||
*/
|
||||
void entityStop(entity_t *entity);
|
||||
|
||||
/**
|
||||
* Turn an entity to face a new direction, instantly, without moving it.
|
||||
*
|
||||
* @param entity Pointer to the entity to turn.
|
||||
* @param direction The direction to face.
|
||||
@@ -101,7 +131,10 @@ bool_t entityCanUnload(entity_t *entity);
|
||||
void entityTurn(entity_t *entity, const entitydir_t direction);
|
||||
|
||||
/**
|
||||
* Make an entity walk in a direction.
|
||||
* Makes an entity walk continuously in a cardinal direction, at walking
|
||||
* speed. Convenience wrapper over entityMove for callers that only think
|
||||
* in terms of the 4 cardinal directions. Must be called every tick the
|
||||
* entity should keep moving.
|
||||
*
|
||||
* @param entity Pointer to the entity to make walk.
|
||||
* @param direction The direction to walk in.
|
||||
@@ -109,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.
|
||||
@@ -117,13 +151,16 @@ void entityWalk(entity_t *entity, const entitydir_t direction);
|
||||
void entityRun(entity_t *entity, const entitydir_t direction);
|
||||
|
||||
/**
|
||||
* Gets the entity at a specific world position.
|
||||
* Finds the closest other entity whose bounds overlap a probe box
|
||||
* projected out from the given entity's own bounds, along its current
|
||||
* facing direction. Used for interaction targeting - continuous-position
|
||||
* aware, unlike an exact tile match.
|
||||
*
|
||||
* @param map Pointer to the map to check.
|
||||
* @param pos The world position to check.
|
||||
* @return Pointer to the entity at the position, or NULL if none.
|
||||
* @param entity Pointer to the entity to probe from.
|
||||
* @param range Distance, in grid units, to project the probe box.
|
||||
* @return Pointer to the closest overlapping entity, or NULL if none.
|
||||
*/
|
||||
entity_t *entityGetAt(const worldpos_t pos);
|
||||
entity_t *entityGetFacing(entity_t *entity, const float_t range);
|
||||
|
||||
/**
|
||||
* Gets the entity with the given global ID, if one is currently loaded.
|
||||
@@ -165,3 +202,12 @@ void entityUpdateChunk(entity_t *entity);
|
||||
* @param pos The world position to place the entity at.
|
||||
*/
|
||||
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);
|
||||
@@ -49,3 +49,26 @@ void entityDirGetRelative(
|
||||
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;
|
||||
}
|
||||
@@ -52,3 +52,22 @@ entitydir_t entityDirGetOpposite(const entitydir_t dir);
|
||||
void entityDirGetRelative(
|
||||
const entitydir_t dir, worldunits_t *relX, worldunits_t *relY
|
||||
);
|
||||
|
||||
/**
|
||||
* Converts a cardinal direction to a unit 2D vector, using the same axis
|
||||
* convention as entityDirGetRelative (north = +y, east = +x).
|
||||
*
|
||||
* @param dir The direction to convert.
|
||||
* @param out Output unit vector for the direction.
|
||||
*/
|
||||
void entityDirToVec2(const entitydir_t dir, vec2 out);
|
||||
|
||||
/**
|
||||
* Quantizes a 2D direction vector to the nearest cardinal direction, by
|
||||
* comparing the magnitude of its x and y components.
|
||||
*
|
||||
* @param direction The direction vector to quantize. Must not be a zero
|
||||
* vector.
|
||||
* @return The nearest cardinal direction.
|
||||
*/
|
||||
entitydir_t entityDirFromVec2(const vec2 direction);
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "entitydir.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
// Distance, in grid units, within which the entity is considered to have
|
||||
// arrived at its path target.
|
||||
#define ENTITY_PATH_ARRIVE_EPSILON 0.05f
|
||||
|
||||
bool_t entityPathStep(
|
||||
entity_t *entity,
|
||||
const worldpos_t target,
|
||||
@@ -24,36 +28,44 @@ bool_t entityPathStep(
|
||||
"Entity pointer is out of bounds"
|
||||
);
|
||||
|
||||
if(worldPosIsEqual(entity->position, target) && entityCanWalk(entity)) {
|
||||
const float_t dx = (float_t)target.x - entity->body.position[0];
|
||||
const float_t dy = (float_t)target.y - entity->body.position[1];
|
||||
|
||||
if(
|
||||
fabsf(dx) <= ENTITY_PATH_ARRIVE_EPSILON &&
|
||||
fabsf(dy) <= ENTITY_PATH_ARRIVE_EPSILON
|
||||
) {
|
||||
entityStop(entity);
|
||||
return true;
|
||||
}
|
||||
if(!entityCanWalk(entity)) return false;
|
||||
|
||||
entitydir_t dir;
|
||||
if(entity->position.x != target.x) {
|
||||
dir = entity->position.x < target.x ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
} else if(entity->position.y != target.y) {
|
||||
dir = entity->position.y < target.y ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
bool_t horizontal;
|
||||
if(fabsf(dx) > fabsf(dy)) {
|
||||
dir = dx > 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
horizontal = true;
|
||||
} else {
|
||||
dir = entity->position.z < target.z ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
dir = dy > 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
horizontal = false;
|
||||
}
|
||||
|
||||
// Was the entity trying to walk on this axis last tick, but ended up not
|
||||
// moving (velocity clamped by a collision)? If so, treat it as blocked.
|
||||
if(walkAround) {
|
||||
const uint8_t axis = horizontal ? 0 : 1;
|
||||
const bool_t blockedLastTick =
|
||||
entity->animation != ENTITY_ANIM_IDLE &&
|
||||
fabsf(entity->body.velocity[axis]) <= ENTITY_MOVE_DEADZONE;
|
||||
|
||||
if(blockedLastTick) {
|
||||
const entitydir_t alt = horizontal
|
||||
? (dy >= 0.0f ? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH)
|
||||
: (dx >= 0.0f ? ENTITY_DIR_EAST : ENTITY_DIR_WEST);
|
||||
entityWalk(entity, alt);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
entityWalk(entity, dir);
|
||||
|
||||
if(walkAround && entity->animation == ENTITY_ANIM_IDLE) {
|
||||
// Primary direction blocked - try perpendicular axes.
|
||||
entitydir_t alt, altOpp;
|
||||
if(dir == ENTITY_DIR_EAST || dir == ENTITY_DIR_WEST) {
|
||||
alt = entity->position.y <= target.y
|
||||
? ENTITY_DIR_NORTH : ENTITY_DIR_SOUTH;
|
||||
} else {
|
||||
alt = entity->position.x <= target.x
|
||||
? ENTITY_DIR_EAST : ENTITY_DIR_WEST;
|
||||
}
|
||||
altOpp = entityDirGetOpposite(alt);
|
||||
entityWalk(entity, alt);
|
||||
if(entity->animation == ENTITY_ANIM_IDLE) entityWalk(entity, altOpp);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
#include "entity.h"
|
||||
|
||||
/**
|
||||
* Attempts to walk the entity one tile toward target. Prefers resolving X
|
||||
* first, then Y, then Z. Does nothing if the entity cannot currently walk.
|
||||
* When walkAround is true and the preferred direction is blocked by an entity,
|
||||
* tries perpendicular directions to navigate around it.
|
||||
* Continuously walks the entity toward target's X/Y position (Z is left to
|
||||
* gravity/ground collision, not deliberate path-stepping). Prefers closing
|
||||
* whichever of X/Y currently differs most. Must be called every tick until
|
||||
* it returns true. When walkAround is true and the last tick's movement on
|
||||
* the current axis was blocked (its velocity on that axis reads as zero
|
||||
* despite currently walking), tries a perpendicular direction instead.
|
||||
*
|
||||
* @param entity Pointer to the entity to move.
|
||||
* @param target The world position to move toward.
|
||||
* @param walkAround Whether to try perpendicular directions when blocked.
|
||||
* @returns true if the entity is already at target, false otherwise.
|
||||
* @param target The world position to move toward (Z is ignored).
|
||||
* @param walkAround Whether to try a perpendicular direction when blocked.
|
||||
* @returns true once the entity is within arrival distance of target.
|
||||
*/
|
||||
bool_t entityPathStep(
|
||||
entity_t *entity,
|
||||
|
||||
@@ -29,15 +29,9 @@ void npcPathAddNode(npc_t *npc, const worldpos_t pos) {
|
||||
void npcPathMovement(entity_t *entity) {
|
||||
npcpath_t *path = &entity->data.npc.moveData.path;
|
||||
if(path->count == 0) return;
|
||||
if(!entityCanWalk(entity)) return;
|
||||
|
||||
// Advance past any waypoints already reached (including the current one).
|
||||
worldpos_t *target = &path->positions[path->index];
|
||||
if(entityPathStep(entity, *target, false)) {
|
||||
const worldpos_t target = path->positions[path->index];
|
||||
if(entityPathStep(entity, target, false)) {
|
||||
path->index = (path->index + 1) % path->count;
|
||||
target = &path->positions[path->index];
|
||||
// New target is the same tile - nothing to do this tick
|
||||
if(worldPosIsEqual(entity->position, *target)) return;
|
||||
entityPathStep(entity, *target, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ void npcRandomTurnMovement(entity_t *entity) {
|
||||
turn->timer -= TIME.delta;
|
||||
if(turn->timer > 0.0f) return;
|
||||
turn->timer = randomFloat(turn->frequencyMin, turn->frequencyMax);
|
||||
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
}
|
||||
|
||||
@@ -16,14 +16,24 @@ void npcRandomWalkInit(npc_t *npc) {
|
||||
walk->frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
||||
walk->frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
||||
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
||||
walk->moveDuration = 0.0f;
|
||||
}
|
||||
|
||||
void npcRandomWalkMovement(entity_t *entity) {
|
||||
npcrandomwalk_t *walk = &entity->data.npc.moveData.randomWalk;
|
||||
|
||||
if(walk->moveDuration > 0.0f) {
|
||||
walk->moveDuration -= TIME.delta;
|
||||
entityWalk(entity, walk->direction);
|
||||
if(walk->moveDuration <= 0.0f) entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
walk->timer -= TIME.delta;
|
||||
if(walk->timer > 0.0f) return;
|
||||
walk->timer = randomFloat(walk->frequencyMin, walk->frequencyMax);
|
||||
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
|
||||
walk->direction = (entitydir_t)(rand() % 4);
|
||||
walk->moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
|
||||
}
|
||||
|
||||
void npcRandomTurnAndWalkInit(npc_t *npc) {
|
||||
@@ -32,21 +42,30 @@ void npcRandomTurnAndWalkInit(npc_t *npc) {
|
||||
tw->walk.frequencyMin = NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT;
|
||||
tw->walk.frequencyMax = NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT;
|
||||
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
||||
tw->walk.moveDuration = 0.0f;
|
||||
}
|
||||
|
||||
void npcRandomTurnAndWalkMovement(entity_t *entity) {
|
||||
npcrandomturnandwalk_t *tw = &entity->data.npc.moveData.randomTurnAndWalk;
|
||||
|
||||
if(tw->walk.moveDuration > 0.0f) {
|
||||
tw->walk.moveDuration -= TIME.delta;
|
||||
entityWalk(entity, tw->walk.direction);
|
||||
if(tw->walk.moveDuration <= 0.0f) entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
tw->turn.timer -= TIME.delta;
|
||||
if(tw->turn.timer <= 0.0f) {
|
||||
tw->turn.timer = randomFloat(tw->turn.frequencyMin, tw->turn.frequencyMax);
|
||||
if(entityCanTurn(entity)) entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
entityTurn(entity, (entitydir_t)(rand() % 4));
|
||||
}
|
||||
|
||||
tw->walk.timer -= TIME.delta;
|
||||
if(tw->walk.timer <= 0.0f) {
|
||||
tw->walk.timer = randomFloat(tw->walk.frequencyMin, tw->walk.frequencyMax);
|
||||
if(entityCanWalk(entity)) entityWalk(entity, (entitydir_t)(rand() % 4));
|
||||
tw->walk.direction = (entitydir_t)(rand() % 4);
|
||||
tw->walk.moveDuration = NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,25 @@
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "npcturn.h"
|
||||
#include "rpg/entity/entitydir.h"
|
||||
|
||||
/** Default min/max seconds between NPC random-walk ticks. */
|
||||
#define NPC_RANDOM_WALK_FREQUENCY_MIN_DEFAULT 2.0f
|
||||
#define NPC_RANDOM_WALK_FREQUENCY_MAX_DEFAULT 5.0f
|
||||
|
||||
/** How long a random-walk move lasts once triggered, roughly one tile's
|
||||
* worth of travel at walking speed. */
|
||||
#define NPC_RANDOM_WALK_MOVE_DURATION_DEFAULT 0.2f
|
||||
|
||||
typedef struct {
|
||||
float_t frequencyMin;
|
||||
float_t frequencyMax;
|
||||
float_t timer;
|
||||
|
||||
// In-progress move state - while moveDuration > 0, movement keeps
|
||||
// walking in direction every tick.
|
||||
float_t moveDuration;
|
||||
entitydir_t direction;
|
||||
} npcrandomwalk_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -25,7 +25,10 @@ bool_t playerCanInteract(entity_t *entity) {
|
||||
|
||||
void playerInput(entity_t *entity) {
|
||||
assertNotNull(entity, "Entity pointer cannot be NULL");
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) return;
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_PLAYER) {
|
||||
entityStop(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle game menu on pause
|
||||
if(uiGameMenuIsOpen() && inputPressed(INPUT_ACTION_PAUSE)) {
|
||||
@@ -38,63 +41,27 @@ void playerInput(entity_t *entity) {
|
||||
}
|
||||
|
||||
// Can player act?
|
||||
if(UI_FOCUS.count > 0) return;
|
||||
|
||||
// Determine direction player may be trying to face/go.
|
||||
const playerinputdirmap_t *dirMap;
|
||||
|
||||
// First pass, we want to prefer a button relative to what the player is
|
||||
// already facing, e.g. if they hold DOWN and start moving down, then tap
|
||||
// right we want to prefer down over right.
|
||||
|
||||
// Determine current dirmap based on facing direction.
|
||||
dirMap = PLAYER_INPUT_DIR_MAP;
|
||||
do {
|
||||
if(dirMap->direction != entity->direction) continue;
|
||||
break;
|
||||
} while((++dirMap)->action != 0xFF);
|
||||
|
||||
// Are we holding that button?
|
||||
if(!inputIsDown(dirMap->action)) {
|
||||
// No, so we need to check all directions.
|
||||
dirMap = PLAYER_INPUT_DIR_MAP;
|
||||
do {
|
||||
if(!inputIsDown(dirMap->action)) continue;
|
||||
break;
|
||||
} while((++dirMap)->action != 0xFF);
|
||||
}
|
||||
|
||||
// Trying to direct?
|
||||
if(dirMap->action != 0xFF) {
|
||||
// Is the player attempting to turn? That is, is the player idle and has the
|
||||
// turn lockout passed?
|
||||
if(entityCanTurn(entity)) {
|
||||
if(entity->direction != dirMap->direction) {
|
||||
entityTurn(entity, dirMap->direction);
|
||||
if(UI_FOCUS.count > 0) {
|
||||
entityStop(entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool_t running = inputIsDown(INPUT_ACTION_CANCEL);
|
||||
if(running && entityCanRun(entity)) {
|
||||
entityRun(entity, dirMap->direction);
|
||||
} else if(entityCanWalk(entity)) {
|
||||
entityWalk(entity, dirMap->direction);
|
||||
}
|
||||
}
|
||||
// Analog/free-angle movement vector from the 4 directional actions -
|
||||
// already normalized so diagonals aren't faster than cardinals, and
|
||||
// preserves real analog magnitude on platforms that bind an actual
|
||||
// gamepad stick to these actions.
|
||||
vec2 moveDir;
|
||||
inputAngle2D(
|
||||
INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT,
|
||||
INPUT_ACTION_DOWN, INPUT_ACTION_UP,
|
||||
moveDir
|
||||
);
|
||||
|
||||
entityMove(entity, moveDir, inputIsDown(INPUT_ACTION_CANCEL));
|
||||
|
||||
// Interaction
|
||||
if(inputPressed(INPUT_ACTION_ACCEPT) && playerCanInteract(entity)) {
|
||||
worldunit_t x, y, z;
|
||||
{
|
||||
worldunits_t relX, relY;
|
||||
entityDirGetRelative(entity->direction, &relX, &relY);
|
||||
x = entity->position.x + relX;
|
||||
y = entity->position.y + relY;
|
||||
z = entity->position.z;
|
||||
}
|
||||
|
||||
entity_t *target = entityGetAt((worldpos_t){ x, y, z });
|
||||
entity_t *target = entityGetFacing(entity, ENTITY_INTERACT_RANGE);
|
||||
if(target == NULL) return;
|
||||
entityInteractWith(entity, target);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,6 @@ typedef struct {
|
||||
void *nothing;
|
||||
} player_t;
|
||||
|
||||
typedef struct {
|
||||
inputaction_t action;
|
||||
entitydir_t direction;
|
||||
} playerinputdirmap_t;
|
||||
|
||||
static const playerinputdirmap_t PLAYER_INPUT_DIR_MAP[] = {
|
||||
{ INPUT_ACTION_UP, ENTITY_DIR_NORTH },
|
||||
{ INPUT_ACTION_DOWN, ENTITY_DIR_SOUTH },
|
||||
{ INPUT_ACTION_LEFT, ENTITY_DIR_WEST },
|
||||
{ INPUT_ACTION_RIGHT, ENTITY_DIR_EAST },
|
||||
|
||||
{ 0xFF, 0xFF }
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a player entity.
|
||||
*
|
||||
|
||||
@@ -168,6 +168,14 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Placeholder-fill with flat ground while the real data streams in
|
||||
// asynchronously - entities standing on this chunk (subject to gravity)
|
||||
// would otherwise fall through the zeroed/empty tiles until
|
||||
// mapChunkLoaded() replaces them with the real data.
|
||||
for(uint32_t i = 0; i < CHUNK_TILE_COUNT; i++) {
|
||||
chunk->tiles[i] = (tile_t){ .shape = TILE_SHAPE_GROUND };
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
MAP.loadQueueCount < MAP_CHUNK_COUNT,
|
||||
"Chunk load queue overflow"
|
||||
|
||||
@@ -38,6 +38,7 @@ void mapAreaInit(
|
||||
area->triggerCount = 0;
|
||||
|
||||
memorySet(area->entities, 0, sizeof(area->entities));
|
||||
memorySet(area->lastStepPosition, 0, sizeof(area->lastStepPosition));
|
||||
}
|
||||
|
||||
bool_t mapAreaIsInside(const maparea_t *area, const worldpos_t position) {
|
||||
@@ -132,9 +133,13 @@ void mapAreaCheckEntity(entity_t *entity) {
|
||||
|
||||
if(isInside && !wasInside) {
|
||||
area->entities[entity->id] = 1;
|
||||
area->lastStepPosition[entity->id] = entity->position;
|
||||
trigger = MAP_TRIGGER_ENTER;
|
||||
} else if(isInside && wasInside) {
|
||||
if(!worldPosIsEqual(entity->position, area->lastStepPosition[entity->id])) {
|
||||
area->lastStepPosition[entity->id] = entity->position;
|
||||
trigger = MAP_TRIGGER_STEP;
|
||||
}
|
||||
} else if(!isInside && wasInside) {
|
||||
area->entities[entity->id] = 0;
|
||||
trigger = MAP_TRIGGER_EXIT;
|
||||
|
||||
@@ -18,6 +18,8 @@ typedef struct maparea_s maparea_t;
|
||||
#define MAP_AREA_NOTIFY_NPC (1 << 1)
|
||||
#define MAP_AREA_NOTIFY_ALL (MAP_AREA_NOTIFY_PLAYER | MAP_AREA_NOTIFY_NPC)
|
||||
|
||||
// Fires once per newly-entered tile while an entity remains inside the
|
||||
// area (not once per frame) - see mapAreaCheckEntity.
|
||||
#define MAP_TRIGGER_STEP (1 << 0)
|
||||
#define MAP_TRIGGER_ENTER (1 << 1)
|
||||
#define MAP_TRIGGER_EXIT (1 << 2)
|
||||
@@ -39,6 +41,11 @@ typedef struct maparea_s {
|
||||
uint8_t trigger;
|
||||
uint8_t entities[ENTITY_COUNT];
|
||||
|
||||
// The floored tile position ENTER or STEP last fired at, per entity.
|
||||
// Used to fire STEP only once per newly-entered tile rather than
|
||||
// every frame the entity remains inside the area.
|
||||
worldpos_t lastStepPosition[ENTITY_COUNT];
|
||||
|
||||
// Incremented every time this area's callback is invoked. Lets other
|
||||
// systems (e.g. cutscenes) detect "has this area fired since I last
|
||||
// checked" without needing to be the callback themselves.
|
||||
@@ -143,8 +150,10 @@ void mapAreaRemove(const uint8_t id);
|
||||
* Checks every active map area against an entity's current position,
|
||||
* for areas whose notify flags include the entity's type. Invokes the
|
||||
* area's callback with MAP_TRIGGER_ENTER the frame the entity first
|
||||
* becomes inside, MAP_TRIGGER_STEP on every subsequent frame it remains
|
||||
* inside, and MAP_TRIGGER_EXIT the frame it leaves.
|
||||
* becomes inside, MAP_TRIGGER_STEP the frame it moves into a different
|
||||
* tile while remaining inside, and MAP_TRIGGER_EXIT the frame it leaves.
|
||||
* Safe (and expected) to call every frame - trigger conditions are
|
||||
* edge-detected against the entity's tile position, not the frame rate.
|
||||
*
|
||||
* @param entity Pointer to the entity to check.
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,28 @@
|
||||
*/
|
||||
|
||||
#include "tileshape.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
// Per-shape corner heights as [sw, se, ne, nw] offsets (0 or 1) from the
|
||||
// tile's base Z layer - transcribed verbatim from the mesh generator's
|
||||
// ramp tables (tools/asset/chunk/__main__.py _RAMP_CORNERS,
|
||||
// editor/client/public/common/chunkterrain.js RAMP_CORNERS) so the
|
||||
// walkable surface always matches what's rendered.
|
||||
static const float_t TILE_SHAPE_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = {
|
||||
[TILE_SHAPE_GROUND] = { 0.0f, 0.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_NORTH] = { 0.0f, 0.0f, 1.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTH] = { 1.0f, 1.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_EAST] = { 0.0f, 1.0f, 1.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_WEST] = { 1.0f, 0.0f, 0.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHEAST] = { 0.0f, 0.0f, 1.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHWEST] = { 0.0f, 0.0f, 0.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST] = { 0.0f, 1.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST] = { 1.0f, 0.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHEAST_INNER] = { 0.0f, 1.0f, 1.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHWEST_INNER] = { 1.0f, 0.0f, 1.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST_INNER] = { 1.0f, 1.0f, 1.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST_INNER] = { 1.0f, 1.0f, 0.0f, 1.0f },
|
||||
};
|
||||
|
||||
bool_t tileShapeIsWalkable(const tileshape_t shape) {
|
||||
switch(shape) {
|
||||
@@ -37,3 +59,23 @@ bool_t tileShapeIsRamp(const tileshape_t shape) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float_t tileShapeGetRampHeight(
|
||||
const tileshape_t shape, const float_t localX, const float_t localY
|
||||
) {
|
||||
assertTrue(localX >= 0.0f && localX <= 1.0f, "localX must be in [0,1]");
|
||||
assertTrue(localY >= 0.0f && localY <= 1.0f, "localY must be in [0,1]");
|
||||
|
||||
const float_t sw = TILE_SHAPE_RAMP_CORNERS[shape][0];
|
||||
const float_t se = TILE_SHAPE_RAMP_CORNERS[shape][1];
|
||||
const float_t ne = TILE_SHAPE_RAMP_CORNERS[shape][2];
|
||||
const float_t nw = TILE_SHAPE_RAMP_CORNERS[shape][3];
|
||||
|
||||
// Planar interpolation per triangle, split along the SW-NE diagonal -
|
||||
// matches the two-triangle quad the mesh generator emits exactly,
|
||||
// rather than a smooth (but unrendered) bilinear blend.
|
||||
if(localY <= localX) {
|
||||
return sw + (se - sw) * localX + (ne - se) * localY;
|
||||
}
|
||||
return sw + (ne - nw) * localX + (nw - sw) * localY;
|
||||
}
|
||||
@@ -43,3 +43,20 @@ bool_t tileShapeIsRamp(const tileshape_t shape);
|
||||
* @return bool_t True if walkable, false if not.
|
||||
*/
|
||||
bool_t tileShapeIsWalkable(const tileshape_t shape);
|
||||
|
||||
/**
|
||||
* Returns the floor height offset (0 to 1, above the tile's base Z
|
||||
* layer) for the given tile shape at a local position within the tile.
|
||||
* Flat ground is always 0. Ramp shapes vary linearly across the tile,
|
||||
* matching the two-triangle mesh generated for rendering (split along
|
||||
* the SW-NE diagonal) exactly, so the walkable surface and the
|
||||
* rendered surface never disagree.
|
||||
*
|
||||
* @param shape The tile shape to query.
|
||||
* @param localX Local X position within the tile, in [0, 1].
|
||||
* @param localY Local Y position within the tile, in [0, 1].
|
||||
* @return float_t The height offset above the tile's base Z layer.
|
||||
*/
|
||||
float_t tileShapeGetRampHeight(
|
||||
const tileshape_t shape, const float_t localX, const float_t localY
|
||||
);
|
||||
+2
-1
@@ -5,5 +5,6 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenestartbattle.c
|
||||
physicsbody.c
|
||||
physicsworld.c
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "physicsbody.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void physicsBodyInit(
|
||||
physicsbody_t *body, const vec3 position, const vec3 extents
|
||||
) {
|
||||
assertNotNull(body, "body must not be null");
|
||||
assertNotNull(position, "position must not be null");
|
||||
assertNotNull(extents, "extents must not be null");
|
||||
assertTrue(extents[0] > 0.0f, "extents.x must be greater than 0");
|
||||
assertTrue(extents[1] > 0.0f, "extents.y must be greater than 0");
|
||||
assertTrue(extents[2] > 0.0f, "extents.z must be greater than 0");
|
||||
|
||||
glm_vec3_copy((float_t *)position, body->position);
|
||||
glm_vec3_copy((float_t *)extents, body->extents);
|
||||
glm_vec3_zero(body->velocity);
|
||||
body->grounded = false;
|
||||
}
|
||||
|
||||
void physicsBodyGetBounds(
|
||||
const physicsbody_t *body, vec3 outMin, vec3 outMax
|
||||
) {
|
||||
assertNotNull(body, "body must not be null");
|
||||
assertNotNull(outMin, "outMin must not be null");
|
||||
assertNotNull(outMax, "outMax must not be null");
|
||||
|
||||
glm_vec3_copy((float_t *)body->position, outMin);
|
||||
glm_vec3_add((float_t *)body->position, (float_t *)body->extents, outMax);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "util/math.h"
|
||||
|
||||
typedef struct physicsbody_s {
|
||||
// Base/foot position, in raw grid units (same convention as worldpos_t -
|
||||
// 1.0 equals one tile in X/Y, one Z-layer in Z). Not pre-scaled by
|
||||
// WORLD_LAYER_HEIGHT, which is a cosmetic render-space value only.
|
||||
vec3 position;
|
||||
|
||||
// Velocity, in grid units per second, per axis.
|
||||
vec3 velocity;
|
||||
|
||||
// Full box size, in grid units. The box is anchored at position on all
|
||||
// three axes and extends in the positive direction - i.e. it occupies
|
||||
// [position, position + extents) - matching the tile grid's own
|
||||
// convention that tile n occupies [n, n + 1).
|
||||
vec3 extents;
|
||||
|
||||
// True if the last physicsWorldStep clamped a downward Z velocity
|
||||
// against a walkable tile beneath the body.
|
||||
bool_t grounded;
|
||||
} physicsbody_t;
|
||||
|
||||
/**
|
||||
* Initializes a physics body at the given position with the given extents.
|
||||
* Velocity is zeroed and grounded is set to false.
|
||||
*
|
||||
* @param body Pointer to the physics body to initialize.
|
||||
* @param position The initial position of the body.
|
||||
* @param extents The size of the body's collision box.
|
||||
*/
|
||||
void physicsBodyInit(
|
||||
physicsbody_t *body, const vec3 position, const vec3 extents
|
||||
);
|
||||
|
||||
/**
|
||||
* Computes the world-space min/max bounds of a physics body's collision
|
||||
* box, from its position and extents. The box is anchored at position
|
||||
* and extends in the positive direction on every axis, so outMin equals
|
||||
* position and outMax equals position + extents.
|
||||
*
|
||||
* @param body Pointer to the physics body.
|
||||
* @param outMin Output, set to the box's minimum corner.
|
||||
* @param outMax Output, set to the box's maximum corner.
|
||||
*/
|
||||
void physicsBodyGetBounds(
|
||||
const physicsbody_t *body, vec3 outMin, vec3 outMax
|
||||
);
|
||||
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "physicsworld.h"
|
||||
#include "assert/assert.h"
|
||||
#include "rpg/overworld/map.h"
|
||||
#include "rpg/overworld/tile.h"
|
||||
#include "rpg/overworld/tileshape.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
|
||||
// Tolerance used when converting a float boundary coordinate into a tile
|
||||
// column/layer index, so that a coordinate sitting exactly on a tile
|
||||
// boundary is treated as belonging to the tile it is entering/leaving,
|
||||
// not the neighbour on the far side of the boundary.
|
||||
#define PHYSICS_EPSILON 0.0001f
|
||||
|
||||
void physicsWorldInit(
|
||||
physicsworld_t *world,
|
||||
const float_t gravity,
|
||||
const float_t terminalVelocity
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertTrue(terminalVelocity > 0.0f, "terminalVelocity must be positive");
|
||||
|
||||
world->gravity = gravity;
|
||||
world->terminalVelocity = terminalVelocity;
|
||||
}
|
||||
|
||||
void physicsWorldStep(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
body->velocity[2] -= world->gravity * dt;
|
||||
body->velocity[2] = mathClamp(
|
||||
body->velocity[2], -world->terminalVelocity, world->terminalVelocity
|
||||
);
|
||||
|
||||
physicsWorldResolveAxisX(world, body, dt, others, othersCount);
|
||||
physicsWorldResolveAxisY(world, body, dt, others, othersCount);
|
||||
physicsWorldResolveAxisZ(world, body, dt, others, othersCount);
|
||||
}
|
||||
|
||||
void physicsWorldResolveAxisX(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
const float_t vx = body->velocity[0];
|
||||
if(vx == 0.0f) return;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
const worldunit_t yStart = (worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const worldunit_t yEnd = (worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
const worldunit_t layerZ =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
|
||||
if(vx > 0.0f) {
|
||||
const worldunit_t oldColMax =
|
||||
(worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const float_t newMax = max[0] + vx * dt;
|
||||
const worldunit_t newColMax =
|
||||
(worldunit_t)floorf(newMax - PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t col = oldColMax + 1; col <= newColMax; col++) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t y = yStart; y <= yEnd; y++) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
col, y, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[0] = (float_t)col - body->extents[0];
|
||||
body->velocity[0] = 0.0f;
|
||||
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[0] += vx * dt;
|
||||
} else {
|
||||
const worldunit_t oldColMin =
|
||||
(worldunit_t)floorf(min[0] + PHYSICS_EPSILON);
|
||||
const float_t newMin = min[0] + vx * dt;
|
||||
const worldunit_t newColMin =
|
||||
(worldunit_t)floorf(newMin + PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t col = oldColMin - 1; col >= newColMin; col--) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t y = yStart; y <= yEnd; y++) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
col, y, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[0] = (float_t)(col + 1);
|
||||
body->velocity[0] = 0.0f;
|
||||
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[0] += vx * dt;
|
||||
}
|
||||
|
||||
physicsWorldResolveBodyOverlap(body, 0, others, othersCount);
|
||||
}
|
||||
|
||||
void physicsWorldResolveAxisY(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
const float_t vy = body->velocity[1];
|
||||
if(vy == 0.0f) return;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
const worldunit_t xStart = (worldunit_t)floorf(min[0] + PHYSICS_EPSILON);
|
||||
const worldunit_t xEnd = (worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const worldunit_t layerZ =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
|
||||
if(vy > 0.0f) {
|
||||
const worldunit_t oldRowMax =
|
||||
(worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
const float_t newMax = max[1] + vy * dt;
|
||||
const worldunit_t newRowMax =
|
||||
(worldunit_t)floorf(newMax - PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t row = oldRowMax + 1; row <= newRowMax; row++) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t x = xStart; x <= xEnd; x++) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
x, row, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[1] = (float_t)row - body->extents[1];
|
||||
body->velocity[1] = 0.0f;
|
||||
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[1] += vy * dt;
|
||||
} else {
|
||||
const worldunit_t oldRowMin =
|
||||
(worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const float_t newMin = min[1] + vy * dt;
|
||||
const worldunit_t newRowMin =
|
||||
(worldunit_t)floorf(newMin + PHYSICS_EPSILON);
|
||||
|
||||
for(worldunit_t row = oldRowMin - 1; row >= newRowMin; row--) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t x = xStart; x <= xEnd; x++) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
x, row, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(blocked) {
|
||||
body->position[1] = (float_t)(row + 1);
|
||||
body->velocity[1] = 0.0f;
|
||||
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
body->position[1] += vy * dt;
|
||||
}
|
||||
|
||||
physicsWorldResolveBodyOverlap(body, 1, others, othersCount);
|
||||
}
|
||||
|
||||
void physicsWorldResolveAxisZ(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
) {
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
const worldunit_t xStart = (worldunit_t)floorf(min[0] + PHYSICS_EPSILON);
|
||||
const worldunit_t xEnd = (worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const worldunit_t yStart = (worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const worldunit_t yEnd = (worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
const worldunit_t layer =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
const float_t vz = body->velocity[2];
|
||||
|
||||
// Whether a column not directly at the current layer should still
|
||||
// count as walkable if its own tile sits one layer above or below -
|
||||
// each column only ever has one real tile, so this is what lets a
|
||||
// column just crossed into horizontally (see the +/-1 layer check in
|
||||
// ResolveAxisX/Y) be recognised here too, whether its tile sits one
|
||||
// layer above (finishing a climb) or below (starting a descent).
|
||||
// Gating on horizontal movement (rather than vz's sign) keeps this
|
||||
// from ever mistaking a genuine ceiling directly above a stationary
|
||||
// body - e.g. the frame right after being blocked by one, when vz has
|
||||
// just been zeroed - for ground to snap up onto.
|
||||
const bool_t movingHorizontally =
|
||||
body->velocity[0] != 0.0f || body->velocity[1] != 0.0f;
|
||||
|
||||
// Walkability across the whole footprint - every spanned column must
|
||||
// have a walkable tile (at the current layer, or the +/-1 fallback
|
||||
// above) for this layer to count as solid, same as before ramps.
|
||||
bool_t solid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && solid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && solid; y++) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t shape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
x, y, layer, movingHorizontally, &unusedBase, &shape
|
||||
)
|
||||
) {
|
||||
solid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ground/ramp height comes from a single reference column - the
|
||||
// footprint's center, not the tallest of every spanned column. A 1-
|
||||
// wide body sitting at a non-integer position always straddles two
|
||||
// columns (e.g. [1.01, 2.01)); using the tallest of them would yank a
|
||||
// body barely touching a taller neighbouring column onto its full
|
||||
// height, rather than the column it's actually standing on.
|
||||
float_t groundHeight = (float_t)layer;
|
||||
if(solid) {
|
||||
const float_t centerX = body->position[0] + body->extents[0] * 0.5f;
|
||||
const float_t centerY = body->position[1] + body->extents[1] * 0.5f;
|
||||
const worldunit_t centerCol =
|
||||
(worldunit_t)floorf(centerX + PHYSICS_EPSILON);
|
||||
const worldunit_t centerRow =
|
||||
(worldunit_t)floorf(centerY + PHYSICS_EPSILON);
|
||||
|
||||
worldunit_t tileBase;
|
||||
tileshape_t shape;
|
||||
physicsWorldFindColumnTile(
|
||||
centerCol, centerRow, layer, movingHorizontally, &tileBase, &shape
|
||||
);
|
||||
|
||||
const float_t localX = mathClamp(centerX - centerCol, 0.0f, 1.0f);
|
||||
const float_t localY = mathClamp(centerY - centerRow, 0.0f, 1.0f);
|
||||
groundHeight =
|
||||
(float_t)tileBase + tileShapeGetRampHeight(shape, localX, localY);
|
||||
}
|
||||
|
||||
// Standing on, or embedded below, the ground/ramp surface - snap up to
|
||||
// it regardless of vertical velocity direction. Gravity only ever
|
||||
// pulls down, so walking onto a rising ramp needs this explicit lift
|
||||
// rather than just a fall-clamp.
|
||||
if(solid && body->position[2] < groundHeight) {
|
||||
body->position[2] = groundHeight;
|
||||
if(vz <= 0.0f) body->velocity[2] = 0.0f;
|
||||
body->grounded = true;
|
||||
physicsWorldResolveBodyOverlap(body, 2, others, othersCount);
|
||||
return;
|
||||
}
|
||||
|
||||
if(vz == 0.0f) return;
|
||||
|
||||
if(vz < 0.0f) {
|
||||
const float_t newZ = body->position[2] + vz * dt;
|
||||
if(solid && newZ < groundHeight) {
|
||||
body->position[2] = groundHeight;
|
||||
body->velocity[2] = 0.0f;
|
||||
body->grounded = true;
|
||||
} else {
|
||||
body->position[2] = newZ;
|
||||
body->grounded = false;
|
||||
}
|
||||
} else {
|
||||
const float_t head = max[2];
|
||||
const worldunit_t ceilLayer = (worldunit_t)ceilf(head - PHYSICS_EPSILON);
|
||||
|
||||
bool_t ceilingSolid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && ceilingSolid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && ceilingSolid; y++) {
|
||||
const worldpos_t pos = { x, y, ceilLayer };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) ceilingSolid = false;
|
||||
}
|
||||
}
|
||||
|
||||
const float_t newHead = head + vz * dt;
|
||||
if(ceilingSolid && newHead > (float_t)ceilLayer) {
|
||||
body->position[2] = (float_t)ceilLayer - body->extents[2];
|
||||
body->velocity[2] = 0.0f;
|
||||
} else {
|
||||
body->position[2] += vz * dt;
|
||||
}
|
||||
}
|
||||
|
||||
physicsWorldResolveBodyOverlap(body, 2, others, othersCount);
|
||||
}
|
||||
|
||||
bool_t physicsWorldFindColumnTile(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t layer,
|
||||
const bool_t movingHorizontally,
|
||||
worldunit_t *outBase,
|
||||
tileshape_t *outShape
|
||||
) {
|
||||
assertNotNull(outBase, "outBase must not be null");
|
||||
assertNotNull(outShape, "outShape must not be null");
|
||||
|
||||
tile_t tile = mapGetTile((worldpos_t){ x, y, layer });
|
||||
worldunit_t base = layer;
|
||||
|
||||
if(!tileShapeIsWalkable(tile.shape) && movingHorizontally) {
|
||||
const tile_t tileAbove =
|
||||
mapGetTile((worldpos_t){ x, y, (worldunit_t)(layer + 1) });
|
||||
if(tileShapeIsWalkable(tileAbove.shape)) {
|
||||
tile = tileAbove;
|
||||
base = (worldunit_t)(layer + 1);
|
||||
} else {
|
||||
const tile_t tileBelow =
|
||||
mapGetTile((worldpos_t){ x, y, (worldunit_t)(layer - 1) });
|
||||
if(tileShapeIsWalkable(tileBelow.shape)) {
|
||||
tile = tileBelow;
|
||||
base = (worldunit_t)(layer - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*outBase = base;
|
||||
*outShape = tile.shape;
|
||||
return tileShapeIsWalkable(tile.shape);
|
||||
}
|
||||
|
||||
void physicsWorldResolveBodyOverlap(
|
||||
physicsbody_t *body,
|
||||
const uint8_t axis,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
) {
|
||||
assertNotNull(body, "body must not be null");
|
||||
assertTrue(axis < 3, "axis must be 0, 1 or 2");
|
||||
|
||||
const float_t v = body->velocity[axis];
|
||||
if(v == 0.0f) return;
|
||||
|
||||
for(uint32_t i = 0; i < othersCount; i++) {
|
||||
physicsbody_t *other = others[i];
|
||||
if(other == NULL || other == body) continue;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
vec3 oMin, oMax;
|
||||
physicsBodyGetBounds(other, oMin, oMax);
|
||||
|
||||
if(min[0] >= oMax[0] || max[0] <= oMin[0]) continue;
|
||||
if(min[1] >= oMax[1] || max[1] <= oMin[1]) continue;
|
||||
if(min[2] >= oMax[2] || max[2] <= oMin[2]) continue;
|
||||
|
||||
if(v > 0.0f) {
|
||||
body->position[axis] -= max[axis] - oMin[axis];
|
||||
} else {
|
||||
body->position[axis] += oMax[axis] - min[axis];
|
||||
}
|
||||
body->velocity[axis] = 0.0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "physicsbody.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
#include "rpg/overworld/tileshape.h"
|
||||
|
||||
#define PHYSICS_WORLD_GRAVITY_DEFAULT 20.0f
|
||||
#define PHYSICS_WORLD_TERMINAL_VELOCITY_DEFAULT 40.0f
|
||||
|
||||
typedef struct physicsworld_s {
|
||||
// Downward acceleration applied to velocity.z every step, in grid units
|
||||
// per second squared.
|
||||
float_t gravity;
|
||||
|
||||
// Maximum magnitude velocity.z may reach while falling, in grid units
|
||||
// per second. A safety clamp bounding how far a single step can move,
|
||||
// to limit (not eliminate) the tunneling limitation documented on
|
||||
// physicsWorldStep.
|
||||
float_t terminalVelocity;
|
||||
} physicsworld_t;
|
||||
|
||||
/**
|
||||
* Initializes a physics world with the given gravity and terminal
|
||||
* velocity.
|
||||
*
|
||||
* @param world Pointer to the physics world to initialize.
|
||||
* @param gravity Downward acceleration applied every step.
|
||||
* @param terminalVelocity Maximum falling speed.
|
||||
*/
|
||||
void physicsWorldInit(
|
||||
physicsworld_t *world,
|
||||
const float_t gravity,
|
||||
const float_t terminalVelocity
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances a body by one timestep: applies gravity, integrates velocity
|
||||
* into position (semi-implicit Euler), and resolves collisions against
|
||||
* the tile map one axis at a time (X, then Y, then Z), clamping position
|
||||
* and zeroing velocity on any axis that hits a tile boundary. After each
|
||||
* axis's tile resolution, also resolves overlap against any given other
|
||||
* bodies (see physicsWorldResolveBodyOverlap) - so another body is
|
||||
* treated as solid too, not just the tile map.
|
||||
*
|
||||
* Ramp tiles are walkable at a continuously-varying height (see
|
||||
* tileShapeGetRampHeight) rather than a flat Z layer - walking across one
|
||||
* lifts or drops the body smoothly to match the tile's sloped surface,
|
||||
* snapping upward as needed since gravity alone only ever pulls down.
|
||||
*
|
||||
* Known limitations, deliberately out of scope for this very basic pass:
|
||||
* - Ceiling/head-bump checks are still flat and ramp-unaware - only the
|
||||
* walking surface accounts for slope, not the underside of a ramp
|
||||
* above the body.
|
||||
* - Descending a fast-dropping ramp relies on gravity to close the gap
|
||||
* each step rather than an explicit "snap down" - imperceptible at
|
||||
* normal walk/run speeds given the default gravity, but a real
|
||||
* asymmetry with the "snap up" case.
|
||||
* - A body whose footprint spans multiple tile columns (unused by any
|
||||
* entity today - all default to a 1x1 footprint) queries each
|
||||
* column's ramp height using its own clamped local coordinate
|
||||
* independently, rather than blending a single surface across tiles.
|
||||
* - Limited wall/hole distinction - horizontal movement checks the
|
||||
* body's current Z layer plus one layer above and below (each column
|
||||
* only ever stores a single tile at one Z layer, so this covers
|
||||
* stepping onto an adjacent ascending or descending ramp/ledge
|
||||
* whose own layer differs from the body's current one). A column
|
||||
* with nothing walkable within one layer either way is still treated
|
||||
* as a solid wall rather than a deeper edge to fall from.
|
||||
* - A single step can tunnel through an intervening solid Z layer if
|
||||
* velocity.z * dt exceeds one grid unit; terminalVelocity bounds this
|
||||
* but does not eliminate it for very small/thin floors.
|
||||
* - Body-vs-body resolution only reacts to the axis currently being
|
||||
* resolved - two bodies already overlapping on every axis, with no
|
||||
* velocity on any of them, are never proactively separated.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to step. Its position/velocity/grounded fields are
|
||||
* updated in place.
|
||||
* @param dt Timestep, in seconds (use DUSK_TIME_STEP for the fixed step).
|
||||
* @param others Array of pointers to other bodies to treat as solid.
|
||||
* Pass NULL (with othersCount 0) to resolve against the tile map only.
|
||||
* @param othersCount Number of entries in others.
|
||||
*/
|
||||
void physicsWorldStep(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the body's movement along the X axis for this step, clamping
|
||||
* position and zeroing velocity.x if a non-walkable column, or another
|
||||
* body, blocks the move. Declared publicly as an internal step helper,
|
||||
* not a stable public API on its own.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to resolve.
|
||||
* @param dt Timestep, in seconds.
|
||||
* @param others Array of pointers to other bodies to treat as solid.
|
||||
* @param othersCount Number of entries in others.
|
||||
*/
|
||||
void physicsWorldResolveAxisX(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the body's movement along the Y axis for this step. See
|
||||
* physicsWorldResolveAxisX.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to resolve.
|
||||
* @param dt Timestep, in seconds.
|
||||
* @param others Array of pointers to other bodies to treat as solid.
|
||||
* @param othersCount Number of entries in others.
|
||||
*/
|
||||
void physicsWorldResolveAxisY(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the body's movement along the Z axis for this step, clamping
|
||||
* against walkable tile planes above and below the body and updating
|
||||
* grounded. See physicsWorldResolveAxisX.
|
||||
*
|
||||
* @param world Physics world configuration.
|
||||
* @param body The body to resolve.
|
||||
* @param dt Timestep, in seconds.
|
||||
* @param others Array of pointers to other bodies to treat as solid.
|
||||
* @param othersCount Number of entries in others.
|
||||
*/
|
||||
void physicsWorldResolveAxisZ(
|
||||
const physicsworld_t *world,
|
||||
physicsbody_t *body,
|
||||
const float_t dt,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the body's overlap against a set of other bodies along a
|
||||
* single axis: if the body's bounds fully overlap (on all 3 axes)
|
||||
* another body's bounds, pushes the body back along the given axis by
|
||||
* the overlap depth on that axis, in the opposite direction of its
|
||||
* current velocity on that axis, and zeroes that velocity component.
|
||||
* Does nothing if the body has no velocity on that axis, since there is
|
||||
* then no direction to know which way to push it out.
|
||||
*
|
||||
* @param body The body to resolve.
|
||||
* @param axis The axis to resolve overlap on (0 = x, 1 = y, 2 = z).
|
||||
* @param others Array of pointers to other bodies to check against.
|
||||
* @param othersCount Number of entries in others.
|
||||
*/
|
||||
void physicsWorldResolveBodyOverlap(
|
||||
physicsbody_t *body,
|
||||
const uint8_t axis,
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the walkable tile for a single tile column (x, y) near the given
|
||||
* layer: checks the layer itself first, and - only if movingHorizontally
|
||||
* is true, so a stationary body never mistakes a genuine ceiling above
|
||||
* it for ground - falls back to one layer above then one layer below.
|
||||
* Each column only ever has one real tile, so this lets a column whose
|
||||
* own tile sits adjacent to the current layer (e.g. finishing a climb
|
||||
* onto, or starting a descent from, an adjacent ramp/ledge) be found.
|
||||
*
|
||||
* @param x Column X coordinate.
|
||||
* @param y Column Y coordinate.
|
||||
* @param layer The layer to check first.
|
||||
* @param movingHorizontally Whether to also try one layer above/below.
|
||||
* @param outBase Output, set to the layer the found tile actually sits
|
||||
* at (layer, layer + 1, or layer - 1).
|
||||
* @param outShape Output, set to the found tile's shape.
|
||||
* @return true if a walkable tile was found.
|
||||
*/
|
||||
bool_t physicsWorldFindColumnTile(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t layer,
|
||||
const bool_t movingHorizontally,
|
||||
worldunit_t *outBase,
|
||||
tileshape_t *outShape
|
||||
);
|
||||
+6
-2
@@ -14,7 +14,6 @@
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "rpg/cutscene/scene/testcutscene.h"
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "rpg/battle/party.h"
|
||||
#include "ui/rpg/textbox/uitextboxminilist.h"
|
||||
#include "time/time.h"
|
||||
#include "rpgcamera.h"
|
||||
@@ -33,8 +32,13 @@ errorret_t rpgInit(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
|
||||
physicsWorldInit(
|
||||
&ENTITY_PHYSICS_WORLD,
|
||||
PHYSICS_WORLD_GRAVITY_DEFAULT,
|
||||
PHYSICS_WORLD_TERMINAL_VELOCITY_DEFAULT
|
||||
);
|
||||
|
||||
backpackInit();
|
||||
partyInit();
|
||||
cutsceneSystemInit();
|
||||
|
||||
errorChain(mapInit());
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
storyflag.c
|
||||
)
|
||||
|
||||
# Story Flag Definitions
|
||||
dusk_run_python(
|
||||
dusk_story_defs
|
||||
tools.story
|
||||
--csv ${CMAKE_CURRENT_SOURCE_DIR}/storyflag.csv
|
||||
--output ${DUSK_GENERATED_HEADERS_DIR}/rpg/story/storyflagvalue.h
|
||||
)
|
||||
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_story_defs)
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "storyflag.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||
STORY_FLAG_VALUES[flag] = value;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
id,description,initial
|
||||
test,"Test flag for debugging purposes",1
|
||||
|
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/story/storyflagvalue.h"
|
||||
|
||||
/**
|
||||
* Gets the value of a story flag.
|
||||
*
|
||||
* @param flag The story flag to get.
|
||||
* @return The value of the story flag.
|
||||
*/
|
||||
#define storyFlagGet(flag) (STORY_FLAG_VALUES[(flag)])
|
||||
|
||||
/**
|
||||
* Sets the value of a story flag.
|
||||
*
|
||||
* @param flag The story flag to set.
|
||||
* @param value The value to set the story flag to.
|
||||
*/
|
||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
|
||||
@@ -1,11 +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 uint8_t storyflagvalue_t;
|
||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
save.c
|
||||
savestream.c
|
||||
savesettings.c
|
||||
)
|
||||
|
||||
+10
-10
@@ -29,8 +29,11 @@ errorret_t saveDispose(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveLoad(const uint8_t slot) {
|
||||
errorret_t saveLoad(const uint8_t slot, yyjson_doc **outDoc) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
assertNotNull(outDoc, "outDoc must not be NULL");
|
||||
|
||||
*outDoc = NULL;
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
file->exists = false;
|
||||
@@ -44,7 +47,8 @@ errorret_t saveLoad(const uint8_t slot) {
|
||||
|
||||
if(!stream.found) errorOk();
|
||||
|
||||
errorret_t ret = saveFileLoad(&stream, file);
|
||||
yyjson_doc *doc = NULL;
|
||||
errorret_t ret = saveFileLoad(&stream, file, &doc);
|
||||
|
||||
#ifdef saveStreamClosePlatform
|
||||
saveStreamClosePlatform(&stream);
|
||||
@@ -52,14 +56,14 @@ errorret_t saveLoad(const uint8_t slot) {
|
||||
|
||||
if(errorIsNotOk(ret)) return ret;
|
||||
|
||||
errorChain(saveStreamVerifyChecksumImpl(&stream, slot));
|
||||
|
||||
*outDoc = doc;
|
||||
file->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveWrite(const uint8_t slot) {
|
||||
errorret_t saveWrite(const uint8_t slot, yyjson_mut_doc *doc) {
|
||||
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
|
||||
assertNotNull(doc, "doc must not be NULL");
|
||||
|
||||
savefile_t *file = &SAVE.files[slot];
|
||||
|
||||
@@ -70,11 +74,7 @@ errorret_t saveWrite(const uint8_t slot) {
|
||||
errorChain(saveStreamOpenWritePlatform(&stream, slot));
|
||||
#endif
|
||||
|
||||
errorret_t ret = saveFileWrite(&stream, file);
|
||||
|
||||
if(errorIsOk(ret)) {
|
||||
ret = saveStreamFinalizeWriteImpl(&stream);
|
||||
}
|
||||
errorret_t ret = saveFileWrite(&stream, file, doc);
|
||||
|
||||
#ifdef saveStreamClosePlatform
|
||||
saveStreamClosePlatform(&stream);
|
||||
|
||||
+20
-8
@@ -9,9 +9,10 @@
|
||||
#include "error/error.h"
|
||||
#include "savefile.h"
|
||||
#include "save/saveplatform.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef struct {
|
||||
/** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */
|
||||
/** Per-slot save file metadata; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */
|
||||
savefile_t files[SAVE_FILE_COUNT_MAX];
|
||||
/** Platform-specific save system state (paths, card handles, etc.). */
|
||||
saveplatform_t platform;
|
||||
@@ -34,20 +35,30 @@ errorret_t saveInit(void);
|
||||
errorret_t saveDispose(void);
|
||||
|
||||
/**
|
||||
* Loads the save file for a given slot from persistent storage.
|
||||
* Loads the save file for a given slot from persistent storage and parses
|
||||
* its JSON payload. The payload is read and parsed fresh on every call and
|
||||
* is not retained anywhere else - the caller owns the returned document
|
||||
* and must release it with yyjson_doc_free() once done reading from it.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the load fails.
|
||||
* @param outDoc Receives the parsed JSON document, or NULL if the slot has
|
||||
* no save file on disk.
|
||||
* @return An error code if the load or checksum validation fails.
|
||||
*/
|
||||
errorret_t saveLoad(const uint8_t slot);
|
||||
errorret_t saveLoad(const uint8_t slot, yyjson_doc **outDoc);
|
||||
|
||||
/**
|
||||
* Writes the save file for a given slot to persistent storage.
|
||||
* Serializes the given JSON document and writes it to persistent storage
|
||||
* for a given slot, alongside its byte size and CRC32 checksum. The
|
||||
* document is serialized to bytes fresh on every call and is not retained
|
||||
* anywhere else - the caller keeps ownership of doc and must dispose of it
|
||||
* with yyjson_mut_doc_free() once done.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return An error code if the write fails.
|
||||
* @param doc The JSON document to serialize and write.
|
||||
* @return An error code if serialization or the write fails.
|
||||
*/
|
||||
errorret_t saveWrite(const uint8_t slot);
|
||||
errorret_t saveWrite(const uint8_t slot, yyjson_mut_doc *doc);
|
||||
|
||||
/**
|
||||
* Deletes the save file for a given slot from persistent storage.
|
||||
@@ -66,7 +77,8 @@ errorret_t saveDelete(const uint8_t slot);
|
||||
bool_t saveExists(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Gets a pointer to the save file data for a given slot.
|
||||
* Gets a pointer to the save file metadata for a given slot. This does
|
||||
* not include the JSON payload itself - see saveLoad().
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
|
||||
* @return A pointer to the savefile_t for the given slot.
|
||||
|
||||
@@ -20,11 +20,21 @@
|
||||
/** Maximum number of independent save slots supported. */
|
||||
#define SAVE_FILE_COUNT_MAX 3
|
||||
|
||||
/**
|
||||
* Slot used to persist game settings. Shares the save slot range with
|
||||
* game saves for now, until settings get a dedicated file of their own.
|
||||
*/
|
||||
#define SAVE_SETTINGS_SLOT 0
|
||||
|
||||
typedef struct {
|
||||
/** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */
|
||||
char_t header[SAVE_FILE_HEADER_SIZE];
|
||||
/** Format version read from the file; used to branch on older layouts. */
|
||||
uint32_t version;
|
||||
/** Byte length of the serialized JSON payload. */
|
||||
uint32_t size;
|
||||
/** CRC32 checksum of the serialized JSON payload, for validation. */
|
||||
uint32_t checksum;
|
||||
/** Runtime flag - true if this slot was successfully loaded or written. */
|
||||
bool_t exists;
|
||||
} savefile_t;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savesettings.h"
|
||||
#include "save/save.h"
|
||||
#include "input/input.h"
|
||||
|
||||
errorret_t saveSettingsWrite(void) {
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *root = yyjson_mut_obj(doc);
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
|
||||
yyjson_mut_val *settings = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, root, "settings", settings);
|
||||
|
||||
yyjson_mut_val *input = yyjson_mut_obj(doc);
|
||||
yyjson_mut_obj_add_val(doc, settings, "input", input);
|
||||
yyjson_mut_obj_add_real(doc, input, "deadzone", INPUT.deadzone);
|
||||
|
||||
errorret_t ret = saveWrite(SAVE_SETTINGS_SLOT, doc);
|
||||
yyjson_mut_doc_free(doc);
|
||||
|
||||
if(errorIsNotOk(ret)) return ret;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveSettingsLoad(void) {
|
||||
yyjson_doc *doc = NULL;
|
||||
errorChain(saveLoad(SAVE_SETTINGS_SLOT, &doc));
|
||||
|
||||
if(!doc) errorOk();
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
yyjson_val *settings = yyjson_obj_get(root, "settings");
|
||||
yyjson_val *input = yyjson_obj_get(settings, "input");
|
||||
yyjson_val *deadzone = yyjson_obj_get(input, "deadzone");
|
||||
|
||||
if(deadzone) INPUT.deadzone = (float_t)yyjson_get_num(deadzone);
|
||||
|
||||
yyjson_doc_free(doc);
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
/**
|
||||
* Gathers all game settings (currently just the input gamepad deadzone)
|
||||
* from their owning runtime state and persists them to the save system
|
||||
* as JSON, under settings.<domain>. Nothing is retained after this call
|
||||
* returns - the JSON is built and written fresh every time.
|
||||
*
|
||||
* @return An error code if serialization or the write fails.
|
||||
*/
|
||||
errorret_t saveSettingsWrite(void);
|
||||
|
||||
/**
|
||||
* Loads all game settings from the save system's JSON payload and
|
||||
* applies them directly to their owning runtime state (e.g.
|
||||
* INPUT.deadzone). Leaves runtime state unchanged if no settings file
|
||||
* exists yet. Nothing is retained after this call returns.
|
||||
*
|
||||
* @return An error code if the load or parse fails.
|
||||
*/
|
||||
errorret_t saveSettingsLoad(void);
|
||||
+75
-251
@@ -8,10 +8,10 @@
|
||||
#include "save/savestream.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/endian.h"
|
||||
#include "util/string.h"
|
||||
#include "util/memory.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
errorret_t saveStreamReadBytesRawImpl(
|
||||
errorret_t saveStreamReadBytesImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
) {
|
||||
#ifdef saveStreamReadBytesPlatform
|
||||
@@ -20,7 +20,7 @@ errorret_t saveStreamReadBytesRawImpl(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesRawImpl(
|
||||
errorret_t saveStreamWriteBytesImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
) {
|
||||
#ifdef saveStreamWriteBytesPlatform
|
||||
@@ -29,51 +29,10 @@ errorret_t saveStreamWriteBytesRawImpl(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBytesImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
) {
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, buf, len));
|
||||
cryptCRC32Update(&stream->checksum, buf, len);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
) {
|
||||
cryptCRC32Update(&stream->checksum, buf, len);
|
||||
errorChain(saveStreamWriteBytesRawImpl(stream, buf, len));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) {
|
||||
uint32_t finalCRC = cryptCRC32End(stream->checksum);
|
||||
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
||||
|
||||
#ifdef saveStreamSeekPlatform
|
||||
errorChain(saveStreamSeekPlatform(stream, SAVE_FILE_HEADER_SIZE));
|
||||
#endif
|
||||
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, &leChecksum, sizeof(uint32_t)
|
||||
));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamVerifyChecksumImpl(
|
||||
savestream_t *stream, const uint8_t slot
|
||||
) {
|
||||
uint32_t computed = cryptCRC32End(stream->checksum);
|
||||
if(computed != stream->expectedChecksum) {
|
||||
errorThrow("Save slot %u has invalid checksum", (uint32_t)slot);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
errorret_t saveStreamReadHeaderImpl(
|
||||
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
|
||||
) {
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, header, SAVE_FILE_HEADER_SIZE));
|
||||
errorChain(saveStreamReadBytesImpl(stream, header, SAVE_FILE_HEADER_SIZE));
|
||||
|
||||
if(
|
||||
header[0] != SAVE_FILE_HEADER[0] ||
|
||||
@@ -83,25 +42,13 @@ errorret_t saveStreamReadHeaderImpl(
|
||||
errorThrow("Save file has invalid header");
|
||||
}
|
||||
|
||||
uint32_t leChecksum;
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, &leChecksum, sizeof(uint32_t)));
|
||||
stream->expectedChecksum = endianLittleToHost32(leChecksum);
|
||||
stream->checksum = cryptCRC32Begin();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteHeaderImpl(
|
||||
savestream_t *stream, const char_t header[SAVE_FILE_HEADER_SIZE]
|
||||
) {
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, header, SAVE_FILE_HEADER_SIZE
|
||||
));
|
||||
|
||||
uint32_t placeholder = 0;
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, &placeholder, sizeof(uint32_t)
|
||||
));
|
||||
stream->checksum = cryptCRC32Begin();
|
||||
errorChain(saveStreamWriteBytesImpl(stream, header, SAVE_FILE_HEADER_SIZE));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -120,100 +67,14 @@ errorret_t saveStreamWriteVersionImpl(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out) {
|
||||
uint8_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint8_t)));
|
||||
*out = (bool_t)(raw != 0);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input) {
|
||||
uint8_t raw = *input ? 1 : 0;
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(int8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input) {
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(int8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt8Impl(
|
||||
savestream_t *stream, const uint8_t *input
|
||||
) {
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out) {
|
||||
uint16_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
uint16_t host = endianLittleToHost16(raw);
|
||||
memoryCopy(out, &host, sizeof(int16_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt16Impl(
|
||||
savestream_t *stream, const int16_t *input
|
||||
) {
|
||||
uint16_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int16_t));
|
||||
raw = endianLittleToHost16(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out) {
|
||||
uint16_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
*out = endianLittleToHost16(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt16Impl(
|
||||
savestream_t *stream, const uint16_t *input
|
||||
) {
|
||||
uint16_t raw = endianLittleToHost16(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
uint32_t host = endianLittleToHost32(raw);
|
||||
memoryCopy(out, &host, sizeof(int32_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt32Impl(
|
||||
savestream_t *stream, const int32_t *input
|
||||
) {
|
||||
uint32_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int32_t));
|
||||
raw = endianLittleToHost32(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out) {
|
||||
errorret_t saveStreamReadSizeImpl(savestream_t *stream, uint32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
*out = endianLittleToHost32(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt32Impl(
|
||||
errorret_t saveStreamWriteSizeImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
) {
|
||||
uint32_t raw = endianLittleToHost32(*input);
|
||||
@@ -221,120 +82,83 @@ errorret_t saveStreamWriteUInt32Impl(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out) {
|
||||
uint64_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
uint64_t host = endianLittleToHost64(raw);
|
||||
memoryCopy(out, &host, sizeof(int64_t));
|
||||
errorret_t saveStreamReadChecksumImpl(savestream_t *stream, uint32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
*out = endianLittleToHost32(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt64Impl(
|
||||
savestream_t *stream, const int64_t *input
|
||||
errorret_t saveStreamWriteChecksumImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
) {
|
||||
uint64_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int64_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
uint32_t raw = endianLittleToHost32(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out) {
|
||||
uint64_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
*out = endianLittleToHost64(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt64Impl(
|
||||
savestream_t *stream, const uint64_t *input
|
||||
errorret_t saveFileWritePayloadImpl(
|
||||
savestream_t *stream,
|
||||
const savefile_t *file,
|
||||
const char_t *json,
|
||||
const size_t len
|
||||
) {
|
||||
uint64_t raw = endianLittleToHost64(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out) {
|
||||
float_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(float_t)));
|
||||
*out = endianLittleToHostFloat(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteFloatImpl(
|
||||
savestream_t *stream, const float_t *input
|
||||
) {
|
||||
float_t raw = endianLittleToHostFloat(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(float_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadStringImpl(
|
||||
savestream_t *stream, char_t *out, const size_t maxLen
|
||||
) {
|
||||
for(size_t i = 0; i < maxLen; i++) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, &out[i], sizeof(char_t)));
|
||||
if(out[i] == '\0') errorOk();
|
||||
}
|
||||
out[maxLen - 1] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteStringImpl(
|
||||
savestream_t *stream, const char_t *input, const size_t maxLen
|
||||
) {
|
||||
size_t len = strlen(input);
|
||||
if(len >= maxLen) len = maxLen - 1;
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, len + 1));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadDateImpl(savestream_t *stream, dusktimeepoch_t *out) {
|
||||
uint64_t raw;
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->time, &raw, sizeof(double_t));
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->timeZone, &raw, sizeof(double_t));
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->offsetTime, &raw, sizeof(double_t));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteDateImpl(
|
||||
savestream_t *stream, const dusktimeepoch_t *input
|
||||
) {
|
||||
uint64_t raw;
|
||||
|
||||
memoryCopy(&raw, &input->time, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
memoryCopy(&raw, &input->timeZone, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
memoryCopy(&raw, &input->offsetTime, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
|
||||
saveFileReadHeader(stream, file->header);
|
||||
saveFileReadVersion(stream, &file->version);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
|
||||
saveFileWriteHeader(stream, file->header);
|
||||
saveFileWriteVersion(stream, &file->version);
|
||||
saveFileWriteSize(stream, &file->size);
|
||||
saveFileWriteChecksum(stream, &file->checksum);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, json, len));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileLoad(
|
||||
savestream_t *stream, savefile_t *file, yyjson_doc **outDoc
|
||||
) {
|
||||
saveFileReadHeader(stream, file->header);
|
||||
saveFileReadVersion(stream, &file->version);
|
||||
saveFileReadSize(stream, &file->size);
|
||||
saveFileReadChecksum(stream, &file->checksum);
|
||||
|
||||
uint8_t *buf = memoryAllocate(file->size);
|
||||
errorret_t ret = saveStreamReadBytesImpl(stream, buf, file->size);
|
||||
|
||||
if(errorIsNotOk(ret)) {
|
||||
memoryFree(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if(cryptCRC32(buf, file->size) != file->checksum) {
|
||||
memoryFree(buf);
|
||||
errorThrow("Save file has invalid checksum");
|
||||
}
|
||||
|
||||
yyjson_doc *doc = yyjson_read((const char_t *)buf, file->size, 0);
|
||||
memoryFree(buf);
|
||||
|
||||
if(!doc) errorThrow("Failed to parse save file JSON");
|
||||
|
||||
*outDoc = doc;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveFileWrite(
|
||||
savestream_t *stream, savefile_t *file, yyjson_mut_doc *doc
|
||||
) {
|
||||
size_t len = 0;
|
||||
char_t *json = yyjson_mut_write(doc, 0, &len);
|
||||
if(!json) errorThrow("Failed to serialize save data to JSON");
|
||||
|
||||
memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE);
|
||||
file->version = SAVE_FILE_VERSION;
|
||||
file->size = (uint32_t)len;
|
||||
file->checksum = cryptCRC32(json, len);
|
||||
|
||||
errorret_t ret = saveFileWritePayloadImpl(stream, file, json, len);
|
||||
|
||||
// json comes from yyjson's own allocator (malloc), not the project
|
||||
// allocator, so it must be released with free() rather than memoryFree().
|
||||
free(json);
|
||||
|
||||
if(errorIsNotOk(ret)) return ret;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
+69
-341
@@ -9,41 +9,17 @@
|
||||
#include "error/error.h"
|
||||
#include "savefile.h"
|
||||
#include "save/saveplatform.h"
|
||||
#include "time/timeepoch.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef struct {
|
||||
/** True if the platform found and opened an existing save file. */
|
||||
bool_t found;
|
||||
uint32_t checksum;
|
||||
uint32_t expectedChecksum;
|
||||
/** Platform-specific stream handle (file handle, card slot, etc.). */
|
||||
saveplatformstream_t platform;
|
||||
} savestream_t;
|
||||
|
||||
/**
|
||||
* Reads bytes from the platform stream without updating the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesRawImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes bytes to the platform stream without updating the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesRawImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads bytes from the platform stream and accumulates them into the CRC.
|
||||
* Reads raw bytes from the platform stream.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Destination buffer.
|
||||
@@ -55,7 +31,7 @@ errorret_t saveStreamReadBytesImpl(
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the CRC then writes bytes to the platform stream.
|
||||
* Writes raw bytes to the platform stream.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Source buffer.
|
||||
@@ -67,29 +43,7 @@ errorret_t saveStreamWriteBytesImpl(
|
||||
);
|
||||
|
||||
/**
|
||||
* Finalizes a write stream: computes the final CRC32, seeks to the
|
||||
* checksum field in the header, and writes it in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @return An error if the seek or write fails.
|
||||
*/
|
||||
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream);
|
||||
|
||||
/**
|
||||
* Verifies that the CRC32 accumulated during loading matches the value
|
||||
* stored in the file header.
|
||||
*
|
||||
* @param stream Active read stream (loading must be complete).
|
||||
* @param slot Slot index used in the error message on mismatch.
|
||||
* @return An error if the checksum does not match.
|
||||
*/
|
||||
errorret_t saveStreamVerifyChecksumImpl(
|
||||
savestream_t *stream, const uint8_t slot
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads and validates the magic header, then reads the stored CRC32 and
|
||||
* resets the running accumulator.
|
||||
* Reads and validates the magic header from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to receive the header.
|
||||
@@ -100,8 +54,7 @@ errorret_t saveStreamReadHeaderImpl(
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes the magic header and a zero CRC32 placeholder, then resets the
|
||||
* running accumulator.
|
||||
* Writes the magic header to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to write.
|
||||
@@ -133,268 +86,94 @@ errorret_t saveStreamWriteVersionImpl(
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a single byte as a boolean (0 = false, non-zero = true).
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the boolean value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out);
|
||||
|
||||
/**
|
||||
* Writes a boolean as a single byte (true = 1, false = 0).
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input);
|
||||
|
||||
/**
|
||||
* Reads a signed 8-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 8-bit integer to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input);
|
||||
|
||||
/**
|
||||
* Reads an unsigned 8-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 8-bit integer to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt8Impl(savestream_t *stream, const uint8_t *input);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 16-bit integer from the stream.
|
||||
* Reads a little-endian uint32 JSON payload size field from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out);
|
||||
errorret_t saveStreamReadSizeImpl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 16-bit integer to the stream in little-endian order.
|
||||
* Writes a uint32 JSON payload size field to the stream in little-endian
|
||||
* order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt16Impl(
|
||||
savestream_t *stream, const int16_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 16-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 16-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt16Impl(
|
||||
savestream_t *stream, const uint16_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 32-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 32-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt32Impl(
|
||||
savestream_t *stream, const int32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 32-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 32-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt32Impl(
|
||||
errorret_t saveStreamWriteSizeImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 64-bit integer from the stream.
|
||||
* Reads a little-endian uint32 CRC32 checksum field from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out);
|
||||
errorret_t saveStreamReadChecksumImpl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 64-bit integer to the stream in little-endian order.
|
||||
* Writes a uint32 CRC32 checksum field to the stream in little-endian
|
||||
* order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt64Impl(
|
||||
savestream_t *stream, const int64_t *input
|
||||
errorret_t saveStreamWriteChecksumImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 64-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 64-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt64Impl(
|
||||
savestream_t *stream, const uint64_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian float from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out);
|
||||
|
||||
/**
|
||||
* Writes a float to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteFloatImpl(
|
||||
savestream_t *stream, const float_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a null-terminated string from the stream up to maxLen bytes
|
||||
* (including the terminator). Always null-terminates the output buffer.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Destination buffer of at least maxLen bytes.
|
||||
* @param maxLen Maximum bytes to read, including the null terminator.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadStringImpl(
|
||||
savestream_t *stream, char_t *out, const size_t maxLen
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a null-terminated string to the stream, truncating to maxLen-1
|
||||
* characters and always appending a null terminator.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Source string.
|
||||
* @param maxLen Maximum bytes to write, including the null terminator.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteStringImpl(
|
||||
savestream_t *stream, const char_t *input, const size_t maxLen
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
|
||||
* (time, timeZone, offsetTime).
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the epoch value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadDateImpl(
|
||||
savestream_t *stream, dusktimeepoch_t *out
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
|
||||
* (time, timeZone, offsetTime).
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Epoch value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteDateImpl(
|
||||
savestream_t *stream, const dusktimeepoch_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the contents of a save slot from the stream into the save file
|
||||
* struct. Use saveFileRead* macros to deserialize fields one at a time.
|
||||
*
|
||||
* @param stream Active read stream for this slot.
|
||||
* @param file Save file struct to populate.
|
||||
* @return An error code if loading fails.
|
||||
*/
|
||||
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file);
|
||||
|
||||
/**
|
||||
* Writes the contents of the save file struct into the stream.
|
||||
* Use saveFileWrite* macros to serialize fields one at a time.
|
||||
* Writes the header, version, size and checksum fields already populated
|
||||
* on file, followed by the raw JSON payload bytes. Split out of
|
||||
* saveFileWrite so the caller can free its serialized JSON buffer exactly
|
||||
* once regardless of which step fails.
|
||||
*
|
||||
* @param stream Active write stream for this slot.
|
||||
* @param file Save file struct to serialize.
|
||||
* @return An error code if writing fails.
|
||||
* @param file Save file metadata with header/version/size/checksum set.
|
||||
* @param json Serialized JSON payload bytes.
|
||||
* @param len Byte length of json.
|
||||
* @return An error code if any write fails.
|
||||
*/
|
||||
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
|
||||
errorret_t saveFileWritePayloadImpl(
|
||||
savestream_t *stream,
|
||||
const savefile_t *file,
|
||||
const char_t *json,
|
||||
const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a save slot's header, version and JSON payload from the stream,
|
||||
* validating the payload against its stored size and CRC32 checksum. The
|
||||
* JSON is parsed fresh into a new document each call; no payload bytes are
|
||||
* kept resident beyond this call.
|
||||
*
|
||||
* @param stream Active read stream for this slot.
|
||||
* @param file Save file metadata struct to populate.
|
||||
* @param outDoc Receives the parsed JSON document, owned by the caller.
|
||||
* @return An error code if loading, validation or parsing fails.
|
||||
*/
|
||||
errorret_t saveFileLoad(
|
||||
savestream_t *stream, savefile_t *file, yyjson_doc **outDoc
|
||||
);
|
||||
|
||||
/**
|
||||
* Serializes the given JSON document and writes the header, version,
|
||||
* size, checksum and payload to the stream. The serialized bytes are not
|
||||
* kept resident beyond this call.
|
||||
*
|
||||
* @param stream Active write stream for this slot.
|
||||
* @param file Save file metadata struct to populate and serialize.
|
||||
* @param doc The JSON document to serialize and write.
|
||||
* @return An error code if serialization or writing fails.
|
||||
*/
|
||||
errorret_t saveFileWrite(
|
||||
savestream_t *stream, savefile_t *file, yyjson_mut_doc *doc
|
||||
);
|
||||
|
||||
#define saveFileReadHeader(stream, header) \
|
||||
errorChain(saveStreamReadHeaderImpl(stream, header))
|
||||
@@ -406,63 +185,12 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
|
||||
#define saveFileWriteVersion(stream, input) \
|
||||
errorChain(saveStreamWriteVersionImpl(stream, input))
|
||||
|
||||
#define saveFileReadBool(stream, out) \
|
||||
errorChain(saveStreamReadBoolImpl(stream, out))
|
||||
#define saveFileWriteBool(stream, input) \
|
||||
errorChain(saveStreamWriteBoolImpl(stream, input))
|
||||
|
||||
#define saveFileReadInt8(stream, out) \
|
||||
errorChain(saveStreamReadInt8Impl(stream, out))
|
||||
#define saveFileWriteInt8(stream, input) \
|
||||
errorChain(saveStreamWriteInt8Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt8(stream, out) \
|
||||
errorChain(saveStreamReadUInt8Impl(stream, out))
|
||||
#define saveFileWriteUInt8(stream, input) \
|
||||
errorChain(saveStreamWriteUInt8Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt16(stream, out) \
|
||||
errorChain(saveStreamReadInt16Impl(stream, out))
|
||||
#define saveFileWriteInt16(stream, input) \
|
||||
errorChain(saveStreamWriteInt16Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt16(stream, out) \
|
||||
errorChain(saveStreamReadUInt16Impl(stream, out))
|
||||
#define saveFileWriteUInt16(stream, input) \
|
||||
errorChain(saveStreamWriteUInt16Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt32(stream, out) \
|
||||
errorChain(saveStreamReadInt32Impl(stream, out))
|
||||
#define saveFileWriteInt32(stream, input) \
|
||||
errorChain(saveStreamWriteInt32Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt32(stream, out) \
|
||||
errorChain(saveStreamReadUInt32Impl(stream, out))
|
||||
#define saveFileWriteUInt32(stream, input) \
|
||||
errorChain(saveStreamWriteUInt32Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt64(stream, out) \
|
||||
errorChain(saveStreamReadInt64Impl(stream, out))
|
||||
#define saveFileWriteInt64(stream, input) \
|
||||
errorChain(saveStreamWriteInt64Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt64(stream, out) \
|
||||
errorChain(saveStreamReadUInt64Impl(stream, out))
|
||||
#define saveFileWriteUInt64(stream, input) \
|
||||
errorChain(saveStreamWriteUInt64Impl(stream, input))
|
||||
|
||||
#define saveFileReadFloat(stream, out) \
|
||||
errorChain(saveStreamReadFloatImpl(stream, out))
|
||||
#define saveFileWriteFloat(stream, input) \
|
||||
errorChain(saveStreamWriteFloatImpl(stream, input))
|
||||
|
||||
#define saveFileReadString(stream, out, maxLen) \
|
||||
errorChain(saveStreamReadStringImpl(stream, out, maxLen))
|
||||
#define saveFileWriteString(stream, input, maxLen) \
|
||||
errorChain(saveStreamWriteStringImpl(stream, input, maxLen))
|
||||
|
||||
#define saveFileReadDate(stream, out) \
|
||||
errorChain(saveStreamReadDateImpl(stream, out))
|
||||
#define saveFileWriteDate(stream, input) \
|
||||
errorChain(saveStreamWriteDateImpl(stream, input))
|
||||
#define saveFileReadSize(stream, out) \
|
||||
errorChain(saveStreamReadSizeImpl(stream, out))
|
||||
#define saveFileWriteSize(stream, input) \
|
||||
errorChain(saveStreamWriteSizeImpl(stream, input))
|
||||
|
||||
#define saveFileReadChecksum(stream, out) \
|
||||
errorChain(saveStreamReadChecksumImpl(stream, out))
|
||||
#define saveFileWriteChecksum(stream, input) \
|
||||
errorChain(saveStreamWriteChecksumImpl(stream, input))
|
||||
|
||||
@@ -11,4 +11,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(overworld)
|
||||
add_subdirectory(battle)
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "scenebattle.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
errorret_t sceneBattleInit(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleUpdate(scenedata_t *sceneData) {
|
||||
battleUpdate();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleRender(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t sceneBattleDispose(scenedata_t *sceneData) {
|
||||
errorOk();
|
||||
}
|
||||
@@ -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 "scene/scenebase.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
} scenebattle_t;
|
||||
|
||||
/**
|
||||
* Initializes the battle scene. The battle itself (BATTLE global) is
|
||||
* expected to already be started, e.g. by a StartBattle cutscene item.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the init failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleInit(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Updates the battle scene, ticking the battle simulation.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the update failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleUpdate(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Renders the battle scene.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the render failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleRender(scenedata_t *sceneData);
|
||||
|
||||
/**
|
||||
* Disposes the battle scene.
|
||||
*
|
||||
* @param sceneData The scene data used for this scene.
|
||||
* @return An error if the dispose failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t sceneBattleDispose(scenedata_t *sceneData);
|
||||
@@ -16,12 +16,5 @@ scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
|
||||
.render = sceneOverworldRender,
|
||||
.dispose = sceneOverworldDispose
|
||||
},
|
||||
|
||||
[SCENE_TYPE_BATTLE] = {
|
||||
.init = sceneBattleInit,
|
||||
.update = sceneBattleUpdate,
|
||||
.render = sceneBattleRender,
|
||||
.dispose = sceneBattleDispose
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -8,11 +8,9 @@
|
||||
#pragma once
|
||||
#include "scene/scenebase.h"
|
||||
#include "scene/overworld/sceneoverworld.h"
|
||||
#include "scene/battle/scenebattle.h"
|
||||
|
||||
typedef union scenedata_u {
|
||||
sceneoverworld_t overworld;
|
||||
scenebattle_t battle;
|
||||
} scenedata_t;
|
||||
|
||||
typedef errorret_t (*scenecallback_t)(scenedata_t *);
|
||||
@@ -28,7 +26,6 @@ typedef enum {
|
||||
SCENE_TYPE_NULL,
|
||||
|
||||
SCENE_TYPE_OVERWORLD,
|
||||
SCENE_TYPE_BATTLE,
|
||||
|
||||
SCENE_TYPE_COUNT
|
||||
} scenetype_t;
|
||||
|
||||
+135
-10
@@ -10,19 +10,144 @@
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/mesh/quad.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
typedef struct {
|
||||
mesh_t mesh;
|
||||
bool_t built;
|
||||
meshvertex_t *vertices;
|
||||
int32_t capacity;
|
||||
int32_t vertexCount;
|
||||
int32_t cachedScanX;
|
||||
int32_t cachedScanY;
|
||||
} uiconsolecache_t;
|
||||
|
||||
uiconsolecache_t UI_CONSOLE_CACHE;
|
||||
|
||||
errorret_t uiConsoleDraw(void) {
|
||||
if(!CONSOLE.visible) errorOk();
|
||||
|
||||
float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
|
||||
errorChain(textDraw(
|
||||
(float_t)SCREEN.scanX,
|
||||
(float_t)SCREEN.scanY + lineH * (float_t)i,
|
||||
CONSOLE.line[i],
|
||||
COLOR_RED,
|
||||
&FONT_DEFAULT
|
||||
));
|
||||
if(
|
||||
CONSOLE.dirty ||
|
||||
UI_CONSOLE_CACHE.cachedScanX != SCREEN.scanX ||
|
||||
UI_CONSOLE_CACHE.cachedScanY != SCREEN.scanY
|
||||
) {
|
||||
errorChain(uiConsoleRebuild());
|
||||
}
|
||||
return spriteBatchFlush();
|
||||
|
||||
if(UI_CONSOLE_CACHE.vertexCount == 0) errorOk();
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = COLOR_RED,
|
||||
.texture = FONT_DEFAULT.texture
|
||||
}
|
||||
};
|
||||
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMaterial(&SHADER_UNLIT, &material));
|
||||
return meshDraw(&UI_CONSOLE_CACHE.mesh, 0, UI_CONSOLE_CACHE.vertexCount);
|
||||
}
|
||||
|
||||
errorret_t uiConsoleRebuild(void) {
|
||||
int32_t spriteCount = 0;
|
||||
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
|
||||
char_t c;
|
||||
int32_t j = 0;
|
||||
while((c = CONSOLE.line[i][j++]) != '\0') {
|
||||
if(c != ' ' && c != '\n') spriteCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const int32_t vertexCount = spriteCount * QUAD_VERTEX_COUNT;
|
||||
if(vertexCount > UI_CONSOLE_CACHE.capacity) {
|
||||
if(UI_CONSOLE_CACHE.built) {
|
||||
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
||||
UI_CONSOLE_CACHE.built = false;
|
||||
}
|
||||
|
||||
if(UI_CONSOLE_CACHE.vertices != NULL) {
|
||||
memoryFree(UI_CONSOLE_CACHE.vertices);
|
||||
}
|
||||
|
||||
UI_CONSOLE_CACHE.vertices = (meshvertex_t *)memoryAllocate(
|
||||
sizeof(meshvertex_t) * vertexCount
|
||||
);
|
||||
UI_CONSOLE_CACHE.capacity = vertexCount;
|
||||
}
|
||||
UI_CONSOLE_CACHE.vertexCount = vertexCount;
|
||||
|
||||
if(vertexCount > 0) {
|
||||
const float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
const float_t charW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
|
||||
int32_t index = 0;
|
||||
int32_t row = 0;
|
||||
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
|
||||
float_t posX = (float_t)SCREEN.scanX;
|
||||
float_t posY = (float_t)SCREEN.scanY + lineH * (float_t)row;
|
||||
|
||||
char_t c;
|
||||
int32_t j = 0;
|
||||
while((c = CONSOLE.line[i][j++]) != '\0') {
|
||||
if(c == '\n') {
|
||||
posX = (float_t)SCREEN.scanX;
|
||||
posY += lineH;
|
||||
row++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(c == ' ') {
|
||||
posX += charW;
|
||||
continue;
|
||||
}
|
||||
|
||||
const spritebatchsprite_t sprite = textGetSprite(
|
||||
(vec2){ posX, posY }, c, &FONT_DEFAULT
|
||||
);
|
||||
spriteBatchBufferToMesh(
|
||||
&sprite, 1,
|
||||
&UI_CONSOLE_CACHE.vertices[index], QUAD_VERTEX_COUNT
|
||||
);
|
||||
|
||||
index += QUAD_VERTEX_COUNT;
|
||||
posX += charW;
|
||||
}
|
||||
|
||||
row++;
|
||||
}
|
||||
|
||||
if(!UI_CONSOLE_CACHE.built) {
|
||||
errorChain(meshInit(
|
||||
&UI_CONSOLE_CACHE.mesh,
|
||||
QUAD_PRIMITIVE_TYPE,
|
||||
UI_CONSOLE_CACHE.capacity,
|
||||
UI_CONSOLE_CACHE.vertices
|
||||
));
|
||||
UI_CONSOLE_CACHE.built = true;
|
||||
} else {
|
||||
errorChain(meshFlush(&UI_CONSOLE_CACHE.mesh, 0, vertexCount));
|
||||
}
|
||||
}
|
||||
|
||||
UI_CONSOLE_CACHE.cachedScanX = SCREEN.scanX;
|
||||
UI_CONSOLE_CACHE.cachedScanY = SCREEN.scanY;
|
||||
CONSOLE.dirty = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiConsoleDispose(void) {
|
||||
if(UI_CONSOLE_CACHE.built) {
|
||||
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
||||
}
|
||||
|
||||
if(UI_CONSOLE_CACHE.vertices != NULL) {
|
||||
memoryFree(UI_CONSOLE_CACHE.vertices);
|
||||
}
|
||||
|
||||
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -9,9 +9,27 @@
|
||||
#include "error/error.h"
|
||||
|
||||
/**
|
||||
* Renders the console history into the scan-safe area.
|
||||
* No-ops when the console is not visible.
|
||||
* Renders the console history into the scan-safe area, drawing a mesh
|
||||
* that is only rebuilt when the history changes or the scan-safe origin
|
||||
* moves, rather than rebuffering vertices every frame. No-ops when the
|
||||
* console is not visible.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiConsoleDraw(void);
|
||||
|
||||
/**
|
||||
* Rebuilds the cached mesh for the console's current history and
|
||||
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
|
||||
* needed; only needs calling directly to force an immediate rebuild.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiConsoleRebuild(void);
|
||||
|
||||
/**
|
||||
* Frees the mesh and vertex buffer built up by uiConsoleDraw().
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiConsoleDispose(void);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include "uifps.h"
|
||||
#include "time/time.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "engine/engine.h"
|
||||
@@ -56,7 +55,6 @@ errorret_t uiFPSDraw() {
|
||||
fpsText, textColor,
|
||||
&FONT_DEFAULT
|
||||
));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
int32_t versionWidth, versionHeight;
|
||||
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
|
||||
@@ -68,5 +66,5 @@ errorret_t uiFPSDraw() {
|
||||
&FONT_DEFAULT
|
||||
));
|
||||
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "uiplayerpos.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/entity/entitytype.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
@@ -44,5 +43,21 @@ errorret_t uiPlayerPosDraw() {
|
||||
errorChain(textDraw(
|
||||
(float_t)SCREEN.scanX, y, text, COLOR_GREEN, &FONT_DEFAULT
|
||||
));
|
||||
return spriteBatchFlush();
|
||||
|
||||
char_t velocityText[64];
|
||||
snprintf(
|
||||
velocityText,
|
||||
sizeof(velocityText),
|
||||
"vel %.2f,%.2f,%.2f",
|
||||
player->body.velocity[0],
|
||||
player->body.velocity[1],
|
||||
player->body.velocity[2]
|
||||
);
|
||||
|
||||
float_t velocityY = y + (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
errorChain(textDraw(
|
||||
(float_t)SCREEN.scanX, velocityY, velocityText, COLOR_GREEN, &FONT_DEFAULT
|
||||
));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -11,5 +11,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
|
||||
add_subdirectory(game)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(backpack)
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "assert/assert.h"
|
||||
@@ -95,7 +94,6 @@ errorret_t uiBackpackDraw(void) {
|
||||
uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth)
|
||||
);
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uibattlemenu.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
|
||||
uibattlemenu_t UI_BATTLE_MENU;
|
||||
|
||||
void uiBattleMenuTargetSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
battlePlayerAttack(UI_BATTLE_MENU.targetFighterIndex[index]);
|
||||
uiMenuClose(&UI_BATTLE_MENU.targetMenu);
|
||||
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
|
||||
}
|
||||
|
||||
void uiBattleMenuOpenTargets(void) {
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
|
||||
const battlefighterteam_t enemyTeam =
|
||||
current->team == BATTLE_FIGHTER_TEAM_ALLY ?
|
||||
BATTLE_FIGHTER_TEAM_ENEMY : BATTLE_FIGHTER_TEAM_ALLY;
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BATTLE_MENU.targetMenu, UI_BATTLE_MENU.targetItems,
|
||||
uiBattleMenuTargetSelected, NULL, NULL
|
||||
);
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
battlefighter_t *candidate = &BATTLE.fighters[i];
|
||||
if(candidate->team != enemyTeam) continue;
|
||||
if(!battleFighterIsAlive(candidate)) continue;
|
||||
|
||||
stringFormat(
|
||||
UI_BATTLE_MENU.targetLabels[menuIndex],
|
||||
UI_BATTLE_MENU_TARGET_LABEL_MAX - 1,
|
||||
"Enemy %u (%u/%u HP)",
|
||||
candidate->id, candidate->health, candidate->healthMax
|
||||
);
|
||||
UI_BATTLE_MENU.targetFighterIndex[menuIndex] = i;
|
||||
MENU_BUTTON(UI_BATTLE_MENU.targetLabels[menuIndex]);
|
||||
}
|
||||
|
||||
MENU_END(UI_BATTLE_MENU.targetItems, 1);
|
||||
|
||||
uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
|
||||
}
|
||||
|
||||
void uiBattleMenuActionSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
switch(index) {
|
||||
case UI_BATTLE_MENU_ACTION_INDEX_ATTACK:
|
||||
uiBattleMenuOpenTargets();
|
||||
break;
|
||||
|
||||
case UI_BATTLE_MENU_ACTION_INDEX_FLEE:
|
||||
battlePlayerFlee();
|
||||
uiMenuClose(&UI_BATTLE_MENU.actionMenu);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t uiBattleMenuInit(void) {
|
||||
memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t));
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_BATTLE_MENU.actionMenu, UI_BATTLE_MENU.actionItems,
|
||||
uiBattleMenuActionSelected, NULL, NULL
|
||||
);
|
||||
MENU_BUTTON("Attack");
|
||||
MENU_BUTTON("Flee");
|
||||
MENU_END(UI_BATTLE_MENU.actionItems, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiBattleMenuUpdate(void) {
|
||||
if(uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) errorOk();
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) errorOk();
|
||||
|
||||
uiMenuOpen(&UI_BATTLE_MENU.actionMenu);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiBattleMenuDraw(void) {
|
||||
if(!uiMenuIsActive(&UI_BATTLE_MENU.actionMenu)) errorOk();
|
||||
|
||||
const float_t width = UI_BATTLE_MENU_WIDTH;
|
||||
const float_t height = UI_BATTLE_MENU_HEIGHT;
|
||||
const float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) - width;
|
||||
const float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - height;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_BATTLE_MENU.actionMenu,
|
||||
x + UI_FRAME_START_X,
|
||||
y + UI_FRAME_START_Y,
|
||||
width - (UI_FRAME_START_X * 2),
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
|
||||
if(uiMenuIsActive(&UI_BATTLE_MENU.targetMenu)) {
|
||||
const float_t targetY = y - height;
|
||||
errorChain(uiFrameDraw(x, targetY, width, height));
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_BATTLE_MENU.targetMenu,
|
||||
x + UI_FRAME_START_X,
|
||||
targetY + UI_FRAME_START_Y,
|
||||
width - (UI_FRAME_START_X * 2),
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uimenu.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
#define UI_BATTLE_MENU_ACTION_ITEM_COUNT 2
|
||||
#define UI_BATTLE_MENU_ACTION_INDEX_ATTACK 0
|
||||
#define UI_BATTLE_MENU_ACTION_INDEX_FLEE 1
|
||||
|
||||
#define UI_BATTLE_MENU_TARGET_ITEM_COUNT BATTLE_FIGHTER_COUNT_MAX
|
||||
#define UI_BATTLE_MENU_TARGET_LABEL_MAX 32
|
||||
|
||||
#define UI_BATTLE_MENU_WIDTH 160.0f
|
||||
#define UI_BATTLE_MENU_HEIGHT 96.0f
|
||||
|
||||
typedef struct {
|
||||
uimenu_t actionMenu;
|
||||
uimenuitem_t actionItems[UI_BATTLE_MENU_ACTION_ITEM_COUNT];
|
||||
|
||||
uimenu_t targetMenu;
|
||||
uimenuitem_t targetItems[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
|
||||
uint8_t targetFighterIndex[UI_BATTLE_MENU_TARGET_ITEM_COUNT];
|
||||
char_t targetLabels[UI_BATTLE_MENU_TARGET_ITEM_COUNT]
|
||||
[UI_BATTLE_MENU_TARGET_LABEL_MAX];
|
||||
} uibattlemenu_t;
|
||||
|
||||
extern uibattlemenu_t UI_BATTLE_MENU;
|
||||
|
||||
/**
|
||||
* Initializes the battle action/target menus.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBattleMenuInit(void);
|
||||
|
||||
/**
|
||||
* Updates the battle menu: opens the action menu whenever it becomes a
|
||||
* player-controlled fighter's turn.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBattleMenuUpdate(void);
|
||||
|
||||
/**
|
||||
* Draws the battle action menu, and the target menu above it when
|
||||
* open. No-op when the action menu isn't active.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiBattleMenuDraw(void);
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "ui/frame/settings/uisettings.h"
|
||||
#include "ui/frame/backpack/uibackpack.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
@@ -85,7 +84,6 @@ errorret_t uiGameMenuDraw(void) {
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user