Compare commits
45 Commits
main
...
06bc4fcd55
| Author | SHA1 | Date | |
|---|---|---|---|
| 06bc4fcd55 | |||
| 42cb84b610 | |||
| f8607d114c | |||
| 56230dd340 | |||
| df9fdf26c8 | |||
| 8f8fa8f8d1 | |||
| 07f98c119a | |||
| d49194fc4d | |||
| e61914bad4 | |||
| 015d90519f | |||
| 7b4f0abfb8 | |||
| 0667a44e14 | |||
| 0852b8463b | |||
| 1436fda858 | |||
| 0826908068 | |||
| 7ceb9e571d | |||
| 38c2f6b6f9 | |||
| b9f06fef7a | |||
| 373f1c1010 | |||
| e80d693d85 | |||
| ef60ddf6a8 | |||
| de0ff2e263 | |||
| 13a435e9c1 | |||
| 725338cd5a | |||
| acd14f46a8 | |||
| ef78f023ed | |||
| 85b6109792 | |||
| 6c5738c863 | |||
| 1174e471f6 | |||
| a618ff46fe | |||
| 1fa5cd316e | |||
| 11e5d37563 | |||
| 9f86c20129 | |||
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 |
@@ -33,3 +33,74 @@ jobs:
|
|||||||
libssl-dev
|
libssl-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ./scripts/test-linux.sh
|
run: ./scripts/test-linux.sh
|
||||||
|
|
||||||
|
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
|
||||||
|
# confirm the emulator doesn't crash. Not yet verified against a real
|
||||||
|
# runner (no CI run has exercised these) -- continue-on-error so a
|
||||||
|
# flaky/broken emulator step doesn't block the required Linux test job.
|
||||||
|
run-tests-gamecube-dolphin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ghcr.io/extremscorner/libogc2:latest
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: apt-get update && apt-get install -y nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3-pip python3-polib python3-pil \
|
||||||
|
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||||
|
dolphin-emu xvfb
|
||||||
|
dkp-pacman -Syu --noconfirm
|
||||||
|
dkp-pacman -S --needed --noconfirm \
|
||||||
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
|
- name: Build GameCube ISO and boot it in Dolphin
|
||||||
|
run: ./scripts/test-gamecube-dolphin.sh
|
||||||
|
|
||||||
|
run-tests-wii-dolphin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ghcr.io/extremscorner/libogc2:latest
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: apt-get update && apt-get install -y nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3-pip python3-polib python3-pil \
|
||||||
|
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||||
|
dolphin-emu xvfb
|
||||||
|
dkp-pacman -Syu --noconfirm
|
||||||
|
dkp-pacman -S --needed --noconfirm \
|
||||||
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
|
- name: Build Wii ISO and boot it in Dolphin
|
||||||
|
run: ./scripts/test-wii-dolphin.sh
|
||||||
|
|
||||||
|
run-tests-psp-ppsspp:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup pspdev
|
||||||
|
uses: ./.github/actions/setup-pspdev
|
||||||
|
- name: Install PPSSPPHeadless build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
|
||||||
|
- name: Build PPSSPPHeadless
|
||||||
|
run: |
|
||||||
|
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
|
||||||
|
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
|
||||||
|
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
|
||||||
|
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
|
||||||
|
run: ./scripts/test-psp-ppsspp.sh
|
||||||
|
|||||||
@@ -1,455 +0,0 @@
|
|||||||
# Dusk — Claude Code rules
|
|
||||||
|
|
||||||
## File headers
|
|
||||||
Every C, H, and JS file starts with:
|
|
||||||
|
|
||||||
```c
|
|
||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
JS files use `//` comment style instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C conventions
|
|
||||||
|
|
||||||
### Types
|
|
||||||
Always use the project-defined aliases instead of bare C primitives:
|
|
||||||
|
|
||||||
| Use | Not |
|
|
||||||
|-----------|--------------|
|
|
||||||
| `bool_t` | `bool` |
|
|
||||||
| `int_t` | `int` |
|
|
||||||
| `float_t` | `float` |
|
|
||||||
| `char_t` | `char` |
|
|
||||||
|
|
||||||
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
|
|
||||||
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
- **Functions** — snake_case, prefixed with their module:
|
|
||||||
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
|
|
||||||
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
|
|
||||||
- **Macros / constants** — UPPER_SNAKE_CASE:
|
|
||||||
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
|
|
||||||
- **Files** — snake_case matching the primary type: `entityposition.c`,
|
|
||||||
`moduleassetbatch.c`
|
|
||||||
|
|
||||||
### Header files (`.h`)
|
|
||||||
- Use `#pragma once` — no include guards.
|
|
||||||
- Declare every public function, `#define`, and `extern` global.
|
|
||||||
- Write a JSDoc block (`/** … */`) above every declaration explaining
|
|
||||||
purpose, `@param`s, and `@returns`.
|
|
||||||
- Only include headers that the `.h` file itself strictly requires for
|
|
||||||
the types it exposes. Move everything else to the `.c` file.
|
|
||||||
Do not use forward declarations as a workaround — use the real
|
|
||||||
include in the `.c` file instead.
|
|
||||||
|
|
||||||
### Implementation files (`.c`)
|
|
||||||
- Contain function bodies only; no declarations.
|
|
||||||
- Pull in whatever additional includes the implementation needs.
|
|
||||||
- Do not use `static` or `inline` on **functions**. Every function,
|
|
||||||
including internal helpers, must be declared in the matching `.h` and
|
|
||||||
defined in the `.c` file. Internal helpers belong near the bottom of
|
|
||||||
the `.c` file, not at the top with a `static` qualifier.
|
|
||||||
`static` and `inline` on functions are only appropriate when the
|
|
||||||
function body is written directly inside a `.h` file.
|
|
||||||
`static` on **variables** (file-scope state) is fine and expected.
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
- Hard-wrap all lines at **80 characters**.
|
|
||||||
|
|
||||||
### Error handling
|
|
||||||
Return `errorret_t` from fallible functions. Use these macros:
|
|
||||||
|
|
||||||
```c
|
|
||||||
errorOk(); // return success
|
|
||||||
errorThrow("msg %d", val); // return failure with message
|
|
||||||
errorChain(someCall()); // propagate failure, continue on success
|
|
||||||
errorIsOk(ret) / errorIsNotOk(ret) // test a result
|
|
||||||
errorCatch(ret); // handle + free an error
|
|
||||||
```
|
|
||||||
|
|
||||||
Never return raw error codes or use `errno` for in-engine errors.
|
|
||||||
|
|
||||||
### Memory
|
|
||||||
Use the project allocator — never raw `malloc`/`free`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
memoryAllocate(size) // allocate
|
|
||||||
memoryFree(ptr) // free
|
|
||||||
memoryZero(dest, size) // zero a block
|
|
||||||
memoryCopy(dest, src, size) // copy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Asserts
|
|
||||||
Prefer specific assert macros over bare `assert()`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
assertNotNull(ptr, "msg");
|
|
||||||
assertTrue(cond, "msg");
|
|
||||||
assertFalse(cond, "msg");
|
|
||||||
assertUnreachable("msg");
|
|
||||||
assertIsMainThread("msg");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build system
|
|
||||||
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|
||||||
PUBLIC
|
|
||||||
myfile.c
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Never add source files to the root `CMakeLists.txt` directly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Platform support
|
|
||||||
|
|
||||||
### Targets
|
|
||||||
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
|
||||||
|
|
||||||
| `DUSK_TARGET_SYSTEM` | Macro defined | 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/dusklinux/ Linux + Knulli platform impl
|
|
||||||
src/duskpsp/ PSP platform impl
|
|
||||||
src/duskvita/ Vita platform impl
|
|
||||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
|
||||||
```
|
|
||||||
|
|
||||||
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
|
||||||
it uses native GameCube/Wii rendering and input APIs.
|
|
||||||
|
|
||||||
### Platform guards
|
|
||||||
Use the compile-time macros for platform-specific code:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#ifdef DUSK_PSP
|
|
||||||
// PSP-only path
|
|
||||||
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
|
||||||
// GameCube / Wii path
|
|
||||||
#else
|
|
||||||
// Generic / Linux fallback
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional capability macros set per-target:
|
|
||||||
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
|
||||||
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
|
||||||
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
|
||||||
|
|
||||||
### Abstraction pattern
|
|
||||||
Platform-specific implementations are wired in via `#define` macros in
|
|
||||||
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
|
||||||
the core calls through. Functions that a platform does not support are
|
|
||||||
simply left undefined — the core guards calls with `#ifdef`.
|
|
||||||
|
|
||||||
### Adding platform-specific code
|
|
||||||
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
|
||||||
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
|
||||||
or capability macro.
|
|
||||||
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
|
||||||
the platform header macros instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new asset loader type
|
|
||||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
|
||||||
`src/dusk/asset/loader/assetloader.h`.
|
|
||||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
|
||||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
|
||||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
|
||||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
|
||||||
`src/dusk/asset/loader/assetloader.c`.
|
|
||||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new entity component
|
|
||||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
|
||||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
|
||||||
`entityMyCompDispose()`.
|
|
||||||
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
|
||||||
3. Add a row to `src/dusk/entity/componentlist.h`:
|
|
||||||
```c
|
|
||||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
|
||||||
```
|
|
||||||
This auto-generates the enum, union field, and definition entry.
|
|
||||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new script (JS) module
|
|
||||||
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
|
||||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
|
||||||
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
|
||||||
- Register props/funcs in `moduleMyModInit()` with
|
|
||||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
|
||||||
`scriptProtoDefineStaticFunc`.
|
|
||||||
2. `#include` the header in
|
|
||||||
`src/dusk/script/module/modulelist.c` and call
|
|
||||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
|
||||||
`moduleListDispose()`).
|
|
||||||
3. For component modules also register in
|
|
||||||
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
|
||||||
so `entity.add()` returns the typed wrapper.
|
|
||||||
4. Create `types/<category>/mymod.d.ts` and add a
|
|
||||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Script module type declarations
|
|
||||||
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
|
||||||
check whether the corresponding `types/**/*.d.ts` needs updating and
|
|
||||||
apply any changes before finishing the task.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JavaScript (asset scripts)
|
|
||||||
- Use `var` for module-level state; `const` for values that never
|
|
||||||
change.
|
|
||||||
- Always use semicolons.
|
|
||||||
- Scene objects are plain objects (`var scene = {}`) with assigned
|
|
||||||
methods.
|
|
||||||
- Export via `module.exports = scene`.
|
|
||||||
- Async scene init should use `async function` and `await`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coding style
|
|
||||||
|
|
||||||
### ASCII only
|
|
||||||
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
|
||||||
Non-ASCII characters are banned even in comments and string literals.
|
|
||||||
Use ASCII-only substitutes instead:
|
|
||||||
- `--` or `-` instead of `—` (em dash)
|
|
||||||
- `->` instead of `→` (arrow)
|
|
||||||
- `x` or `*` instead of `×` (multiplication)
|
|
||||||
|
|
||||||
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
|
||||||
|
|
||||||
### Indentation
|
|
||||||
2 spaces. No tabs.
|
|
||||||
|
|
||||||
### Keyword and operator spacing
|
|
||||||
No space between a keyword or function name and its opening parenthesis:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if(!ptr) return;
|
|
||||||
for(uint8_t i = 0; i < count; i++) {
|
|
||||||
while(entry->state != DONE) {
|
|
||||||
switch(type) {
|
|
||||||
sizeof(assetbatch_t)
|
|
||||||
memoryZero(ptr, size)
|
|
||||||
```
|
|
||||||
|
|
||||||
Spaces around all binary operators and after every comma:
|
|
||||||
|
|
||||||
```c
|
|
||||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
|
||||||
(size_t)end - (size_t)start
|
|
||||||
foo(a, b, c)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Braces
|
|
||||||
Opening brace on the **same line** as the statement (K&R style) for all
|
|
||||||
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void assetEntryLock(assetentry_t *entry) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dirty) {
|
|
||||||
...
|
|
||||||
} else {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guard returns
|
|
||||||
Short guards go on one line with no braces:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if(!ptr) return;
|
|
||||||
if(!b || !b->batch) return jerry_undefined();
|
|
||||||
if(!(flags & DIRTY)) return;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blank lines
|
|
||||||
- One blank line between functions; no blank line at the start or end of
|
|
||||||
a function body.
|
|
||||||
- One blank line between logical blocks inside a function body.
|
|
||||||
- No trailing blank lines at the end of a file.
|
|
||||||
|
|
||||||
### Pointer placement
|
|
||||||
`*` is attached to the variable name, not the type:
|
|
||||||
|
|
||||||
```c
|
|
||||||
assetentry_t *entry
|
|
||||||
const char_t *name
|
|
||||||
void *ptr
|
|
||||||
uint8_t *d = (uint8_t *)dest;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Casts
|
|
||||||
Space between cast and operand:
|
|
||||||
|
|
||||||
```c
|
|
||||||
(assetbatch_t *)user
|
|
||||||
(uint8_t *)dest
|
|
||||||
(textureformat_t)v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Return
|
|
||||||
No parentheses around the return value:
|
|
||||||
|
|
||||||
```c
|
|
||||||
return ptr;
|
|
||||||
return MEMORY_POINTERS_IN_USE;
|
|
||||||
```
|
|
||||||
|
|
||||||
### switch / case
|
|
||||||
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
switch(type) {
|
|
||||||
case ASSET_LOADER_TYPE_TEXTURE:
|
|
||||||
descs[i].input.texture = (textureformat_t)v;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-line function signatures
|
|
||||||
When parameters don't fit on one line, put each on its own line indented
|
|
||||||
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
|
||||||
its own line at column 0:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void assetEntryInit(
|
|
||||||
assetentry_t *entry,
|
|
||||||
const char_t *name,
|
|
||||||
const assetloadertype_t type,
|
|
||||||
assetloaderinput_t *input
|
|
||||||
) {
|
|
||||||
|
|
||||||
errorret_t memoryCompare(
|
|
||||||
const void *a,
|
|
||||||
const void *b,
|
|
||||||
const size_t size
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Structs and enums
|
|
||||||
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
|
||||||
brace and name on the same line:
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef struct {
|
|
||||||
errorcode_t code;
|
|
||||||
char_t *message;
|
|
||||||
} errorstate_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ASSET_LOADER_TYPE_NULL,
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
|
||||||
} assetloadertype_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Designated initialisers
|
|
||||||
Spaces inside braces; `.field = value`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
jsassetentry_t e = { .entry = entry };
|
|
||||||
assetbatchloadedpend_t init = { .batch = batch };
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ternary operator
|
|
||||||
Spaces around `?` and `:`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
|
||||||
```
|
|
||||||
|
|
||||||
### const placement
|
|
||||||
`const` before the type, `*` attached to the variable:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const char_t *name
|
|
||||||
const void *src
|
|
||||||
const size_t size
|
|
||||||
```
|
|
||||||
|
|
||||||
### Comments in `.c` files
|
|
||||||
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
|
||||||
functions follow one another with a single blank line between them.
|
|
||||||
- Multi-line explanatory comments inside function bodies use `//` lines:
|
|
||||||
```c
|
|
||||||
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
|
||||||
// so their finalizers fire before assetDispose() checks ref counts.
|
|
||||||
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
|
||||||
```
|
|
||||||
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
|
||||||
function bodies.
|
|
||||||
|
|
||||||
### Comments in `.h` files
|
|
||||||
Every public declaration gets a Javadoc block (`/** … */`) with
|
|
||||||
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
|
||||||
above the declaration with no blank line in between.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Color system
|
|
||||||
|
|
||||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
|
||||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
|
||||||
|
|
||||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
|
||||||
The script emits four `#define` variants per color plus a bare alias:
|
|
||||||
|
|
||||||
```
|
|
||||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
|
||||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
|
||||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
|
||||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
|
||||||
COLOR_<NAME> COLOR_<NAME>_4B
|
|
||||||
```
|
|
||||||
|
|
||||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
|
||||||
|
|
||||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
|
||||||
hand-edit the generated header.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
- Tests live in `test/` mirroring `src/dusk/` structure.
|
|
||||||
- Use cmocka; include `dusktest.h`.
|
|
||||||
- Test functions: `static void test_something(void **state)`.
|
|
||||||
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
|
||||||
leaks.
|
|
||||||
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Player: dynamic capsule body, moved relative to the camera by PLAYER's
|
||||||
|
// own update callback (see entityplayer.c).
|
||||||
|
class Player {
|
||||||
|
constructor() {
|
||||||
|
this.entity = new Entity();
|
||||||
|
|
||||||
|
this.position = this.entity.add(POSITION);
|
||||||
|
this.position.setLocalPosition(0.0, 2.0, 0.0);
|
||||||
|
|
||||||
|
this.physics = this.entity.add(PHYSICS);
|
||||||
|
this.physics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
||||||
|
|
||||||
|
this.renderable = this.entity.add(RENDERABLE);
|
||||||
|
this.renderable.setMesh(0, MESH_CAPSULE);
|
||||||
|
this.renderable.setColor(0, 0, 255, 255);
|
||||||
|
|
||||||
|
this.entity.add(PLAYER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Player;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Entry point run once at startup (see game.c). Swap which scene module
|
||||||
|
// gets passed to Scene.set() here to change what the game boots into.
|
||||||
|
var overworldScene = require('./overworldscene.js');
|
||||||
|
Scene.set(overworldScene);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
|
||||||
|
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
|
||||||
|
// puts parts of the floor's own geometry near/behind the camera's view
|
||||||
|
// direction, which the PSP's legacy GU pipeline can't clip properly and
|
||||||
|
// drops the whole triangle instead of clipping it.
|
||||||
|
var CAMERA_ORBIT_RADIUS = 18.0;
|
||||||
|
var CAMERA_ORBIT_HEIGHT = 10.0;
|
||||||
|
var CAMERA_ORBIT_SPEED = 0.5;
|
||||||
|
|
||||||
|
var Player = require('./Player.js');
|
||||||
|
|
||||||
|
var cameraOrbitAngle = 0.0;
|
||||||
|
var cameraPosition = null;
|
||||||
|
|
||||||
|
// Orbits the camera around the world origin at a fixed radius/height/
|
||||||
|
// speed, always looking back at the origin. Called once up front (so the
|
||||||
|
// very first rendered frame is already positioned correctly) and then
|
||||||
|
// once per frame via update() below.
|
||||||
|
function updateCameraOrbit() {
|
||||||
|
cameraOrbitAngle += Time.delta * CAMERA_ORBIT_SPEED;
|
||||||
|
|
||||||
|
var eyeX = Math.cos(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
|
||||||
|
var eyeY = CAMERA_ORBIT_HEIGHT;
|
||||||
|
var eyeZ = Math.sin(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
|
||||||
|
|
||||||
|
cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Called once by Scene.set(), right after it creates and activates the
|
||||||
|
// scene this module owns.
|
||||||
|
init: function() {
|
||||||
|
// Camera, orbiting the origin (see updateCameraOrbit above).
|
||||||
|
var camera = new Entity();
|
||||||
|
cameraPosition = camera.add(POSITION);
|
||||||
|
camera.add(CAMERA);
|
||||||
|
updateCameraOrbit();
|
||||||
|
|
||||||
|
// Static ground plane. Physics ignores the entity's position
|
||||||
|
// component -- the shape's own normal/distance fully define the
|
||||||
|
// plane in world space.
|
||||||
|
var plane = new Entity();
|
||||||
|
var planePosition = plane.add(POSITION);
|
||||||
|
planePosition.setLocalPosition(-10.0, 0.0, -10.0);
|
||||||
|
planePosition.setLocalScale(20.0, 1.0, 20.0);
|
||||||
|
|
||||||
|
var planePhysics = plane.add(PHYSICS);
|
||||||
|
planePhysics.setBodyType(PHYSICS_BODY_STATIC);
|
||||||
|
planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
var planeRenderable = plane.add(RENDERABLE);
|
||||||
|
planeRenderable.setMesh(0, MESH_PLANE);
|
||||||
|
planeRenderable.setColor(128, 128, 128, 255);
|
||||||
|
|
||||||
|
new Player();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called once per engine frame while this module is the active scene
|
||||||
|
// (see Scene.set(), engineUpdate() -> moduleSceneUpdateCurrent()).
|
||||||
|
update: function() {
|
||||||
|
if(cameraPosition) updateCameraOrbit();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called once by Scene.set() when this module is replaced by another,
|
||||||
|
// right before the scene it owns is destroyed.
|
||||||
|
dispose: function() {
|
||||||
|
cameraPosition = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Turn things off we don't need
|
||||||
|
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JERRY_EXT ON CACHE BOOL "" FORCE)
|
||||||
|
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
|
||||||
|
|
||||||
|
# Fetch Jerry
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
jerryscript
|
||||||
|
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
|
||||||
|
GIT_TAG float32-fix
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(jerryscript)
|
||||||
|
|
||||||
|
# Mark found
|
||||||
|
set(jerryscript_FOUND ON)
|
||||||
|
|
||||||
|
# Define targets
|
||||||
|
if(TARGET jerryscript-core)
|
||||||
|
set(JERRY_CORE_TARGET jerryscript-core)
|
||||||
|
elseif(TARGET jerry-core)
|
||||||
|
set(JERRY_CORE_TARGET jerry-core)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET jerryscript-ext)
|
||||||
|
set(JERRY_EXT_TARGET jerryscript-ext)
|
||||||
|
elseif(TARGET jerry-ext)
|
||||||
|
set(JERRY_EXT_TARGET jerry-ext)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET jerryscript-port-default)
|
||||||
|
set(JERRY_PORT_TARGET jerryscript-port-default)
|
||||||
|
elseif(TARGET jerry-port-default)
|
||||||
|
set(JERRY_PORT_TARGET jerry-port-default)
|
||||||
|
elseif(TARGET jerryscript-port)
|
||||||
|
set(JERRY_PORT_TARGET jerryscript-port)
|
||||||
|
elseif(TARGET jerry-port)
|
||||||
|
set(JERRY_PORT_TARGET jerry-port)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT JERRY_CORE_TARGET)
|
||||||
|
message(FATAL_ERROR "JerryScript core target not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT JERRY_EXT_TARGET)
|
||||||
|
message(FATAL_ERROR "JerryScript ext target not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT JERRY_PORT_TARGET)
|
||||||
|
message(FATAL_ERROR "JerryScript port target not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach(tgt IN ITEMS
|
||||||
|
${JERRY_CORE_TARGET}
|
||||||
|
${JERRY_EXT_TARGET}
|
||||||
|
${JERRY_PORT_TARGET}
|
||||||
|
)
|
||||||
|
if(TARGET ${tgt})
|
||||||
|
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
|
||||||
|
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
|
||||||
|
JERRY_NUMBER_TYPE_FLOAT64=0
|
||||||
|
JERRY_BUILTIN_DATE=0
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# Export include dirs through the targets
|
||||||
|
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
|
||||||
|
${jerryscript_SOURCE_DIR}/jerry-core/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
|
||||||
|
${jerryscript_SOURCE_DIR}/jerry-ext/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
|
||||||
|
${jerryscript_SOURCE_DIR}/jerry-port/default/include
|
||||||
|
)
|
||||||
|
|
||||||
|
# Suppress JerryScript-only warning
|
||||||
|
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
|
||||||
|
-Wno-error
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
|
||||||
|
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
|
||||||
|
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
|
||||||
@@ -6,7 +6,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
|
|
||||||
# Link libraries
|
# Link libraries
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||||
# bba
|
bba
|
||||||
)
|
)
|
||||||
|
|
||||||
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
if(NOT CMAKE_BUILD_TYPE)
|
||||||
|
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
||||||
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
|
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
|
||||||
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
||||||
@@ -55,16 +59,9 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_DISPLAY_WIDTH=480
|
DUSK_DISPLAY_WIDTH=480
|
||||||
DUSK_DISPLAY_HEIGHT=272
|
DUSK_DISPLAY_HEIGHT=272
|
||||||
DUSK_THREAD_PTHREAD
|
DUSK_THREAD_PTHREAD
|
||||||
DUSK_TIME_DYNAMIC
|
|
||||||
DUSK_DISPLAY_OVERSCAN=6
|
DUSK_DISPLAY_OVERSCAN=6
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|
||||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_ASSERTIONS_FAKED
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Postbuild, create .pbp file for PSP.
|
# Postbuild, create .pbp file for PSP.
|
||||||
create_pbp_file(
|
create_pbp_file(
|
||||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM ghcr.io/extremscorner/libogc2
|
||||||
|
WORKDIR /workdir
|
||||||
|
RUN apt update && \
|
||||||
|
dkp-pacman -Syu --noconfirm && \
|
||||||
|
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso dolphin-emu xvfb && \
|
||||||
|
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd
|
||||||
|
VOLUME ["/workdir"]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM pspdev/pspdev:latest
|
||||||
|
WORKDIR /workdir
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
python3 \
|
||||||
|
py3-pip \
|
||||||
|
py3-dotenv \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
sdl2-dev \
|
||||||
|
zlib-dev \
|
||||||
|
linux-headers
|
||||||
|
|
||||||
|
# PPSSPP has no Alpine/Linux distro package -- build the "PPSSPPHeadless"
|
||||||
|
# target (the CLI/no-window build PPSSPP's own CI uses for automated runs)
|
||||||
|
# from source instead. The target name and exact CMake options have moved
|
||||||
|
# around across PPSSPP versions -- if this breaks, check
|
||||||
|
# https://github.com/hrydgard/ppsspp's CMakeLists.txt for the current name.
|
||||||
|
RUN git clone --recursive --depth 1 \
|
||||||
|
https://github.com/hrydgard/ppsspp.git /opt/ppsspp && \
|
||||||
|
cd /opt/ppsspp && \
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=Release && \
|
||||||
|
cmake --build build --target PPSSPPHeadless -- -j$(nproc) && \
|
||||||
|
cp build/PPSSPPHeadless /usr/local/bin/PPSSPPHeadless && \
|
||||||
|
rm -rf /opt/ppsspp
|
||||||
|
|
||||||
|
VOLUME ["/workdir"]
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-dolphin-test -f docker/dolphin-test/Dockerfile .
|
||||||
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin-test /bin/bash -c "./scripts/test-gamecube-dolphin.sh"
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$DEVKITPRO" ]; then
|
||||||
|
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOLPHIN_BIN="${DOLPHIN_BIN:-dolphin-emu}"
|
||||||
|
if ! command -v "$DOLPHIN_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "$DOLPHIN_BIN not found. Install Dolphin Emulator (e.g. 'apt install dolphin-emu')"
|
||||||
|
echo "or set DOLPHIN_BIN to its path."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v xvfb-run >/dev/null 2>&1; then
|
||||||
|
echo "xvfb-run not found. Install it (e.g. 'apt install xvfb') -- Dolphin's"
|
||||||
|
echo "Qt frontend needs a display even in batch mode."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/build-gamecube-iso.sh
|
||||||
|
|
||||||
|
DUSK_TEST_DOLPHIN_SECONDS="${DUSK_TEST_DOLPHIN_SECONDS:-20}"
|
||||||
|
ISO="build-gamecube-iso/Dusk-NTSC-U.iso"
|
||||||
|
|
||||||
|
echo "Booting $ISO in Dolphin for up to ${DUSK_TEST_DOLPHIN_SECONDS}s (batch mode, headless via Xvfb)..."
|
||||||
|
|
||||||
|
# Dolphin's batch mode (-b -e) runs the disc until told to stop -- there's
|
||||||
|
# no "the game exited cleanly" signal to wait for, so we bound it with
|
||||||
|
# timeout and treat "still running when the clock ran out" as success.
|
||||||
|
set +e
|
||||||
|
xvfb-run -a timeout "${DUSK_TEST_DOLPHIN_SECONDS}" "$DOLPHIN_BIN" -b -e "$ISO"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# timeout returns 124 when it had to kill Dolphin after the duration
|
||||||
|
# elapsed -- that means the disc booted and kept running, the expected
|
||||||
|
# smoke-test-passed outcome, not a failure. Any other non-zero status
|
||||||
|
# means Dolphin crashed or refused to boot the disc.
|
||||||
|
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
|
||||||
|
echo "Dolphin exited with unexpected status $status -- treating as a failure."
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "GameCube smoke test passed (Dolphin ran $ISO for ${DUSK_TEST_DOLPHIN_SECONDS}s without crashing)."
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-psp-test -f docker/psp-test/Dockerfile .
|
||||||
|
docker run --rm -v "$(pwd):/workdir" dusk-psp-test /bin/bash -c "./scripts/test-psp-ppsspp.sh"
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$PSPDEV" ]; then
|
||||||
|
echo "PSPDEV environment variable is not set. Please set it to the path of your PSP development environment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PPSSPP doesn't ship a standard Linux package -- build its "PPSSPPHeadless"
|
||||||
|
# target from source (https://github.com/hrydgard/ppsspp) and either put it
|
||||||
|
# on PATH or point PPSSPP_HEADLESS_BIN at it. See docker/psp-test/Dockerfile
|
||||||
|
# for a from-scratch build.
|
||||||
|
PPSSPP_HEADLESS_BIN="${PPSSPP_HEADLESS_BIN:-PPSSPPHeadless}"
|
||||||
|
if ! command -v "$PPSSPP_HEADLESS_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "$PPSSPP_HEADLESS_BIN not found. Build PPSSPP's 'PPSSPPHeadless' target"
|
||||||
|
echo "from source or set PPSSPP_HEADLESS_BIN to its path."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/build-psp.sh
|
||||||
|
|
||||||
|
DUSK_TEST_PPSSPP_SECONDS="${DUSK_TEST_PPSSPP_SECONDS:-20}"
|
||||||
|
EBOOT="build-psp/EBOOT.PBP"
|
||||||
|
|
||||||
|
echo "Booting $EBOOT in PPSSPPHeadless (timeout ${DUSK_TEST_PPSSPP_SECONDS}s)..."
|
||||||
|
|
||||||
|
# PPSSPPHeadless is purpose-built for automated/CI runs -- it renders
|
||||||
|
# without a window and is expected to exit on its own once --timeout
|
||||||
|
# elapses, unlike Dolphin's batch mode which has to be killed externally.
|
||||||
|
# Check `"$PPSSPP_HEADLESS_BIN" --help` if these flags don't match your
|
||||||
|
# PPSSPP checkout -- the headless CLI has changed across versions.
|
||||||
|
set +e
|
||||||
|
"$PPSSPP_HEADLESS_BIN" --timeout="${DUSK_TEST_PPSSPP_SECONDS}" "$EBOOT"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$status" -ne 0 ]; then
|
||||||
|
echo "PPSSPPHeadless exited with status $status"
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PSP smoke test passed (PPSSPPHeadless ran EBOOT.PBP for ${DUSK_TEST_PPSSPP_SECONDS}s without crashing)."
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t dusk-dolphin-test -f docker/dolphin-test/Dockerfile .
|
||||||
|
docker run --rm -v "$(pwd):/workdir" dusk-dolphin-test /bin/bash -c "./scripts/test-wii-dolphin.sh"
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "$DEVKITPRO" ]; then
|
||||||
|
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOLPHIN_BIN="${DOLPHIN_BIN:-dolphin-emu}"
|
||||||
|
if ! command -v "$DOLPHIN_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "$DOLPHIN_BIN not found. Install Dolphin Emulator (e.g. 'apt install dolphin-emu')"
|
||||||
|
echo "or set DOLPHIN_BIN to its path."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v xvfb-run >/dev/null 2>&1; then
|
||||||
|
echo "xvfb-run not found. Install it (e.g. 'apt install xvfb') -- Dolphin's"
|
||||||
|
echo "Qt frontend needs a display even in batch mode."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
./scripts/build-wii-iso.sh
|
||||||
|
|
||||||
|
DUSK_TEST_DOLPHIN_SECONDS="${DUSK_TEST_DOLPHIN_SECONDS:-20}"
|
||||||
|
ISO="build-wii-iso/Dusk-NTSC-U.iso"
|
||||||
|
|
||||||
|
echo "Booting $ISO in Dolphin for up to ${DUSK_TEST_DOLPHIN_SECONDS}s (batch mode, headless via Xvfb)..."
|
||||||
|
|
||||||
|
# Dolphin's batch mode (-b -e) runs the disc until told to stop -- there's
|
||||||
|
# no "the game exited cleanly" signal to wait for, so we bound it with
|
||||||
|
# timeout and treat "still running when the clock ran out" as success.
|
||||||
|
set +e
|
||||||
|
xvfb-run -a timeout "${DUSK_TEST_DOLPHIN_SECONDS}" "$DOLPHIN_BIN" -b -e "$ISO"
|
||||||
|
status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# timeout returns 124 when it had to kill Dolphin after the duration
|
||||||
|
# elapsed -- that means the disc booted and kept running, the expected
|
||||||
|
# smoke-test-passed outcome, not a failure. Any other non-zero status
|
||||||
|
# means Dolphin crashed or refused to boot the disc.
|
||||||
|
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
|
||||||
|
echo "Dolphin exited with unexpected status $status -- treating as a failure."
|
||||||
|
exit "$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wii smoke test passed (Dolphin ran $ISO for ${DUSK_TEST_DOLPHIN_SECONDS}s without crashing)."
|
||||||
+1
-5
@@ -4,6 +4,7 @@
|
|||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
add_subdirectory(dusk)
|
add_subdirectory(dusk)
|
||||||
|
add_subdirectory(duskrpg)
|
||||||
|
|
||||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||||
add_subdirectory(dusklinux)
|
add_subdirectory(dusklinux)
|
||||||
@@ -15,11 +16,6 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
|||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
add_subdirectory(duskgl)
|
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")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||||
add_subdirectory(duskdolphin)
|
add_subdirectory(duskdolphin)
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -32,6 +32,15 @@ if(NOT yyjson_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(NOT jerryscript_FOUND)
|
||||||
|
find_package(jerryscript REQUIRED)
|
||||||
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
jerryscript::core
|
||||||
|
jerryscript::ext
|
||||||
|
jerryscript::port
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(DUSK_BACKTRACE)
|
if(DUSK_BACKTRACE)
|
||||||
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
|
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
|
||||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||||
@@ -58,17 +67,20 @@ add_subdirectory(assert)
|
|||||||
add_subdirectory(asset)
|
add_subdirectory(asset)
|
||||||
add_subdirectory(console)
|
add_subdirectory(console)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
|
add_subdirectory(entity)
|
||||||
add_subdirectory(log)
|
add_subdirectory(log)
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
add_subdirectory(error)
|
add_subdirectory(error)
|
||||||
|
add_subdirectory(game)
|
||||||
add_subdirectory(input)
|
add_subdirectory(input)
|
||||||
add_subdirectory(locale)
|
add_subdirectory(locale)
|
||||||
add_subdirectory(rpg)
|
|
||||||
add_subdirectory(scene)
|
add_subdirectory(scene)
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(network)
|
add_subdirectory(network)
|
||||||
|
add_subdirectory(physics)
|
||||||
add_subdirectory(save)
|
add_subdirectory(save)
|
||||||
|
add_subdirectory(script)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
add_subdirectory(thread)
|
add_subdirectory(thread)
|
||||||
@@ -6,5 +6,7 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
easing.c
|
easing.c
|
||||||
|
keyframe.c
|
||||||
|
keyframeset.c
|
||||||
animation.c
|
animation.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,48 +5,70 @@
|
|||||||
|
|
||||||
#include "animation.h"
|
#include "animation.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
|
#include "time/time.h"
|
||||||
|
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t **tracks,
|
||||||
uint16_t keyframeCount
|
uint16_t *trackCounts,
|
||||||
|
uint16_t trackCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
|
||||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
|
||||||
|
|
||||||
anim->keyframes = keyframes;
|
keyframeSetInit(
|
||||||
anim->keyframeCount = keyframeCount;
|
&anim->keyframes, tracks, trackCounts, NULL, NULL, trackCount, NULL
|
||||||
|
);
|
||||||
|
anim->time = 0.0f;
|
||||||
|
anim->speed = 1.0f;
|
||||||
|
anim->loop = false;
|
||||||
|
anim->playing = false;
|
||||||
|
anim->eventTime = 0.0f;
|
||||||
|
anim->eventCallback = NULL;
|
||||||
|
anim->eventUser = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t animationGetValue(animation_t *anim, const float_t time) {
|
void animationUpdate(animation_t *anim) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
if(!anim->playing) return;
|
||||||
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
|
||||||
assertTrue(time >= 0, "Time must be non-negative.");
|
|
||||||
|
|
||||||
keyframe_t *start;
|
float_t duration = keyframeSetGetDuration(&anim->keyframes);
|
||||||
keyframe_t *end;
|
float_t prevTime = anim->time;
|
||||||
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
anim->time += TIME.delta * anim->speed;
|
||||||
keyframe_t *current = anim->keyframes;
|
|
||||||
start = current;
|
|
||||||
|
|
||||||
do {
|
// Loops that overshoot duration wrap back into [0, duration) -- an event
|
||||||
if(current->time > time) {
|
// near the end must still be checked across that wrap, as two segments:
|
||||||
end = current;
|
// (prevTime, duration] then [0, newTime].
|
||||||
break;
|
bool_t wrapped = anim->loop && duration > 0.0f && anim->time >= duration;
|
||||||
|
|
||||||
|
if(anim->loop) {
|
||||||
|
if(duration > 0.0f) anim->time = mathModFloat(anim->time, duration);
|
||||||
|
} else if(anim->time >= duration) {
|
||||||
|
anim->time = duration;
|
||||||
|
anim->playing = false;
|
||||||
}
|
}
|
||||||
start = current;
|
|
||||||
current++;
|
|
||||||
|
|
||||||
if(current > last) {
|
if(!anim->eventCallback) return;
|
||||||
end = start;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while(true);
|
|
||||||
|
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
bool_t crossed = wrapped
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
? (prevTime < anim->eventTime || anim->time >= anim->eventTime)
|
||||||
|
: (prevTime < anim->eventTime && anim->time >= anim->eventTime);
|
||||||
|
if(crossed) anim->eventCallback(anim, anim->eventUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
void animationSetEvent(
|
||||||
|
animation_t *anim,
|
||||||
|
const float_t time,
|
||||||
|
const animationeventcallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
|
anim->eventTime = time;
|
||||||
|
anim->eventCallback = callback;
|
||||||
|
anim->eventUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex) {
|
||||||
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
|
return keyframeSetGetValue(&anim->keyframes, trackIndex, anim->time);
|
||||||
}
|
}
|
||||||
@@ -4,31 +4,99 @@
|
|||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "keyframe.h"
|
#include "keyframeset.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct animation_t animation_t;
|
||||||
keyframe_t *keyframes;
|
|
||||||
uint16_t keyframeCount;
|
/**
|
||||||
|
* Callback fired once when animationUpdate() advances time past
|
||||||
|
* eventTime (see animationSetEvent()).
|
||||||
|
*
|
||||||
|
* @param anim The animation whose event fired.
|
||||||
|
* @param user The user pointer passed to animationSetEvent().
|
||||||
|
*/
|
||||||
|
typedef void (*animationeventcallback_t)(animation_t *anim, void *user);
|
||||||
|
|
||||||
|
typedef struct animation_t {
|
||||||
|
/** The animation's tracks/channels and their raw keyframe data. */
|
||||||
|
keyframeset_t keyframes;
|
||||||
|
|
||||||
|
/** Current playback position, in seconds. */
|
||||||
|
float_t time;
|
||||||
|
|
||||||
|
/** Playback rate multiplier; 1.0 = normal speed. */
|
||||||
|
float_t speed;
|
||||||
|
|
||||||
|
/** True if animationUpdate() should wrap time back to 0 on reaching the
|
||||||
|
* final keyframe, rather than holding there and clearing playing. */
|
||||||
|
bool_t loop;
|
||||||
|
|
||||||
|
/** True while animationUpdate() should advance time each call. */
|
||||||
|
bool_t playing;
|
||||||
|
|
||||||
|
/** Time (seconds) at which eventCallback fires; see animationSetEvent(). */
|
||||||
|
float_t eventTime;
|
||||||
|
/** Callback fired once when time advances past eventTime, or NULL. */
|
||||||
|
animationeventcallback_t eventCallback;
|
||||||
|
/** User pointer passed to eventCallback unchanged. */
|
||||||
|
void *eventUser;
|
||||||
} animation_t;
|
} animation_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an animation.
|
* Initializes an animation: time 0, speed 1.0, not looping, not playing.
|
||||||
|
* See keyframeSetInit() -- tracks/trackCounts and the keyframe_t arrays
|
||||||
|
* they point to are not copied, and must outlive this animation.
|
||||||
*
|
*
|
||||||
* @param anim The animation to initialize.
|
* @param anim The animation to initialize.
|
||||||
* @param keyframes The keyframes to use for the animation.
|
* @param tracks Array of trackCount keyframe_t arrays.
|
||||||
* @param keyframeCount The number of keyframes in the animation.
|
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
||||||
|
* @param trackCount The number of tracks/channels in this animation.
|
||||||
*/
|
*/
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t **tracks,
|
||||||
uint16_t keyframeCount
|
uint16_t *trackCounts,
|
||||||
|
uint16_t trackCount
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of the animation at a given time.
|
* Advances an animation's time by TIME.delta * speed. No-op if not
|
||||||
|
* playing. Duration is the latest of every track's final keyframe time
|
||||||
|
* (see keyframeSetGetDuration()). If loop is set, time wraps back into
|
||||||
|
* [0, duration) on reaching it; otherwise time is clamped there and
|
||||||
|
* playing is cleared.
|
||||||
|
*
|
||||||
|
* If eventCallback is set, it fires once when this call advances time
|
||||||
|
* from at or before eventTime to after it (checked across the loop wrap
|
||||||
|
* point too, so an event near the end of a looping animation still fires
|
||||||
|
* every loop).
|
||||||
|
*
|
||||||
|
* @param anim The animation to update.
|
||||||
|
*/
|
||||||
|
void animationUpdate(animation_t *anim);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets (or clears, with callback NULL) the single time-triggered event
|
||||||
|
* fired by animationUpdate().
|
||||||
|
*
|
||||||
|
* @param anim The animation to set the event on.
|
||||||
|
* @param time The time, in seconds, at which callback fires.
|
||||||
|
* @param callback The callback to invoke, or NULL to clear any event.
|
||||||
|
* @param user Arbitrary pointer forwarded to callback unchanged.
|
||||||
|
*/
|
||||||
|
void animationSetEvent(
|
||||||
|
animation_t *anim,
|
||||||
|
const float_t time,
|
||||||
|
const animationeventcallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of one of the animation's tracks at its current
|
||||||
|
* playback time.
|
||||||
*
|
*
|
||||||
* @param anim The animation to get the value from.
|
* @param anim The animation to get the value from.
|
||||||
* @param time The time at which to get the value, in seconds.
|
* @param trackIndex The track to evaluate, in [0, trackCount).
|
||||||
* @return The value of the animation at the given time.
|
* @return The interpolated value of that track at the current time.
|
||||||
*/
|
*/
|
||||||
float_t animationGetValue(animation_t *anim, const float_t time);
|
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
#include "keyframe.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
|
||||||
|
float_t keyframeGetValue(
|
||||||
|
keyframe_t *keyframes,
|
||||||
|
const uint16_t keyframeCount,
|
||||||
|
const float_t time
|
||||||
|
) {
|
||||||
|
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||||
|
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||||
|
assertTrue(time >= 0, "Time must be non-negative.");
|
||||||
|
|
||||||
|
keyframe_t *first = keyframes;
|
||||||
|
keyframe_t *last = keyframes + keyframeCount - 1;
|
||||||
|
|
||||||
|
// Clamp to the boundary keyframes' values directly rather than
|
||||||
|
// interpolating -- start == end at either boundary would otherwise
|
||||||
|
// divide by zero.
|
||||||
|
if(time <= first->time) return first->value;
|
||||||
|
if(time >= last->time) return last->value;
|
||||||
|
|
||||||
|
keyframe_t *start = first;
|
||||||
|
keyframe_t *end;
|
||||||
|
keyframe_t *current = first;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(current->time > time) {
|
||||||
|
end = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = current;
|
||||||
|
current++;
|
||||||
|
} while(true);
|
||||||
|
|
||||||
|
float_t t = (time - start->time) / (end->time - start->time);
|
||||||
|
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||||
|
}
|
||||||
@@ -11,3 +11,17 @@ typedef struct {
|
|||||||
float_t value;
|
float_t value;
|
||||||
easingtype_t easing;
|
easingtype_t easing;
|
||||||
} keyframe_t;
|
} keyframe_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the eased, interpolated value of a keyframe array at a given time.
|
||||||
|
*
|
||||||
|
* @param keyframes The keyframes to evaluate, ascending by time.
|
||||||
|
* @param keyframeCount The number of keyframes.
|
||||||
|
* @param time The time at which to get the value, in seconds.
|
||||||
|
* @return The interpolated value at the given time.
|
||||||
|
*/
|
||||||
|
float_t keyframeGetValue(
|
||||||
|
keyframe_t *keyframes,
|
||||||
|
const uint16_t keyframeCount,
|
||||||
|
const float_t time
|
||||||
|
);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
#include "keyframeset.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void keyframeSetInit(
|
||||||
|
keyframeset_t *set,
|
||||||
|
keyframe_t **tracks,
|
||||||
|
uint16_t *trackCounts,
|
||||||
|
keyframesetcallback_t *callbacks,
|
||||||
|
void **users,
|
||||||
|
uint16_t trackCount,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
|
assertNotNull(tracks, "Tracks pointer cannot be null.");
|
||||||
|
assertNotNull(trackCounts, "Track counts pointer cannot be null.");
|
||||||
|
assertTrue(trackCount > 0, "Track count must be more than 0.");
|
||||||
|
|
||||||
|
set->tracks = tracks;
|
||||||
|
set->trackCounts = trackCounts;
|
||||||
|
set->trackCount = trackCount;
|
||||||
|
set->callbacks = callbacks;
|
||||||
|
set->users = users;
|
||||||
|
set->user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t keyframeSetGetValue(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const uint16_t trackIndex,
|
||||||
|
const float_t time
|
||||||
|
) {
|
||||||
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
|
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
|
||||||
|
|
||||||
|
return keyframeGetValue(
|
||||||
|
set->tracks[trackIndex], set->trackCounts[trackIndex], time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void keyframeSetGetValues(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const float_t time,
|
||||||
|
float_t *outValues
|
||||||
|
) {
|
||||||
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
|
assertNotNull(outValues, "Output values pointer cannot be null.");
|
||||||
|
|
||||||
|
for(uint16_t i = 0; i < set->trackCount; i++) {
|
||||||
|
outValues[i] = keyframeGetValue(set->tracks[i], set->trackCounts[i], time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t keyframeSetGetDuration(keyframeset_t *set) {
|
||||||
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
|
|
||||||
|
float_t duration = 0.0f;
|
||||||
|
for(uint16_t i = 0; i < set->trackCount; i++) {
|
||||||
|
float_t trackDuration = set->tracks[i][set->trackCounts[i] - 1].time;
|
||||||
|
if(trackDuration > duration) duration = trackDuration;
|
||||||
|
}
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex) {
|
||||||
|
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
||||||
|
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
|
||||||
|
|
||||||
|
if(!set->callbacks || !set->callbacks[trackIndex]) return;
|
||||||
|
set->callbacks[trackIndex](
|
||||||
|
set, trackIndex, set->users ? set->users[trackIndex] : NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "keyframe.h"
|
||||||
|
|
||||||
|
typedef struct keyframeset_t keyframeset_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback associated with one track of a keyframe set (see
|
||||||
|
* keyframeSetFireCallback()).
|
||||||
|
*
|
||||||
|
* @param set The keyframe set the track belongs to.
|
||||||
|
* @param trackIndex The track this callback is registered for.
|
||||||
|
* @param user The per-track user pointer passed to keyframeSetInit().
|
||||||
|
*/
|
||||||
|
typedef void (*keyframesetcallback_t)(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const uint16_t trackIndex,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of N parallel keyframe tracks (e.g. one per animated channel --
|
||||||
|
* position.x/y/z, bone rotations, etc.) sharing a single timeline. Each
|
||||||
|
* track is independent: its own keyframe array and count, evaluated at
|
||||||
|
* whatever time is asked for.
|
||||||
|
*/
|
||||||
|
typedef struct keyframeset_t {
|
||||||
|
/** Caller-owned array of trackCount keyframe_t arrays. */
|
||||||
|
keyframe_t **tracks;
|
||||||
|
/** Caller-owned array of trackCount counts, one per entry in tracks. */
|
||||||
|
uint16_t *trackCounts;
|
||||||
|
uint16_t trackCount;
|
||||||
|
|
||||||
|
/** Caller-owned array of trackCount callbacks, one per track. Entries
|
||||||
|
* (or the whole array) may be NULL for tracks with no callback. */
|
||||||
|
keyframesetcallback_t *callbacks;
|
||||||
|
/** Caller-owned array of trackCount user pointers, one per track,
|
||||||
|
* passed to that track's callbacks[] entry. May be NULL. */
|
||||||
|
void **users;
|
||||||
|
/** Extra user data for the set as a whole, not tied to any one track. */
|
||||||
|
void *user;
|
||||||
|
} keyframeset_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a keyframe set. None of the passed-in arrays (or the
|
||||||
|
* keyframe_t arrays tracks points to) are copied -- they must outlive
|
||||||
|
* this keyframeset_t.
|
||||||
|
*
|
||||||
|
* @param set The keyframe set to initialize.
|
||||||
|
* @param tracks Array of trackCount keyframe_t arrays.
|
||||||
|
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
||||||
|
* @param callbacks Array of trackCount callbacks, one per track, or NULL
|
||||||
|
* if no track has one.
|
||||||
|
* @param users Array of trackCount user pointers, matching callbacks, or
|
||||||
|
* NULL.
|
||||||
|
* @param trackCount The number of tracks.
|
||||||
|
* @param user Extra user data for the set as a whole; may be NULL.
|
||||||
|
*/
|
||||||
|
void keyframeSetInit(
|
||||||
|
keyframeset_t *set,
|
||||||
|
keyframe_t **tracks,
|
||||||
|
uint16_t *trackCounts,
|
||||||
|
keyframesetcallback_t *callbacks,
|
||||||
|
void **users,
|
||||||
|
uint16_t trackCount,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of a single track at a given time.
|
||||||
|
*
|
||||||
|
* @param set The keyframe set to evaluate.
|
||||||
|
* @param trackIndex The track to evaluate, in [0, set->trackCount).
|
||||||
|
* @param time The time at which to get the value, in seconds.
|
||||||
|
* @return The interpolated value of that track at the given time.
|
||||||
|
*/
|
||||||
|
float_t keyframeSetGetValue(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const uint16_t trackIndex,
|
||||||
|
const float_t time
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of every track at a given time.
|
||||||
|
*
|
||||||
|
* @param set The keyframe set to evaluate.
|
||||||
|
* @param time The time at which to get the values, in seconds.
|
||||||
|
* @param outValues Destination array of at least set->trackCount floats.
|
||||||
|
*/
|
||||||
|
void keyframeSetGetValues(
|
||||||
|
keyframeset_t *set,
|
||||||
|
const float_t time,
|
||||||
|
float_t *outValues
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the set's duration: the latest final-keyframe time across every
|
||||||
|
* track, i.e. how long it takes for every track to finish.
|
||||||
|
*
|
||||||
|
* @param set The keyframe set to measure.
|
||||||
|
* @return The set's duration, in seconds.
|
||||||
|
*/
|
||||||
|
float_t keyframeSetGetDuration(keyframeset_t *set);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes a track's callback, if it (and its callbacks array) is set.
|
||||||
|
* No-op otherwise.
|
||||||
|
*
|
||||||
|
* @param set The keyframe set the track belongs to.
|
||||||
|
* @param trackIndex The track whose callback to invoke.
|
||||||
|
*/
|
||||||
|
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex);
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
|
||||||
|
// Release-style CMake configs (Release, RelWithDebInfo, MinSizeRel) define
|
||||||
|
// NDEBUG automatically; fake assertions there rather than requiring every
|
||||||
|
// platform's CMakeLists to set DUSK_ASSERTIONS_FAKED itself.
|
||||||
|
#if defined(NDEBUG) && !defined(DUSK_ASSERTIONS_FAKED)
|
||||||
|
#define DUSK_ASSERTIONS_FAKED
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef DUSK_TEST_ASSERT
|
#ifdef DUSK_TEST_ASSERT
|
||||||
#include <cmocka.h>
|
#include <cmocka.h>
|
||||||
|
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(locale)
|
add_subdirectory(locale)
|
||||||
add_subdirectory(json)
|
add_subdirectory(json)
|
||||||
add_subdirectory(chunk)
|
|
||||||
add_subdirectory(dmf)
|
add_subdirectory(dmf)
|
||||||
|
add_subdirectory(animation)
|
||||||
+1
-1
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
cutscenestartbattle.c
|
assetanimationloader.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "assetanimationloader.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "asset/loader/assetloading.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
|
||||||
|
errorret_t assetAnimationLoaderAsync(assetloading_t *loading) {
|
||||||
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
|
assertNotMainThread("Async loader should not be on main thread.");
|
||||||
|
|
||||||
|
if(
|
||||||
|
loading->loading.animation.state != ASSET_ANIMATION_LOADING_STATE_READ_FILE
|
||||||
|
) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(loading->loading.animation.buffer, "Buffer already defined?");
|
||||||
|
|
||||||
|
assetfile_t *file = &loading->loading.animation.file;
|
||||||
|
assetLoaderErrorChain(
|
||||||
|
loading, assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||||
|
);
|
||||||
|
|
||||||
|
if(file->size > ASSET_ANIMATION_FILE_SIZE_MAX) {
|
||||||
|
assetLoaderErrorThrow(
|
||||||
|
loading, "Animation JSON exceeds maximum allowed size"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *buffer;
|
||||||
|
size_t size;
|
||||||
|
assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
|
||||||
|
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||||
|
|
||||||
|
loading->loading.animation.buffer = buffer;
|
||||||
|
loading->loading.animation.size = size;
|
||||||
|
loading->loading.animation.state = ASSET_ANIMATION_LOADING_STATE_PARSE;
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationLoaderSync(assetloading_t *loading) {
|
||||||
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
|
assertTrue(loading->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
|
||||||
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
|
switch(loading->loading.animation.state) {
|
||||||
|
case ASSET_ANIMATION_LOADING_STATE_INITIAL:
|
||||||
|
loading->loading.animation.state =
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_READ_FILE;
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||||
|
errorOk();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ASSET_ANIMATION_LOADING_STATE_PARSE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *buffer = loading->loading.animation.buffer;
|
||||||
|
assertNotNull(buffer, "Animation buffer should have been loaded by now.");
|
||||||
|
|
||||||
|
yyjson_doc *doc = yyjson_read(
|
||||||
|
(char *)buffer,
|
||||||
|
loading->loading.animation.size,
|
||||||
|
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
|
||||||
|
);
|
||||||
|
memoryFree(buffer);
|
||||||
|
loading->loading.animation.buffer = NULL;
|
||||||
|
|
||||||
|
if(!doc) assetLoaderErrorThrow(loading, "Failed to parse animation JSON");
|
||||||
|
|
||||||
|
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||||
|
yyjson_val *channelsVal = yyjson_obj_get(root, "channels");
|
||||||
|
if(!channelsVal || !yyjson_is_arr(channelsVal)) {
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
assetLoaderErrorThrow(loading, "Animation JSON missing 'channels' array");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t channelCount = (uint16_t)yyjson_arr_size(channelsVal);
|
||||||
|
if(channelCount == 0) {
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
assetLoaderErrorThrow(loading, "Animation must have at least one channel");
|
||||||
|
}
|
||||||
|
|
||||||
|
keyframe_t **tracks = memoryAllocate(channelCount * sizeof(keyframe_t *));
|
||||||
|
uint16_t *trackCounts = memoryAllocate(channelCount * sizeof(uint16_t));
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *channelJson;
|
||||||
|
uint16_t parsed = 0;
|
||||||
|
yyjson_arr_foreach(channelsVal, idx, max, channelJson) {
|
||||||
|
keyframe_t *keyframes;
|
||||||
|
uint16_t count;
|
||||||
|
errorret_t ret =
|
||||||
|
assetAnimationParseChannel(channelJson, &keyframes, &count);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
assetAnimationFreeChannels(tracks, parsed);
|
||||||
|
memoryFree(trackCounts);
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorChain(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks[idx] = keyframes;
|
||||||
|
trackCounts[idx] = count;
|
||||||
|
parsed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
animationInit(
|
||||||
|
&loading->entry->data.animation, tracks, trackCounts, channelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
yyjson_val *loopVal = yyjson_obj_get(root, "loop");
|
||||||
|
if(loopVal) loading->entry->data.animation.loop = yyjson_get_bool(loopVal);
|
||||||
|
|
||||||
|
yyjson_val *speedVal = yyjson_obj_get(root, "speed");
|
||||||
|
if(speedVal) {
|
||||||
|
loading->entry->data.animation.speed = (float_t)yyjson_get_num(speedVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
yyjson_doc_free(doc);
|
||||||
|
|
||||||
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationDispose(assetentry_t *entry) {
|
||||||
|
assertNotNull(entry, "Asset entry cannot be NULL");
|
||||||
|
assertTrue(entry->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
|
||||||
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
|
// A load that failed before animationInit() ran (e.g. a parse error)
|
||||||
|
// never populated these -- nothing to free in that case.
|
||||||
|
keyframeset_t *set = &entry->data.animation.keyframes;
|
||||||
|
if(set->tracks) {
|
||||||
|
assetAnimationFreeChannels(set->tracks, set->trackCount);
|
||||||
|
memoryFree(set->trackCounts);
|
||||||
|
set->tracks = NULL;
|
||||||
|
set->trackCounts = NULL;
|
||||||
|
set->trackCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationParseEasing(
|
||||||
|
const char_t *name,
|
||||||
|
easingtype_t *outEasing
|
||||||
|
) {
|
||||||
|
if(stringEquals(name, "LINEAR")) *outEasing = EASING_LINEAR;
|
||||||
|
else if(stringEquals(name, "IN_SINE")) *outEasing = EASING_IN_SINE;
|
||||||
|
else if(stringEquals(name, "OUT_SINE")) *outEasing = EASING_OUT_SINE;
|
||||||
|
else if(stringEquals(name, "IN_OUT_SINE")) *outEasing = EASING_IN_OUT_SINE;
|
||||||
|
else if(stringEquals(name, "IN_QUAD")) *outEasing = EASING_IN_QUAD;
|
||||||
|
else if(stringEquals(name, "OUT_QUAD")) *outEasing = EASING_OUT_QUAD;
|
||||||
|
else if(stringEquals(name, "IN_OUT_QUAD")) *outEasing = EASING_IN_OUT_QUAD;
|
||||||
|
else if(stringEquals(name, "IN_CUBIC")) *outEasing = EASING_IN_CUBIC;
|
||||||
|
else if(stringEquals(name, "OUT_CUBIC")) *outEasing = EASING_OUT_CUBIC;
|
||||||
|
else if(stringEquals(name, "IN_OUT_CUBIC")) *outEasing = EASING_IN_OUT_CUBIC;
|
||||||
|
else if(stringEquals(name, "IN_QUART")) *outEasing = EASING_IN_QUART;
|
||||||
|
else if(stringEquals(name, "OUT_QUART")) *outEasing = EASING_OUT_QUART;
|
||||||
|
else if(stringEquals(name, "IN_OUT_QUART")) *outEasing = EASING_IN_OUT_QUART;
|
||||||
|
else if(stringEquals(name, "IN_BACK")) *outEasing = EASING_IN_BACK;
|
||||||
|
else if(stringEquals(name, "OUT_BACK")) *outEasing = EASING_OUT_BACK;
|
||||||
|
else if(stringEquals(name, "IN_OUT_BACK")) *outEasing = EASING_IN_OUT_BACK;
|
||||||
|
else errorThrow("Unknown easing type '%s'", name);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetAnimationParseChannel(
|
||||||
|
yyjson_val *channelJson,
|
||||||
|
keyframe_t **outKeyframes,
|
||||||
|
uint16_t *outCount
|
||||||
|
) {
|
||||||
|
if(!yyjson_is_arr(channelJson)) {
|
||||||
|
errorThrow("Animation channel must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t count = yyjson_arr_size(channelJson);
|
||||||
|
if(count == 0) {
|
||||||
|
errorThrow("Animation channel must have at least one keyframe");
|
||||||
|
}
|
||||||
|
|
||||||
|
keyframe_t *keyframes = memoryAllocate(count * sizeof(keyframe_t));
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *keyframeJson;
|
||||||
|
yyjson_arr_foreach(channelJson, idx, max, keyframeJson) {
|
||||||
|
yyjson_val *timeVal = yyjson_obj_get(keyframeJson, "time");
|
||||||
|
yyjson_val *valueVal = yyjson_obj_get(keyframeJson, "value");
|
||||||
|
if(!timeVal || !valueVal) {
|
||||||
|
memoryFree(keyframes);
|
||||||
|
errorThrow("Animation keyframe missing 'time' or 'value'");
|
||||||
|
}
|
||||||
|
|
||||||
|
keyframes[idx].time = (float_t)yyjson_get_num(timeVal);
|
||||||
|
keyframes[idx].value = (float_t)yyjson_get_num(valueVal);
|
||||||
|
|
||||||
|
yyjson_val *easingVal = yyjson_obj_get(keyframeJson, "easing");
|
||||||
|
if(!easingVal) {
|
||||||
|
keyframes[idx].easing = EASING_LINEAR;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t ret = assetAnimationParseEasing(
|
||||||
|
yyjson_get_str(easingVal), &keyframes[idx].easing
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
memoryFree(keyframes);
|
||||||
|
errorChain(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*outKeyframes = keyframes;
|
||||||
|
*outCount = (uint16_t)count;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count) {
|
||||||
|
for(uint16_t i = 0; i < count; i++) memoryFree(tracks[i]);
|
||||||
|
memoryFree(tracks);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* 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 "asset/assetfile.h"
|
||||||
|
#include "animation/animation.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
|
#define ASSET_ANIMATION_FILE_SIZE_MAX 1024*256
|
||||||
|
|
||||||
|
typedef struct assetloading_s assetloading_t;
|
||||||
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON animation file format:
|
||||||
|
* {
|
||||||
|
* "loop": false,
|
||||||
|
* "speed": 1.0,
|
||||||
|
* "channels": [
|
||||||
|
* [
|
||||||
|
* { "time": 0.0, "value": 0.0, "easing": "LINEAR" },
|
||||||
|
* { "time": 1.0, "value": 10.0 }
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* "loop" and "speed" are optional, defaulting to false and 1.0 (see
|
||||||
|
* animationInit()). "channels" is required and must have at least one
|
||||||
|
* entry; each entry is itself a non-empty array of keyframes, ascending
|
||||||
|
* by "time", sharing one timeline with every other channel (see
|
||||||
|
* keyframeset_t). Each keyframe requires "time" and "value"; "easing" is
|
||||||
|
* optional and defaults to "LINEAR" (see easingtype_t for the full set of
|
||||||
|
* names, e.g. "IN_QUAD", "OUT_BACK", "IN_OUT_CUBIC").
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_INITIAL,
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_READ_FILE,
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_PARSE,
|
||||||
|
ASSET_ANIMATION_LOADING_STATE_DONE
|
||||||
|
} assetanimationloadingstate_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
assetfile_t file;
|
||||||
|
assetanimationloadingstate_t state;
|
||||||
|
uint8_t *buffer;
|
||||||
|
size_t size;
|
||||||
|
} assetanimationloaderloading_t;
|
||||||
|
|
||||||
|
typedef animation_t assetanimationoutput_t;
|
||||||
|
|
||||||
|
errorret_t assetAnimationLoaderAsync(assetloading_t *loading);
|
||||||
|
errorret_t assetAnimationLoaderSync(assetloading_t *loading);
|
||||||
|
errorret_t assetAnimationDispose(assetentry_t *entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Maps a JSON easing name (e.g. "IN_OUT_QUAD") to its
|
||||||
|
* easingtype_t.
|
||||||
|
*
|
||||||
|
* @param name The easing name to parse.
|
||||||
|
* @param outEasing Destination for the parsed easing type.
|
||||||
|
* @return Error state; fails if name doesn't match any easingtype_t.
|
||||||
|
*/
|
||||||
|
errorret_t assetAnimationParseEasing(
|
||||||
|
const char_t *name,
|
||||||
|
easingtype_t *outEasing
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Parses one "channels" array entry into a heap-allocated
|
||||||
|
* keyframe_t array (memoryAllocate) -- freed later by
|
||||||
|
* assetAnimationFreeChannels() (on a parse failure) or
|
||||||
|
* assetAnimationDispose() (once loaded).
|
||||||
|
*
|
||||||
|
* @param channelJson The channel's JSON array of keyframe objects.
|
||||||
|
* @param outKeyframes Destination for the newly allocated keyframe array.
|
||||||
|
* @param outCount Destination for the number of keyframes parsed.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t assetAnimationParseChannel(
|
||||||
|
yyjson_val *channelJson,
|
||||||
|
keyframe_t **outKeyframes,
|
||||||
|
uint16_t *outCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Frees the first count entries of tracks (each a
|
||||||
|
* memoryAllocate'd keyframe_t array from assetAnimationParseChannel()),
|
||||||
|
* then frees tracks itself. Used both to unwind a partially-parsed
|
||||||
|
* channel list on failure and to free a fully-loaded animation's
|
||||||
|
* channels in assetAnimationDispose().
|
||||||
|
*
|
||||||
|
* @param tracks The channel array to free.
|
||||||
|
* @param count The number of entries in tracks to free.
|
||||||
|
*/
|
||||||
|
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count);
|
||||||
@@ -46,9 +46,9 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
|||||||
.dispose = assetJsonDispose
|
.dispose = assetJsonDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
[ASSET_LOADER_TYPE_CHUNK] = {
|
[ASSET_LOADER_TYPE_ANIMATION] = {
|
||||||
.loadSync = assetChunkLoaderSync,
|
.loadSync = assetAnimationLoaderSync,
|
||||||
.loadAsync = assetChunkLoaderAsync,
|
.loadAsync = assetAnimationLoaderAsync,
|
||||||
.dispose = assetChunkDispose
|
.dispose = assetAnimationDispose
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#include "asset/loader/display/assettilesetloader.h"
|
#include "asset/loader/display/assettilesetloader.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "asset/loader/json/assetjsonloader.h"
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "asset/loader/chunk/assetchunkloader.h"
|
#include "asset/loader/animation/assetanimationloader.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_LOADER_TYPE_NULL,
|
ASSET_LOADER_TYPE_NULL,
|
||||||
@@ -23,7 +23,7 @@ typedef enum {
|
|||||||
ASSET_LOADER_TYPE_TILESET,
|
ASSET_LOADER_TYPE_TILESET,
|
||||||
ASSET_LOADER_TYPE_LOCALE,
|
ASSET_LOADER_TYPE_LOCALE,
|
||||||
ASSET_LOADER_TYPE_JSON,
|
ASSET_LOADER_TYPE_JSON,
|
||||||
ASSET_LOADER_TYPE_CHUNK,
|
ASSET_LOADER_TYPE_ANIMATION,
|
||||||
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
ASSET_LOADER_TYPE_COUNT
|
||||||
} assetloadertype_t;
|
} assetloadertype_t;
|
||||||
@@ -35,7 +35,7 @@ typedef union {
|
|||||||
assettilesetloaderloading_t tileset;
|
assettilesetloaderloading_t tileset;
|
||||||
assetlocaleloaderloading_t locale;
|
assetlocaleloaderloading_t locale;
|
||||||
assetjsonloaderloading_t json;
|
assetjsonloaderloading_t json;
|
||||||
assetchunkloaderloading_t chunk;
|
assetanimationloaderloading_t animation;
|
||||||
} assetloaderloading_t;
|
} assetloaderloading_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -45,7 +45,7 @@ typedef union {
|
|||||||
assettilesetoutput_t tileset;
|
assettilesetoutput_t tileset;
|
||||||
assetlocaleoutput_t locale;
|
assetlocaleoutput_t locale;
|
||||||
assetjsonoutput_t json;
|
assetjsonoutput_t json;
|
||||||
assetchunkoutput_t chunk;
|
assetanimationoutput_t animation;
|
||||||
} assetloaderoutput_t;
|
} assetloaderoutput_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -53,7 +53,6 @@ typedef union {
|
|||||||
assettilesetloaderinput_t tileset;
|
assettilesetloaderinput_t tileset;
|
||||||
assetlocaleloaderinput_t locale;
|
assetlocaleloaderinput_t locale;
|
||||||
assetjsonloaderinput_t json;
|
assetjsonloaderinput_t json;
|
||||||
assetchunkloaderinput_t chunk;
|
|
||||||
} assetloaderinput_t;
|
} assetloaderinput_t;
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "assetchunkloader.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/endian.h"
|
|
||||||
#include "asset/loader/assetloading.h"
|
|
||||||
#include "asset/loader/assetentry.h"
|
|
||||||
#include "asset/loader/assetloader.h"
|
|
||||||
#include "asset/asset.h"
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
|
||||||
|
|
||||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
|
||||||
|
|
||||||
assetfile_t *file = &loading->loading.chunk.file;
|
|
||||||
assetLoaderErrorChain(loading,
|
|
||||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
|
||||||
);
|
|
||||||
|
|
||||||
uint8_t *data = memoryAllocate(file->size);
|
|
||||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
|
||||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
|
||||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
|
||||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
|
||||||
assertTrue(
|
|
||||||
file->lastRead == file->size,
|
|
||||||
"Failed to read entire chunk file."
|
|
||||||
);
|
|
||||||
|
|
||||||
loading->loading.chunk.data = data;
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
|
||||||
assertIsMainThread("Must be called from the main thread.");
|
|
||||||
|
|
||||||
assetchunkoutput_t *out = &loading->entry->data.chunk;
|
|
||||||
|
|
||||||
switch(loading->loading.chunk.state) {
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
|
||||||
errorOk();
|
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
|
||||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
|
||||||
uint8_t m = loading->loading.chunk.modelIndex;
|
|
||||||
if(out->modelEntries[m] == NULL) {
|
|
||||||
out->modelEntries[m] = assetLock(
|
|
||||||
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
|
|
||||||
);
|
|
||||||
assertNotNull(
|
|
||||||
out->modelEntries[m], "Failed to lock model."
|
|
||||||
);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
if(out->modelEntries[m]->state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
assetLoaderErrorThrow(
|
|
||||||
loading, "Model failed to load: %s", out->modelNames[m]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if(out->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
loading->loading.chunk.modelIndex++;
|
|
||||||
}
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
||||||
errorOk();
|
|
||||||
|
|
||||||
default:
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t *data = loading->loading.chunk.data;
|
|
||||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
|
||||||
|
|
||||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
|
||||||
memoryFree(data);
|
|
||||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
|
||||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
|
||||||
memoryFree(data);
|
|
||||||
assetLoaderErrorThrow(
|
|
||||||
loading, "Unsupported chunk file version %u", version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t offset = 8;
|
|
||||||
|
|
||||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
|
||||||
out->tiles = memoryAllocate(tileSize);
|
|
||||||
memoryCopy(out->tiles, data + offset, tileSize);
|
|
||||||
offset += tileSize;
|
|
||||||
|
|
||||||
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
|
|
||||||
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
|
|
||||||
*shape = endianLittleToHost32(*shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
out->meshCount = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
assertTrue(
|
|
||||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
|
||||||
"Chunk mesh count exceeds maximum."
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
|
||||||
uint8_t nameLen = 0;
|
|
||||||
while(
|
|
||||||
data[offset + nameLen] != '\0' &&
|
|
||||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
|
||||||
) {
|
|
||||||
nameLen++;
|
|
||||||
}
|
|
||||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
|
||||||
out->modelNames[m][nameLen] = '\0';
|
|
||||||
offset += nameLen + 1;
|
|
||||||
|
|
||||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
|
||||||
offset += sizeof(vec3);
|
|
||||||
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
|
|
||||||
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
|
|
||||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryFree(data);
|
|
||||||
loading->loading.chunk.data = NULL;
|
|
||||||
|
|
||||||
if(out->meshCount == 0) {
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
|
||||||
loading->loading.chunk.modelIndex = 0;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
|
||||||
assertNotNull(entry, "Entry cannot be NULL");
|
|
||||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
|
||||||
assertIsMainThread("Must be called from the main thread.");
|
|
||||||
|
|
||||||
assetchunkoutput_t *out = &entry->data.chunk;
|
|
||||||
|
|
||||||
if(out->tiles != NULL) {
|
|
||||||
memoryFree(out->tiles);
|
|
||||||
out->tiles = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
|
||||||
if(out->modelEntries[m] == NULL) continue;
|
|
||||||
assetUnlockEntry(out->modelEntries[m]);
|
|
||||||
out->modelEntries[m] = NULL;
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "asset/assetfile.h"
|
|
||||||
#include "rpg/overworld/chunk.h"
|
|
||||||
|
|
||||||
#define ASSET_CHUNK_FILE_VERSION 4
|
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
void *nothing;
|
|
||||||
} assetchunkloaderinput_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_DONE
|
|
||||||
} assetchunkloadingstate_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
assetfile_t file;
|
|
||||||
assetchunkloadingstate_t state;
|
|
||||||
uint8_t *data;
|
|
||||||
uint8_t modelIndex;
|
|
||||||
} assetchunkloaderloading_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
tile_t *tiles;
|
|
||||||
uint8_t meshCount;
|
|
||||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
|
||||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
|
||||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
|
||||||
} assetchunkoutput_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
|
||||||
* the loading buffer so the sync phase can parse without blocking the
|
|
||||||
* main thread on I/O.
|
|
||||||
*
|
|
||||||
* @param loading Loading information for the asset being loaded.
|
|
||||||
* @return Error code indicating success or failure of the load operation.
|
|
||||||
*/
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
|
||||||
* read by the async phase and populates the output assetchunkoutput_t with
|
|
||||||
* tile data and model paths.
|
|
||||||
*
|
|
||||||
* @param loading Loading information for the asset being loaded.
|
|
||||||
* @return Error code indicating success or failure of the load operation.
|
|
||||||
*/
|
|
||||||
errorret_t assetChunkLoaderSync(assetloading_t *loading);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposer for chunk assets.
|
|
||||||
*
|
|
||||||
* @param entry Asset entry containing the chunk data to dispose.
|
|
||||||
* @return Error code indicating success or failure of the dispose operation.
|
|
||||||
*/
|
|
||||||
errorret_t assetChunkDispose(assetentry_t *entry);
|
|
||||||
@@ -127,14 +127,6 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
errorChain(ret);
|
errorChain(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
|
||||||
// VBO owns the data now; CPU copy is no longer needed. The platform
|
|
||||||
// mesh object itself still needs meshDispose later - tracked via
|
|
||||||
// out->meshInitialized, independent of the CPU buffer's lifetime.
|
|
||||||
memoryFree(out->vertices);
|
|
||||||
out->vertices = NULL;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ void consolePrint(const char_t *message, ...) {
|
|||||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||||
);
|
);
|
||||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||||
|
CONSOLE.dirty = true;
|
||||||
|
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexUnlock(&CONSOLE.printMutex);
|
threadMutexUnlock(&CONSOLE.printMutex);
|
||||||
@@ -55,7 +56,7 @@ void consoleUpdate(void) {
|
|||||||
if(TIME.dynamicUpdate) return;
|
if(TIME.dynamicUpdate) return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(inputPressed(INPUT_ACTION_CONSOLE)) {
|
if(inputPressed(INPUT_BIND_CONSOLE)) {
|
||||||
CONSOLE.visible = !CONSOLE.visible;
|
CONSOLE.visible = !CONSOLE.visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ typedef struct {
|
|||||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||||
bool_t visible;
|
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
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadmutex_t printMutex;
|
threadmutex_t printMutex;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ mesh_t CUBE_MESH_SIMPLE;
|
|||||||
meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||||
|
|
||||||
errorret_t cubeInit() {
|
errorret_t cubeInit() {
|
||||||
vec3 min = { 0.0f, 0.0f, 0.0f };
|
vec3 min = { -0.5f, -0.5f, -0.5f };
|
||||||
vec3 max = { 1.0f, 1.0f, 1.0f };
|
vec3 max = { 0.5f, 0.5f, 0.5f };
|
||||||
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max);
|
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max);
|
||||||
errorChain(meshInit(
|
errorChain(meshInit(
|
||||||
&CUBE_MESH_SIMPLE,
|
&CUBE_MESH_SIMPLE,
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ extern mesh_t CUBE_MESH_SIMPLE;
|
|||||||
extern meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
extern meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the simple unit cube mesh (0,0,0) to (1,1,1).
|
* Initializes the simple unit cube mesh, centered at (0,0,0), spanning
|
||||||
|
* (-0.5,-0.5,-0.5) to (0.5,0.5,0.5) -- matching the centered convention
|
||||||
|
* physics shapes and the sphere mesh use (position = center).
|
||||||
*
|
*
|
||||||
* @return Error for initialization of the cube mesh.
|
* @return Error for initialization of the cube mesh.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ void planeBuffer(
|
|||||||
|
|
||||||
switch(axis) {
|
switch(axis) {
|
||||||
case PLANE_AXIS_XY: {
|
case PLANE_AXIS_XY: {
|
||||||
/* Flat in XY at z = min[2]; spans X and Y. */
|
// Flat in XY at z = min[2]; spans X and Y.
|
||||||
|
// +Z normal: CCW when viewed from +Z (matches cube.c's front face).
|
||||||
const float_t z = min[2];
|
const float_t z = min[2];
|
||||||
PLANE_VERT(0, min[0], min[1], z, u0, v0)
|
PLANE_VERT(0, min[0], min[1], z, u0, v0)
|
||||||
PLANE_VERT(1, max[0], min[1], z, u1, v0)
|
PLANE_VERT(1, max[0], min[1], z, u1, v0)
|
||||||
@@ -71,19 +72,24 @@ void planeBuffer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case PLANE_AXIS_XZ: {
|
case PLANE_AXIS_XZ: {
|
||||||
/* Flat in XZ at y = min[1]; spans X and Z. */
|
// Flat in XZ at y = min[1]; spans X and Z.
|
||||||
|
// +Y normal: CCW when viewed from +Y (matches cube.c's top face).
|
||||||
|
// X and Z swap handedness relative to XY/YZ (right-hand rule with Y
|
||||||
|
// up), so the corner order here is deliberately not a straight copy
|
||||||
|
// of the XY/YZ pattern -- copying it as-is would flip this to -Y.
|
||||||
const float_t y = min[1];
|
const float_t y = min[1];
|
||||||
PLANE_VERT(0, min[0], y, min[2], u0, v0)
|
PLANE_VERT(0, min[0], y, min[2], u0, v0)
|
||||||
PLANE_VERT(1, max[0], y, min[2], u1, v0)
|
PLANE_VERT(1, max[0], y, max[2], u1, v1)
|
||||||
PLANE_VERT(2, max[0], y, max[2], u1, v1)
|
PLANE_VERT(2, max[0], y, min[2], u1, v0)
|
||||||
PLANE_VERT(3, min[0], y, min[2], u0, v0)
|
PLANE_VERT(3, min[0], y, min[2], u0, v0)
|
||||||
PLANE_VERT(4, max[0], y, max[2], u1, v1)
|
PLANE_VERT(4, min[0], y, max[2], u0, v1)
|
||||||
PLANE_VERT(5, min[0], y, max[2], u0, v1)
|
PLANE_VERT(5, max[0], y, max[2], u1, v1)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case PLANE_AXIS_YZ: {
|
case PLANE_AXIS_YZ: {
|
||||||
/* Flat in YZ at x = min[0]; spans Y and Z. */
|
// Flat in YZ at x = min[0]; spans Y and Z.
|
||||||
|
// +X normal: CCW when viewed from +X (matches cube.c's right face).
|
||||||
const float_t x = min[0];
|
const float_t x = min[0];
|
||||||
PLANE_VERT(0, x, min[1], min[2], u0, v0)
|
PLANE_VERT(0, x, min[1], min[2], u0, v0)
|
||||||
PLANE_VERT(1, x, max[1], min[2], u1, v0)
|
PLANE_VERT(1, x, max[1], min[2], u1, v0)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
|
|||||||
|
|
||||||
errorret_t shaderBind(shader_t *shader) {
|
errorret_t shaderBind(shader_t *shader) {
|
||||||
assertNotNull(shader, "Shader cannot be null");
|
assertNotNull(shader, "Shader cannot be null");
|
||||||
|
if(bound == shader) errorOk();
|
||||||
errorChain(shaderBindPlatform(shader));
|
errorChain(shaderBindPlatform(shader));
|
||||||
bound = shader;
|
bound = shader;
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -39,3 +39,16 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
|||||||
sprite.uvMax[1] = uv[3];
|
sprite.uvMax[1] = uv[3];
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spritebatchsprite_t spriteBatchSpriteTranslate(
|
||||||
|
const spritebatchsprite_t *sprite,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
) {
|
||||||
|
spritebatchsprite_t out = *sprite;
|
||||||
|
out.min[0] += x;
|
||||||
|
out.min[1] += y;
|
||||||
|
out.max[0] += x;
|
||||||
|
out.max[1] += y;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,3 +38,19 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
|||||||
const float_t width,
|
const float_t width,
|
||||||
const float_t height
|
const float_t height
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of sprite translated by (x, y). Used to reposition a
|
||||||
|
* sprite cached relative to origin (0,0) at draw time, instead of
|
||||||
|
* re-deriving its geometry from scratch every frame.
|
||||||
|
*
|
||||||
|
* @param sprite The cached sprite, relative to origin (0,0).
|
||||||
|
* @param x X offset to translate by.
|
||||||
|
* @param y Y offset to translate by.
|
||||||
|
* @returns The translated sprite.
|
||||||
|
*/
|
||||||
|
spritebatchsprite_t spriteBatchSpriteTranslate(
|
||||||
|
const spritebatchsprite_t *sprite,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
font.c
|
||||||
text.c
|
text.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "font.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
|
||||||
|
font_t FONT_DEFAULT;
|
||||||
|
static texture_t FONT_DEFAULT_TEXTURE;
|
||||||
|
static tileset_t FONT_DEFAULT_TILESET;
|
||||||
|
|
||||||
|
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||||
|
FONT_DEFAULT_TILE_HEIGHT
|
||||||
|
] = {
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
|
||||||
|
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
|
||||||
|
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
|
||||||
|
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
|
||||||
|
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
|
||||||
|
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
|
||||||
|
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
|
||||||
|
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
|
||||||
|
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
|
||||||
|
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
|
||||||
|
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
|
||||||
|
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
|
||||||
|
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
|
||||||
|
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
|
||||||
|
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
|
||||||
|
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
|
||||||
|
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
|
||||||
|
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
|
||||||
|
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
|
||||||
|
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
|
||||||
|
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
|
||||||
|
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
|
||||||
|
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
|
||||||
|
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
|
||||||
|
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
|
||||||
|
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
|
||||||
|
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
|
||||||
|
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
|
||||||
|
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
|
||||||
|
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
||||||
|
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
||||||
|
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
|
||||||
|
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
||||||
|
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
||||||
|
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
|
||||||
|
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
|
||||||
|
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
|
||||||
|
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
|
||||||
|
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
|
||||||
|
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
|
||||||
|
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
|
||||||
|
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
|
||||||
|
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
||||||
|
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
|
||||||
|
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||||
|
};
|
||||||
|
|
||||||
|
errorret_t fontInitDefault(void) {
|
||||||
|
const int32_t width = (int32_t)mathNextPowTwo(
|
||||||
|
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
||||||
|
);
|
||||||
|
const int32_t height = (int32_t)mathNextPowTwo(
|
||||||
|
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
|
||||||
|
);
|
||||||
|
|
||||||
|
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
|
||||||
|
memoryZero(pixels, sizeof(color_t) * width * height);
|
||||||
|
|
||||||
|
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
|
||||||
|
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
|
||||||
|
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
|
||||||
|
|
||||||
|
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
|
||||||
|
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
|
||||||
|
|
||||||
|
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
|
||||||
|
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
|
||||||
|
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
|
||||||
|
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
|
||||||
|
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
|
||||||
|
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
|
||||||
|
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
|
||||||
|
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
|
||||||
|
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
|
||||||
|
|
||||||
|
const texturedata_t data = { .rgbaColors = pixels };
|
||||||
|
errorret_t textureResult = textureInit(
|
||||||
|
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
|
||||||
|
);
|
||||||
|
memoryFree(pixels);
|
||||||
|
errorChain(textureResult);
|
||||||
|
|
||||||
|
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
|
||||||
|
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t fontDisposeDefault(void) {
|
||||||
|
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
||||||
|
FONT_DEFAULT.texture = NULL;
|
||||||
|
FONT_DEFAULT.tileset = NULL;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
#include "display/texture/tileset.h"
|
#include "display/texture/tileset.h"
|
||||||
|
|
||||||
@@ -13,3 +14,53 @@ typedef struct {
|
|||||||
texture_t *texture;
|
texture_t *texture;
|
||||||
tileset_t *tileset;
|
tileset_t *tileset;
|
||||||
} font_t;
|
} font_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pixel width/height of a single default-font glyph tile. Matches the
|
||||||
|
* glyph grid baked into the (now retired) ui/minogram.png + .dtf asset
|
||||||
|
* pair this data was extracted from.
|
||||||
|
*/
|
||||||
|
#define FONT_DEFAULT_TILE_WIDTH 6
|
||||||
|
#define FONT_DEFAULT_TILE_HEIGHT 10
|
||||||
|
|
||||||
|
/** Grid layout of the generated default-font texture, in tiles. */
|
||||||
|
#define FONT_DEFAULT_COLUMNS 16
|
||||||
|
#define FONT_DEFAULT_ROWS 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
||||||
|
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
||||||
|
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
|
||||||
|
*/
|
||||||
|
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
||||||
|
|
||||||
|
extern font_t FONT_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hard coded bitmap data for the built-in default font, extracted
|
||||||
|
* pixel-for-pixel from the original ui/minogram.png glyph atlas (alpha
|
||||||
|
* >= 128 counts as set). Indexed [glyph][row], where glyph 0 corresponds
|
||||||
|
* to TEXT_CHAR_START ('!') and glyphs run consecutively through the
|
||||||
|
* printable ASCII range. Each row byte holds FONT_DEFAULT_TILE_WIDTH bit
|
||||||
|
* flags, one per pixel column: bit (FONT_DEFAULT_TILE_WIDTH - 1) is the
|
||||||
|
* leftmost pixel and bit 0 is the rightmost; 1 means the pixel is set,
|
||||||
|
* 0 means it is not.
|
||||||
|
*/
|
||||||
|
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||||
|
FONT_DEFAULT_TILE_HEIGHT
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the built-in default font (texture + tileset) directly from
|
||||||
|
* FONT_DEFAULT_GLYPHS, without going through the asset system.
|
||||||
|
*
|
||||||
|
* @return Either an error or success result.
|
||||||
|
*/
|
||||||
|
errorret_t fontInitDefault(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the built-in default font created by fontInitDefault.
|
||||||
|
*
|
||||||
|
* @return Either an error or success result.
|
||||||
|
*/
|
||||||
|
errorret_t fontDisposeDefault(void);
|
||||||
|
|||||||
@@ -9,34 +9,15 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "asset/asset.h"
|
|
||||||
#include "asset/loader/display/assettextureloader.h"
|
|
||||||
#include "asset/loader/display/assettilesetloader.h"
|
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
|
|
||||||
font_t FONT_DEFAULT;
|
|
||||||
|
|
||||||
errorret_t textInit(void) {
|
errorret_t textInit(void) {
|
||||||
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
|
errorChain(fontInitDefault());
|
||||||
assetentry_t *entryTexture = assetLock(
|
|
||||||
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
|
|
||||||
);
|
|
||||||
assetentry_t *entryTileset = assetLock(
|
|
||||||
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
|
||||||
);
|
|
||||||
errorChain(assetRequireLoaded(entryTexture));
|
|
||||||
errorChain(assetRequireLoaded(entryTileset));
|
|
||||||
|
|
||||||
FONT_DEFAULT.texture = &entryTexture->data.texture;
|
|
||||||
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t textDispose(void) {
|
errorret_t textDispose(void) {
|
||||||
FONT_DEFAULT.texture = NULL;
|
errorChain(fontDisposeDefault());
|
||||||
FONT_DEFAULT.tileset = NULL;
|
|
||||||
assetUnlock("ui/minogram.png");
|
|
||||||
assetUnlock("ui/minogram.dtf");
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +97,89 @@ errorret_t textDraw(
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t textBuildSpriteCache(
|
||||||
|
const char_t *text,
|
||||||
|
const font_t *font,
|
||||||
|
spritebatchsprite_t *sprites,
|
||||||
|
const uint32_t spritesMax,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
) {
|
||||||
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
assertNotNull(font, "Font cannot be NULL");
|
||||||
|
assertNotNull(sprites, "Sprites cannot be NULL");
|
||||||
|
assertNotNull(outWidth, "Output width cannot be NULL");
|
||||||
|
assertNotNull(outHeight, "Output height cannot be NULL");
|
||||||
|
|
||||||
|
uint32_t count = 0;
|
||||||
|
float_t posX = 0.0f;
|
||||||
|
float_t posY = 0.0f;
|
||||||
|
int32_t width = 0;
|
||||||
|
int32_t lineWidth = 0;
|
||||||
|
int32_t height = font->tileset->tileHeight;
|
||||||
|
|
||||||
|
char_t c;
|
||||||
|
int32_t i = 0;
|
||||||
|
while((c = text[i++]) != '\0') {
|
||||||
|
if(c == '\n') {
|
||||||
|
if(lineWidth > width) width = lineWidth;
|
||||||
|
lineWidth = 0;
|
||||||
|
posX = 0.0f;
|
||||||
|
posY += font->tileset->tileHeight;
|
||||||
|
height += font->tileset->tileHeight;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(c == ' ') {
|
||||||
|
posX += font->tileset->tileWidth;
|
||||||
|
lineWidth += font->tileset->tileWidth;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(count < spritesMax, "Text produces too many sprites");
|
||||||
|
sprites[count++] = textGetSprite((vec2){ posX, posY }, c, font);
|
||||||
|
|
||||||
|
posX += font->tileset->tileWidth;
|
||||||
|
lineWidth += font->tileset->tileWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lineWidth > width) width = lineWidth;
|
||||||
|
*outWidth = width;
|
||||||
|
*outHeight = height;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t textDrawSpriteCache(
|
||||||
|
const spritebatchsprite_t *sprites,
|
||||||
|
const uint32_t spriteCount,
|
||||||
|
spritebatchsprite_t *scratch,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const color_t color,
|
||||||
|
texture_t *texture
|
||||||
|
) {
|
||||||
|
assertNotNull(scratch, "Scratch buffer cannot be NULL");
|
||||||
|
assertNotNull(texture, "Texture cannot be NULL");
|
||||||
|
if(spriteCount == 0) errorOk();
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < spriteCount; i++) {
|
||||||
|
scratch[i] = sprites[i];
|
||||||
|
scratch[i].min[0] += x;
|
||||||
|
scratch[i].min[1] += y;
|
||||||
|
scratch[i].max[0] += x;
|
||||||
|
scratch[i].max[1] += y;
|
||||||
|
}
|
||||||
|
|
||||||
|
shadermaterial_t material = {
|
||||||
|
.unlit = {
|
||||||
|
.color = color,
|
||||||
|
.texture = texture
|
||||||
|
}
|
||||||
|
};
|
||||||
|
errorChain(spriteBatchBuffer(scratch, spriteCount, &SHADER_UNLIT, material));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
void textMeasure(
|
void textMeasure(
|
||||||
const char_t *text,
|
const char_t *text,
|
||||||
const font_t *font,
|
const font_t *font,
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
|
|
||||||
#define TEXT_CHAR_START '!'
|
#define TEXT_CHAR_START '!'
|
||||||
|
|
||||||
extern font_t FONT_DEFAULT;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the text system.
|
* Initializes the text system.
|
||||||
*
|
*
|
||||||
@@ -60,6 +58,54 @@ errorret_t textDraw(
|
|||||||
font_t *font
|
font_t *font
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a cache of sprites (glyph geometry + UVs, relative to origin
|
||||||
|
* 0,0) for a string of text. Callers that redraw the same text every
|
||||||
|
* frame (e.g. UI labels/widgets) should build this once and reuse it
|
||||||
|
* via textDrawSpriteCache, instead of re-deriving glyph geometry every
|
||||||
|
* frame the way textDraw does.
|
||||||
|
*
|
||||||
|
* @param text The null-terminated string to build sprites for.
|
||||||
|
* @param font Font to use for tile lookup.
|
||||||
|
* @param sprites Destination array to write sprites into.
|
||||||
|
* @param spritesMax Capacity of the sprites array.
|
||||||
|
* @param outWidth Pointer to store the measured width in pixels.
|
||||||
|
* @param outHeight Pointer to store the measured height in pixels.
|
||||||
|
* @return The number of sprites written.
|
||||||
|
*/
|
||||||
|
uint32_t textBuildSpriteCache(
|
||||||
|
const char_t *text,
|
||||||
|
const font_t *font,
|
||||||
|
spritebatchsprite_t *sprites,
|
||||||
|
const uint32_t spritesMax,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a previously-built sprite cache (see textBuildSpriteCache) at the
|
||||||
|
* given position in a single batched draw call.
|
||||||
|
*
|
||||||
|
* @param sprites Cached sprites, relative to origin 0,0.
|
||||||
|
* @param spriteCount Number of sprites in the cache.
|
||||||
|
* @param scratch Caller-owned scratch buffer, at least spriteCount
|
||||||
|
* entries, used to translate the cached sprites into position.
|
||||||
|
* @param x The x-coordinate to draw the text at.
|
||||||
|
* @param y The y-coordinate to draw the text at.
|
||||||
|
* @param color The color to draw the text in.
|
||||||
|
* @param texture The font's texture to sample glyphs from.
|
||||||
|
* @return Either an error or success result.
|
||||||
|
*/
|
||||||
|
errorret_t textDrawSpriteCache(
|
||||||
|
const spritebatchsprite_t *sprites,
|
||||||
|
const uint32_t spriteCount,
|
||||||
|
spritebatchsprite_t *scratch,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const color_t color,
|
||||||
|
texture_t *texture
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measures the width and height of the given text string when rendered.
|
* Measures the width and height of the given text string when rendered.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,16 +10,23 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "rpg/rpg.h"
|
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
#include "script/scriptmanager.h"
|
||||||
|
#include "script/module/scene/modulescene.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
|
#include "network/socket/client/client.h"
|
||||||
|
#include "network/socket/client/clientroster.h"
|
||||||
|
#include "network/socket/server/serveronline.h"
|
||||||
|
#include "game/game.h"
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savesettings.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -37,13 +44,15 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(systemInit());
|
errorChain(systemInit());
|
||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
// errorChain(saveInit());
|
errorChain(scriptManagerInit());
|
||||||
|
errorChain(saveInit());
|
||||||
|
errorChain(saveSettingsLoad());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
errorChain(rpgInit());
|
|
||||||
errorChain(networkInit());
|
errorChain(networkInit());
|
||||||
errorChain(sceneInit());
|
errorChain(sceneInit());
|
||||||
|
errorChain(gameInit());
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
|
|
||||||
@@ -53,8 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
consolePrint("Assertions real");
|
consolePrint("Assertions real");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
// sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -62,17 +70,30 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
|
errorChain(clientUpdate());
|
||||||
|
errorChain(serverOnlineUpdate());
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
|
|
||||||
|
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
||||||
|
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
errorChain(rpgUpdate());
|
|
||||||
|
errorChain(gameUpdate());
|
||||||
|
errorChain(moduleSceneUpdateCurrent());
|
||||||
errorChain(sceneUpdate());
|
errorChain(sceneUpdate());
|
||||||
errorChain(assetUpdate());
|
errorChain(assetUpdate());
|
||||||
errorChain(uiUpdate());
|
errorChain(uiUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
errorChain(displayUpdate());
|
errorChain(displayUpdate());
|
||||||
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
if(
|
||||||
|
dialogType == SYSTEM_DIALOG_TYPE_NONE &&
|
||||||
|
inputPressed(INPUT_BIND_RAGEQUIT)
|
||||||
|
) {
|
||||||
|
ENGINE.running = false;
|
||||||
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,14 +102,20 @@ void engineExit(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
|
errorChain(gameDispose());
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
|
|
||||||
|
errorChain(serverOnlineStop());
|
||||||
|
client_t *localClient = clientRosterFindLocal();
|
||||||
|
if(localClient) localClientDisconnect(&localClient->local);
|
||||||
|
|
||||||
errorChain(networkDispose());
|
errorChain(networkDispose());
|
||||||
errorChain(rpgDispose());
|
|
||||||
localeManagerDispose();
|
localeManagerDispose();
|
||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
// errorChain(saveDispose());
|
errorChain(saveDispose());
|
||||||
|
errorChain(scriptManagerDispose());
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ errorret_t engineUpdate(void);
|
|||||||
* Shuts down the engine.
|
* Shuts down the engine.
|
||||||
*/
|
*/
|
||||||
errorret_t engineDispose(void);
|
errorret_t engineDispose(void);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
|
entity.c
|
||||||
|
entitymanager.c
|
||||||
|
component.c
|
||||||
|
entityprefab.c
|
||||||
|
)
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(component)
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitymanager.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
||||||
|
[COMPONENT_TYPE_NULL] = { 0 },
|
||||||
|
|
||||||
|
#define X(enm, type, field, iMethod, dMethod, rMethod) \
|
||||||
|
[COMPONENT_TYPE_##enm] = { \
|
||||||
|
.enumName = #enm, \
|
||||||
|
.name = #field, \
|
||||||
|
.init = iMethod, \
|
||||||
|
.dispose = dMethod, \
|
||||||
|
.render = rMethod \
|
||||||
|
},
|
||||||
|
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
[COMPONENT_TYPE_COUNT] = { 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
void componentInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &mgr->components[index];
|
||||||
|
memoryZero(cmp, sizeof(component_t));
|
||||||
|
|
||||||
|
cmp->type = type;
|
||||||
|
if(COMPONENT_DEFINITIONS[type].init) {
|
||||||
|
COMPONENT_DEFINITIONS[type].init(mgr, entityId, componentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void * componentGetData(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &mgr->components[index];
|
||||||
|
assertTrue(cmp->type == type, "Component type mismatch");
|
||||||
|
|
||||||
|
return &cmp->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentindex_t componentGetIndex(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const componenttype_t type,
|
||||||
|
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||||
|
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
|
||||||
|
assertNotNull(outEntities, "Output entities array cannot be null");
|
||||||
|
assertNotNull(outComponents, "Output components array cannot be null");
|
||||||
|
|
||||||
|
entityid_t written = 0;
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
componentid_t used = mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + i
|
||||||
|
];
|
||||||
|
if(used == COMPONENT_ID_INVALID) continue;
|
||||||
|
assertTrue(
|
||||||
|
mgr->components[componentGetIndex(i, used)].type == type,
|
||||||
|
"Component type mismatch in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
(mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0,
|
||||||
|
"Inactive entity in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
used < ENTITY_COMPONENT_COUNT_MAX,
|
||||||
|
"Component ID OOB in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
|
||||||
|
"Component index OOB in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
outComponents[written] = used;
|
||||||
|
outEntities[written++] = i;
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t componentRenderAll(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
for(entityid_t eid = 0; eid < ENTITY_COUNT_MAX; eid++) {
|
||||||
|
if(!(mgr->entities[eid].state & ENTITY_STATE_ACTIVE)) continue;
|
||||||
|
for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) {
|
||||||
|
component_t *cmp = &mgr->components[componentGetIndex(eid, cid)];
|
||||||
|
if(cmp->type == COMPONENT_TYPE_NULL) continue;
|
||||||
|
if(!COMPONENT_DEFINITIONS[cmp->type].render) continue;
|
||||||
|
errorChain(COMPONENT_DEFINITIONS[cmp->type].render(mgr, eid, cid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void componentDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &mgr->components[index];
|
||||||
|
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
||||||
|
|
||||||
|
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
||||||
|
COMPONENT_DEFINITIONS[cmp->type].dispose(mgr, entityId, componentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp->type = COMPONENT_TYPE_NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define X(enumName, type, field, init, dispose, render) \
|
||||||
|
// do nothing
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
#define X(enumName, type, field, init, dispose, render) type field;
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
} componentdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback signature for a component's init/dispose hooks.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
typedef void (*componentcallback_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback signature for a component's render hook.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
typedef errorret_t (*componentcallbackerror_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char_t *enumName;
|
||||||
|
const char_t *name;
|
||||||
|
componentcallback_t init;
|
||||||
|
componentcallback_t dispose;
|
||||||
|
componentcallbackerror_t render;
|
||||||
|
} componentdefinition_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
COMPONENT_TYPE_NULL,
|
||||||
|
|
||||||
|
#define X(enumName, type, field, init, dispose, render) \
|
||||||
|
COMPONENT_TYPE_##enumName,
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
COMPONENT_TYPE_COUNT
|
||||||
|
} componenttype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
componenttype_t type;
|
||||||
|
componentdata_t data;
|
||||||
|
} component_t;
|
||||||
|
|
||||||
|
extern componentdefinition_t COMPONENT_DEFINITIONS[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a component of the given type for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param type The type of the component to initialize.
|
||||||
|
*/
|
||||||
|
void componentInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the pointer to the data of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param type The type of the component to get, only used for assertion.
|
||||||
|
* @return A pointer to the component data.
|
||||||
|
*/
|
||||||
|
void * componentGetData(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The index of the component in the component array.
|
||||||
|
*/
|
||||||
|
componentindex_t componentGetIndex(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity IDs of all entities with a component of the given type.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to search.
|
||||||
|
* @param type The type of the component to get entities for.
|
||||||
|
* @param outEntities An array to write the entity IDs to, must be at least
|
||||||
|
* ENTITY_COUNT_MAX in size.
|
||||||
|
* @param outComponents An array to write the component IDs to.
|
||||||
|
* @return The number of entity IDs written to outEntities.
|
||||||
|
*/
|
||||||
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const componenttype_t type,
|
||||||
|
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||||
|
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void componentDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the render callback on every active component that defines one.
|
||||||
|
* Iterates all active entities and all their component slots. No-op for
|
||||||
|
* components whose definition has render == NULL.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to render.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t componentRenderAll(entitymanager_t *mgr);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(display)
|
||||||
|
add_subdirectory(physics)
|
||||||
|
add_subdirectory(trigger)
|
||||||
|
add_subdirectory(animation)
|
||||||
+1
-1
@@ -6,5 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
inputvita.c
|
entityanimation.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityanimation.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
void entityAnimationInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
memoryZero(animComp, sizeof(entityanimation_t));
|
||||||
|
|
||||||
|
entityUpdateAdd(mgr, entityId, entityAnimationUpdate, componentId, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityanimation_t *entityAnimationGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_ANIMATION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationSetKeyframes(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
keyframe_t **channelTracks,
|
||||||
|
uint16_t *channelTrackCounts,
|
||||||
|
const uint16_t channelCount
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animationInit(
|
||||||
|
&animComp->anim, channelTracks, channelTrackCounts, channelCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationPlay(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.time = 0.0f;
|
||||||
|
animComp->anim.playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationStop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t entityAnimationIsPlaying(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
return animComp->anim.playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationSetLoop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const bool_t loop
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationSetSpeed(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const float_t speed
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.speed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t entityAnimationGetValue(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint16_t channelIndex
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
return animationGetValue(&animComp->anim, channelIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationUpdate(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animationUpdate(&animComp->anim);
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "animation/animation.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
animation_t anim;
|
||||||
|
} entityanimation_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the animation component: no keyframes set, and registers
|
||||||
|
* entityAnimationUpdate as an update callback. Call
|
||||||
|
* entityAnimationSetKeyframes() before entityAnimationPlay().
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityAnimationInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying animation structure (temporarily) for the given
|
||||||
|
* entity. Prefer the dedicated getters/setters where possible.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The animation component data for the given entity and
|
||||||
|
* component ID.
|
||||||
|
*/
|
||||||
|
entityanimation_t *entityAnimationGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the entity's keyframes, stopped, at speed 1.0, non-looping. See
|
||||||
|
* keyframeSetInit() -- channelTracks/channelTrackCounts and the
|
||||||
|
* keyframe_t arrays they point to are not copied, and must outlive this
|
||||||
|
* component (e.g. static/const arrays owned by the caller).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param channelTracks Array of channelCount keyframe_t arrays -- one per
|
||||||
|
* animated channel (e.g. position.x/y/z), sharing this animation's
|
||||||
|
* timeline.
|
||||||
|
* @param channelTrackCounts Array of channelCount keyframe counts,
|
||||||
|
* matching channelTracks.
|
||||||
|
* @param channelCount The number of channels.
|
||||||
|
*/
|
||||||
|
void entityAnimationSetKeyframes(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
keyframe_t **channelTracks,
|
||||||
|
uint16_t *channelTrackCounts,
|
||||||
|
const uint16_t channelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts (or restarts) playback from time 0.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityAnimationPlay(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops playback without resetting the current time.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityAnimationStop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the animation is currently playing.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return True if playing.
|
||||||
|
*/
|
||||||
|
bool_t entityAnimationIsPlaying(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the animation loops on reaching its final keyframe, rather
|
||||||
|
* than stopping there.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param loop True to loop, false to stop at the end.
|
||||||
|
*/
|
||||||
|
void entityAnimationSetLoop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const bool_t loop
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the animation's playback rate multiplier.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param speed The new playback rate; 1.0 = normal speed.
|
||||||
|
*/
|
||||||
|
void entityAnimationSetSpeed(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const float_t speed
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates one of the animation's channels at its current playback
|
||||||
|
* time, regardless of whether it's currently playing.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param channelIndex The channel to evaluate, in [0, channelCount) as
|
||||||
|
* passed to entityAnimationSetKeyframes().
|
||||||
|
* @return That channel's value at the current time.
|
||||||
|
*/
|
||||||
|
float_t entityAnimationGetValue(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint16_t channelIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-tick update for the animation component: calls animationUpdate()
|
||||||
|
* (a no-op if not playing). Registered automatically as an update
|
||||||
|
* callback by entityAnimationInit.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void entityAnimationUpdate(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
+3
-4
@@ -6,8 +6,7 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
npc.c
|
entityposition.c
|
||||||
npcturn.c
|
entitycamera.c
|
||||||
npcwalk.c
|
entityrenderable.c
|
||||||
npcpath.c
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "entity/entity.h"
|
||||||
|
#include "entity/component/display/entityposition.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
|
||||||
|
void entityCameraInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
cam->nearClip = 0.1f;
|
||||||
|
cam->farClip = 5000.0f;
|
||||||
|
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||||
|
cam->perspective.fov = glm_rad(45.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetProjection(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 out
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
|
||||||
|
if(
|
||||||
|
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
|
||||||
|
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
|
||||||
|
) {
|
||||||
|
glm_mat4_identity(out);
|
||||||
|
glm_perspective(
|
||||||
|
cam->perspective.fov,
|
||||||
|
SCREEN.aspect,
|
||||||
|
cam->nearClip,
|
||||||
|
cam->farClip,
|
||||||
|
out
|
||||||
|
);
|
||||||
|
|
||||||
|
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
|
||||||
|
out[1][1] *= -1.0f;
|
||||||
|
}
|
||||||
|
} else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||||
|
glm_mat4_identity(out);
|
||||||
|
glm_ortho(
|
||||||
|
cam->orthographic.left,
|
||||||
|
cam->orthographic.right,
|
||||||
|
cam->orthographic.top,
|
||||||
|
cam->orthographic.bottom,
|
||||||
|
cam->nearClip,
|
||||||
|
cam->farClip,
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityCameraGetCurrent(entitymanager_t *mgr) {
|
||||||
|
entityid_t camEnts[ENTITY_COUNT_MAX];
|
||||||
|
componentid_t camComps[ENTITY_COUNT_MAX];
|
||||||
|
entityid_t count = componentGetEntitiesWithComponent(
|
||||||
|
mgr, COMPONENT_TYPE_CAMERA, camEnts, camComps
|
||||||
|
);
|
||||||
|
if(count == 0) return ENTITY_ID_INVALID;
|
||||||
|
return camEnts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetForward(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
) {
|
||||||
|
componentid_t posComp = entityGetComponent(
|
||||||
|
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
mat4 transform;
|
||||||
|
entityPositionGetTransform(mgr, entityId, posComp, transform);
|
||||||
|
// transform is an object->world matrix; column 2 is the entity's local Z
|
||||||
|
// axis expressed in world space. Cameras look down their local -Z.
|
||||||
|
float_t fx = -transform[2][0];
|
||||||
|
float_t fz = -transform[2][2];
|
||||||
|
float_t len = sqrtf(fx * fx + fz * fz);
|
||||||
|
if(len > 1e-6f) { fx /= len; fz /= len; }
|
||||||
|
out[0] = fx;
|
||||||
|
out[1] = fz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetRight(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
) {
|
||||||
|
componentid_t posComp = entityGetComponent(
|
||||||
|
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
mat4 transform;
|
||||||
|
entityPositionGetTransform(mgr, entityId, posComp, transform);
|
||||||
|
// transform is an object->world matrix; column 0 is the entity's local X
|
||||||
|
// (right) axis expressed in world space.
|
||||||
|
float_t rx = transform[0][0];
|
||||||
|
float_t rz = transform[0][2];
|
||||||
|
float_t len = sqrtf(rx * rx + rz * rz);
|
||||||
|
if(len > 1e-6f) { rx /= len; rz /= len; }
|
||||||
|
out[0] = rx;
|
||||||
|
out[1] = rz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraLookAtPixelPerfect(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t ent,
|
||||||
|
const componentid_t posComp,
|
||||||
|
const componentid_t camComp,
|
||||||
|
const vec3 point,
|
||||||
|
const vec3 eyeOffset,
|
||||||
|
const float_t scale
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
mgr, ent, camComp, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
float_t dist = (
|
||||||
|
(float_t)SCREEN.height / (2.0f * scale * tanf(cam->perspective.fov * 0.5f))
|
||||||
|
);
|
||||||
|
|
||||||
|
vec3 eye = {
|
||||||
|
point[0] + eyeOffset[0],
|
||||||
|
point[1] + dist + eyeOffset[1],
|
||||||
|
point[2] + eyeOffset[2]
|
||||||
|
};
|
||||||
|
vec3 up = { 0.0f, 0.0f, -1.0f };
|
||||||
|
entityPositionLookAt(mgr, ent, posComp, eye, (float_t *)point, up);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
||||||
|
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED,
|
||||||
|
ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC
|
||||||
|
} entitycameraprojectiontype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float_t fov;
|
||||||
|
} perspective;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float_t left;
|
||||||
|
float_t right;
|
||||||
|
float_t top;
|
||||||
|
float_t bottom;
|
||||||
|
} orthographic;
|
||||||
|
};
|
||||||
|
|
||||||
|
float_t nearClip;
|
||||||
|
float_t farClip;
|
||||||
|
entitycameraprojectiontype_t projType;
|
||||||
|
} entitycamera_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity camera component.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityCameraInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders out the projection matrix for the given camera.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param out The output projection matrix.
|
||||||
|
*/
|
||||||
|
void entityCameraGetProjection(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 out
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the entity ID of the first active camera, or ENTITY_ID_INVALID if
|
||||||
|
* none are active.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to search.
|
||||||
|
*/
|
||||||
|
entityid_t entityCameraGetCurrent(entitymanager_t *mgr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the camera's horizontal forward direction (XZ plane) from its
|
||||||
|
* position component. Automatically finds the position component on the
|
||||||
|
* entity.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The camera entity ID.
|
||||||
|
* @param out Output vec2: {forwardX, forwardZ} normalized.
|
||||||
|
*/
|
||||||
|
void entityCameraGetForward(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the camera's horizontal right direction (XZ plane) from its position
|
||||||
|
* component. Automatically finds the position component on the entity.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The camera entity ID.
|
||||||
|
* @param out Output vec2: {rightX, rightZ} normalized.
|
||||||
|
*/
|
||||||
|
void entityCameraGetRight(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions the camera to look at a 3D point at a pixel-perfect distance
|
||||||
|
* derived from the camera's FOV and screen height.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param ent The camera entity ID.
|
||||||
|
* @param posComp The position component ID.
|
||||||
|
* @param camComp The camera component ID.
|
||||||
|
* @param point World position to look at.
|
||||||
|
* @param eyeOffset Offset added to the eye position only (not the target).
|
||||||
|
* @param scale Pixels per world unit. 1.0 = pixel perfect, 2.0 = 2px per unit.
|
||||||
|
*/
|
||||||
|
void entityCameraLookAtPixelPerfect(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t ent,
|
||||||
|
const componentid_t posComp,
|
||||||
|
const componentid_t camComp,
|
||||||
|
const vec3 point,
|
||||||
|
const vec3 eyeOffset,
|
||||||
|
const float_t scale
|
||||||
|
);
|
||||||
@@ -0,0 +1,669 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityPositionInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
|
||||||
|
pos->flags = 0;
|
||||||
|
pos->parentEntityId = ENTITY_ID_INVALID;
|
||||||
|
pos->parentComponentId = COMPONENT_ID_INVALID;
|
||||||
|
pos->childCount = 0;
|
||||||
|
glm_vec3_zero(pos->position);
|
||||||
|
glm_vec3_zero(pos->rotation);
|
||||||
|
glm_vec3_one(pos->scale);
|
||||||
|
glm_mat4_identity(pos->localTransform);
|
||||||
|
glm_mat4_identity(pos->worldTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionLookAt(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 eye,
|
||||||
|
vec3 target,
|
||||||
|
vec3 up
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
// glm_lookat() produces a view matrix (world -> eye space). Every other
|
||||||
|
// setter treats localTransform as this entity's placement in world space
|
||||||
|
// (eye -> world), so invert it here to keep that meaning consistent --
|
||||||
|
// callers that need a view matrix (e.g. scene rendering) invert it back.
|
||||||
|
mat4 view;
|
||||||
|
glm_lookat(eye, target, up, view);
|
||||||
|
glm_mat4_inv(view, pos->localTransform);
|
||||||
|
// localTransform is now authoritative; PRS cache is stale.
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY)
|
||||||
|
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
||||||
|
ENTITY_POSITION_FLAG_POSITION_DIRTY);
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
glm_mat4_copy(
|
||||||
|
pos->parentEntityId == ENTITY_ID_INVALID
|
||||||
|
? pos->localTransform : pos->worldTransform,
|
||||||
|
dest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureLocal(pos);
|
||||||
|
glm_mat4_copy(pos->localTransform, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->position, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->position, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
dest[0] = pos->worldTransform[3][0];
|
||||||
|
dest[1] = pos->worldTransform[3][1];
|
||||||
|
dest[2] = pos->worldTransform[3][2];
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_vec3_copy(position, pos->position);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
mat4 invParent;
|
||||||
|
glm_mat4_inv(parent->worldTransform, invParent);
|
||||||
|
vec3 localPos;
|
||||||
|
glm_mat4_mulv3(invParent, position, 1.0f, localPos);
|
||||||
|
glm_vec3_copy(localPos, pos->position);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_vec3_copy(position, pos->position);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->rotation, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->rotation, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
const float_t (*wt)[4] = pos->worldTransform;
|
||||||
|
const float_t sx = sqrtf(
|
||||||
|
wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]
|
||||||
|
);
|
||||||
|
const float_t sy = sqrtf(
|
||||||
|
wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]
|
||||||
|
);
|
||||||
|
const float_t sz = sqrtf(
|
||||||
|
wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]
|
||||||
|
);
|
||||||
|
const float_t r00 = sx > 0.0f ? wt[0][0]/sx : 0.0f;
|
||||||
|
const float_t r10 = sy > 0.0f ? wt[1][0]/sy : 0.0f;
|
||||||
|
const float_t r20 = sz > 0.0f ? wt[2][0]/sz : 0.0f;
|
||||||
|
const float_t r01 = sx > 0.0f ? wt[0][1]/sx : 0.0f;
|
||||||
|
const float_t r11 = sy > 0.0f ? wt[1][1]/sy : 0.0f;
|
||||||
|
const float_t r21 = sz > 0.0f ? wt[2][1]/sz : 0.0f;
|
||||||
|
const float_t r22 = sz > 0.0f ? wt[2][2]/sz : 0.0f;
|
||||||
|
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
|
||||||
|
dest[1] = asinf(sinBeta);
|
||||||
|
const float_t cosBeta = cosf(dest[1]);
|
||||||
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
|
dest[0] = atan2f(-r21, r22);
|
||||||
|
dest[2] = atan2f(-r10, r00);
|
||||||
|
} else {
|
||||||
|
dest[2] = 0.0f;
|
||||||
|
dest[0] = (sinBeta > 0.0f) ? atan2f(r01, r11) : -atan2f(r01, r11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_vec3_copy(rotation, pos->rotation);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_vec3_copy(rotation, pos->rotation);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
|
||||||
|
// Build target world rotation matrix (unit scale) from XYZ euler.
|
||||||
|
const float_t c0 = cosf(rotation[0]), s0 = sinf(rotation[0]);
|
||||||
|
const float_t c1 = cosf(rotation[1]), s1 = sinf(rotation[1]);
|
||||||
|
const float_t c2 = cosf(rotation[2]), s2 = sinf(rotation[2]);
|
||||||
|
const float_t s0s1 = s0*s1, c0s1 = c0*s1;
|
||||||
|
// Named wr[col_stored][row_stored] matching cglm column-major layout.
|
||||||
|
const float_t wr00 = c1*c2;
|
||||||
|
const float_t wr01 = c0*s2 + s0s1*c2;
|
||||||
|
const float_t wr02 = s0*s2 - c0s1*c2;
|
||||||
|
const float_t wr10 = -c1*s2;
|
||||||
|
const float_t wr11 = c0*c2 - s0s1*s2;
|
||||||
|
const float_t wr12 = s0*c2 + c0s1*s2;
|
||||||
|
const float_t wr20 = s1;
|
||||||
|
const float_t wr21 = -s0*c1;
|
||||||
|
const float_t wr22 = c0*c1;
|
||||||
|
|
||||||
|
// Normalize parent world columns to extract pure rotation.
|
||||||
|
const float_t (*pt)[4] = parent->worldTransform;
|
||||||
|
const float_t psx = sqrtf(
|
||||||
|
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
|
||||||
|
);
|
||||||
|
const float_t psy = sqrtf(
|
||||||
|
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
|
||||||
|
);
|
||||||
|
const float_t psz = sqrtf(
|
||||||
|
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
|
||||||
|
);
|
||||||
|
const float_t pr00 = psx > 0.f ? pt[0][0]/psx : 0.f;
|
||||||
|
const float_t pr01 = psx > 0.f ? pt[0][1]/psx : 0.f;
|
||||||
|
const float_t pr02 = psx > 0.f ? pt[0][2]/psx : 0.f;
|
||||||
|
const float_t pr10 = psy > 0.f ? pt[1][0]/psy : 0.f;
|
||||||
|
const float_t pr11 = psy > 0.f ? pt[1][1]/psy : 0.f;
|
||||||
|
const float_t pr12 = psy > 0.f ? pt[1][2]/psy : 0.f;
|
||||||
|
const float_t pr20 = psz > 0.f ? pt[2][0]/psz : 0.f;
|
||||||
|
const float_t pr21 = psz > 0.f ? pt[2][1]/psz : 0.f;
|
||||||
|
const float_t pr22 = psz > 0.f ? pt[2][2]/psz : 0.f;
|
||||||
|
|
||||||
|
// local_R = parent_R^T * world_R (R^-1 == R^T for orthogonal matrices).
|
||||||
|
// Compute only the 7 entries of the local rotation matrix needed for XYZ
|
||||||
|
// euler extraction (stored column-major: [col][row] = math [row][col]).
|
||||||
|
// sinBeta = stored[2][0] = math[0][2]
|
||||||
|
// r21/r22 = stored[2][1..2] = math[1..2][2]
|
||||||
|
// r10/r00 = stored[1][0], stored[0][0] = math[0][1], math[0][0]
|
||||||
|
// gimbal = stored[0][1], stored[1][1] = math[1][0], math[1][1]
|
||||||
|
const float_t lr00 = pr00*wr00 + pr01*wr10 + pr02*wr20; // math[0][0]
|
||||||
|
const float_t lr10 = pr00*wr01 + pr01*wr11 + pr02*wr21; // math[0][1]
|
||||||
|
const float_t lr20 = pr00*wr02 + pr01*wr12 + pr02*wr22; // [0][2] -> sinBeta
|
||||||
|
const float_t lr01 = pr10*wr00 + pr11*wr10 + pr12*wr20; // math[1][0]
|
||||||
|
const float_t lr11 = pr10*wr01 + pr11*wr11 + pr12*wr21; // math[1][1]
|
||||||
|
const float_t lr21 = pr10*wr02 + pr11*wr12 + pr12*wr22; // [1][2] -> r21
|
||||||
|
const float_t lr22 = pr20*wr02 + pr21*wr12 + pr22*wr22; // [2][2] -> r22
|
||||||
|
|
||||||
|
const float_t sinBeta = glm_clamp(lr20, -1.0f, 1.0f);
|
||||||
|
pos->rotation[1] = asinf(sinBeta);
|
||||||
|
const float_t cosBeta = cosf(pos->rotation[1]);
|
||||||
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
|
pos->rotation[0] = atan2f(-lr21, lr22);
|
||||||
|
pos->rotation[2] = atan2f(-lr10, lr00);
|
||||||
|
} else {
|
||||||
|
pos->rotation[2] = 0.0f;
|
||||||
|
pos->rotation[0] = (sinBeta > 0.0f)
|
||||||
|
? atan2f(lr01, lr11) : -atan2f(lr01, lr11);
|
||||||
|
}
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->scale, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->scale, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
const float_t (*wt)[4] = pos->worldTransform;
|
||||||
|
dest[0] = sqrtf(wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]);
|
||||||
|
dest[1] = sqrtf(wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]);
|
||||||
|
dest[2] = sqrtf(wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_vec3_copy(scale, pos->scale);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_vec3_copy(scale, pos->scale);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
const float_t (*pt)[4] = parent->worldTransform;
|
||||||
|
const float_t psx = sqrtf(
|
||||||
|
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
|
||||||
|
);
|
||||||
|
const float_t psy = sqrtf(
|
||||||
|
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
|
||||||
|
);
|
||||||
|
const float_t psz = sqrtf(
|
||||||
|
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
|
||||||
|
);
|
||||||
|
pos->scale[0] = psx > 0.0f ? scale[0] / psx : scale[0];
|
||||||
|
pos->scale[1] = psy > 0.0f ? scale[1] / psy : scale[1];
|
||||||
|
pos->scale[2] = psz > 0.0f ? scale[2] / psz : scale[2];
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t parentEntityId,
|
||||||
|
const componentid_t parentComponentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove from old parent's child list.
|
||||||
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
entityposition_t *oldParent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId,
|
||||||
|
COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < oldParent->childCount; i++) {
|
||||||
|
if(
|
||||||
|
oldParent->childEntityIds[i] == entityId &&
|
||||||
|
oldParent->childComponentIds[i] == componentId
|
||||||
|
) {
|
||||||
|
oldParent->childCount--;
|
||||||
|
for(uint8_t j = i; j < oldParent->childCount; j++) {
|
||||||
|
oldParent->childEntityIds[j] = oldParent->childEntityIds[j + 1];
|
||||||
|
oldParent->childComponentIds[j] = oldParent->childComponentIds[j + 1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos->parentEntityId = parentEntityId;
|
||||||
|
pos->parentComponentId = parentComponentId;
|
||||||
|
|
||||||
|
// Register with new parent.
|
||||||
|
if(parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
|
||||||
|
parent->childEntityIds[parent->childCount] = entityId;
|
||||||
|
parent->childComponentIds[parent->childCount] = componentId;
|
||||||
|
parent->childCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityPositionGetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
return pos->parentEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityposition_t *entityPositionGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos) {
|
||||||
|
pos->flags = (
|
||||||
|
pos->flags |
|
||||||
|
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
||||||
|
ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
) & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos) {
|
||||||
|
if(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY) return;
|
||||||
|
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||||
|
for(uint8_t i = 0; i < pos->childCount; i++) {
|
||||||
|
entityposition_t *child = componentGetData(
|
||||||
|
mgr, pos->childEntityIds[i], pos->childComponentIds[i],
|
||||||
|
COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionMarkDirty(mgr, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionDisposeDeep(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = entityPositionGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
// Detach from parent so the parent's child list stays consistent.
|
||||||
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
entityPositionSetParent(
|
||||||
|
mgr, entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the child list before disposing self (entityDispose invalidates
|
||||||
|
// pos).
|
||||||
|
uint8_t childCount = pos->childCount;
|
||||||
|
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
for(uint8_t i = 0; i < childCount; i++) {
|
||||||
|
childEntityIds[i] = pos->childEntityIds[i];
|
||||||
|
childComponentIds[i] = pos->childComponentIds[i];
|
||||||
|
// Sever child's parent link so it won't try to modify our disposed
|
||||||
|
// data.
|
||||||
|
entityposition_t *child = entityPositionGet(
|
||||||
|
mgr, childEntityIds[i], childComponentIds[i]
|
||||||
|
);
|
||||||
|
child->parentEntityId = ENTITY_ID_INVALID;
|
||||||
|
child->parentComponentId = COMPONENT_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityDispose(mgr, entityId);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < childCount; i++) {
|
||||||
|
entityPositionDisposeDeep(mgr, childEntityIds[i], childComponentIds[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionDecompose(entityposition_t *pos) {
|
||||||
|
// Translation: column 3
|
||||||
|
pos->position[0] = pos->localTransform[3][0];
|
||||||
|
pos->position[1] = pos->localTransform[3][1];
|
||||||
|
pos->position[2] = pos->localTransform[3][2];
|
||||||
|
|
||||||
|
// Scale: length of each basis column (xyz only)
|
||||||
|
pos->scale[0] = sqrtf(
|
||||||
|
pos->localTransform[0][0] * pos->localTransform[0][0] +
|
||||||
|
pos->localTransform[0][1] * pos->localTransform[0][1] +
|
||||||
|
pos->localTransform[0][2] * pos->localTransform[0][2]
|
||||||
|
);
|
||||||
|
pos->scale[1] = sqrtf(
|
||||||
|
pos->localTransform[1][0] * pos->localTransform[1][0] +
|
||||||
|
pos->localTransform[1][1] * pos->localTransform[1][1] +
|
||||||
|
pos->localTransform[1][2] * pos->localTransform[1][2]
|
||||||
|
);
|
||||||
|
pos->scale[2] = sqrtf(
|
||||||
|
pos->localTransform[2][0] * pos->localTransform[2][0] +
|
||||||
|
pos->localTransform[2][1] * pos->localTransform[2][1] +
|
||||||
|
pos->localTransform[2][2] * pos->localTransform[2][2]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Normalize columns to isolate the rotation matrix (no mat4 needed).
|
||||||
|
const float_t invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
|
||||||
|
const float_t invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
|
||||||
|
const float_t invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
|
||||||
|
|
||||||
|
const float_t r00 = pos->localTransform[0][0] * invS0;
|
||||||
|
const float_t r01 = pos->localTransform[0][1] * invS0;
|
||||||
|
const float_t r02 = pos->localTransform[0][2] * invS0;
|
||||||
|
const float_t r10 = pos->localTransform[1][0] * invS1;
|
||||||
|
const float_t r11 = pos->localTransform[1][1] * invS1;
|
||||||
|
const float_t r20 = pos->localTransform[2][0] * invS2;
|
||||||
|
const float_t r21 = pos->localTransform[2][1] * invS2;
|
||||||
|
const float_t r22 = pos->localTransform[2][2] * invS2;
|
||||||
|
|
||||||
|
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
|
||||||
|
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
|
||||||
|
pos->rotation[1] = asinf(sinBeta);
|
||||||
|
const float_t cosBeta = cosf(pos->rotation[1]);
|
||||||
|
|
||||||
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
|
pos->rotation[0] = atan2f(-r21, r22);
|
||||||
|
pos->rotation[2] = atan2f(-r10, r00);
|
||||||
|
} else {
|
||||||
|
// Gimbal lock: pin Z to 0, recover X.
|
||||||
|
pos->rotation[2] = 0.0f;
|
||||||
|
pos->rotation[0] = (sinBeta > 0.0f)
|
||||||
|
? atan2f(r01, r11)
|
||||||
|
: -atan2f(r01, r11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionEnsurePRS(entityposition_t *pos) {
|
||||||
|
if(!(pos->flags & ENTITY_POSITION_FLAG_PRS_DIRTY)) return;
|
||||||
|
entityPositionDecompose(pos);
|
||||||
|
pos->flags &= ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionEnsureLocal(entityposition_t *pos) {
|
||||||
|
const uint8_t dirty = pos->flags & (
|
||||||
|
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
);
|
||||||
|
if(!dirty) return;
|
||||||
|
|
||||||
|
if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
|
||||||
|
// Rotation or scale changed: rebuild cols 0-2 analytically (XYZ euler).
|
||||||
|
const float_t c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
|
||||||
|
const float_t c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
|
||||||
|
const float_t c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
|
||||||
|
const float_t s0s1 = s0 * s1;
|
||||||
|
const float_t c0s1 = c0 * s1;
|
||||||
|
|
||||||
|
pos->localTransform[0][0] = c1 * c2 * pos->scale[0];
|
||||||
|
pos->localTransform[0][1] = (c0 * s2 + s0s1 * c2) * pos->scale[0];
|
||||||
|
pos->localTransform[0][2] = (s0 * s2 - c0s1 * c2) * pos->scale[0];
|
||||||
|
pos->localTransform[0][3] = 0.0f;
|
||||||
|
|
||||||
|
pos->localTransform[1][0] = -c1 * s2 * pos->scale[1];
|
||||||
|
pos->localTransform[1][1] = (c0 * c2 - s0s1 * s2) * pos->scale[1];
|
||||||
|
pos->localTransform[1][2] = (s0 * c2 + c0s1 * s2) * pos->scale[1];
|
||||||
|
pos->localTransform[1][3] = 0.0f;
|
||||||
|
|
||||||
|
pos->localTransform[2][0] = s1 * pos->scale[2];
|
||||||
|
pos->localTransform[2][1] = -s0 * c1 * pos->scale[2];
|
||||||
|
pos->localTransform[2][2] = c0 * c1 * pos->scale[2];
|
||||||
|
pos->localTransform[2][3] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirty & ENTITY_POSITION_FLAG_POSITION_DIRTY) {
|
||||||
|
// Only position changed: update column 3 only (no trig needed).
|
||||||
|
pos->localTransform[3][0] = pos->position[0];
|
||||||
|
pos->localTransform[3][1] = pos->position[1];
|
||||||
|
pos->localTransform[3][2] = pos->position[2];
|
||||||
|
pos->localTransform[3][3] = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos->flags &= ~(
|
||||||
|
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos) {
|
||||||
|
if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return;
|
||||||
|
entityPositionEnsureLocal(pos);
|
||||||
|
|
||||||
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
// Parented: world = parent.world x local. worldTransform must be
|
||||||
|
// written because children (and this node's getters) read it.
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId,
|
||||||
|
COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
glm_mat4_mul(
|
||||||
|
parent->worldTransform, pos->localTransform, pos->worldTransform
|
||||||
|
);
|
||||||
|
} else if(pos->childCount > 0) {
|
||||||
|
// Parentless root with children: children need a valid worldTransform
|
||||||
|
// to multiply against, but world == local, so just copy.
|
||||||
|
glm_mat4_copy(pos->localTransform, pos->worldTransform);
|
||||||
|
}
|
||||||
|
// Parentless leaf: world == local. Getters read localTransform directly;
|
||||||
|
// no copy needed.
|
||||||
|
|
||||||
|
pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/** Maximum number of child position components this node can track. */
|
||||||
|
#define ENTITY_POSITION_CHILDREN_MAX 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PRS cache is stale. localTransform was written directly (e.g. lookAt) and
|
||||||
|
* position/rotation/scale need to be decomposed before they can be read.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_PRS_DIRTY (1 << 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Columns 0-2 of localTransform are stale. Rotation or scale changed; the
|
||||||
|
* basis vectors need to be rebuilt analytically before the matrix can be used.
|
||||||
|
* Does not imply column 3 (translation) is stale.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_ROTATION_DIRTY (1 << 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column 3 of localTransform is stale. Position changed; only the
|
||||||
|
* translation column needs to be written. Does not imply columns 0-2 are
|
||||||
|
* stale.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_POSITION_DIRTY (1 << 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* worldTransform is stale. Either the local matrix changed or an ancestor
|
||||||
|
* moved; worldTransform must be recomputed before world data can be read.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 3)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/*
|
||||||
|
* Hot fields - flag checks, parent/child traversal (markDirty, ensureWorld)
|
||||||
|
* only touch these. Kept at the front so they share the first cache line.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** ENTITY_POSITION_FLAG_* bitmask; describes which caches are stale. */
|
||||||
|
uint8_t flags;
|
||||||
|
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */
|
||||||
|
entityid_t parentEntityId;
|
||||||
|
/** Component ID of the parent position, or COMPONENT_ID_INVALID if none. */
|
||||||
|
componentid_t parentComponentId;
|
||||||
|
/** Number of currently registered children. */
|
||||||
|
uint8_t childCount;
|
||||||
|
/** Entity IDs of child nodes. */
|
||||||
|
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
/** Component IDs of child position components. */
|
||||||
|
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Warm fields - read/written by PRS getters/setters.
|
||||||
|
* Accessed more often than the matrices but less often than flags.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Cached local position (XYZ). Stale when PRS_DIRTY is set. */
|
||||||
|
vec3 position;
|
||||||
|
/** Cached local rotation (XYZ euler, radians). Stale when PRS_DIRTY. */
|
||||||
|
vec3 rotation;
|
||||||
|
/** Cached local scale (XYZ). Stale when PRS_DIRTY is set. */
|
||||||
|
vec3 scale;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cold fields - only touched when actually rebuilding transforms.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
|
||||||
|
mat4 localTransform;
|
||||||
|
/** World transform matrix, recomputed lazily from the parent chain. */
|
||||||
|
mat4 worldTransform;
|
||||||
|
} entityposition_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the entity position component, setting identity transforms and
|
||||||
|
* zeroing all parent/child state.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityPositionInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions and orients the entity at eye, facing target. Stores this as
|
||||||
|
* the entity's normal world-space placement (consistent with every other
|
||||||
|
* setter), not as a view matrix -- invert entityPositionGetTransform()'s
|
||||||
|
* result to get a view matrix for rendering.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param eye The eye/camera position.
|
||||||
|
* @param target The target point to look at.
|
||||||
|
* @param up The up vector.
|
||||||
|
*/
|
||||||
|
void entityPositionLookAt(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 eye,
|
||||||
|
vec3 target,
|
||||||
|
vec3 up
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space transform matrix, recomputing it lazily if dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination matrix.
|
||||||
|
*/
|
||||||
|
void entityPositionGetTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the local transform matrix (does not include parent transforms).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination matrix.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached local position (XYZ). Decomposes localTransform into PRS
|
||||||
|
* first if ENTITY_POSITION_FLAG_PRS_DIRTY is set; never triggers a matrix
|
||||||
|
* rebuild or world-transform update.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space position. For parentless entities this is the same as
|
||||||
|
* the local position.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the world-space position. For parentless entities this is equivalent
|
||||||
|
* to entityPositionSetLocalPosition. For parented entities the position is
|
||||||
|
* converted to local space via the inverted parent world transform.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param position The desired world-space position.
|
||||||
|
*/
|
||||||
|
void entityPositionSetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local position, marks localTransform and worldTransform (self +
|
||||||
|
* descendants) dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param position The new local position.
|
||||||
|
*/
|
||||||
|
void entityPositionSetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached local euler rotation (XYZ, radians). Decomposes
|
||||||
|
* localTransform first if ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space euler rotation (XYZ, radians) by decomposing the
|
||||||
|
* world transform. For parentless entities this is the same as local
|
||||||
|
* rotation.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local euler rotation (XYZ, radians) and marks transforms dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param rotation The new local rotation.
|
||||||
|
*/
|
||||||
|
void entityPositionSetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the world-space euler rotation (XYZ, radians). For parentless
|
||||||
|
* entities this is equivalent to entityPositionSetLocalRotation. For
|
||||||
|
* parented entities the rotation is converted to local space by removing
|
||||||
|
* the parent world rotation.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param rotation The desired world-space euler rotation.
|
||||||
|
*/
|
||||||
|
void entityPositionSetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached local scale. Decomposes localTransform first if
|
||||||
|
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space scale by extracting column lengths from the world
|
||||||
|
* transform. For parentless entities this is the same as local scale.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local scale and marks transforms dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param scale The new local scale.
|
||||||
|
*/
|
||||||
|
void entityPositionSetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the world-space scale. For parentless entities this is equivalent to
|
||||||
|
* entityPositionSetLocalScale. For parented entities the scale is converted
|
||||||
|
* to local space by dividing by the parent world scale (assumes no shear).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param scale The desired world-space scale.
|
||||||
|
*/
|
||||||
|
void entityPositionSetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the parent of this entity's position component.
|
||||||
|
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param entityId The child entity ID.
|
||||||
|
* @param componentId The child component ID.
|
||||||
|
* @param parentEntityId The parent entity ID.
|
||||||
|
* @param parentComponentId The parent component ID.
|
||||||
|
*/
|
||||||
|
void entityPositionSetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t parentEntityId,
|
||||||
|
const componentid_t parentComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity ID of this position component's parent.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The parent entity ID, or ENTITY_ID_INVALID if unparented.
|
||||||
|
*/
|
||||||
|
entityid_t entityPositionGetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a direct pointer to the entity position component data.
|
||||||
|
* After modifying localTransform directly, call entityPositionMarkDirty() to
|
||||||
|
* set ENTITY_POSITION_FLAG_WORLD_DIRTY on self and descendants. After
|
||||||
|
* modifying PRS directly, call entityPositionRebuild() instead.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return Pointer to the component data.
|
||||||
|
*/
|
||||||
|
entityposition_t *entityPositionGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals that the PRS cache was modified externally. Sets both
|
||||||
|
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
* so all of localTransform is rebuilt lazily on the next read, clears
|
||||||
|
* ENTITY_POSITION_FLAG_PRS_DIRTY, propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
|
||||||
|
* to self and all descendants.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns pos and its descendants.
|
||||||
|
* @param pos The position component whose PRS was modified.
|
||||||
|
*/
|
||||||
|
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets ENTITY_POSITION_FLAG_WORLD_DIRTY on this node and all descendants,
|
||||||
|
* indicating that worldTransform must be recomputed before it is read.
|
||||||
|
* Call this after modifying localTransform directly.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns pos and its descendants.
|
||||||
|
* @param pos The position component to mark dirty.
|
||||||
|
*/
|
||||||
|
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes this entity and all of its position-component descendants
|
||||||
|
* recursively. Detaches from any parent before destroying.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The root entity ID.
|
||||||
|
* @param componentId The root position component ID.
|
||||||
|
*/
|
||||||
|
void entityPositionDisposeDeep(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decomposes the local transform matrix back into the position, rotation
|
||||||
|
* (XYZ euler, radians), and scale cache fields.
|
||||||
|
*
|
||||||
|
* @param pos The position component to decompose.
|
||||||
|
*/
|
||||||
|
void entityPositionDecompose(entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Decomposes localTransform into the PRS cache if
|
||||||
|
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param pos The position component to update.
|
||||||
|
*/
|
||||||
|
void entityPositionEnsurePRS(entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Rebuilds localTransform from the PRS cache, touching only the
|
||||||
|
* columns flagged as stale (ROTATION_DIRTY and/or POSITION_DIRTY).
|
||||||
|
*
|
||||||
|
* @param pos The position component to update.
|
||||||
|
*/
|
||||||
|
void entityPositionEnsureLocal(entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Recomputes worldTransform from the parent chain if
|
||||||
|
* ENTITY_POSITION_FLAG_WORLD_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns pos and its ancestors.
|
||||||
|
* @param pos The position component to update.
|
||||||
|
*/
|
||||||
|
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos);
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityrenderable.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "display/shader/shadermaterial.h"
|
||||||
|
#include "display/shader/shaderunlit.h"
|
||||||
|
#include "display/display.h"
|
||||||
|
#include "display/mesh/cube.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityRenderableInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
memoryZero(r, sizeof(entityrenderable_t));
|
||||||
|
r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL;
|
||||||
|
r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT;
|
||||||
|
r->data.material.material.unlit.color = COLOR_WHITE;
|
||||||
|
r->data.material.meshes[0] = &CUBE_MESH_SIMPLE;
|
||||||
|
r->data.material.meshOffsets[0] = 0;
|
||||||
|
r->data.material.meshCounts[0] = -1;
|
||||||
|
r->data.material.meshCount = 1;
|
||||||
|
r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderabletype_t type
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetPriority(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const int8_t priority
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetColor(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const color_t color
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||||
|
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set color"
|
||||||
|
);
|
||||||
|
r->data.material.material.unlit.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetMesh(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t slot,
|
||||||
|
mesh_t *mesh
|
||||||
|
) {
|
||||||
|
assertNotNull(mesh, "Mesh cannot be null");
|
||||||
|
assertTrue(slot < ENTITY_RENDERABLE_MESHES_MAX, "Mesh slot out of bounds");
|
||||||
|
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||||
|
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set a mesh"
|
||||||
|
);
|
||||||
|
|
||||||
|
r->data.material.meshes[slot] = mesh;
|
||||||
|
r->data.material.meshOffsets[slot] = 0;
|
||||||
|
r->data.material.meshCounts[slot] = -1;
|
||||||
|
if(slot >= r->data.material.meshCount) {
|
||||||
|
r->data.material.meshCount = slot + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
errorret_t (*draw)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
),
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(draw, "Draw callback cannot be null");
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->type = ENTITY_RENDERABLE_TYPE_CUSTOM;
|
||||||
|
r->data.custom.draw = draw;
|
||||||
|
r->data.custom.drawUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
switch(r->type) {
|
||||||
|
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
|
||||||
|
return entityRenderableDrawSpritebatch(&r->data.spritebatch);
|
||||||
|
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
|
||||||
|
return entityRenderableDrawMaterial(&r->data.material);
|
||||||
|
case ENTITY_RENDERABLE_TYPE_CUSTOM:
|
||||||
|
return entityRenderableDrawCustom(
|
||||||
|
mgr, entityId, componentId, &r->data.custom
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
assertUnreachable("Invalid renderable type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDrawSpritebatch(
|
||||||
|
const entityrenderablespritebatch_t *sb
|
||||||
|
) {
|
||||||
|
if(sb->spriteCount == 0) errorOk();
|
||||||
|
|
||||||
|
errorChain(displaySetState((displaystate_t){
|
||||||
|
.flags = DISPLAY_STATE_FLAG_BLEND
|
||||||
|
}));
|
||||||
|
|
||||||
|
spriteBatchClear();
|
||||||
|
shadermaterial_t mat;
|
||||||
|
memoryZero(&mat, sizeof(shadermaterial_t));
|
||||||
|
mat.unlit.texture = sb->texture;
|
||||||
|
mat.unlit.color = COLOR_WHITE;
|
||||||
|
errorChain(spriteBatchBuffer(
|
||||||
|
sb->sprites, sb->spriteCount,
|
||||||
|
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
|
||||||
|
));
|
||||||
|
return spriteBatchFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m) {
|
||||||
|
errorChain(displaySetState(m->state));
|
||||||
|
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
|
||||||
|
assertNotNull(shader, "Shader cannot be null for material type");
|
||||||
|
errorChain(shaderBind(shader));
|
||||||
|
errorChain(shaderSetMaterial(shader, &m->material));
|
||||||
|
for(uint8_t i = 0; i < m->meshCount; i++) {
|
||||||
|
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDrawCustom(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderablecustom_t *custom
|
||||||
|
) {
|
||||||
|
return custom->draw(mgr, entityId, componentId, custom->drawUser);
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "display/mesh/mesh.h"
|
||||||
|
#include "display/shader/shadermaterial.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/displaystate.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
||||||
|
#define ENTITY_RENDERABLE_MESHES_MAX 8
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ENTITY_RENDERABLE_TYPE_CUSTOM = 0,
|
||||||
|
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
|
||||||
|
ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||||
|
} entityrenderabletype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
|
||||||
|
uint32_t spriteCount;
|
||||||
|
texture_t *texture;
|
||||||
|
} entityrenderablespritebatch_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
mesh_t *meshes[ENTITY_RENDERABLE_MESHES_MAX];
|
||||||
|
int32_t meshOffsets[ENTITY_RENDERABLE_MESHES_MAX];
|
||||||
|
int32_t meshCounts[ENTITY_RENDERABLE_MESHES_MAX];
|
||||||
|
uint8_t meshCount;
|
||||||
|
shaderlistshader_t shaderType;
|
||||||
|
shadermaterial_t material;
|
||||||
|
displaystate_t state;
|
||||||
|
} entityrenderablematerial_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
errorret_t (*draw)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
void *drawUser;
|
||||||
|
} entityrenderablecustom_t;
|
||||||
|
|
||||||
|
typedef union entityrenderabledata_u {
|
||||||
|
entityrenderablespritebatch_t spritebatch;
|
||||||
|
entityrenderablematerial_t material;
|
||||||
|
entityrenderablecustom_t custom;
|
||||||
|
} entityrenderabledata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
entityrenderabletype_t type;
|
||||||
|
entityrenderabledata_t data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render priority. 0 = auto (derived from type/flags). Higher values
|
||||||
|
* render later (on top of lower values). Range: [-128..127] with 0 auto.
|
||||||
|
*/
|
||||||
|
int8_t priority;
|
||||||
|
} entityrenderable_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the entity renderable component. Defaults to
|
||||||
|
* ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL with the unlit shader, a white
|
||||||
|
* cube, and depth-test enabled.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to initialize the component for.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
*/
|
||||||
|
void entityRenderableInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes the entity renderable component.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to dispose the component for.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
*/
|
||||||
|
void entityRenderableDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the rendering type for the renderable component. Resets
|
||||||
|
* type-specific data to zero.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param type The rendering type to use.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderabletype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the render priority. 0 = auto (derived from type/flags). Higher
|
||||||
|
* values render later (on top). Use non-zero to force ordering.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param priority The priority value, or 0 for auto.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetPriority(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const int8_t priority
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the unlit material color. Only meaningful when the renderable is
|
||||||
|
* (or defaults to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param color The color to tint the material with.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetColor(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const color_t color
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets one of the material renderable's meshes, drawn in full (no offset/
|
||||||
|
* count override). Only meaningful when the renderable is (or defaults
|
||||||
|
* to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts otherwise.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param slot Index into the material's meshes array (0 to
|
||||||
|
* ENTITY_RENDERABLE_MESHES_MAX - 1).
|
||||||
|
* @param mesh The mesh to draw in that slot.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetMesh(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t slot,
|
||||||
|
mesh_t *mesh
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the draw callback, switching the type to
|
||||||
|
* ENTITY_RENDERABLE_TYPE_CUSTOM.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
* @param draw The draw callback to assign.
|
||||||
|
* @param user Userdata passed to the callback.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
errorret_t (*draw)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
),
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the entity using its renderable component data.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to draw.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
* @return Any error state that happened.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Draws a spritebatch-type renderable.
|
||||||
|
*
|
||||||
|
* @param sb The spritebatch data to draw.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDrawSpritebatch(
|
||||||
|
const entityrenderablespritebatch_t *sb
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Draws a shader-material-type renderable.
|
||||||
|
*
|
||||||
|
* @param m The material data to draw.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes a custom-type renderable's draw callback.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity being drawn.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
* @param custom The custom draw data.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDrawCustom(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderablecustom_t *custom
|
||||||
|
);
|
||||||
+1
-1
@@ -6,5 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
entityitem.c
|
entityphysics.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityphysics.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
void entityPhysicsInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
memoryZero(phys, sizeof(entityphysics_t));
|
||||||
|
|
||||||
|
// Default to cube
|
||||||
|
phys->type = PHYSICS_BODY_DYNAMIC;
|
||||||
|
phys->shape.type = PHYSICS_SHAPE_CUBE;
|
||||||
|
phys->shape.data.cube.halfExtents[0] = 0.5f;
|
||||||
|
phys->shape.data.cube.halfExtents[1] = 0.5f;
|
||||||
|
phys->shape.data.cube.halfExtents[2] = 0.5f;
|
||||||
|
phys->gravityScale = 1.0f;
|
||||||
|
phys->onGround = false;
|
||||||
|
phys->collideMask = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityphysics_t *entityPhysicsGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_PHYSICS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
phys->shape = shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicsshape_t entityPhysicsGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsGetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
glm_vec3_copy(phys->velocity, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 velocity
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
glm_vec3_copy(velocity, phys->velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsApplyImpulse(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 impulse
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
if(phys->type == PHYSICS_BODY_STATIC) return;
|
||||||
|
glm_vec3_add(phys->velocity, impulse, phys->velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t entityPhysicsIsOnGround(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->onGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsbodytype_t type
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
phys->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicsbodytype_t entityPhysicsGetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
phys->collideMask = mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t entityPhysicsGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->collideMask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "physics/physicsshape.h"
|
||||||
|
#include "physics/physicsbodytype.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
physicsbodytype_t type;
|
||||||
|
physicsshape_t shape;
|
||||||
|
vec3 velocity;
|
||||||
|
float_t gravityScale;
|
||||||
|
bool_t onGround;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitmask of collision layers/categories this body belongs to. Two
|
||||||
|
* bodies only collide with each other if (a.collideMask & b.collideMask)
|
||||||
|
* is non-zero -- i.e. they share at least one set bit. Defaults to 0x1
|
||||||
|
* (only bit 0 set).
|
||||||
|
*/
|
||||||
|
uint32_t collideMask;
|
||||||
|
} entityphysics_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the physics component: defaults to a dynamic 1x1x1 cube
|
||||||
|
* body, zero velocity, unit gravity scale, and onGround false.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityPhysicsInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying physics structure (temporarily) for the given entity.
|
||||||
|
* This is really just intended for doing operations faster than using the
|
||||||
|
* getters and setters, but it is preferred that you use those.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The physics component data for the given entity and component ID.
|
||||||
|
*/
|
||||||
|
entityphysics_t *entityPhysicsGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the shape of the entity's physics body. This will not reset the body
|
||||||
|
* state, so if you change from a cube to a sphere, it will keep the same
|
||||||
|
* velocity and onGround state.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param shape The new shape to set on the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shape of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The shape of the physics body.
|
||||||
|
*/
|
||||||
|
physicsshape_t entityPhysicsGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the velocity of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest The destination vec3 to write the velocity to.
|
||||||
|
*/
|
||||||
|
void entityPhysicsGetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the velocity of the entity's physics body. This is not an impulse, so
|
||||||
|
* it will be affected by mass and drag.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param velocity The new velocity to set on the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 velocity
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies an impulse to the entity's physics body. This is an immediate
|
||||||
|
* velocity change that is not affected by mass or drag. No-op on STATIC
|
||||||
|
* bodies.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param impulse The impulse to apply to the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsApplyImpulse(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 impulse
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the entity's physics body rested on a surface during the
|
||||||
|
* last physicsWorldStep.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return True if the body is on the ground, false otherwise.
|
||||||
|
*/
|
||||||
|
bool_t entityPhysicsIsOnGround(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the body type of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param type The body type to set.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsbodytype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the body type of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The body type of the physics body.
|
||||||
|
*/
|
||||||
|
physicsbodytype_t entityPhysicsGetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the collision mask of the entity's physics body. Two bodies only
|
||||||
|
* collide if their masks share at least one set bit.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param mask The new collision mask to set on the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the collision mask of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The collision mask of the physics body.
|
||||||
|
*/
|
||||||
|
uint32_t entityPhysicsGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
+2
-1
@@ -6,5 +6,6 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
assetvita.c
|
entitytrigger.c
|
||||||
|
entitytriggerevents.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitytrigger.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
void entityTriggerInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
memoryZero(trig, sizeof(entitytrigger_t));
|
||||||
|
|
||||||
|
trig->shape.type = PHYSICS_SHAPE_CUBE;
|
||||||
|
trig->shape.data.cube.halfExtents[0] = 0.5f;
|
||||||
|
trig->shape.data.cube.halfExtents[1] = 0.5f;
|
||||||
|
trig->shape.data.cube.halfExtents[2] = 0.5f;
|
||||||
|
trig->collideMask = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
entitytrigger_t *entityTriggerGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_TRIGGER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
trig->shape = shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicsshape_t entityTriggerGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
return trig->shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
trig->collideMask = mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t entityTriggerGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
return trig->collideMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t entityTriggerGetOccupantCount(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
return trig->occupantCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityTriggerGetOccupantEntityId(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t index
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
if(index >= trig->occupantCount) return ENTITY_ID_INVALID;
|
||||||
|
return trig->occupants[index].entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t entityTriggerIsOccupyingEntity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t otherEntityId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->occupantCount; i++) {
|
||||||
|
if(trig->occupants[i].entityId == otherEntityId) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "physics/physicsshape.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/** Maximum number of entities a single trigger can track at once. */
|
||||||
|
#define ENTITY_TRIGGER_OCCUPANTS_MAX 8
|
||||||
|
|
||||||
|
/** Maximum number of subscribers per trigger event (onEnter/onActive/...). */
|
||||||
|
#define ENTITY_TRIGGER_CALLBACK_COUNT_MAX 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked for a trigger's onEnter/onActive/onMove/onLeave events.
|
||||||
|
* See entityTriggerOnEnterAdd() and its onActive/onMove/onLeave siblings.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that entered/is inside/left the
|
||||||
|
* trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
* @param user The user pointer passed to the matching *Add() call.
|
||||||
|
*/
|
||||||
|
typedef void (*triggercallback_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Tracks one entity currently overlapping a trigger volume. */
|
||||||
|
typedef struct {
|
||||||
|
/** The occupying entity's ID. */
|
||||||
|
entityid_t entityId;
|
||||||
|
/** The occupying entity's POSITION component ID. */
|
||||||
|
componentid_t componentId;
|
||||||
|
/** The occupying entity's local position as of the last step, used to
|
||||||
|
* detect movement (see entityTriggerOnMoveAdd()). */
|
||||||
|
vec3 lastPosition;
|
||||||
|
} entitytriggeroccupant_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/** The trigger volume's shape, in the owning entity's local space. */
|
||||||
|
physicsshape_t shape;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitmask of collision layers/categories this trigger detects. An
|
||||||
|
* entity's PHYSICS body is only tested against this trigger if
|
||||||
|
* (trigger.collideMask & body.collideMask) is non-zero -- i.e. they
|
||||||
|
* share at least one set bit. Defaults to 0x1 (only bit 0 set). See
|
||||||
|
* entityPhysicsSetCollideMask().
|
||||||
|
*/
|
||||||
|
uint32_t collideMask;
|
||||||
|
|
||||||
|
/** Number of entities currently tracked in occupants. */
|
||||||
|
uint8_t occupantCount;
|
||||||
|
/** Entities currently overlapping this trigger. */
|
||||||
|
entitytriggeroccupant_t occupants[ENTITY_TRIGGER_OCCUPANTS_MAX];
|
||||||
|
|
||||||
|
/** Fired the step an entity begins overlapping this trigger. */
|
||||||
|
uint8_t onEnterCount;
|
||||||
|
triggercallback_t onEnter[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onEnterUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
|
||||||
|
/** Fired every step an entity continues overlapping this trigger,
|
||||||
|
* starting the step after onEnter fired. */
|
||||||
|
uint8_t onActiveCount;
|
||||||
|
triggercallback_t onActive[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onActiveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
|
||||||
|
/** Fired alongside onActive on any step where the entity's position
|
||||||
|
* changed since the previous step. */
|
||||||
|
uint8_t onMoveCount;
|
||||||
|
triggercallback_t onMove[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onMoveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
|
||||||
|
/** Fired the step an entity stops overlapping this trigger. */
|
||||||
|
uint8_t onLeaveCount;
|
||||||
|
triggercallback_t onLeave[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onLeaveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
} entitytrigger_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the trigger component: defaults to a 1x1x1 cube volume, no
|
||||||
|
* tracked occupants, and no event subscribers.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying trigger structure (temporarily) for the given
|
||||||
|
* entity. Prefer the dedicated getters/setters where possible.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The trigger component data for the given entity and component ID.
|
||||||
|
*/
|
||||||
|
entitytrigger_t *entityTriggerGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the shape of the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param shape The new shape to set on the trigger volume.
|
||||||
|
*/
|
||||||
|
void entityTriggerSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shape of the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The shape of the trigger volume.
|
||||||
|
*/
|
||||||
|
physicsshape_t entityTriggerGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the collision mask of the trigger volume. An entity's PHYSICS body
|
||||||
|
* is only detected if their masks share at least one set bit.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param mask The new collision mask to set on the trigger volume.
|
||||||
|
*/
|
||||||
|
void entityTriggerSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the collision mask of the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The collision mask of the trigger volume.
|
||||||
|
*/
|
||||||
|
uint32_t entityTriggerGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of entities currently overlapping this trigger.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The number of currently tracked occupants.
|
||||||
|
*/
|
||||||
|
uint8_t entityTriggerGetOccupantCount(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity ID of the occupant at the given index, in [0,
|
||||||
|
* entityTriggerGetOccupantCount()). Order is not stable across steps.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param index The occupant index.
|
||||||
|
* @return The occupying entity's ID, or ENTITY_ID_INVALID if index is out
|
||||||
|
* of range.
|
||||||
|
*/
|
||||||
|
entityid_t entityTriggerGetOccupantEntityId(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t index
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a specific entity is currently overlapping this trigger.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param otherEntityId The entity ID to check for.
|
||||||
|
* @return True if otherEntityId is a currently tracked occupant.
|
||||||
|
*/
|
||||||
|
bool_t entityTriggerIsOccupyingEntity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t otherEntityId
|
||||||
|
);
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitytriggerevents.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityTriggerOnEnterAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onEnterCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onEnter callback slots full"
|
||||||
|
);
|
||||||
|
trig->onEnter[trig->onEnterCount] = callback;
|
||||||
|
trig->onEnterUser[trig->onEnterCount] = user;
|
||||||
|
trig->onEnterCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnEnterRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
|
||||||
|
if(trig->onEnter[i] != callback) continue;
|
||||||
|
trig->onEnterCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onEnterCount; j++) {
|
||||||
|
trig->onEnter[j] = trig->onEnter[j + 1];
|
||||||
|
trig->onEnterUser[j] = trig->onEnterUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnActiveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onActiveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onActive callback slots full"
|
||||||
|
);
|
||||||
|
trig->onActive[trig->onActiveCount] = callback;
|
||||||
|
trig->onActiveUser[trig->onActiveCount] = user;
|
||||||
|
trig->onActiveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnActiveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
|
||||||
|
if(trig->onActive[i] != callback) continue;
|
||||||
|
trig->onActiveCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onActiveCount; j++) {
|
||||||
|
trig->onActive[j] = trig->onActive[j + 1];
|
||||||
|
trig->onActiveUser[j] = trig->onActiveUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnMoveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onMoveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onMove callback slots full"
|
||||||
|
);
|
||||||
|
trig->onMove[trig->onMoveCount] = callback;
|
||||||
|
trig->onMoveUser[trig->onMoveCount] = user;
|
||||||
|
trig->onMoveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnMoveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
|
||||||
|
if(trig->onMove[i] != callback) continue;
|
||||||
|
trig->onMoveCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onMoveCount; j++) {
|
||||||
|
trig->onMove[j] = trig->onMove[j + 1];
|
||||||
|
trig->onMoveUser[j] = trig->onMoveUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnLeaveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onLeaveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onLeave callback slots full"
|
||||||
|
);
|
||||||
|
trig->onLeave[trig->onLeaveCount] = callback;
|
||||||
|
trig->onLeaveUser[trig->onLeaveCount] = user;
|
||||||
|
trig->onLeaveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnLeaveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
|
||||||
|
if(trig->onLeave[i] != callback) continue;
|
||||||
|
trig->onLeaveCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onLeaveCount; j++) {
|
||||||
|
trig->onLeave[j] = trig->onLeave[j + 1];
|
||||||
|
trig->onLeaveUser[j] = trig->onLeaveUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireEnter(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
|
||||||
|
trig->onEnter[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onEnterUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireActive(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
|
||||||
|
trig->onActive[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onActiveUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireMove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
|
||||||
|
trig->onMove[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onMoveUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireLeave(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
|
||||||
|
trig->onLeave[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onLeaveUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entitytrigger.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onEnter event, fired the step an
|
||||||
|
* entity begins overlapping the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnEnterAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onEnter callback. No-op if not
|
||||||
|
* currently subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnEnterAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnEnterRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onActive event, fired every step an
|
||||||
|
* entity continues overlapping the trigger volume (starting the step
|
||||||
|
* after onEnter fired).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnActiveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onActive callback. No-op if not
|
||||||
|
* currently subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnActiveAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnActiveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onMove event, fired alongside
|
||||||
|
* onActive on any step where the overlapping entity's position changed
|
||||||
|
* since the previous step.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnMoveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onMove callback. No-op if not currently
|
||||||
|
* subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnMoveAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnMoveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onLeave event, fired the step an
|
||||||
|
* entity stops overlapping the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnLeaveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onLeave callback. No-op if not
|
||||||
|
* currently subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnLeaveAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnLeaveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onEnter callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() when an entity begins overlapping.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that entered the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireEnter(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onActive callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() for every entity still overlapping.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID still inside the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireActive(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onMove callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() when an overlapping entity's position
|
||||||
|
* changed since the previous step.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that moved within the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireMove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onLeave callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() when an entity stops overlapping.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that left the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireLeave(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/component/display/entityposition.h"
|
||||||
|
#include "entity/component/display/entitycamera.h"
|
||||||
|
#include "entity/component/display/entityrenderable.h"
|
||||||
|
#include "entity/component/physics/entityphysics.h"
|
||||||
|
#include "entity/component/trigger/entitytrigger.h"
|
||||||
|
#include "entity/component/animation/entityanimation.h"
|
||||||
|
|
||||||
|
// Name (Uppercase)
|
||||||
|
// Structure
|
||||||
|
// Field name (lowercase)
|
||||||
|
// Init function (optional)
|
||||||
|
// Dispose function (optional)
|
||||||
|
// Render function (optional)
|
||||||
|
|
||||||
|
X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL)
|
||||||
|
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL)
|
||||||
|
X(RENDERABLE, entityrenderable_t, renderable,
|
||||||
|
entityRenderableInit, entityRenderableDispose, NULL)
|
||||||
|
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL)
|
||||||
|
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL)
|
||||||
|
X(ANIMATION, entityanimation_t, animation, entityAnimationInit, NULL, NULL)
|
||||||
|
|
||||||
|
// Game-specific components
|
||||||
|
#include "entity/gamecomponentlist.h"
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitymanager.h"
|
||||||
|
#include "component/display/entityposition.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityInit(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
|
||||||
|
memoryZero(ent, sizeof(entity_t));
|
||||||
|
|
||||||
|
// Mark all component types not using this entity.
|
||||||
|
for(
|
||||||
|
componenttype_t compType = 0;
|
||||||
|
compType < COMPONENT_TYPE_COUNT;
|
||||||
|
compType++
|
||||||
|
) {
|
||||||
|
mgr->entitiesWithComponent[
|
||||||
|
compType * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = COMPONENT_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
ent->state |= ENTITY_STATE_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentid_t entityAddComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
componentindex_t compInd;
|
||||||
|
|
||||||
|
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||||
|
compInd = componentGetIndex(entityId, i);
|
||||||
|
if(mgr->components[compInd].type != COMPONENT_TYPE_NULL) {
|
||||||
|
assertTrue(
|
||||||
|
mgr->components[compInd].type != type,
|
||||||
|
"Entity already has component of this type"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
componentInit(mgr, entityId, i, type);
|
||||||
|
mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = i;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertUnreachable("Entity has no more component slots available");
|
||||||
|
return COMPONENT_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentid_t entityGetComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
componentid_t compId = mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
];
|
||||||
|
if(compId == COMPONENT_ID_INVALID) return compId;
|
||||||
|
assertTrue(
|
||||||
|
mgr->components[componentGetIndex(entityId, compId)].type == type,
|
||||||
|
"Component type mismatch"
|
||||||
|
);
|
||||||
|
return compId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entitySetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
stringCopy(mgr->entities[entityId].name, name, ENTITY_NAME_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *entityGetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId
|
||||||
|
) {
|
||||||
|
return mgr->entities[entityId].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityFindByName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
if(!(mgr->entities[i].state & ENTITY_STATE_ACTIVE)) continue;
|
||||||
|
if(stringEquals(mgr->entities[i].name, name)) return i;
|
||||||
|
}
|
||||||
|
return ENTITY_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
componentid_t posComp = entityGetComponent(
|
||||||
|
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(posComp != COMPONENT_ID_INVALID) {
|
||||||
|
entityPositionDisposeDeep(mgr, entityId, posComp);
|
||||||
|
} else {
|
||||||
|
entityDispose(mgr, entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityUpdate(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
for(uint8_t i = 0; i < ent->updateCount; i++) {
|
||||||
|
ent->onUpdate[i](
|
||||||
|
mgr, entityId, ent->updateComponentId[i], ent->updateUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDispose(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
componentindex_t compInd;
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < ent->disposeCount; i++) {
|
||||||
|
ent->onDispose[i](
|
||||||
|
mgr, entityId, ent->disposeComponentId[i], ent->disposeUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||||
|
compInd = componentGetIndex(entityId, i);
|
||||||
|
componenttype_t type = mgr->components[compInd].type;
|
||||||
|
if(type == COMPONENT_TYPE_NULL) continue;
|
||||||
|
mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = COMPONENT_ID_INVALID;
|
||||||
|
componentDispose(mgr, entityId, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
ent->state = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityUpdateAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
assertTrue(
|
||||||
|
ent->updateCount < ENTITY_UPDATE_CALLBACK_COUNT_MAX,
|
||||||
|
"Entity update callback slots full"
|
||||||
|
);
|
||||||
|
ent->onUpdate[ent->updateCount] = callback;
|
||||||
|
ent->updateComponentId[ent->updateCount] = componentId;
|
||||||
|
ent->updateUser[ent->updateCount] = user;
|
||||||
|
ent->updateCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityUpdateRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
for(uint8_t i = 0; i < ent->updateCount; i++) {
|
||||||
|
if(ent->onUpdate[i] != callback) continue;
|
||||||
|
ent->updateCount--;
|
||||||
|
for(uint8_t j = i; j < ent->updateCount; j++) {
|
||||||
|
ent->onUpdate[j] = ent->onUpdate[j + 1];
|
||||||
|
ent->updateComponentId[j] = ent->updateComponentId[j + 1];
|
||||||
|
ent->updateUser[j] = ent->updateUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDisposeAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
assertTrue(
|
||||||
|
ent->disposeCount < ENTITY_DISPOSE_CALLBACK_COUNT_MAX,
|
||||||
|
"Entity dispose callback slots full"
|
||||||
|
);
|
||||||
|
ent->onDispose[ent->disposeCount] = callback;
|
||||||
|
ent->disposeComponentId[ent->disposeCount] = componentId;
|
||||||
|
ent->disposeUser[ent->disposeCount] = user;
|
||||||
|
ent->disposeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDisposeRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
for(uint8_t i = 0; i < ent->disposeCount; i++) {
|
||||||
|
if(ent->onDispose[i] != callback) continue;
|
||||||
|
ent->disposeCount--;
|
||||||
|
for(uint8_t j = i; j < ent->disposeCount; j++) {
|
||||||
|
ent->onDispose[j] = ent->onDispose[j + 1];
|
||||||
|
ent->disposeComponentId[j] = ent->disposeComponentId[j + 1];
|
||||||
|
ent->disposeUser[j] = ent->disposeUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "component.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define ENTITY_STATE_ACTIVE (1 << 0)
|
||||||
|
|
||||||
|
#define ENTITY_UPDATE_CALLBACK_COUNT_MAX 5
|
||||||
|
#define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5
|
||||||
|
#define ENTITY_NAME_MAX 32
|
||||||
|
|
||||||
|
typedef void (*entitycallback_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t name[ENTITY_NAME_MAX];
|
||||||
|
uint8_t state;
|
||||||
|
uint8_t updateCount;
|
||||||
|
uint8_t disposeCount;
|
||||||
|
entitycallback_t onUpdate[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
|
||||||
|
componentid_t updateComponentId[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
|
||||||
|
void *updateUser[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
|
||||||
|
entitycallback_t onDispose[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
|
||||||
|
componentid_t disposeComponentId[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
|
||||||
|
void *disposeUser[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
|
||||||
|
} entity_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity with the given ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to initialize.
|
||||||
|
*/
|
||||||
|
void entityInit(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a component of the given type to the entity with the given ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to add the component to.
|
||||||
|
* @param type The type of the component to add.
|
||||||
|
* @return The ID of the entity with component.
|
||||||
|
*/
|
||||||
|
componentid_t entityAddComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ID of the component of the given type on the entity with the
|
||||||
|
* given ID, or COMPONENT_ID_INVALID if the entity lacks the component.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to get the component from.
|
||||||
|
* @param type The type of the component to get.
|
||||||
|
* @return The ID of the component.
|
||||||
|
*/
|
||||||
|
componentid_t entityGetComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an entity's name. Names are optional -- an entity's name is empty
|
||||||
|
* by default (entityInit zeroes it) -- but are required to make the
|
||||||
|
* entity referenceable by name, e.g. as a parent via
|
||||||
|
* entityPositionSetParent().
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to name.
|
||||||
|
* @param name The name to give the entity. Must be shorter than
|
||||||
|
* ENTITY_NAME_MAX.
|
||||||
|
*/
|
||||||
|
void entitySetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an entity's name, or an empty string if it has none.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to get the name of.
|
||||||
|
* @return The entity's name.
|
||||||
|
*/
|
||||||
|
const char_t *entityGetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first active entity with the given name.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to search.
|
||||||
|
* @param name The name to search for.
|
||||||
|
* @return The entity ID, or ENTITY_ID_INVALID if no active entity matches.
|
||||||
|
*/
|
||||||
|
entityid_t entityFindByName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs all registered update callbacks for the entity.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to update.
|
||||||
|
*/
|
||||||
|
void entityUpdate(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of an entity with the given ID. Fires all dispose callbacks
|
||||||
|
* before cleaning up components and state.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to dispose of.
|
||||||
|
*/
|
||||||
|
void entityDispose(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of an entity and all of its position-component descendants
|
||||||
|
* recursively. If the entity has no position component, behaves like
|
||||||
|
* entityDispose.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The root entity ID.
|
||||||
|
*/
|
||||||
|
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an update callback, invoked each time entityUpdate is called.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to register on.
|
||||||
|
* @param callback The function to call.
|
||||||
|
* @param componentId The component ID passed back to the callback.
|
||||||
|
* @param user Userdata passed back to the callback.
|
||||||
|
*/
|
||||||
|
void entityUpdateAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously registered update callback.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to remove from.
|
||||||
|
* @param callback The function to remove.
|
||||||
|
*/
|
||||||
|
void entityUpdateRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a dispose callback, invoked at the start of entityDispose
|
||||||
|
* before any component or state cleanup.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to register on.
|
||||||
|
* @param callback The function to call.
|
||||||
|
* @param componentId The component ID passed back to the callback.
|
||||||
|
* @param user Userdata passed back to the callback.
|
||||||
|
*/
|
||||||
|
void entityDisposeAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously registered dispose callback.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to remove from.
|
||||||
|
* @param callback The function to remove.
|
||||||
|
*/
|
||||||
|
void entityDisposeRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
#define ENTITY_COUNT_MAX 64
|
||||||
|
#define ENTITY_COMPONENT_COUNT_MAX 16
|
||||||
|
|
||||||
|
#define ENTITY_ID_INVALID 0xFF
|
||||||
|
#define COMPONENT_ID_INVALID 0xFF
|
||||||
|
|
||||||
|
typedef uint8_t entityid_t;
|
||||||
|
typedef uint8_t componentid_t;
|
||||||
|
typedef uint16_t componentindex_t;
|
||||||
|
|
||||||
|
// Forward declared here (rather than in component.h) so every entity/
|
||||||
|
// component header can reference entitymanager_t* regardless of include
|
||||||
|
// order, without needing a full entitymanager.h include.
|
||||||
|
typedef struct entitymanager_t entitymanager_t;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitymanager.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
|
||||||
|
void entityManagerInit(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
memoryZero(mgr, sizeof(entitymanager_t));
|
||||||
|
memorySet(
|
||||||
|
mgr->entitiesWithComponent, COMPONENT_ID_INVALID,
|
||||||
|
sizeof(componentid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
consolePrint(
|
||||||
|
"Entity manager size: %zu bytes (%.2f KB)",
|
||||||
|
sizeof(entitymanager_t),
|
||||||
|
sizeof(entitymanager_t) / 1024.0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityManagerAdd(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0) continue;
|
||||||
|
entityInit(mgr, i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
assertUnreachable("No more entity IDs available");
|
||||||
|
return ENTITY_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityManagerUpdate(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
entityid_t i = 0;
|
||||||
|
while(i < ENTITY_COUNT_MAX) {
|
||||||
|
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0) {
|
||||||
|
entityUpdate(mgr, i);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityManagerDispose(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue;
|
||||||
|
entityDispose(mgr, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity.h"
|
||||||
|
|
||||||
|
typedef struct entitymanager_t {
|
||||||
|
entity_t entities[ENTITY_COUNT_MAX];
|
||||||
|
component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX];
|
||||||
|
componentid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX];
|
||||||
|
} entitymanager_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity manager, marking all entities and components as
|
||||||
|
* unused.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to initialize.
|
||||||
|
*/
|
||||||
|
void entityManagerInit(entitymanager_t *mgr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds / Reserves a new entity ID in the given manager.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to allocate the entity in.
|
||||||
|
* @return The new entity ID.
|
||||||
|
*/
|
||||||
|
entityid_t entityManagerAdd(entitymanager_t *mgr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates all active entities in the given manager.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to update.
|
||||||
|
*/
|
||||||
|
void entityManagerUpdate(entitymanager_t *mgr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the entity manager, in turn freeing all entities and
|
||||||
|
* components.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to dispose.
|
||||||
|
*/
|
||||||
|
void entityManagerDispose(entitymanager_t *mgr);
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityprefab.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
// Pulls in every prefab's real header (entityprefablist.h's one-time
|
||||||
|
// #include lines) exactly once -- each header's #pragma once then makes
|
||||||
|
// the array-building pass below a no-op for those lines, leaving only
|
||||||
|
// bare X(...) invocations to expand.
|
||||||
|
#define X(enumName, extends, apply) // do nothing
|
||||||
|
#include "entityprefablist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
entityprefab_t ENTITY_PREFABS[] = {
|
||||||
|
#define X(enm, extendsName, applyFn) \
|
||||||
|
{ .name = #enm, .extends = extendsName, .apply = applyFn },
|
||||||
|
#include "entityprefablist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
// Sentinel: name[0] == '\0' marks the end for callers iterating without
|
||||||
|
// a separate count.
|
||||||
|
{ 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
errorret_t entityPrefabInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entityprefab_t prefab
|
||||||
|
) {
|
||||||
|
assertNotNull(prefab.apply, "Prefab apply function cannot be null");
|
||||||
|
if(prefab.extends[0] != '\0') {
|
||||||
|
errorChain(entityPrefabResolveAndApply(mgr, entityId, prefab.extends));
|
||||||
|
}
|
||||||
|
errorChain(prefab.apply(mgr, entityId));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityPrefabResolveAndApply(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
for(size_t i = 0; ENTITY_PREFABS[i].name[0] != '\0'; i++) {
|
||||||
|
if(!stringEquals(ENTITY_PREFABS[i].name, name)) continue;
|
||||||
|
errorChain(entityPrefabInit(mgr, entityId, ENTITY_PREFABS[i]));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorThrow("Unknown entity prefab '%s'", name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define ENTITY_PREFAB_NAME_MAX 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a prefab to an entity, e.g. adding and configuring whatever
|
||||||
|
* components make up the prefab.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to apply the prefab to.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
typedef errorret_t (*entityprefabapply_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t name[ENTITY_PREFAB_NAME_MAX];
|
||||||
|
char_t extends[ENTITY_PREFAB_NAME_MAX];
|
||||||
|
entityprefabapply_t apply;
|
||||||
|
} entityprefab_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The C-coded prefab registry, built from entityprefablist.h. Terminated
|
||||||
|
* by a sentinel entry whose name[0] is '\0' -- iterate with a
|
||||||
|
* `for(...; ENTITY_PREFABS[i].name[0] != '\0'; ...)` style loop rather
|
||||||
|
* than a fixed count.
|
||||||
|
*/
|
||||||
|
extern entityprefab_t ENTITY_PREFABS[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity from a prefab, running its apply function. If the
|
||||||
|
* prefab declares a parent (prefab.extends is non-empty), that parent is
|
||||||
|
* resolved and applied first (see entityPrefabResolveAndApply()), so this
|
||||||
|
* prefab's own apply function runs on top of/after it.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to apply the prefab to.
|
||||||
|
* @param prefab The prefab to apply.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityPrefabInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entityprefab_t prefab
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a prefab by name and applies it to the given entity, by
|
||||||
|
* searching the C-coded ENTITY_PREFABS[] registry (see
|
||||||
|
* entityprefablist.h) for a matching name. Throws if no prefab with that
|
||||||
|
* name is registered.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to apply the prefab to.
|
||||||
|
* @param name The prefab name to resolve.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityPrefabResolveAndApply(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Name (Uppercase)
|
||||||
|
// Extends (name of another prefab to apply first, or "" for none)
|
||||||
|
// Apply function
|
||||||
|
|
||||||
|
// Game-specific prefabs
|
||||||
|
#include "entity/gameprefablist.h"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Header-only: game.h's implementation lives in src/duskrpg/game/game.c
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the game. Called once, after the engine and all of its
|
||||||
|
* subsystems have finished initializing.
|
||||||
|
*
|
||||||
|
* @return An error code indicating success or failure.
|
||||||
|
*/
|
||||||
|
errorret_t gameInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the game. Called once per frame.
|
||||||
|
*
|
||||||
|
* @return An error code indicating success or failure.
|
||||||
|
*/
|
||||||
|
errorret_t gameUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the game. Called once, before the engine disposes of its
|
||||||
|
* own subsystems.
|
||||||
|
*
|
||||||
|
* @return An error code indicating success or failure.
|
||||||
|
*/
|
||||||
|
errorret_t gameDispose(void);
|
||||||
@@ -10,12 +10,3 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
inputbutton.c
|
inputbutton.c
|
||||||
inputaction.c
|
inputaction.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# Input Action Definitions
|
|
||||||
dusk_run_python(
|
|
||||||
dusk_input_csv_defs
|
|
||||||
tools.input
|
|
||||||
--csv ${CMAKE_CURRENT_SOURCE_DIR}/input.csv
|
|
||||||
--output ${DUSK_GENERATED_HEADERS_DIR}/input/inputactiondefs.h
|
|
||||||
)
|
|
||||||
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_input_csv_defs)
|
|
||||||
+35
-62
@@ -11,7 +11,6 @@
|
|||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
input_t INPUT;
|
input_t INPUT;
|
||||||
|
|
||||||
@@ -19,24 +18,10 @@ errorret_t inputInit(void) {
|
|||||||
memoryZero(&INPUT, sizeof(input_t));
|
memoryZero(&INPUT, sizeof(input_t));
|
||||||
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
||||||
|
|
||||||
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
for(uint8_t i = 0; i < INPUT_BIND_COUNT; i++) {
|
||||||
INPUT.actions[i].action = (inputaction_t)i;
|
INPUT.actions[i].action = (inputbind_t)i;
|
||||||
INPUT.actions[i].lastValue = 0.0f;
|
INPUT.actions[i].lastValue = 0.0f;
|
||||||
INPUT.actions[i].currentValue = 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
|
#ifdef inputInitPlatform
|
||||||
@@ -67,7 +52,7 @@ void inputUpdate(void) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
action++;
|
action++;
|
||||||
} while(action < &INPUT.actions[INPUT_ACTION_COUNT]);
|
} while(action < &INPUT.actions[INPUT_BIND_COUNT]);
|
||||||
|
|
||||||
// For each button...
|
// For each button...
|
||||||
inputbuttondata_t *cur = &INPUT_BUTTON_DATA[0];
|
inputbuttondata_t *cur = &INPUT_BUTTON_DATA[0];
|
||||||
@@ -98,76 +83,64 @@ void inputUpdate(void) {
|
|||||||
|
|
||||||
cur++;
|
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) {
|
float_t inputGetCurrentValue(const inputbind_t action) {
|
||||||
#ifdef DUSK_TIME_DYNAMIC
|
#ifdef DUSK_TIME_DYNAMIC
|
||||||
if(TIME.dynamicUpdate) return inputGetCurrentValueDynamic(action);
|
if(TIME.dynamicUpdate) return inputGetCurrentValueDynamic(action);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds");
|
assertTrue(action < INPUT_BIND_COUNT, "Input action out of bounds");
|
||||||
return INPUT.actions[action].currentValue;
|
return INPUT.actions[action].currentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetLastValue(const inputaction_t action) {
|
float_t inputGetLastValue(const inputbind_t action) {
|
||||||
#ifdef DUSK_TIME_DYNAMIC
|
#ifdef DUSK_TIME_DYNAMIC
|
||||||
if(TIME.dynamicUpdate) return inputGetLastValueDynamic(action);
|
if(TIME.dynamicUpdate) return inputGetLastValueDynamic(action);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds");
|
assertTrue(action < INPUT_BIND_COUNT, "Input action out of bounds");
|
||||||
return INPUT.actions[action].lastValue;
|
return INPUT.actions[action].lastValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DUSK_TIME_DYNAMIC
|
#ifdef DUSK_TIME_DYNAMIC
|
||||||
float_t inputGetCurrentValueDynamic(const inputaction_t action) {
|
float_t inputGetCurrentValueDynamic(const inputbind_t action) {
|
||||||
assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds");
|
assertTrue(action < INPUT_BIND_COUNT, "Input action out of bounds");
|
||||||
return INPUT.actions[action].currentDynamicValue;
|
return INPUT.actions[action].currentDynamicValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t inputGetLastValueDynamic(const inputaction_t action) {
|
float_t inputGetLastValueDynamic(const inputbind_t action) {
|
||||||
assertTrue(action < INPUT_ACTION_COUNT, "Input action out of bounds");
|
assertTrue(action < INPUT_BIND_COUNT, "Input action out of bounds");
|
||||||
return INPUT.actions[action].lastDynamicValue;
|
return INPUT.actions[action].lastDynamicValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputIsDownDynamic(const inputaction_t action) {
|
bool_t inputIsDownDynamic(const inputbind_t action) {
|
||||||
return inputGetCurrentValueDynamic(action) > 0.0f;
|
return inputGetCurrentValueDynamic(action) > 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputWasDownDynamic(const inputaction_t action) {
|
bool_t inputWasDownDynamic(const inputbind_t action) {
|
||||||
return inputGetLastValueDynamic(action) > 0.0f;
|
return inputGetLastValueDynamic(action) > 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputPressedDynamic(const inputaction_t action) {
|
bool_t inputPressedDynamic(const inputbind_t action) {
|
||||||
return inputIsDownDynamic(action) && !inputWasDownDynamic(action);
|
return inputIsDownDynamic(action) && !inputWasDownDynamic(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputReleasedDynamic(const inputaction_t action) {
|
bool_t inputReleasedDynamic(const inputbind_t action) {
|
||||||
return !inputIsDownDynamic(action) && inputWasDownDynamic(action);
|
return !inputIsDownDynamic(action) && inputWasDownDynamic(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t inputAxisDynamic(
|
float_t inputAxisDynamic(
|
||||||
const inputaction_t neg,
|
const inputbind_t neg,
|
||||||
const inputaction_t pos
|
const inputbind_t pos
|
||||||
) {
|
) {
|
||||||
return inputGetCurrentValueDynamic(pos) -
|
return inputGetCurrentValueDynamic(pos) -
|
||||||
inputGetCurrentValueDynamic(neg);
|
inputGetCurrentValueDynamic(neg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputAxis2DDynamic(
|
void inputAxis2DDynamic(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
) {
|
) {
|
||||||
assertNotNull(result, "Result vector cannot be null");
|
assertNotNull(result, "Result vector cannot be null");
|
||||||
@@ -176,8 +149,8 @@ float_t inputGetLastValue(const inputaction_t action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void inputAngle2DDynamic(
|
void inputAngle2DDynamic(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
) {
|
) {
|
||||||
assertNotNull(result, "Result vector cannot be null");
|
assertNotNull(result, "Result vector cannot be null");
|
||||||
@@ -198,32 +171,32 @@ float_t inputGetLastValue(const inputaction_t action) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool_t inputIsDown(const inputaction_t action) {
|
bool_t inputIsDown(const inputbind_t action) {
|
||||||
return inputGetCurrentValue(action) > 0.0f;
|
return inputGetCurrentValue(action) > 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputWasDown(const inputaction_t action) {
|
bool_t inputWasDown(const inputbind_t action) {
|
||||||
return inputGetLastValue(action) > 0.0f;
|
return inputGetLastValue(action) > 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputPressed(const inputaction_t action) {
|
bool_t inputPressed(const inputbind_t action) {
|
||||||
return inputIsDown(action) && !inputWasDown(action);
|
return inputIsDown(action) && !inputWasDown(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t inputReleased(const inputaction_t action) {
|
bool_t inputReleased(const inputbind_t action) {
|
||||||
return !inputIsDown(action) && inputWasDown(action);
|
return !inputIsDown(action) && inputWasDown(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t inputAxis(const inputaction_t neg, const inputaction_t pos) {
|
float_t inputAxis(const inputbind_t neg, const inputbind_t pos) {
|
||||||
assertTrue(neg < INPUT_ACTION_COUNT, "Negative input action out of bounds");
|
assertTrue(neg < INPUT_BIND_COUNT, "Negative input action out of bounds");
|
||||||
assertTrue(pos < INPUT_ACTION_COUNT, "Positive input action out of bounds");
|
assertTrue(pos < INPUT_BIND_COUNT, "Positive input action out of bounds");
|
||||||
|
|
||||||
return inputGetCurrentValue(pos) - inputGetCurrentValue(neg);
|
return inputGetCurrentValue(pos) - inputGetCurrentValue(neg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputAxis2D(
|
void inputAxis2D(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
) {
|
) {
|
||||||
assertNotNull(result, "Result vector cannot be null");
|
assertNotNull(result, "Result vector cannot be null");
|
||||||
@@ -232,8 +205,8 @@ void inputAxis2D(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void inputAngle2D(
|
void inputAngle2D(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
) {
|
) {
|
||||||
assertNotNull(result, "Result vector cannot be null");
|
assertNotNull(result, "Result vector cannot be null");
|
||||||
@@ -253,12 +226,12 @@ void inputAngle2D(
|
|||||||
result[1] = y;
|
result[1] = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputBind(const inputbutton_t button, const inputaction_t act) {
|
void inputBind(const inputbutton_t button, const inputbind_t act) {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
act < INPUT_ACTION_COUNT,
|
act < INPUT_BIND_COUNT,
|
||||||
"Invalid input action"
|
"Invalid input action"
|
||||||
);
|
);
|
||||||
assertTrue(act != INPUT_ACTION_NULL, "Cannot bind to NULL action");
|
assertTrue(act != INPUT_BIND_NULL, "Cannot bind to NULL action");
|
||||||
|
|
||||||
// Get the button data for this button.
|
// Get the button data for this button.
|
||||||
inputbuttondata_t *data = inputButtonGetData(button);
|
inputbuttondata_t *data = inputButtonGetData(button);
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
id,
|
|
||||||
UP,
|
|
||||||
DOWN,
|
|
||||||
LEFT,
|
|
||||||
RIGHT,
|
|
||||||
ACCEPT,
|
|
||||||
CANCEL,
|
|
||||||
PAUSE,
|
|
||||||
RAGEQUIT,
|
|
||||||
CONSOLE,
|
|
||||||
POINTERX,
|
|
||||||
POINTERY,
|
|
||||||
|
+25
-25
@@ -15,7 +15,7 @@
|
|||||||
#define INPUT_DEADZONE_DEFAULT 0.1f
|
#define INPUT_DEADZONE_DEFAULT 0.1f
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
inputactiondata_t actions[INPUT_BIND_COUNT];
|
||||||
|
|
||||||
inputplatform_t platform;
|
inputplatform_t platform;
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ void inputUpdate(void);
|
|||||||
* @param action The input action to get the value for.
|
* @param action The input action to get the value for.
|
||||||
* @return The current value of the action (0.0f to 1.0f).
|
* @return The current value of the action (0.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
float_t inputGetCurrentValue(const inputaction_t action);
|
float_t inputGetCurrentValue(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the last value of a specific input action.
|
* Gets the last value of a specific input action.
|
||||||
@@ -51,7 +51,7 @@ float_t inputGetCurrentValue(const inputaction_t action);
|
|||||||
* @param action The input action to get the value for.
|
* @param action The input action to get the value for.
|
||||||
* @return The last value of the action (0.0f to 1.0f).
|
* @return The last value of the action (0.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
float_t inputGetLastValue(const inputaction_t action);
|
float_t inputGetLastValue(const inputbind_t action);
|
||||||
|
|
||||||
#ifdef DUSK_TIME_DYNAMIC
|
#ifdef DUSK_TIME_DYNAMIC
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +60,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to get the value for.
|
* @param action The input action to get the value for.
|
||||||
* @return The current value of the action (0.0f to 1.0f).
|
* @return The current value of the action (0.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
float_t inputGetCurrentValueDynamic(const inputaction_t action);
|
float_t inputGetCurrentValueDynamic(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the last value of a specific input action (dynamic timestep).
|
* Gets the last value of a specific input action (dynamic timestep).
|
||||||
@@ -68,7 +68,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to get the value for.
|
* @param action The input action to get the value for.
|
||||||
* @return The last value of the action (0.0f to 1.0f).
|
* @return The last value of the action (0.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
float_t inputGetLastValueDynamic(const inputaction_t action);
|
float_t inputGetLastValueDynamic(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an action is currently down on the dynamic timestep.
|
* Checks if an action is currently down on the dynamic timestep.
|
||||||
@@ -76,7 +76,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action is currently down.
|
* @return true if the action is currently down.
|
||||||
*/
|
*/
|
||||||
bool_t inputIsDownDynamic(const inputaction_t action);
|
bool_t inputIsDownDynamic(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an action was down on the previous dynamic frame.
|
* Checks if an action was down on the previous dynamic frame.
|
||||||
@@ -84,7 +84,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action was down last dynamic frame.
|
* @return true if the action was down last dynamic frame.
|
||||||
*/
|
*/
|
||||||
bool_t inputWasDownDynamic(const inputaction_t action);
|
bool_t inputWasDownDynamic(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an action was pressed this dynamic frame (down now, not before).
|
* Checks if an action was pressed this dynamic frame (down now, not before).
|
||||||
@@ -92,7 +92,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action was just pressed this dynamic frame.
|
* @return true if the action was just pressed this dynamic frame.
|
||||||
*/
|
*/
|
||||||
bool_t inputPressedDynamic(const inputaction_t action);
|
bool_t inputPressedDynamic(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if an action was released this dynamic frame (up now, down before).
|
* Checks if an action was released this dynamic frame (up now, down before).
|
||||||
@@ -100,7 +100,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action was just released this dynamic frame.
|
* @return true if the action was just released this dynamic frame.
|
||||||
*/
|
*/
|
||||||
bool_t inputReleasedDynamic(const inputaction_t action);
|
bool_t inputReleasedDynamic(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of an input axis on the dynamic timestep.
|
* Gets the value of an input axis on the dynamic timestep.
|
||||||
@@ -110,8 +110,8 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @return The current axis value (-1.0f to 1.0f).
|
* @return The current axis value (-1.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
float_t inputAxisDynamic(
|
float_t inputAxisDynamic(
|
||||||
const inputaction_t neg,
|
const inputbind_t neg,
|
||||||
const inputaction_t pos
|
const inputbind_t pos
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,8 +124,8 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param result vec2 to store the result (-1.0f to 1.0f per axis).
|
* @param result vec2 to store the result (-1.0f to 1.0f per axis).
|
||||||
*/
|
*/
|
||||||
void inputAxis2DDynamic(
|
void inputAxis2DDynamic(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -139,8 +139,8 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param result vec2 to store the result.
|
* @param result vec2 to store the result.
|
||||||
*/
|
*/
|
||||||
void inputAngle2DDynamic(
|
void inputAngle2DDynamic(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
@@ -151,7 +151,7 @@ float_t inputGetLastValue(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action is currently pressed, false otherwise.
|
* @return true if the action is currently pressed, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t inputIsDown(const inputaction_t action);
|
bool_t inputIsDown(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a specific input action was pressed in the last update.
|
* Checks if a specific input action was pressed in the last update.
|
||||||
@@ -159,7 +159,7 @@ bool_t inputIsDown(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action was pressed in the last update, false otherwise.
|
* @return true if the action was pressed in the last update, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t inputWasDown(const inputaction_t action);
|
bool_t inputWasDown(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a specific input action was down this frame but not in the the
|
* Checks if a specific input action was down this frame but not in the the
|
||||||
@@ -168,7 +168,7 @@ bool_t inputWasDown(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action is currently pressed, false otherwise.
|
* @return true if the action is currently pressed, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t inputPressed(const inputaction_t action);
|
bool_t inputPressed(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a specific input action was released this frame.
|
* Checks if a specific input action was released this frame.
|
||||||
@@ -176,7 +176,7 @@ bool_t inputPressed(const inputaction_t action);
|
|||||||
* @param action The input action to check.
|
* @param action The input action to check.
|
||||||
* @return true if the action was released this frame, false otherwise.
|
* @return true if the action was released this frame, false otherwise.
|
||||||
*/
|
*/
|
||||||
bool_t inputReleased(const inputaction_t action);
|
bool_t inputReleased(const inputbind_t action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of an input axis, defined by two actions (negative and
|
* Gets the value of an input axis, defined by two actions (negative and
|
||||||
@@ -186,7 +186,7 @@ bool_t inputReleased(const inputaction_t action);
|
|||||||
* @param pos The action representing the positive direction of the axis.
|
* @param pos The action representing the positive direction of the axis.
|
||||||
* @return The current value of the axis (-1.0f to 1.0f).
|
* @return The current value of the axis (-1.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
float_t inputAxis(const inputaction_t neg, const inputaction_t pos);
|
float_t inputAxis(const inputbind_t neg, const inputbind_t pos);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the values of a 2D input axis, defined by two pairs of actions (negative
|
* Gets the values of a 2D input axis, defined by two pairs of actions (negative
|
||||||
@@ -199,8 +199,8 @@ float_t inputAxis(const inputaction_t neg, const inputaction_t pos);
|
|||||||
* @param result A vec2 to store the resulting axis values (-1.0f to 1.0f).
|
* @param result A vec2 to store the resulting axis values (-1.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
void inputAxis2D(
|
void inputAxis2D(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,8 +215,8 @@ void inputAxis2D(
|
|||||||
* @param result A vec2 to store the resulting axis values (-1.0f to 1.0f).
|
* @param result A vec2 to store the resulting axis values (-1.0f to 1.0f).
|
||||||
*/
|
*/
|
||||||
void inputAngle2D(
|
void inputAngle2D(
|
||||||
const inputaction_t negX, const inputaction_t posX,
|
const inputbind_t negX, const inputbind_t posX,
|
||||||
const inputaction_t negY, const inputaction_t posY,
|
const inputbind_t negY, const inputbind_t posY,
|
||||||
vec2 result
|
vec2 result
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ void inputAngle2D(
|
|||||||
* @param button The input button to bind.
|
* @param button The input button to bind.
|
||||||
* @param action The input action to bind the button to.
|
* @param action The input action to bind the button to.
|
||||||
*/
|
*/
|
||||||
void inputBind(const inputbutton_t button, const inputaction_t act);
|
void inputBind(const inputbutton_t button, const inputbind_t act);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies deadzone formula to a raw input value. This will;
|
* Applies deadzone formula to a raw input value. This will;
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
inputaction_t inputActionGetByName(const char_t *name) {
|
inputbind_t inputActionGetByName(const char_t *name) {
|
||||||
assertNotNull(name, "name must not be NULL");
|
assertNotNull(name, "name must not be NULL");
|
||||||
|
|
||||||
for(inputaction_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
for(inputbind_t i = 0; i < INPUT_BIND_COUNT; i++) {
|
||||||
if(INPUT_ACTION_IDS[i] == NULL) continue;
|
if(INPUT_BIND_IDS[i] == NULL) continue;
|
||||||
if(stringCompareInsensitive(INPUT_ACTION_IDS[i], name) != 0) continue;
|
if(stringCompareInsensitive(INPUT_BIND_IDS[i], name) != 0) continue;
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
return INPUT_ACTION_COUNT;
|
return INPUT_BIND_COUNT;
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/inputactiondefs.h"
|
#include "input/inputbind.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputaction_t action;
|
inputbind_t action;
|
||||||
float_t lastValue;
|
float_t lastValue;
|
||||||
float_t currentValue;
|
float_t currentValue;
|
||||||
|
|
||||||
@@ -21,19 +18,12 @@ typedef struct {
|
|||||||
float_t lastDynamicValue;
|
float_t lastDynamicValue;
|
||||||
float_t currentDynamicValue;
|
float_t currentDynamicValue;
|
||||||
#endif
|
#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;
|
} inputactiondata_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets an input action by its name.
|
* Gets an input action by its name.
|
||||||
*
|
*
|
||||||
* @param name The name of the input action.
|
* @param name The name of the input action.
|
||||||
* @return The input action, or INPUT_ACTION_COUNT if not found.
|
* @return The input action, or INPUT_BIND_COUNT if not found.
|
||||||
*/
|
*/
|
||||||
inputaction_t inputActionGetByName(const char_t *name);
|
inputbind_t inputActionGetByName(const char_t *name);
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "inputaction.h"
|
#include "input/inputaction.h"
|
||||||
#include "input/inputplatform.h"
|
#include "input/inputplatform.h"
|
||||||
|
|
||||||
#ifndef inputButtonGetValuePlatform
|
#ifndef inputButtonGetValuePlatform
|
||||||
@@ -77,7 +77,7 @@ typedef struct {
|
|||||||
inputbutton_t button;
|
inputbutton_t button;
|
||||||
float_t curVal;
|
float_t curVal;
|
||||||
float_t lastVal;
|
float_t lastVal;
|
||||||
inputaction_t action;
|
inputbind_t action;
|
||||||
} inputbuttondata_t;
|
} inputbuttondata_t;
|
||||||
|
|
||||||
extern inputbuttondata_t INPUT_BUTTON_DATA[];
|
extern inputbuttondata_t INPUT_BUTTON_DATA[];
|
||||||
|
|||||||
@@ -7,4 +7,9 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
network.c
|
network.c
|
||||||
networkinfo.c
|
networkinfo.c
|
||||||
|
networkaddr.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(http)
|
||||||
|
add_subdirectory(socket)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
networkhttpheader.c
|
||||||
|
networkhttpurl.c
|
||||||
|
networkhttprequest.c
|
||||||
|
networkhttpprocess.c
|
||||||
|
networkhttpthread.c
|
||||||
|
networkhttp.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttp.h"
|
||||||
|
#include "networkhttprequest.h"
|
||||||
|
#include "networkhttpthread.h"
|
||||||
|
|
||||||
|
networkhttp_t NETWORK_HTTP;
|
||||||
|
|
||||||
|
errorret_t networkHttpInit(void) {
|
||||||
|
networkHttpRequestPoolInit();
|
||||||
|
|
||||||
|
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
|
||||||
|
threadStart(&NETWORK_HTTP.thread);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpUpdate(void) {
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
const networkhttprequeststate_t state = request->state;
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
|
||||||
|
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
|
||||||
|
eventInvoke(&request->onComplete, request);
|
||||||
|
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
|
||||||
|
eventInvoke(&request->onError, request);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkHttpRequestReset(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpDispose(void) {
|
||||||
|
threadStop(&NETWORK_HTTP.thread);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "thread/thread.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
thread_t thread;
|
||||||
|
} networkhttp_t;
|
||||||
|
|
||||||
|
extern networkhttp_t NETWORK_HTTP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the HTTP client: prepares the request pool and starts
|
||||||
|
* the background worker thread. Called once during engine startup.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches any requests that finished on the background thread since
|
||||||
|
* the last call: fires onComplete/onError on the main thread, frees
|
||||||
|
* the response, and returns the slot to the pool. Call once per frame.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the background worker thread and disposes of the HTTP client.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpDispose(void);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttpheader.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
|
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
|
||||||
|
memoryZero(list, sizeof(networkhttpheaderlist_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t networkHttpHeaderIsReserved(const char_t *name) {
|
||||||
|
return
|
||||||
|
stringCompareInsensitive(name, "Host") == 0 ||
|
||||||
|
stringCompareInsensitive(name, "Connection") == 0 ||
|
||||||
|
stringCompareInsensitive(name, "Content-Length") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpHeaderListAdd(
|
||||||
|
networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name,
|
||||||
|
const char_t *value,
|
||||||
|
const bool_t skipReserved
|
||||||
|
) {
|
||||||
|
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
|
||||||
|
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
|
||||||
|
|
||||||
|
networkhttpheader_t *header = &list->headers[list->count];
|
||||||
|
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
|
||||||
|
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
|
||||||
|
list->count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||||
|
const networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
for(uint32_t i = 0; i < list->count; i++) {
|
||||||
|
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
|
||||||
|
return &list->headers[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
|
||||||
|
#define NETWORK_HTTP_HEADER_NAME_MAX 64
|
||||||
|
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||||
|
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||||
|
} networkhttpheader_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
|
||||||
|
uint32_t count;
|
||||||
|
} networkhttpheaderlist_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears a header list, removing all entries.
|
||||||
|
*
|
||||||
|
* @param list The header list to clear.
|
||||||
|
*/
|
||||||
|
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a header name is reserved, meaning it is computed
|
||||||
|
* internally by the request writer and must not be set by the caller.
|
||||||
|
* This is Host, Connection and Content-Length.
|
||||||
|
*
|
||||||
|
* @param name The header name to check, compared case-insensitively.
|
||||||
|
* @return true if the header name is reserved.
|
||||||
|
*/
|
||||||
|
bool_t networkHttpHeaderIsReserved(const char_t *name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a header to the list, silently doing nothing if the list is
|
||||||
|
* already full or if skipReserved is true and the name is reserved (see
|
||||||
|
* networkHttpHeaderIsReserved).
|
||||||
|
*
|
||||||
|
* @param list The header list to append to.
|
||||||
|
* @param name The header name.
|
||||||
|
* @param value The header value.
|
||||||
|
* @param skipReserved If true, reserved header names are ignored.
|
||||||
|
*/
|
||||||
|
void networkHttpHeaderListAdd(
|
||||||
|
networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name,
|
||||||
|
const char_t *value,
|
||||||
|
const bool_t skipReserved
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first header in the list matching the given name, compared
|
||||||
|
* case-insensitively.
|
||||||
|
*
|
||||||
|
* @param list The header list to search.
|
||||||
|
* @param name The header name to search for.
|
||||||
|
* @return Pointer to the matching header, or NULL if not found.
|
||||||
|
*/
|
||||||
|
const networkhttpheader_t * networkHttpHeaderListFind(
|
||||||
|
const networkhttpheaderlist_t *list,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttpprocess.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
const char_t * networkHttpProcessMethodName(
|
||||||
|
const networkhttpmethod_t method
|
||||||
|
) {
|
||||||
|
switch(method) {
|
||||||
|
case NETWORK_HTTP_METHOD_POST: return "POST";
|
||||||
|
case NETWORK_HTTP_METHOD_PUT: return "PUT";
|
||||||
|
case NETWORK_HTTP_METHOD_GET:
|
||||||
|
default:
|
||||||
|
return "GET";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessBuildHead(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *outLength
|
||||||
|
) {
|
||||||
|
size_t cursor = 0;
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
|
||||||
|
));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, "Connection: close\r\n"
|
||||||
|
));
|
||||||
|
|
||||||
|
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
|
||||||
|
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(request->bodyLength > 0) {
|
||||||
|
char_t lengthStr[24];
|
||||||
|
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(
|
||||||
|
dest, destSize, &cursor, "Content-Length: "
|
||||||
|
));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
|
||||||
|
|
||||||
|
*outLength = cursor;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessSendAll(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length
|
||||||
|
) {
|
||||||
|
size_t sentTotal = 0;
|
||||||
|
while(sentTotal < length) {
|
||||||
|
size_t sent = 0;
|
||||||
|
errorChain(networkSocketPlatformSend(
|
||||||
|
sock, data + sentTotal, length - sentTotal, &sent
|
||||||
|
));
|
||||||
|
if(sent == 0) errorThrow("Connection closed while sending data");
|
||||||
|
sentTotal += sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessReceiveHeaders(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
char_t *headerBuf,
|
||||||
|
const size_t headerBufSize,
|
||||||
|
uint8_t *leftover,
|
||||||
|
const size_t leftoverBufSize,
|
||||||
|
size_t *outLeftoverLength
|
||||||
|
) {
|
||||||
|
size_t headerLength = 0;
|
||||||
|
headerBuf[0] = '\0';
|
||||||
|
|
||||||
|
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
size_t received = 0;
|
||||||
|
errorChain(networkSocketPlatformReceive(
|
||||||
|
sock, chunk, sizeof(chunk), &received
|
||||||
|
));
|
||||||
|
if(received == 0) {
|
||||||
|
errorThrow("Connection closed before response headers completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(headerLength + received >= headerBufSize) {
|
||||||
|
errorThrow("Response headers too large");
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryCopy(headerBuf + headerLength, chunk, received);
|
||||||
|
headerLength += received;
|
||||||
|
headerBuf[headerLength] = '\0';
|
||||||
|
|
||||||
|
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
|
||||||
|
if(terminator == NULL) continue;
|
||||||
|
|
||||||
|
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
|
||||||
|
const size_t bodyStart = terminatorOffset + 4;
|
||||||
|
const size_t extra = headerLength - bodyStart;
|
||||||
|
|
||||||
|
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
|
||||||
|
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
|
||||||
|
*outLeftoverLength = extra;
|
||||||
|
|
||||||
|
// Keep the trailing "\r\n" of the last header line, drop the blank
|
||||||
|
// line so callers can split on "\r\n" without a special case.
|
||||||
|
headerBuf[terminatorOffset + 2] = '\0';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessParseStatusLine(
|
||||||
|
const char_t *line,
|
||||||
|
uint16_t *outStatus
|
||||||
|
) {
|
||||||
|
const char_t *space = strchr(line, ' ');
|
||||||
|
if(space == NULL) errorThrow("Malformed status line: %s", line);
|
||||||
|
|
||||||
|
char_t statusStr[4];
|
||||||
|
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
|
||||||
|
memoryCopy(statusStr, space + 1, 3);
|
||||||
|
statusStr[3] = '\0';
|
||||||
|
|
||||||
|
if(!stringToU16(statusStr, outStatus)) {
|
||||||
|
errorThrow("Malformed status code: %s", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpProcessParseHeaderLine(
|
||||||
|
const char_t *line,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
) {
|
||||||
|
const char_t *colon = strchr(line, ':');
|
||||||
|
if(colon == NULL) return;
|
||||||
|
|
||||||
|
const size_t nameLength = (size_t)(colon - line);
|
||||||
|
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
|
||||||
|
|
||||||
|
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
|
||||||
|
memoryCopy(name, line, nameLength);
|
||||||
|
name[nameLength] = '\0';
|
||||||
|
|
||||||
|
const char_t *valueStart = colon + 1;
|
||||||
|
while(*valueStart == ' ') valueStart++;
|
||||||
|
|
||||||
|
const size_t valueLength = strlen(valueStart);
|
||||||
|
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
|
||||||
|
|
||||||
|
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
|
||||||
|
memoryCopy(value, valueStart, valueLength + 1);
|
||||||
|
|
||||||
|
networkHttpHeaderListAdd(outHeaders, name, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessParseHeaders(
|
||||||
|
char_t *headerBuf,
|
||||||
|
uint16_t *outStatus,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
) {
|
||||||
|
char_t *cursor = headerBuf;
|
||||||
|
bool_t first = true;
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
char_t *lineEnd = strstr(cursor, "\r\n");
|
||||||
|
const bool_t isLast = lineEnd == NULL;
|
||||||
|
if(lineEnd != NULL) *lineEnd = '\0';
|
||||||
|
|
||||||
|
if(first) {
|
||||||
|
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
|
||||||
|
first = false;
|
||||||
|
} else if(cursor[0] != '\0') {
|
||||||
|
networkHttpProcessParseHeaderLine(cursor, outHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isLast) break;
|
||||||
|
cursor = lineEnd + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessReadBody(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const networkhttpheaderlist_t *headers,
|
||||||
|
const uint8_t *leftover,
|
||||||
|
const size_t leftoverLength,
|
||||||
|
uint8_t **outBody,
|
||||||
|
size_t *outBodyLength
|
||||||
|
) {
|
||||||
|
const networkhttpheader_t *contentLength =
|
||||||
|
networkHttpHeaderListFind(headers, "Content-Length");
|
||||||
|
|
||||||
|
if(contentLength != NULL) {
|
||||||
|
int64_t expected = 0;
|
||||||
|
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
|
||||||
|
errorThrow("Malformed Content-Length header: %s", contentLength->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(expected == 0) {
|
||||||
|
*outBody = NULL;
|
||||||
|
*outBodyLength = 0;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *body = memoryAllocate((size_t)expected);
|
||||||
|
const size_t initialCopy =
|
||||||
|
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
|
||||||
|
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
|
||||||
|
size_t received = initialCopy;
|
||||||
|
|
||||||
|
while(received < (size_t)expected) {
|
||||||
|
size_t got = 0;
|
||||||
|
errorret_t ret = networkSocketPlatformReceive(
|
||||||
|
sock, body + received, (size_t)expected - received, &got
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
memoryFree(body);
|
||||||
|
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||||
|
}
|
||||||
|
if(got == 0) {
|
||||||
|
memoryFree(body);
|
||||||
|
errorThrow("Connection closed before full body was received");
|
||||||
|
}
|
||||||
|
received += got;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outBody = body;
|
||||||
|
*outBodyLength = (size_t)expected;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No Content-Length: read until the connection closes.
|
||||||
|
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
|
||||||
|
if(leftoverLength > capacity) capacity = leftoverLength;
|
||||||
|
|
||||||
|
uint8_t *body = memoryAllocate(capacity);
|
||||||
|
size_t length = 0;
|
||||||
|
if(leftoverLength > 0) {
|
||||||
|
memoryCopy(body, leftover, leftoverLength);
|
||||||
|
length = leftoverLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(;;) {
|
||||||
|
if(length == capacity) {
|
||||||
|
const size_t newCapacity = capacity * 2;
|
||||||
|
memoryResize((void **)&body, capacity, newCapacity);
|
||||||
|
capacity = newCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t got = 0;
|
||||||
|
errorret_t ret = networkSocketPlatformReceive(
|
||||||
|
sock, body + length, capacity - length, &got
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(ret)) {
|
||||||
|
memoryFree(body);
|
||||||
|
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
|
||||||
|
}
|
||||||
|
if(got == 0) break;
|
||||||
|
length += got;
|
||||||
|
}
|
||||||
|
|
||||||
|
*outBody = body;
|
||||||
|
*outBodyLength = length;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpProcessAttempt(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
networkhttpresponse_t *outResponse
|
||||||
|
) {
|
||||||
|
memoryZero(outResponse, sizeof(networkhttpresponse_t));
|
||||||
|
|
||||||
|
networksocketplatform_t sock;
|
||||||
|
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
|
||||||
|
|
||||||
|
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
|
||||||
|
size_t headLength = 0;
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
|
||||||
|
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
|
||||||
|
));
|
||||||
|
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||||
|
&sock, (const uint8_t *)head, headLength
|
||||||
|
));
|
||||||
|
|
||||||
|
if(request->bodyLength > 0) {
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
|
||||||
|
&sock, request->body, request->bodyLength
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
|
||||||
|
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
|
||||||
|
size_t leftoverLength = 0;
|
||||||
|
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
|
||||||
|
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
|
||||||
|
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
|
||||||
|
));
|
||||||
|
|
||||||
|
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
|
||||||
|
headerBuf, &outResponse->status, &outResponse->headers
|
||||||
|
));
|
||||||
|
|
||||||
|
errorret_t bodyRet = networkHttpProcessReadBody(
|
||||||
|
&sock, &outResponse->headers, leftover, leftoverLength,
|
||||||
|
&outResponse->body, &outResponse->bodyLength
|
||||||
|
);
|
||||||
|
networkSocketPlatformClose(&sock);
|
||||||
|
errorChain(bodyRet);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "network/http/networkhttprequest.h"
|
||||||
|
#include "network/http/networkhttpurl.h"
|
||||||
|
#include "network/networksocketplatform.h"
|
||||||
|
|
||||||
|
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
|
||||||
|
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
|
||||||
|
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
|
||||||
|
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to chain an error, closing sock first if expr failed. Used
|
||||||
|
* for every step of an attempt after the socket has connected, since
|
||||||
|
* failing partway through must not leak the socket.
|
||||||
|
*/
|
||||||
|
#define networkHttpProcessErrorChain(sock, _expr) { \
|
||||||
|
errorret_t _nhpErr = (_expr); \
|
||||||
|
if(errorIsNotOk(_nhpErr)) { \
|
||||||
|
networkSocketPlatformClose(sock); \
|
||||||
|
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the wire method name ("GET", "POST", "PUT") for method.
|
||||||
|
*
|
||||||
|
* @param method The method to name.
|
||||||
|
* @return A static string naming the method.
|
||||||
|
*/
|
||||||
|
const char_t * networkHttpProcessMethodName(
|
||||||
|
const networkhttpmethod_t method
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the request line, headers and blank line terminator (but not
|
||||||
|
* the body) for request, targeting target, into dest.
|
||||||
|
*
|
||||||
|
* @param request The request being sent.
|
||||||
|
* @param target The resolved host/port/path for this attempt.
|
||||||
|
* @param dest The destination buffer.
|
||||||
|
* @param destSize The size of dest, including the null terminator.
|
||||||
|
* @param outLength The number of bytes written is written here.
|
||||||
|
* @return An error if the result would not fit in dest.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessBuildHead(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
char_t *dest,
|
||||||
|
const size_t destSize,
|
||||||
|
size_t *outLength
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends length bytes of data over sock, looping until all of it has
|
||||||
|
* been written.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param data The data to send.
|
||||||
|
* @param length The number of bytes in data.
|
||||||
|
* @return An error if the send failed or the connection closed early.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessSendAll(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const uint8_t *data,
|
||||||
|
const size_t length
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads from sock until the blank line ("\r\n\r\n") terminating the
|
||||||
|
* response headers has been seen, writing the status line and header
|
||||||
|
* lines (without the blank line) into headerBuf as a null-terminated
|
||||||
|
* string. Any body bytes read past the terminator in the same receive
|
||||||
|
* are written to leftover.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param headerBuf The destination buffer for the header text.
|
||||||
|
* @param headerBufSize The size of headerBuf, including the null
|
||||||
|
* terminator.
|
||||||
|
* @param leftover Destination buffer for any body bytes read early.
|
||||||
|
* @param leftoverBufSize The size of leftover.
|
||||||
|
* @param outLeftoverLength The number of bytes written to leftover is
|
||||||
|
* written here.
|
||||||
|
* @return An error if the headers could not be read, were malformed,
|
||||||
|
* or did not fit within the given buffers.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessReceiveHeaders(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
char_t *headerBuf,
|
||||||
|
const size_t headerBufSize,
|
||||||
|
uint8_t *leftover,
|
||||||
|
const size_t leftoverBufSize,
|
||||||
|
size_t *outLeftoverLength
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a "HTTP/1.1 <status> <reason>" status line.
|
||||||
|
*
|
||||||
|
* @param line The status line, null-terminated.
|
||||||
|
* @param outStatus The parsed status code is written here.
|
||||||
|
* @return An error if the status line is malformed.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessParseStatusLine(
|
||||||
|
const char_t *line,
|
||||||
|
uint16_t *outStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a single "Name: Value" response header line and appends it to
|
||||||
|
* outHeaders. Silently does nothing if the line has no colon or the
|
||||||
|
* name/value are too long to fit a networkhttpheader_t.
|
||||||
|
*
|
||||||
|
* @param line The header line, null-terminated.
|
||||||
|
* @param outHeaders The header list to append to.
|
||||||
|
*/
|
||||||
|
void networkHttpProcessParseHeaderLine(
|
||||||
|
const char_t *line,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the status line and every header line out of headerBuf (as
|
||||||
|
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
|
||||||
|
* place to split it into lines.
|
||||||
|
*
|
||||||
|
* @param headerBuf The header text to parse.
|
||||||
|
* @param outStatus The parsed status code is written here.
|
||||||
|
* @param outHeaders The header list to populate.
|
||||||
|
* @return An error if the status line is malformed.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessParseHeaders(
|
||||||
|
char_t *headerBuf,
|
||||||
|
uint16_t *outStatus,
|
||||||
|
networkhttpheaderlist_t *outHeaders
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the response body from sock. If headers contains a
|
||||||
|
* Content-Length, reads exactly that many bytes; otherwise reads until
|
||||||
|
* the connection closes. Either way, leftover/leftoverLength (body
|
||||||
|
* bytes already read while looking for the header terminator) are
|
||||||
|
* included at the start of the result.
|
||||||
|
*
|
||||||
|
* @param sock The connected socket.
|
||||||
|
* @param headers The parsed response headers.
|
||||||
|
* @param leftover Body bytes already read past the header terminator.
|
||||||
|
* @param leftoverLength The number of bytes in leftover.
|
||||||
|
* @param outBody A memoryAllocate'd buffer holding the body is written
|
||||||
|
* here, or NULL if the body is empty. Owned by the caller.
|
||||||
|
* @param outBodyLength The number of bytes in *outBody is written here.
|
||||||
|
* @return An error if the body could not be fully read.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessReadBody(
|
||||||
|
networksocketplatform_t *sock,
|
||||||
|
const networkhttpheaderlist_t *headers,
|
||||||
|
const uint8_t *leftover,
|
||||||
|
const size_t leftoverLength,
|
||||||
|
uint8_t **outBody,
|
||||||
|
size_t *outBodyLength
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a single blocking HTTP exchange: connects to target, writes
|
||||||
|
* the request head + body, and reads back one parsed response. Does
|
||||||
|
* not follow redirects -- that is the caller's responsibility.
|
||||||
|
*
|
||||||
|
* @param request The request to send (method, headers, body).
|
||||||
|
* @param target The resolved host/port/path to connect to for this
|
||||||
|
* attempt (may differ from the request's own URL if this is a
|
||||||
|
* redirected attempt).
|
||||||
|
* @param outResponse The parsed response is written here on success.
|
||||||
|
* @return An error if the exchange could not be completed.
|
||||||
|
*/
|
||||||
|
errorret_t networkHttpProcessAttempt(
|
||||||
|
const networkhttprequest_t *request,
|
||||||
|
const networkhttpurl_t *target,
|
||||||
|
networkhttpresponse_t *outResponse
|
||||||
|
);
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "networkhttprequest.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
|
||||||
|
|
||||||
|
void networkHttpRequestPoolInit(void) {
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
memoryZero(request, sizeof(networkhttprequest_t));
|
||||||
|
threadMutexInit(&request->mutex);
|
||||||
|
eventInit(
|
||||||
|
&request->onComplete, request->onCompleteCallbacks,
|
||||||
|
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&request->onError, request->onErrorCallbacks,
|
||||||
|
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
networkhttprequest_t * networkHttpRequestClaim(void) {
|
||||||
|
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
|
||||||
|
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertUnreachable("No available HTTP request slots.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void networkHttpRequestReset(networkhttprequest_t *request) {
|
||||||
|
assertNotNull(request, "request must not be NULL");
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
|
||||||
|
if(request->body != NULL) memoryFree(request->body);
|
||||||
|
request->body = NULL;
|
||||||
|
request->bodyLength = 0;
|
||||||
|
|
||||||
|
if(request->response.body != NULL) memoryFree(request->response.body);
|
||||||
|
memoryZero(&request->response, sizeof(networkhttpresponse_t));
|
||||||
|
|
||||||
|
networkHttpHeaderListClear(&request->requestHeaders);
|
||||||
|
|
||||||
|
eventInit(
|
||||||
|
&request->onComplete, request->onCompleteCallbacks,
|
||||||
|
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
eventInit(
|
||||||
|
&request->onError, request->onErrorCallbacks,
|
||||||
|
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
|
||||||
|
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t networkHttpRequest(
|
||||||
|
const networkhttpmethod_t method,
|
||||||
|
const char_t *url,
|
||||||
|
const networkhttpheader_t *headers,
|
||||||
|
const uint32_t headerCount,
|
||||||
|
const networkhttpheader_t *queryParams,
|
||||||
|
const uint32_t queryParamCount,
|
||||||
|
const uint8_t *body,
|
||||||
|
const size_t bodyLength,
|
||||||
|
const eventcallback_t onComplete,
|
||||||
|
const eventcallback_t onError,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(url, "url must not be NULL");
|
||||||
|
|
||||||
|
networkhttprequest_t *request = networkHttpRequestClaim();
|
||||||
|
request->method = method;
|
||||||
|
|
||||||
|
errorChain(networkHttpUrlBuild(
|
||||||
|
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
|
||||||
|
));
|
||||||
|
|
||||||
|
networkHttpHeaderListClear(&request->requestHeaders);
|
||||||
|
for(uint32_t i = 0; i < headerCount; i++) {
|
||||||
|
networkHttpHeaderListAdd(
|
||||||
|
&request->requestHeaders, headers[i].name, headers[i].value, true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
request->bodyLength = bodyLength;
|
||||||
|
if(bodyLength > 0) {
|
||||||
|
assertNotNull(body, "body must not be NULL when bodyLength > 0");
|
||||||
|
request->body = memoryAllocate(bodyLength);
|
||||||
|
memoryCopy(request->body, body, bodyLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
|
||||||
|
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
|
||||||
|
|
||||||
|
threadMutexLock(&request->mutex);
|
||||||
|
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
|
||||||
|
threadMutexUnlock(&request->mutex);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user