Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -0,0 +1,858 @@
|
|||||||
|
# Dusk Code Check
|
||||||
|
|
||||||
|
A working checklist for walking through `src/dusk/` (the core, platform-
|
||||||
|
agnostic engine layer) one subsystem at a time and reviewing how it
|
||||||
|
actually looks today -- not just what it's supposed to do. Pairs with
|
||||||
|
`STATUS.md` (maturity snapshot) and `ROADMAP.md` (forward-looking work);
|
||||||
|
this doc is about auditing what's already there.
|
||||||
|
|
||||||
|
Each section below: **Purpose**, **Key files**, and a **Review status**
|
||||||
|
line to fill in as each system gets walked through (e.g. `Not started` /
|
||||||
|
`In progress` / `Reviewed 2026-08-01: <one-line finding>`).
|
||||||
|
|
||||||
|
First full pass completed 2026-08-01 (all 23 sections below reviewed).
|
||||||
|
See "Top cross-cutting findings" right after the index for the
|
||||||
|
highest-priority items across all of them before diving into any one
|
||||||
|
section -- this doc is long, don't make someone read all 23 to find
|
||||||
|
the load-bearing bugs.
|
||||||
|
|
||||||
|
Game-specific code lives in `src/duskrpg/` (not covered here) and
|
||||||
|
platform-specific implementations live in `src/dusk<platform>/` /
|
||||||
|
`src/duskgl/` / `src/dusksdl2/` (also not covered here) -- see `CLAUDE.md`
|
||||||
|
for the layer structure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [error](#error)
|
||||||
|
- [assert](#assert)
|
||||||
|
- [util](#util)
|
||||||
|
- [event](#event)
|
||||||
|
- [time](#time)
|
||||||
|
- [thread](#thread)
|
||||||
|
- [log](#log)
|
||||||
|
- [system](#system)
|
||||||
|
- [console](#console)
|
||||||
|
- [asset](#asset)
|
||||||
|
- [locale](#locale)
|
||||||
|
- [save](#save)
|
||||||
|
- [network](#network)
|
||||||
|
- [display](#display)
|
||||||
|
- [ui](#ui)
|
||||||
|
- [entity](#entity)
|
||||||
|
- [scene](#scene)
|
||||||
|
- [physics](#physics)
|
||||||
|
- [animation](#animation)
|
||||||
|
- [script](#script)
|
||||||
|
- [input](#input)
|
||||||
|
- [engine](#engine)
|
||||||
|
- [game](#game)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top cross-cutting findings
|
||||||
|
|
||||||
|
Ranked roughly by real-world impact, not by section order. Each links
|
||||||
|
to its full writeup below.
|
||||||
|
|
||||||
|
1. **Bounds/capacity checks that matter at runtime are `assert*` calls,
|
||||||
|
which compile to no-ops in release (`DUSK_ASSERTIONS_FAKED`, set
|
||||||
|
whenever `NDEBUG` is defined) -- confirmed by reading `assert.h`
|
||||||
|
directly.** This shows up in [entity](#entity) (`entitymanager.c`,
|
||||||
|
`component.c` -- reachable from JerryScript via `moduleentity.c`
|
||||||
|
with no return-value check) and [scene](#scene) (`sceneCreate`'s
|
||||||
|
pool-exhaustion path, reachable from `new Scene()`). Untrusted
|
||||||
|
script input can silently get back an invalid ID that later indexes
|
||||||
|
a fixed array out of bounds in a release build -- the exact
|
||||||
|
"validate untrusted data with errorThrow, not asserts" case
|
||||||
|
CLAUDE.md/project memory already calls out, just not applied here.
|
||||||
|
2. **FIXED 2026-08-01.** `util/ref.c`'s `refUnlock` decremented an
|
||||||
|
unsigned `count` guarded only by a tautological `count >= 0` assert
|
||||||
|
([util](#util)) -- a double-unlock silently underflowed to
|
||||||
|
`UINT32_MAX` instead of tripping an assert. This wasn't just a
|
||||||
|
`util/` nit: it's the lock/unlock primitive `asset/loader/assetentry.c`
|
||||||
|
builds on (see [asset](#asset)), so an over-unlocked asset entry
|
||||||
|
became permanently un-reapable rather than erroring loudly. Now
|
||||||
|
asserts `count > 0` before decrementing; the pointless
|
||||||
|
`count >= 0` check in `refLock` was removed too.
|
||||||
|
3. **`texture.c:42-52`'s `textureInit()` reads `texture->format` to
|
||||||
|
decide which `data` fields to validate *before* the struct is
|
||||||
|
zeroed a few lines later** ([display](#display)) -- it's validating
|
||||||
|
whatever stale value happened to be in the caller's (possibly
|
||||||
|
stack) memory, not the `format` parameter actually being requested.
|
||||||
|
4. **FIXED 2026-08-01 (confirmed real before fixing).** PSP's
|
||||||
|
`timeGetRealTimeZonePSP` returned hours; every other platform
|
||||||
|
(`timeGetRealTimeZoneLinux`/Dolphin) returns seconds, and
|
||||||
|
`timeEpochInit`/`timeEpochGetHours` etc. add the timezone value
|
||||||
|
directly to a Unix-epoch-seconds timestamp -- confirmed by reading
|
||||||
|
all three platform implementations plus `timeepoch.c` directly, not
|
||||||
|
just trusting the review agent's claim. The PSP function's own
|
||||||
|
comment even said "Return timezone offset in hours". Now divides by
|
||||||
|
`1000000.0` only (microseconds -> seconds), matching the other
|
||||||
|
platforms. See [time](#time).
|
||||||
|
5. **Discussed, not changed.** `consolePrint` aborts the process (via
|
||||||
|
`stringFormatVA`'s `assert(ret < destSize)`) instead of truncating
|
||||||
|
when a single debug line exceeds 511 bytes. This is a real, if rare,
|
||||||
|
crash risk (any format producing >511 bytes of runtime-variable
|
||||||
|
content -- a long path, a long error string -- takes the whole
|
||||||
|
process down over a debug print), but changing `stringFormatVA`
|
||||||
|
itself would affect every caller in the codebase, and a
|
||||||
|
console-local truncating rewrite is easy to do wrong (vsnprintf
|
||||||
|
directly, bypassing the project's string-utils convention) without
|
||||||
|
more time to get right. Left as-is pending a decision on which
|
||||||
|
fix shape is wanted. See [console](#console).
|
||||||
|
6. **FIXED 2026-08-01.** `thread.c`'s `threadHandler` signaled
|
||||||
|
`THREAD_STATE_STOPPED` before resetting `threadId = 0`, and
|
||||||
|
`threadStartRequest` wasn't synchronized against that reset -- a
|
||||||
|
real (if rare-window) race, not hypothetical: the test suite was
|
||||||
|
already working around it by re-calling `threadInit()` instead of
|
||||||
|
exercising true stop/start reuse. `threadId` is now reset inside the
|
||||||
|
same mutex-locked section before the STOPPED signal/unlock;
|
||||||
|
`test_thread_restart` now loops 20x on the same `thread_t` without
|
||||||
|
re-initializing, which would have caught the old race. See
|
||||||
|
[thread](#thread).
|
||||||
|
7. **FIXED 2026-08-01.** `tools/color.py` emitted invalid C float
|
||||||
|
literals (`0f`, `1f`, etc.) for any whole-number CSV channel --
|
||||||
|
confirmed by actually running the generator against `color.csv`
|
||||||
|
before and after. Was dormant only because `COLOR_*_3F`/`_4F` were
|
||||||
|
unreferenced anywhere in the repo; the first real use of a float
|
||||||
|
color variant wouldn't have compiled. Now routes channel values
|
||||||
|
through Python `float()` before interpolating (always stringifies
|
||||||
|
with a decimal point, e.g. `0.0`/`1.0`), and `test/display/test_color.c`
|
||||||
|
gained two tests that reference `COLOR_BLACK_3F`/`COLOR_WHITE_4F`/
|
||||||
|
`COLOR_RED_3B`/`COLOR_GRAY_3F` directly -- giving the previously-dead
|
||||||
|
generated macros real compile-time + value coverage instead of
|
||||||
|
silently bit-rotting again. See [display](#display).
|
||||||
|
8. **`assetModelLoaderAsync` is confirmed fully dead** -- model loading
|
||||||
|
is genuinely synchronous end-to-end (verified by tracing the state
|
||||||
|
machine, not just repeating STATUS.md's claim). See [asset](#asset).
|
||||||
|
9. **No cycle guard on prefab `extends` chains** in
|
||||||
|
`scenePrefabResolveAndApply` -- a self-referencing or A->B->A prefab
|
||||||
|
table is a stack-overflow crash from what looks like a harmless
|
||||||
|
typo in hand-authored data. See [scene](#scene).
|
||||||
|
10. **Untested-code hot spots worth prioritizing** if test investment
|
||||||
|
is on the table: `util/ref.c`/`random.c`/`crypt.c`/`endian.c`
|
||||||
|
(zero tests), `animation/easing.c` (13 of 16 functions untested),
|
||||||
|
`event/`, `console/`, `system/`, `log/`, `input/`, `engine/`,
|
||||||
|
`locale/`, `save/`, and most of `display/` (mesh, shader, texture,
|
||||||
|
tileset, text, screen, framebuffer) have no dedicated tests at
|
||||||
|
all.
|
||||||
|
|
||||||
|
Convention-only items (real, but lower stakes than the above): raw
|
||||||
|
stdlib string functions (`strlen`/`strchr`/`strstr`/etc.) show up
|
||||||
|
instead of `util/string.h` wrappers in `network/http/` and
|
||||||
|
`asset/loader/locale/assetlocaleloader.c` ([network](#network),
|
||||||
|
[asset](#asset)); several `display/mesh/` files use `//` header
|
||||||
|
comments and `/* */` inline comments where CLAUDE.md wants the
|
||||||
|
opposite ([display](#display)); `animation/`'s core files all use `//`
|
||||||
|
copyright headers instead of `/** */` ([animation](#animation)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## error
|
||||||
|
|
||||||
|
**Purpose:** The engine's single error-handling convention. Every
|
||||||
|
fallible function returns `errorret_t`, built with `errorOk()` /
|
||||||
|
`errorThrow(fmt, ...)` / `errorChain(call)` and consumed with
|
||||||
|
`errorIsOk()`/`errorIsNotOk()`/`errorCatch()`. No `errno`, no raw error
|
||||||
|
codes anywhere else in the engine.
|
||||||
|
|
||||||
|
**Key files:** `error.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Solid, and tested unusually well
|
||||||
|
(`test/error/test_error.c` covers thread isolation and concurrent
|
||||||
|
throws, not just happy path). Two issues: `errorChainImpl` (error.c:86-91)
|
||||||
|
hand-rolls `snprintf`/`strlen` instead of `util/string.h`'s
|
||||||
|
`stringFormat`/`stringFormatVA` wrappers that `errorThrowImpl` uses one
|
||||||
|
function above it -- an inconsistency, not just a style nit, since
|
||||||
|
CLAUDE.md requires the wrappers everywhere. `errorThrowWithCode`
|
||||||
|
(error.h:100) is dead code -- zero call sites anywhere in `src/`/`test/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## assert
|
||||||
|
|
||||||
|
**Purpose:** Debug-time invariant checks -- `assertNotNull`, `assertTrue`,
|
||||||
|
`assertFalse`, `assertUnreachable`, `assertIsMainThread`. Distinct from
|
||||||
|
`error/`: asserts are for programmer mistakes/invariants, `errorret_t` is
|
||||||
|
for runtime-recoverable failures (see `CLAUDE.md`'s note on validating
|
||||||
|
untrusted data with `errorThrow` rather than asserting).
|
||||||
|
|
||||||
|
**Key files:** `assert.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Clean, no issues found. Impl
|
||||||
|
functions + macros wrapping `__FILE__`/`__LINE__` and a proper
|
||||||
|
`DUSK_ASSERTIONS_FAKED` no-op path for release builds;
|
||||||
|
`assertNotNullImpl` (assert.c:109-111) does a real "touch" dereference
|
||||||
|
after the null check as a belt-and-braces addition. Test coverage
|
||||||
|
exercises both success and `expect_assert_failure` failure paths for
|
||||||
|
every variant, including `assertDeprecated`/`assertIsMainThread`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## util
|
||||||
|
|
||||||
|
**Purpose:** Small stand-alone helpers with no engine-state dependencies:
|
||||||
|
`array` (dynamic array), `crypt` (hashing/checksum), `endian` (byte-order
|
||||||
|
swap), `math` (clamp/lerp/pow2/etc.), `memory` (`memoryAllocate`/`Free`/
|
||||||
|
`Zero`/`Copy`/`Compare` -- the only allocator the rest of the engine is
|
||||||
|
allowed to call directly), `random`, `ref` (refcounted handle with
|
||||||
|
lock/unlock callbacks), `sort` (bubble sort used for one-time static list
|
||||||
|
ordering), `string` (`stringCopy`/`Compare`/`Format`/etc. -- required in
|
||||||
|
place of libc `str*` functions per `CLAUDE.md`).
|
||||||
|
|
||||||
|
**Key files:** `array.c/h`, `crypt.c/h`, `endian.c/h`, `math.c/h`,
|
||||||
|
`memory.c/h`, `random.c/h`, `ref.c/h`, `sort.c/h`, `string.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Several real issues, worst was
|
||||||
|
in `ref.c`: `refUnlock` (ref.c:37-39) decremented `count` (`uint32_t`)
|
||||||
|
guarded only by `assertTrue(ref->count >= 0, ...)` -- tautologically
|
||||||
|
always true on an unsigned type, so a double-unlock silently underflowed
|
||||||
|
to `UINT32_MAX` and kept invoking `onUnlock` instead of tripping an
|
||||||
|
assert (confirmed by reading ref.c directly). **Fixed 2026-08-01** --
|
||||||
|
`refUnlock` now asserts `count > 0` before decrementing; also fixed
|
||||||
|
`refInit`'s doc comment, which claimed a fresh ref starts at count 1
|
||||||
|
when the code has always started it at 0. Same tautological unsigned
|
||||||
|
`>= 0` assert remains in `memory.c:98` (lower stakes -- `memoryFree`'s
|
||||||
|
own counter, not a shared lock/unlock primitive other subsystems build
|
||||||
|
on -- left as-is). `memory.h:11` declares
|
||||||
|
`static size_t MEMORY_POINTERS_IN_USE = 0;` directly in the header --
|
||||||
|
confirmed present -- meaning every TU including it gets its own private
|
||||||
|
copy (only "works" because the accessors all live in memory.c).
|
||||||
|
`random.c`'s `randomInt` (confirmed, random.c:15-16) does
|
||||||
|
`rand() % (max - min)` with no `max > min` check (UB on `max <= min`),
|
||||||
|
and `srand()` is never called anywhere in the repo -- unseeded,
|
||||||
|
deterministic RNG. `sort.c:1` has `#include <stdlib.h>` before the
|
||||||
|
copyright header (confirmed). Test coverage: array/math/memory/sort/
|
||||||
|
string are thorough; `ref.c`, `random.c`, `crypt.c`, `endian.c` have
|
||||||
|
**zero** tests. `randomInt`/`randomFloat` also appear unused
|
||||||
|
engine-wide (dead code, not just untested).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## event
|
||||||
|
|
||||||
|
**Purpose:** Generic pub/sub primitive -- `event_t` wraps a caller-owned
|
||||||
|
array of `eventcallback_t`/user-pointer pairs; `eventInit`/`eventSubscribe`/
|
||||||
|
`eventInvoke`. Used for things like `uifullbox_t`'s transition-end
|
||||||
|
callback and `uiloading_t`'s show/hide callbacks. Per `STATUS.md`: "Clean,
|
||||||
|
small, finished", built to replace the old input system's callback
|
||||||
|
pattern.
|
||||||
|
|
||||||
|
**Key files:** `event.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Well-formed -- correctly uses
|
||||||
|
swap-with-last for O(1) unsubscribe, asserts on capacity overflow and
|
||||||
|
duplicate-subscribe, no dead code (`eventUnsubscribe` used in 3 other
|
||||||
|
files). No `errorret_t` needed here since these are programmer-error
|
||||||
|
assertions rather than runtime-data validation, consistent with
|
||||||
|
CLAUDE.md's assert-vs-error guidance. One gap: `test/event/` doesn't
|
||||||
|
exist -- zero unit coverage, including the capacity-exceeded and
|
||||||
|
double-subscribe assert paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## time
|
||||||
|
|
||||||
|
**Purpose:** Frame delta/elapsed time (`TIME.delta`, exposed to JS via
|
||||||
|
`moduleTime`) plus `timeepoch_t` wall-clock timestamps (used by
|
||||||
|
`uifps.c` for the FPS counter).
|
||||||
|
|
||||||
|
**Key files:** `time.c/h`, `timeepoch.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. `time.c:52-56` unconditionally
|
||||||
|
formats an epoch string into a 256-byte stack buffer every single
|
||||||
|
`timeUpdate()` call purely to feed a commented-out `consolePrint` --
|
||||||
|
burns the leap-year/day-counting loop three times a frame for a
|
||||||
|
discarded value. `timeepoch.c`'s day-counting loops have no
|
||||||
|
handling/guard for negative epoch/timezone values (pre-1970). Test
|
||||||
|
coverage is solid for `time.c` (mocked ticks, boundary/backwards-time
|
||||||
|
cases) but `timeepoch.c`'s date math (leap year, rollover, format
|
||||||
|
specifiers) has no dedicated tests despite being the more bug-prone
|
||||||
|
half. The PSP-specific timezone unit bug flagged in the cross-cutting
|
||||||
|
findings (#4) was confirmed and fixed 2026-08-01 --
|
||||||
|
`timeGetRealTimeZonePSP` was returning hours where `timeepoch.c`
|
||||||
|
expects seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## thread
|
||||||
|
|
||||||
|
**Purpose:** Thin cross-platform thread/mutex/thread-local wrappers.
|
||||||
|
Used by the asset system's background loading and the POSIX console's
|
||||||
|
input-polling thread.
|
||||||
|
|
||||||
|
**Key files:** `thread.c/h`, `threadmutex.c/h`, `threadlocal.h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Conventions followed
|
||||||
|
throughout. Real race condition, **fixed 2026-08-01**: `threadHandler`
|
||||||
|
used to unlock `stateMutex` and signal `THREAD_STATE_STOPPED` *before*
|
||||||
|
resetting `thread->threadId = 0`, but `threadStartRequest` asserts
|
||||||
|
`threadId == 0` with no synchronization against that reset --
|
||||||
|
`threadStop()` immediately followed by `threadStart()` on the same
|
||||||
|
struct could intermittently trip the assert. Telling sign it was real,
|
||||||
|
not theoretical: `test/thread/test_thread.c` already re-called
|
||||||
|
`threadInit()` before restarting specifically "so threadId/state are
|
||||||
|
reset" -- the test suite was working around the bug rather than
|
||||||
|
exercising true reuse. `threadId` is now reset inside the same
|
||||||
|
mutex-locked section, before the signal/unlock; the test now loops
|
||||||
|
stop/start 20x on the same `thread_t` with no `threadInit()` in
|
||||||
|
between. Otherwise test coverage is good (real pthreads, try-lock
|
||||||
|
coordination, a 4-thread x 10k-iteration mutex contention test).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## log
|
||||||
|
|
||||||
|
**Purpose:** `logDebug`/`logError` -- the engine's only print/debug-output
|
||||||
|
path. Header-only declarations; each platform provides the actual
|
||||||
|
implementation (see `src/dusk<platform>/log/`).
|
||||||
|
|
||||||
|
**Key files:** `log.h` (declarations only, no `.c` in core).
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Clean 2-function API; all
|
||||||
|
three platform implementations (`dusklinux`, `duskpsp`, `duskdolphin`)
|
||||||
|
match the declared signatures with correct `va_copy` usage for the
|
||||||
|
dual stdout+file writes. Dolphin's `logError` correctly falls back to
|
||||||
|
raw VIDEO/console init when the framebuffer isn't ready yet and blocks
|
||||||
|
on a START-button dismiss loop; PSP writes to
|
||||||
|
`ms0:/PSP/GAME/Dusk/*.log`. No bugs found. No `test/log/` exists --
|
||||||
|
understandable for I/O/side-effect code, but zero coverage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## system
|
||||||
|
|
||||||
|
**Purpose:** Very early engine init (`systemInit`), the
|
||||||
|
`systemplatform_t` enum (via the `systemplatformlist.h` X-macro pattern),
|
||||||
|
and native OS dialog boxes (`systemdialogtype_t`: render-blocking vs
|
||||||
|
tick-blocking).
|
||||||
|
|
||||||
|
**Key files:** `system.c/h`, `systemplatformlist.h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. X-macro platform enum and
|
||||||
|
compile-time `#error` guards for missing platform macros used
|
||||||
|
correctly; all three platforms define the required
|
||||||
|
`systemInitPlatform`/`systemGetActiveDialogTypePlatform`/
|
||||||
|
`systemGetCyclesPlatform`. Dead code confirmed: `systemPSPGetLanguage()`
|
||||||
|
(`duskpsp/system/systempsp.c:51`) and `systemGetLanguageDolphin()`
|
||||||
|
(`duskdolphin/system/systemdolphin.c:40`) are defined but never called
|
||||||
|
anywhere -- likely intended for locale wiring that hasn't landed. No
|
||||||
|
`test/system/` exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## console
|
||||||
|
|
||||||
|
**Purpose:** In-engine dev console: fixed-size scrollback
|
||||||
|
(`CONSOLE_HISTORY_MAX` lines x `CONSOLE_LINE_MAX` chars), `consolePrint`,
|
||||||
|
a `dirty` flag consumers (namely `ui/debug/uiconsole.c`) watch to know
|
||||||
|
when to rebuild their own cached representation. POSIX builds also poll
|
||||||
|
stdin on a background thread for interactive script execution.
|
||||||
|
|
||||||
|
**Key files:** `console.c/h`, `consoledefs.h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Two real issues. `consolePrint`
|
||||||
|
(console.c:32-44) gets its length from `stringFormatVA(buffer,
|
||||||
|
CONSOLE_LINE_MAX, ...)`, and `stringFormatVA` (util/string.c:100)
|
||||||
|
**asserts** `ret < destSize` rather than truncating -- so any single
|
||||||
|
debug line over 511 bytes aborts the process instead of just getting
|
||||||
|
cut off, a harsh failure mode for a dev console. Separately,
|
||||||
|
`consoleUpdate`'s doc ("Processes pending queued script lines") and
|
||||||
|
`CONSOLE_EXEC_BUFFER_MAX` (consoledefs.h:12) describe a queued-script-
|
||||||
|
execution feature that doesn't exist -- `console_t` has no exec-queue
|
||||||
|
fields, `consoleUpdate` only toggles `visible`, and
|
||||||
|
`CONSOLE_EXEC_BUFFER_MAX` is unreferenced elsewhere (dead constant,
|
||||||
|
stale doc). No `test/console/` exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## asset
|
||||||
|
|
||||||
|
**Purpose:** The asset pipeline: `dusk.dsk` zip archive reading
|
||||||
|
(`assetfile.c/h`), lock/ref-counted entries (`asset/loader/assetentry.c/h`,
|
||||||
|
built on `util/ref.h`), batch loading (`assetbatch.c/h`), and the loader
|
||||||
|
registry (`asset/loader/assetloader.c/h`, plus one subfolder per loader
|
||||||
|
type: `dmf/` mesh+model, `display/` texture+tileset, `locale/`, `json/`,
|
||||||
|
`animation/`). See `CLAUDE.md`'s "Adding a new asset loader type" for the
|
||||||
|
extension pattern. Per `STATUS.md`: model loading's "async" path is
|
||||||
|
actually synchronous -- the one known stub in core.
|
||||||
|
|
||||||
|
**Key files:** `asset.c/h`, `assetfile.c/h`, `assetbatch.c/h`,
|
||||||
|
`loader/assetentry.c/h`, `loader/assetloader.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Core is well-structured and the
|
||||||
|
multi-pass reaper correctly handles parent/child dependency chains.
|
||||||
|
Found: `assetFileInit` (assetfile.c:29) has `if(!zip_stat(...) == 0)` --
|
||||||
|
precedence makes this `(!zip_stat(...)) == 0`, which happens to
|
||||||
|
evaluate correctly for zip's 0/nonzero convention but reads as broken
|
||||||
|
and should just be `zip_stat(...) != 0`. More seriously,
|
||||||
|
`assetEntryLock`/`Unlock` sit on top of `util/ref.c`'s confirmed
|
||||||
|
double-unlock underflow bug (see **util** above) -- an over-unlocked
|
||||||
|
asset entry silently becomes un-reapable forever instead of asserting.
|
||||||
|
**Confirmed via code-trace** (not just citing STATUS.md):
|
||||||
|
`assetModelLoaderAsync` (assetmodelloader.c:19-23) is genuinely dead --
|
||||||
|
`assetEntryStartLoading` unconditionally sets
|
||||||
|
`ASSET_ENTRY_STATE_PENDING_SYNC`, and `assetModelLoaderSync`'s state
|
||||||
|
machine never transitions to `PENDING_ASYNC` the way mesh/texture/
|
||||||
|
tileset/locale/json/animation all do, so model loading runs fully
|
||||||
|
synchronously via `assetRequireLoaded`'s busy-loop; grep confirms
|
||||||
|
`assetModelLoaderAsync` has no callers. `assetlocaleloader.c` uses raw
|
||||||
|
stdlib (`strstr`/`strncmp`/`strchr`/`strtol`/`atoi`) instead of
|
||||||
|
`util/string.h` wrappers throughout, and tests line-prefixes via
|
||||||
|
`memoryCompare(lineBuffer, "msgid", 5)` without confirming the reused
|
||||||
|
stack buffer actually holds 5 valid bytes at that point -- a short line
|
||||||
|
could spuriously prefix-match against stale buffer contents. Test
|
||||||
|
coverage: good breadth for entry lifecycle, tileset/JSON/animation
|
||||||
|
loaders, and locale parsing; no dedicated tests for mesh/model/texture
|
||||||
|
loader internals or `assetfile.c`'s line reader.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## locale
|
||||||
|
|
||||||
|
**Purpose:** Active-locale tracking (`LOCALE.locale`/`LOCALE.entry`) over
|
||||||
|
`.po`-derived locale assets loaded through the asset system. See
|
||||||
|
`assets/locale/en_US.po` -- per memory, currently placeholder/test
|
||||||
|
content, not real shipped game text.
|
||||||
|
|
||||||
|
**Key files:** `localemanager.c/h`, `localeinfo.h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Clean, no bugs found --
|
||||||
|
correctly balances `assetEntryLock`/`Unlock` in both `SetLocale` and
|
||||||
|
`Dispose` (unlocks the old entry before adopting the new one). Minor
|
||||||
|
nit: `localeinfo.h` declares `static const localeinfo_t` globals
|
||||||
|
(`LOCALE_EN_US` etc.) directly in the header, so every including TU
|
||||||
|
gets its own duplicate copy -- harmless given how few TUs include it.
|
||||||
|
No `test/locale/` directory; `test/asset/test_assetlocale.c` tests the
|
||||||
|
loader, not `localemanager.c` itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## save
|
||||||
|
|
||||||
|
**Purpose:** Slot-based save data (`SAVE_FILE_COUNT_MAX` fixed slots),
|
||||||
|
yyjson documents persisted with size + CRC32 checksum. `saveLoad`/
|
||||||
|
`saveSave` read/write JSON fresh every call; callers own the returned
|
||||||
|
doc and must free it. Actual file I/O goes through platform stream hooks
|
||||||
|
(`saveplatform_t`, one impl per platform) -- core `save.c` must never
|
||||||
|
touch the filesystem directly. `savesettings.c/h` layers engine settings
|
||||||
|
(display/audio/input/general) on top. Per `CLAUDE.md`: "mature, but
|
||||||
|
undocumented" beyond the one paragraph there -- no tests.
|
||||||
|
|
||||||
|
**Key files:** `save.c/h`, `savefile.h`, `savestream.c/h`,
|
||||||
|
`savesettings.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Clean and convention-compliant
|
||||||
|
-- `save.c` genuinely never touches the filesystem directly (every I/O
|
||||||
|
path goes through `saveXxxPlatform` macros, all three platforms define
|
||||||
|
them), yyjson doc ownership is honored at every call site checked, and
|
||||||
|
CRC32 is validated before parsing with every error path freeing `buf`.
|
||||||
|
One real gap: `saveFileLoad`'s `memoryAllocate(file->size)`
|
||||||
|
(savestream.c:122) allocates straight from an on-disk size field with
|
||||||
|
no sanity bound before the allocation -- a corrupted/tampered save
|
||||||
|
could request an oversized allocation, worth a max-size guard given the
|
||||||
|
"never trust incoming data" principle (arguably applies to save files,
|
||||||
|
not just network packets). Confirmed no `test/save/` exists. `saveDelete`/
|
||||||
|
`saveExists`/`saveGet` are public API not yet called anywhere in the
|
||||||
|
repo -- likely just unconsumed so far, not truly dead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## network
|
||||||
|
|
||||||
|
**Purpose:** Connection-state layer only (`networkstate_t`:
|
||||||
|
`DISCONNECTED`/`CONNECTING`/`CONNECTED`/`DISCONNECTING`) plus a one-off
|
||||||
|
HTTP client (`network/http/`: request/header/URL/thread/process split
|
||||||
|
across files). **Not** a multiplayer/replication protocol yet -- that's
|
||||||
|
`ROADMAP.md` items 8-17. Platform-specific connection polling (PSP's
|
||||||
|
`sceNetApctl`, GameCube/Wii's `if_config()`) lives under
|
||||||
|
`src/dusk<platform>/network/`. Has HTTP tests only (`test/network/http/`).
|
||||||
|
|
||||||
|
**Key files:** `network.c/h`, `networkinfo.c/h`, `http/networkhttp.c/h`,
|
||||||
|
`http/networkhttprequest.c/h`, `http/networkhttpthread.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Genuinely stays a thin
|
||||||
|
connection-state layer (grepped for "packet"/"replicat"/"multiplayer"/
|
||||||
|
"udp" -- no hits, no scope creep). The HTTP client correctly validates
|
||||||
|
untrusted server input with `errorThrow` rather than trusting it
|
||||||
|
(malformed status lines, oversized headers, bad `Content-Length`,
|
||||||
|
early-closed connections all throw) -- exactly what CLAUDE.md calls
|
||||||
|
out. `networkhttpthread.c`'s request handoff is actually correct
|
||||||
|
despite request fields being written outside the mutex: the final
|
||||||
|
`state = PENDING` write happens under the same mutex the worker
|
||||||
|
acquires, giving a valid release/acquire handoff (worth a comment, not
|
||||||
|
a bug). One recurring convention violation:
|
||||||
|
`networkhttpprocess.c`/`networkhttpurl.c` use raw stdlib
|
||||||
|
`strlen`/`strchr`/`strstr`/`strncasecmp`/`strcspn` instead of
|
||||||
|
`util/string.h` wrappers in several places -- `util/string.h` currently
|
||||||
|
has no direct substitute for `strchr`/`strstr`/`strcspn`, so this may
|
||||||
|
be a missing-utility gap rather than pure oversight. No leaks found
|
||||||
|
(request/response bodies freed exactly once via
|
||||||
|
`networkHttpRequestReset`, redirect loop frees the intermediate body).
|
||||||
|
Real test coverage exists: `test/network/http/test_networkhttp.c` runs
|
||||||
|
six tests against a fake loopback server (GET/POST/PUT, query encoding,
|
||||||
|
redirects, connection-refused), each asserting zero leaked allocations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## display
|
||||||
|
|
||||||
|
**Purpose:** The engine's most mature, most-optimized subsystem --
|
||||||
|
platform-agnostic rendering built on top of `duskgl`/`duskdolphin`.
|
||||||
|
Subfolders: `mesh/` (primitive builders: cube/sphere/capsule/plane/
|
||||||
|
triprism/quad, plus the shared `meshvertex_t`/`mesh_t` API), `shader/`
|
||||||
|
(currently one material type, unlit), `spritebatch/` (the shared
|
||||||
|
`SPRITEBATCH_SPRITES_MAX`-capacity quad batcher everything 2D goes
|
||||||
|
through -- flushes on shader/material change), `texture/` (texture +
|
||||||
|
tileset + palette), `text/` (`font_t`/`FONT_DEFAULT`'s baked-in bitmap
|
||||||
|
glyphs, `textBuildSpriteCache`/`textDrawSpriteCache`'s build-once/
|
||||||
|
translate-per-frame pattern), `screen/` (scan-safe area), `framebuffer/`.
|
||||||
|
`color.csv` + `tools/color/csv/__main__.py` code-generate `color.h` --
|
||||||
|
never hand-edit the generated header.
|
||||||
|
|
||||||
|
**Key files:** `display.c/h`, `mesh/mesh.c/h`, `spritebatch/spritebatch.c/h`,
|
||||||
|
`spritebatch/spritebatchsprite.c/h`, `text/font.c/h`, `text/text.c/h`,
|
||||||
|
`texture/texture.c/h`, `texture/tileset.c/h`, `shader/shader.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. "Most mature" oversold it
|
||||||
|
slightly -- real bugs found, several confirmed by direct read:
|
||||||
|
- **texture.c:42-52 (confirmed)** -- `textureInit()` branches on
|
||||||
|
`texture->format` to validate the incoming `data` union *before*
|
||||||
|
`memoryZero(texture, ...)` runs a few lines later, so it's reading
|
||||||
|
whatever stale/uninitialized value was already in the caller's
|
||||||
|
struct, not the `format` parameter actually being requested.
|
||||||
|
Validation is effectively checking garbage.
|
||||||
|
- `spritebatchsprite.c:20-25` -- `spriteBatchSpriteTilesetPosition`'s
|
||||||
|
zero-width/height early return zeroes `sprite.min`/`max` but leaves
|
||||||
|
`uvMin`/`uvMax` uninitialized (stack garbage); the existing
|
||||||
|
zero-size test doesn't check uv fields, so this isn't caught.
|
||||||
|
- `mesh.c:43` -- `meshFlush`'s offset assert (`vertexOffset <
|
||||||
|
vertCount - 1`) wrongly rejects a valid offset at the very last
|
||||||
|
vertex; off-by-one.
|
||||||
|
- `text.c:34-37` -- bounds assert uses `tileIndex <= tileCount`
|
||||||
|
(should be `<`), currently unreachable in practice since a preceding
|
||||||
|
clamp already guards it, but the assert itself is wrong.
|
||||||
|
- `framebuffer.c:23-28` -- `frameBufferBind(NULL)` recurses into
|
||||||
|
binding the backbuffer without `errorChain`, discarding any failure
|
||||||
|
and unconditionally returning `errorOk()`.
|
||||||
|
- `displayInit()`'s six static "SIMPLE" primitive meshes
|
||||||
|
(quad/cube/sphere/plane/capsule/triprism) are never disposed in
|
||||||
|
`displayDispose()` -- one VBO+VAO leaked per primitive on the
|
||||||
|
non-legacy GL path.
|
||||||
|
- **Confirmed and fixed 2026-08-01.** `tools/color.py` (not the
|
||||||
|
`tools/color/csv/__main__.py` path CLAUDE.md's doc comment names --
|
||||||
|
that's a stale path, only a `__pycache__/` remnant exists there; the
|
||||||
|
live script is `tools/color.py`) emitted invalid C float literals
|
||||||
|
for whole-number CSV channels (`0`, `1` -- every black/white/primary
|
||||||
|
color), e.g. `color3f(0f, 0f, 0f)`. Was harmless *only* because
|
||||||
|
`COLOR_*_3F`/`_4F` were never referenced anywhere in `src/`/`test/`
|
||||||
|
-- confirmed by actually running the generator before and after the
|
||||||
|
fix, not just reading the source. Now routes channel values through
|
||||||
|
Python `float()` first (always stringifies with a decimal point);
|
||||||
|
`test/display/test_color.c` gained tests referencing
|
||||||
|
`COLOR_BLACK_3F`/`COLOR_WHITE_4F`/`COLOR_RED_3B`/`COLOR_GRAY_3F`
|
||||||
|
directly so the previously-dead macros now have real compile-time +
|
||||||
|
value coverage.
|
||||||
|
- Style drift: `mesh.h` uses `//` header comments instead of `/** */`;
|
||||||
|
sphere/capsule/triprism use `/* */` block comments inside function
|
||||||
|
bodies where CLAUDE.md mandates `//`; every primitive builder's
|
||||||
|
JSDoc still documents a `@param color` that no longer exists in the
|
||||||
|
signature (leftover from before color moved out of `meshvertex_t`).
|
||||||
|
- The primitive `*Buffer()` flexible-geometry functions (sphereBuffer,
|
||||||
|
capsuleBuffer, etc.) are confirmed unused outside their own
|
||||||
|
`*Init()` -- only the fixed default mesh instances are actually
|
||||||
|
consumed (via `script/module/display/modulemesh.c`).
|
||||||
|
|
||||||
|
Test coverage: `test/display/` only registers `test_color.c` and
|
||||||
|
`test_spritebatchsprite.c` -- mesh/shader/texture/tileset/palette/text/
|
||||||
|
font/screen/framebuffer all have zero unit tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ui
|
||||||
|
|
||||||
|
**Purpose:** Flat, static, singleton-driven widget framework -- no scene
|
||||||
|
graph. Top level is a compile-time X-macro registry
|
||||||
|
(`uielementlist.h`/`uielement.c/h`) of `{init, update, draw, dispose,
|
||||||
|
order}` tuples (screens: frame/confirm/settings/overlays/console/fps),
|
||||||
|
spliced with `duskrpg`'s own list. Composition below that is `uimenu_t`
|
||||||
|
(`widget/uimenu.c/h`) -- a flat tagged-union array of widgets laid out in
|
||||||
|
a simple row/column grid via the `MENU_*` authoring macros; no nesting.
|
||||||
|
Widgets: `uilabel`/`uiwidgetlabel` (cached glyph sprites, the pattern
|
||||||
|
every other widget's text now uses), `uibutton`, `uicheckbox`,
|
||||||
|
`uislider`, `uidropdown`, `uitab`, `uiscrolling` (empty placeholder).
|
||||||
|
`focus/` is the gamepad/keyboard directional-navigation stack (no mouse/
|
||||||
|
pointer hit-testing exists anywhere). `frame/` is the 9-slice box plus
|
||||||
|
the confirm dialog and settings screens. `overlay/` is fullscreen
|
||||||
|
fades/loading/crop bars. `debug/` is the FPS counter and dev console
|
||||||
|
renderer.
|
||||||
|
|
||||||
|
As of 2026-07-31: slider/tab/frame (`uiFrameDrawCached`)/duskrpg's
|
||||||
|
textbox all cache their sprite geometry instead of rebuilding it every
|
||||||
|
frame; console uses a fixed-size vertex buffer instead of alloc/free.
|
||||||
|
Still open: sprite-batch flushes are keyed on material/color, so
|
||||||
|
per-widget highlight-color changes still force a flush per widget (see
|
||||||
|
`ROADMAP.md` item 5 in the debt backlog). No JS bindings exist for UI
|
||||||
|
yet. `test/ui/`/`test/display/test_spritebatchsprite.c` cover the new
|
||||||
|
caching *logic* only -- nothing exercises real `uiXxxDraw()` calls, since
|
||||||
|
those need a live GL context (`FONT_DEFAULT`/`UI_FRAME` textures) this
|
||||||
|
repo's test binaries don't have.
|
||||||
|
|
||||||
|
**Key files:** `ui.c/h`, `uielement.c/h`, `focus/uifocus.c/h`,
|
||||||
|
`widget/uimenu.c/h`, `widget/uilabel.c/h`, `frame/uiframe.c/h`,
|
||||||
|
`debug/uiconsole.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-07-31 (this session's sprite-caching
|
||||||
|
pass) -- see notes above; per-material flush granularity and hit-testing
|
||||||
|
still open.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## entity
|
||||||
|
|
||||||
|
**Purpose:** Fixed-size-array ECS. `entitymanager_t` owns
|
||||||
|
`ENTITY_COUNT_MAX` entities, each with up to `ENTITY_COMPONENT_COUNT_MAX`
|
||||||
|
components stored in a tagged union (`componentdata_t` -- flagged in
|
||||||
|
`PSP_OPTIMIZATION_PLAN.md` as the biggest PSP memory-waste candidate,
|
||||||
|
since every entity reserves space for every component type's largest
|
||||||
|
variant). `componentlist.h`'s `X()` macro auto-generates the component
|
||||||
|
enum/union/dispatch table from `entity/component/**`; engine-level
|
||||||
|
`entityprefablist.h` is an empty sentinel (real prefabs live in
|
||||||
|
`duskrpg/entity/entityprefablist.h`, resolved via
|
||||||
|
`entityPrefabResolveAndApply`). No JSON serialize/deserialize (removed by
|
||||||
|
design in favor of C-coded prefabs or JerryScript `Entity`/`Component`).
|
||||||
|
|
||||||
|
**Key files:** `entitymanager.c/h`, `entity.c/h`, `component.c/h`,
|
||||||
|
`componentlist.h`, `entityprefab.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Structurally sound (sentinel
|
||||||
|
prefab list, X-macro shape both match CLAUDE.md), but there's a real
|
||||||
|
security-relevant gap: **every capacity/bounds guard
|
||||||
|
(`entitymanager.c:35`, `entity.c:56`, `component.c:37-39,58-62,75-76,
|
||||||
|
140-141`) is an `assert*` call**, and `assert.h` compiles these to
|
||||||
|
`((void)0)` under `DUSK_ASSERTIONS_FAKED` (auto-set whenever `NDEBUG`
|
||||||
|
is defined -- confirmed by reading assert.h directly, this is a real
|
||||||
|
release-mode no-op, not a hypothetical). `moduleentity.c:30,67` create
|
||||||
|
entities/components from JerryScript with no return-value check, so a
|
||||||
|
script that exceeds `ENTITY_COUNT_MAX`/the per-entity component cap
|
||||||
|
silently gets back an invalid ID in release builds, which then indexes
|
||||||
|
fixed arrays out-of-bounds later. This directly contradicts the
|
||||||
|
project's own "untrusted-data limits need errorThrow, not asserts"
|
||||||
|
guidance -- and script input is about as untrusted as it gets. Minor:
|
||||||
|
`component.h:100`/`component.c:52` misplace the `*` in `void *
|
||||||
|
componentGetData(`. Dead code: `entityUpdateRemove`/
|
||||||
|
`entityDisposeRemove`/`entityDisposeAdd` have zero call sites.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## scene
|
||||||
|
|
||||||
|
**Purpose:** Same shape as `entity/` one level up -- `SCENE_COUNT_MAX`
|
||||||
|
fixed-count scenes, each an isolated entity-manager pool; engine-level
|
||||||
|
`sceneprefablist.h` is an empty sentinel (real prefabs in
|
||||||
|
`duskrpg/scene/`). As of the `Scene.set()` work (2026-07-31), JerryScript
|
||||||
|
can install a `{init, update, dispose}` module as the active scene via
|
||||||
|
`modulescene.c`'s `moduleSceneSetStatic`/`moduleSceneUpdateCurrent`/
|
||||||
|
`moduleSceneTeardownCurrent` (see `script` below) -- scene IDs are reused
|
||||||
|
from a small pool immediately after `sceneDestroy`, so don't assume a
|
||||||
|
freshly created scene has a "new" ID.
|
||||||
|
|
||||||
|
**Key files:** `scene.c/h`, `scenebase.h`, `sceneprefab.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Shares entity/'s assert-not-
|
||||||
|
errorThrow bounds gap (`sceneCreate` hits `assertUnreachable` then
|
||||||
|
falls through to `SCENE_ID_INVALID` on pool exhaustion in release --
|
||||||
|
reachable from `new Scene()` in JS). Additional finding:
|
||||||
|
`scenePrefabResolveAndApply`/`scenePrefabInit` (sceneprefab.c:31-54)
|
||||||
|
recurse through a prefab's `extends` chain with **no cycle guard** -- a
|
||||||
|
self-referencing or A→B→A prefab table causes unbounded recursion/
|
||||||
|
stack overflow from what's otherwise a one-typo mistake in a
|
||||||
|
hand-authored prefab list. No dead code, no use-after-free. Tests cover
|
||||||
|
create/destroy/isolation/update cadence well but have zero coverage for
|
||||||
|
pool exhaustion, `sceneRender()`, or `sceneprefab.c` (no prefab test
|
||||||
|
file exists at all).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## physics
|
||||||
|
|
||||||
|
**Purpose:** `physicsworld_t` step simulation over `physicsbodytype_t`
|
||||||
|
(static/dynamic/kinematic) and `physicshapetype_t` (cube/sphere/capsule/
|
||||||
|
plane/custom, plus mesh shapes via `physicsshapemesh.c/h`).
|
||||||
|
`triggersystem.c/h` layers trigger-volume enter/exit events on top. Per
|
||||||
|
`STATUS.md`: "mature, recently churned" -- a revert/re-disable of "old
|
||||||
|
ent code" suggests component wiring isn't fully settled yet. Well
|
||||||
|
tested (`test/physics/`).
|
||||||
|
|
||||||
|
**Key files:** `physicsworld.c/h`, `physicsbodytype.h`, `physicsshape.h`,
|
||||||
|
`physicsshapemesh.c/h`, `triggersystem.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. `STATUS.md`'s "component
|
||||||
|
wiring isn't fully settled" caveat looks stale on closer look:
|
||||||
|
`physicsWorldStep`/`triggerSystemStep` are live-wired in `scene.c:89-90`
|
||||||
|
and `duskrpg`'s player prefab actively uses both PHYSICS and TRIGGER --
|
||||||
|
no `#if 0`/TODO/commented-out code found; the disable-then-revert in
|
||||||
|
history reads like a same-day whole-ECS revert, not something
|
||||||
|
physics-specific. Two real bugs found instead:
|
||||||
|
`physicsbodytype.h:23-27` documents kinematic bodies as capable of
|
||||||
|
being a player controller, but `physicsworld.c:124,166-167` only ever
|
||||||
|
writes `onGround` for the dynamic side of a collision pair -- a
|
||||||
|
kinematic controller's `onGround` is permanently false (latent today,
|
||||||
|
since the one existing player prefab uses DYNAMIC, not KINEMATIC).
|
||||||
|
`physicstest.c:213`'s barycentric interior-test branch has no guard
|
||||||
|
against a zero-area triangle (divide-by-zero/NaN risk in mesh-sphere
|
||||||
|
collision). Dead code: `physicsShapeMeshCreate` (mesh-terrain
|
||||||
|
collision) has zero production callers despite being well-tested in
|
||||||
|
isolation; `entityPhysicsGetShape`/`GetCollideMask` are unwired to the
|
||||||
|
JS module. Test depth is genuinely strong for what's implemented
|
||||||
|
(gravity convergence, dynamic-vs-dynamic separation, mask filtering,
|
||||||
|
mesh sphere/capsule resting) -- "well tested" is earned for the
|
||||||
|
features that exist, though the unused mesh-collision path and
|
||||||
|
missing trigger→JS bridge mean the subsystem's *integration* maturity
|
||||||
|
is a bit narrower than its test depth suggests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## animation
|
||||||
|
|
||||||
|
**Purpose:** Keyframe + easing only -- no blend trees or state machines
|
||||||
|
yet. `keyframe_t`/`keyframeset_t` interpolate values over time via
|
||||||
|
`easing.c/h`'s easing functions; `animation.c/h` ties a keyframe set to
|
||||||
|
playback state. Per `STATUS.md`: terse commit history ("ANIM") suggests
|
||||||
|
still iterating.
|
||||||
|
|
||||||
|
**Key files:** `animation.c/h`, `easing.c/h`, `keyframe.c/h`,
|
||||||
|
`keyframeset.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. All 8 core files use `//`
|
||||||
|
copyright headers instead of the required `/** */` block, inconsistent
|
||||||
|
with the correctly-formatted `entityanimation.{c,h}` next door. Math
|
||||||
|
checks out: all 16 easing functions satisfy f(0)=0/f(1)=1 with no
|
||||||
|
precision issues, and keyframe interpolation clamps (doesn't
|
||||||
|
extrapolate) outside the time range with no reachable divide-by-zero
|
||||||
|
-- though it doesn't validate that keyframe times are ascending. 15 of
|
||||||
|
16 `easing.h` declarations lack Javadoc. Biggest gap: **no
|
||||||
|
`test_easing.c` exists** -- only linear/in-quad get incidental coverage
|
||||||
|
via `test_keyframe.c`, so 13 of 16 easing functions are completely
|
||||||
|
untested. Dead code (test-only, unused by any `duskrpg` game code):
|
||||||
|
`keyframeSetGetValues`, `keyframeSetFireCallback`, `animationSetEvent`,
|
||||||
|
`entityAnimationSetKeyframes`/`GetValue`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## script
|
||||||
|
|
||||||
|
**Purpose:** Embedded JerryScript integration. `scriptmanager.c/h` owns
|
||||||
|
the JerryScript context lifecycle and the exec/call helpers
|
||||||
|
(`scriptManagerExec`/`ExecFile`/`CallGlobal`/`CallValue`, the last two
|
||||||
|
sharing exception + promise-draining logic). `scriptproto.c/h` is the
|
||||||
|
`scriptproto_t` prototype-wrapping helper every module builds on.
|
||||||
|
`module/` holds each registered JS-facing module:
|
||||||
|
- `modulebase.h` -- the shared macro toolkit (`moduleBaseFunction`,
|
||||||
|
`moduleBaseRequireArgs`/`RequireString`/etc., `moduleBaseDefineProp`/
|
||||||
|
`Func`/`StaticFunc`, `moduleBaseSetWrappedPointer`, `moduleBaseThrowError`).
|
||||||
|
- `moduleplatform.h`, `entity/moduleentity.c/h` +
|
||||||
|
`entity/component/*` (generic `Component` plus typed Position/Physics/
|
||||||
|
Renderable wrappers via `modulecomponentlist.c/h`), `scene/modulescene.c/h`
|
||||||
|
(`Scene`, including `Scene.set()`), `display/modulemesh.c/h`,
|
||||||
|
`time/moduletime.c/h`, `require/modulerequire.c/h` (CommonJS-style
|
||||||
|
`require()`/`module.exports`, with a directory stack for relative
|
||||||
|
resolution -- `scriptManagerExecFile` now pushes its own file's
|
||||||
|
directory before exec so a top-level entry script's relative
|
||||||
|
`require()` calls resolve correctly).
|
||||||
|
- `modulelist.c/h` -- registers/disposes every module above in order;
|
||||||
|
`moduleListDispose()` is now actually called from
|
||||||
|
`scriptManagerDispose()` (was a latent bug -- it wasn't, until
|
||||||
|
2026-07-31).
|
||||||
|
|
||||||
|
Entry point convention: `assets/scripts/init.js` does
|
||||||
|
`require('./scenefile.js')` + `Scene.set(module)`. Has unit tests
|
||||||
|
(`test/script/`), including one that reads the real shipped
|
||||||
|
`overworldscene.js` off disk rather than an embedded copy.
|
||||||
|
|
||||||
|
**Key files:** `scriptmanager.c/h`, `scriptproto.c/h`,
|
||||||
|
`module/modulebase.h`, `module/modulelist.c/h`,
|
||||||
|
`module/require/modulerequire.c/h`, `module/scene/modulescene.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-07-31 (this session's Scene.set() +
|
||||||
|
require()-dir-stack work) -- see notes above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## input
|
||||||
|
|
||||||
|
**Purpose:** Bind-based input (`inputaction.c/h` maps
|
||||||
|
`INPUT_BIND_*` names to raw `inputbutton_t`s per platform), with a
|
||||||
|
configurable analog deadzone. Platform button/axis backends live under
|
||||||
|
`src/dusk<platform>/input/`; capability macros (`DUSK_INPUT_GAMEPAD`/
|
||||||
|
`KEYBOARD`/`POINTER`) gate what's available per target. Pointer axis
|
||||||
|
values are plumbed through (`DUSK_INPUT_POINTER`), but nothing in `ui/`
|
||||||
|
consumes them yet -- no hit-testing exists (see `ui` above).
|
||||||
|
|
||||||
|
**Key files:** `input.c/h`, `inputaction.c/h`, `inputbutton.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Clean and convention-compliant;
|
||||||
|
deadzone math (`inputDeadzone`, input.c:244) is correct and safe. One
|
||||||
|
naming nit: `inputBind`'s param is named `act` in input.h:216 while its
|
||||||
|
own doc comment calls it `action`. **Confirmed, not just cited from
|
||||||
|
STATUS.md**: pointer input is genuinely wired end-to-end, not a
|
||||||
|
dead-end at the platform layer -- SDL2's `inputUpdateSDL2` populates
|
||||||
|
`INPUT.platform.mouseX/Y`/scroll from `SDL_GetMouseState`,
|
||||||
|
`inputButtonGetValueSDL2` handles the pointer-axis cases, and
|
||||||
|
`duskrpg/input/inputbindmap.h` binds `mouse_x`/`mouse_y` to
|
||||||
|
`INPUT_BIND_POINTERX/Y`. The dead end is specifically at the
|
||||||
|
*consumption* layer: grepping `src/dusk/ui/` for those binds or any
|
||||||
|
pointer-axis read turns up nothing -- exactly as STATUS.md says, just
|
||||||
|
now confirmed rather than assumed. No tests exist for `input/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## engine
|
||||||
|
|
||||||
|
**Purpose:** Top-level init/update/dispose orchestration
|
||||||
|
(`engineInit`/`engineUpdate`/`engineDispose`) -- the thing `main.c` calls,
|
||||||
|
which in turn calls into every subsystem above plus `game/gameInit` etc.
|
||||||
|
`ENGINE.running`/`ENGINE.version` are the only bits of global state
|
||||||
|
outside the per-subsystem structs.
|
||||||
|
|
||||||
|
**Key files:** `engine.c/h`.
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01, confirmed by direct read of
|
||||||
|
`engineInit`/`engineDispose`. Init order is safe (time -> console ->
|
||||||
|
system -> input -> asset -> scriptManager -> save/saveSettings ->
|
||||||
|
locale -> display -> ui -> network -> scene -> game), with display
|
||||||
|
before UI and scriptManager before scene/game so JS `Entity`/`Scene`
|
||||||
|
wrappers exist before any script runs. `engineDispose` is **not** a
|
||||||
|
clean reverse of that order, though: `localeManagerDispose()` runs
|
||||||
|
*before* `uiDispose()`/`displayDispose()`, even though locale was
|
||||||
|
initialized *before* both -- correct reverse order would tear down
|
||||||
|
UI/display first, and as written there's a real risk of UI/display
|
||||||
|
teardown code touching already-freed locale data (e.g. any localized
|
||||||
|
string still held/logged during dispose). Separately, `void
|
||||||
|
engineExit(void)` (engine.c:95) is defined but **not declared in
|
||||||
|
engine.h**, and a repo-wide grep finds no caller anywhere -- dead,
|
||||||
|
unreachable, and in violation of the "every public function must be
|
||||||
|
declared in its header" rule. No tests exist for `engine/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## game
|
||||||
|
|
||||||
|
**Purpose:** Intentionally header-only in core -- just declares
|
||||||
|
`gameInit`/`gameUpdate`/`gameDispose`. The real implementation is
|
||||||
|
`src/duskrpg/game/game.c` (currently: runs `scripts/init.js` on init).
|
||||||
|
|
||||||
|
**Key files:** `game.h` (no `.c` in core).
|
||||||
|
|
||||||
|
**Review status:** Reviewed 2026-08-01. Confirmed: no `.c` file exists
|
||||||
|
in `src/dusk/game/` (only `game.h` + `CMakeLists.txt`) -- a pure
|
||||||
|
interface, as intended. The real `src/duskrpg/game/game.c`
|
||||||
|
implementation is a thin JS-bootstrap shim: `gameInit` just does
|
||||||
|
`errorChain(scriptManagerExecFile("scripts/init.js", NULL))`, while
|
||||||
|
`gameUpdate`/`gameDispose` are both empty (`errorOk()` only). Not a
|
||||||
|
bug -- consistent with the ongoing migration of game logic into JS --
|
||||||
|
but worth flagging as "not yet fleshed out" rather than broken if
|
||||||
|
anything is expected to happen there per-frame at the C layer. No
|
||||||
|
tests exist for this layer.
|
||||||
@@ -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,5 +1,9 @@
|
|||||||
# Dusk — Claude Code rules
|
# Dusk — Claude Code rules
|
||||||
|
|
||||||
|
See `STATUS.md` for a periodically-refreshed inventory of subsystem
|
||||||
|
maturity, test coverage gaps, and known open issues — check it before
|
||||||
|
assuming a subsystem is fully wired up or before picking a next task.
|
||||||
|
|
||||||
## File headers
|
## File headers
|
||||||
Every C, H, and JS file starts with:
|
Every C, H, and JS file starts with:
|
||||||
|
|
||||||
@@ -124,18 +128,16 @@ Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
|||||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
|
||||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||||
|
|
||||||
### Layer structure
|
### Layer structure
|
||||||
```
|
```
|
||||||
src/dusk/ core, platform-agnostic game logic
|
src/dusk/ core, platform-agnostic game logic
|
||||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP)
|
||||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
||||||
src/dusklinux/ Linux + Knulli platform impl
|
src/dusklinux/ Linux + Knulli platform impl
|
||||||
src/duskpsp/ PSP platform impl
|
src/duskpsp/ PSP platform impl
|
||||||
src/duskvita/ Vita platform impl
|
|
||||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -190,21 +192,110 @@ simply left undefined — the core guards calls with `#ifdef`.
|
|||||||
## Adding a new entity component
|
## Adding a new entity component
|
||||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
||||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
||||||
`entityMyCompDispose()`.
|
`entityMyCompDispose()`, `entityMyCompRender()`.
|
||||||
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
2. Add the include to `src/dusk/entity/componentlist.h` header block
|
||||||
3. Add a row to `src/dusk/entity/componentlist.h`:
|
(or `src/duskrpg/entity/gamecomponentlist.h` for a game-specific
|
||||||
|
component, appended after the engine's inbuilt ones).
|
||||||
|
3. Add a row:
|
||||||
```c
|
```c
|
||||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
||||||
```
|
```
|
||||||
This auto-generates the enum, union field, and definition entry.
|
Params are `(enumName, type, field, init, dispose, render)` — pass
|
||||||
|
`NULL` for any callback the component doesn't need. This
|
||||||
|
auto-generates the enum, union field, and definition entry.
|
||||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
4. If JS-facing, create the script module and `.d.ts` (see below).
|
||||||
|
|
||||||
|
Entities/components/scenes have no JSON serialize/deserialize path —
|
||||||
|
that was removed in favor of building scenes from C-coded prefabs
|
||||||
|
(below) or from JerryScript (`Entity`/`Component`/`Scene`, see
|
||||||
|
"Adding a new script (JS) module").
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Adding a new entity/scene prefab
|
||||||
|
Entity prefabs (`src/dusk/entity/entityprefab.h`) and scene prefabs
|
||||||
|
(`src/dusk/scene/sceneprefab.h`) follow the same pattern:
|
||||||
|
1. Write an apply function: `errorret_t entityPrefabXxxApply(mgr,
|
||||||
|
entityId)` (or `errorret_t scenePrefabXxxApply(sceneId)`), building
|
||||||
|
up the entity/scene with the normal component/entity APIs.
|
||||||
|
2. Add an entry to the sentinel-terminated `ENTITY_PREFABS[]` (in
|
||||||
|
`src/dusk/entity/entityprefablist.h`, or a game-specific list it
|
||||||
|
includes) or `SCENE_PREFABS[]`:
|
||||||
|
```c
|
||||||
|
{ .name = "MY_PREFAB", .extends = "", .apply = entityPrefabXxxApply }
|
||||||
|
```
|
||||||
|
`extends` names another prefab to apply first (recurses through
|
||||||
|
`entityPrefabResolveAndApply`/`scenePrefabResolveAndApply`), or `""`
|
||||||
|
for none. Do not add an enum or count field — the array is iterated
|
||||||
|
until `.name[0] == '\0'`.
|
||||||
|
3. `entityPrefabResolveAndApply`/`scenePrefabResolveAndApply` only
|
||||||
|
resolve names against the C-coded registry above — there is no JSON
|
||||||
|
asset fallback. Throws if no prefab with that name is registered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a new cutscene item type
|
||||||
|
1. Create `src/duskrpg/cutscene/item/<category>/cutsceneMyItem.h/.c`
|
||||||
|
with a data struct (e.g. `cutscenemyitem_t`) and
|
||||||
|
`cutsceneMyItemStart(item, data)` / `cutsceneMyItemUpdate(item,
|
||||||
|
data)` (the latter returns `true` once the item has completed). Add
|
||||||
|
a matching `cutscenemyitemdata_t` runtime-data struct only if the
|
||||||
|
item needs per-run state across ticks (most don't).
|
||||||
|
2. Add `CUTSCENE_ITEM_TYPE_MY_ITEM` to the enum and a union member to
|
||||||
|
`cutsceneitem_t` (and `cutsceneitemdata_t` if it has runtime data) in
|
||||||
|
`src/duskrpg/cutscene/item/cutsceneitem.h`.
|
||||||
|
3. Register the `{ start, update }` pair in `CUTSCENE_ITEM_CALLBACKS[]`
|
||||||
|
in `cutsceneitem.c`.
|
||||||
|
4. Add an authoring macro to `src/duskrpg/cutscene/cutscene.h`:
|
||||||
|
```c
|
||||||
|
#define CUTSCENE_MY_ITEM(ARGS...) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_MY_ITEM, .myItem = { ARGS } }
|
||||||
|
```
|
||||||
|
used inside a `CUTSCENE(NAME, SIZE, PAUSE_TYPE, ...)` block.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Save system
|
||||||
|
Save data lives under `src/dusk/save/` (`save.h`/`savefile.h`/
|
||||||
|
`saveplatform.h`). Slots are fixed-count (`SAVE_FILE_COUNT_MAX`), each
|
||||||
|
holding a yyjson document persisted with its byte size and a CRC32
|
||||||
|
checksum. `saveLoad`/`saveSave` read/write the JSON fresh every call —
|
||||||
|
callers own the returned/passed `yyjson_doc`/`yyjson_mut_doc` and must
|
||||||
|
free it themselves. Actual file I/O goes through platform-specific
|
||||||
|
`saveplatform_t`/stream hooks (one implementation per platform under
|
||||||
|
`src/dusk<platform>/save/`) — do not add direct filesystem calls to the
|
||||||
|
core `save.c`, extend the platform stream hooks instead.
|
||||||
|
|
||||||
|
## Network system
|
||||||
|
`src/dusk/network/` (`network.h`) is a connection-state layer only — a
|
||||||
|
`networkstate_t` state machine (`DISCONNECTED`/`CONNECTING`/`CONNECTED`/
|
||||||
|
`DISCONNECTING`) plus an HTTP client (`network/http/`) used for one-off
|
||||||
|
requests. It is not a multiplayer/replication protocol — that layer
|
||||||
|
doesn't exist yet (see `ROADMAP.md` items on the socket server/client
|
||||||
|
and packet handlers). Platform-specific connection logic (e.g. PSP's
|
||||||
|
`sceNetApctl` polling, GameCube/Wii's `if_config()`) lives under
|
||||||
|
`src/dusk<platform>/network/`, wired through `networkplatform.h` macros
|
||||||
|
the same way display/input are. Whenever this eventually grows a
|
||||||
|
multiplayer protocol, apply `ROADMAP.md`'s principle: never trust
|
||||||
|
incoming packet data — validate defensively with `errorret_t`/
|
||||||
|
`errorThrow()`, not asserts.
|
||||||
|
|
||||||
## Adding a new script (JS) module
|
## Adding a new script (JS) module
|
||||||
|
Dusk embeds JerryScript (`src/dusk/script/`, fetched via
|
||||||
|
`cmake/modules/Findjerryscript.cmake`). Today only `Entity`, the
|
||||||
|
generic `Component` wrapper, and `Scene` are registered (see
|
||||||
|
`src/dusk/script/module/modulelist.c`) — no per-component-type typed
|
||||||
|
wrappers exist yet (e.g. no `.position` on a `POSITION` component);
|
||||||
|
`entity.add(TYPE)`/`entity.getComponent(TYPE)` always return the
|
||||||
|
generic `Component`.
|
||||||
|
|
||||||
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
||||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
||||||
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
- Use `moduleBaseFunction(name)` to define JS-callable functions —
|
||||||
|
these are the one exception to "no `static` in `.c` files": the
|
||||||
|
macro itself expands to a `static jerry_value_t name(...)`
|
||||||
|
JerryScript external-handler trampoline, never called by name
|
||||||
|
from other C files, so it isn't declared in the `.h`.
|
||||||
- Register props/funcs in `moduleMyModInit()` with
|
- Register props/funcs in `moduleMyModInit()` with
|
||||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
||||||
`scriptProtoDefineStaticFunc`.
|
`scriptProtoDefineStaticFunc`.
|
||||||
@@ -212,9 +303,12 @@ simply left undefined — the core guards calls with `#ifdef`.
|
|||||||
`src/dusk/script/module/modulelist.c` and call
|
`src/dusk/script/module/modulelist.c` and call
|
||||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
||||||
`moduleListDispose()`).
|
`moduleListDispose()`).
|
||||||
3. For component modules also register in
|
3. For a component module that adds a *typed* wrapper for a specific
|
||||||
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
component type, create
|
||||||
so `entity.add()` returns the typed wrapper.
|
`src/dusk/script/module/entity/component/modulecomponentlist.c` (it
|
||||||
|
doesn't exist yet — the first such module creates it) so
|
||||||
|
`entity.add()` can return the typed wrapper instead of the generic
|
||||||
|
`Component`.
|
||||||
4. Create `types/<category>/mymod.d.ts` and add a
|
4. Create `types/<category>/mymod.d.ts` and add a
|
||||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
`/// <reference path="..." />` line to `types/index.d.ts`.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# PSP Optimization Plan
|
||||||
|
|
||||||
|
A survey of concrete memory and CPU (especially floating-point) optimization
|
||||||
|
opportunities for the PSP target, done 2026-07-31 on branch `ac2` at commit
|
||||||
|
`e61914ba` + this session's scripting work. Point-in-time findings, not a
|
||||||
|
substitute for reading the referenced source before acting on it.
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
The PSP (Allegrex MIPS CPU, 222-333MHz, single core, 32-64MB main RAM, 2MB
|
||||||
|
eDRAM, 16KB I-cache / 16KB D-cache, no virtual memory or memory protection)
|
||||||
|
is the tightest-constrained target this engine ships to. Two rules guide
|
||||||
|
everything below, both already correctly applied in one place in this
|
||||||
|
codebase (`entityposition_t` caching its transform matrices instead of
|
||||||
|
recomputing them from position/rotation/scale every read — see "Already
|
||||||
|
correct" below):
|
||||||
|
|
||||||
|
- **Memory is finite and cannot be compacted.** No VM means a fragmented
|
||||||
|
heap after a few minutes of play can fail an allocation even with
|
||||||
|
"enough" total free bytes. Prefer static/pool allocation over
|
||||||
|
malloc/free churn; prefer trading memory for CPU only when the memory
|
||||||
|
cost is bounded and paid once.
|
||||||
|
- **CPU is finite and floating-point math is not free**, especially
|
||||||
|
trig/sqrt on the plain scalar FPU. Prefer caching a computed result
|
||||||
|
behind a dirty flag over recomputing it unconditionally; prefer
|
||||||
|
avoiding a sqrt via squared-distance comparison wherever only a
|
||||||
|
yes/no or ordering result is needed.
|
||||||
|
|
||||||
|
## Priority 1 — Entity/component memory: a union tax paid on every slot, times 4 scenes
|
||||||
|
|
||||||
|
**The single biggest finding.** `entitymanager_t` (`src/dusk/entity/entitymanager.h:11-15`)
|
||||||
|
is a flat, statically-sized struct:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct entitymanager_t {
|
||||||
|
entity_t entities[ENTITY_COUNT_MAX]; // 64
|
||||||
|
component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX]; // 64*16 = 1024
|
||||||
|
componentid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX];
|
||||||
|
} entitymanager_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
`component_t` (`src/dusk/entity/component.h:69-72`) holds a **tagged union**
|
||||||
|
(`componentdata_t`) sized to its largest variant. That variant is
|
||||||
|
`entityrenderable_t`'s spritebatch payload (`entityrenderable.h:25-66`):
|
||||||
|
`spritebatchsprite_t sprites[64]` at 40 bytes each (`vec3 min + vec3 max +
|
||||||
|
vec2 uvMin + vec2 uvMax`) = **2560 bytes**, versus ~28-300 bytes for every
|
||||||
|
other component type (camera, physics, animation, position, trigger).
|
||||||
|
|
||||||
|
Because the union is embedded by value in a flat array — not behind a
|
||||||
|
pointer, not sparse — **every one of the 1024 component slots in every
|
||||||
|
entity manager costs ~2580 bytes, whether it holds a 28-byte camera or
|
||||||
|
nothing at all.** `1024 * 2580 ≈ 2.52MB` per `entitymanager_t`, confirmed
|
||||||
|
by the engine's own startup log every run: `Entity manager size: 2684992
|
||||||
|
bytes (2622.06 KB)`.
|
||||||
|
|
||||||
|
`scene_t` (`src/dusk/scene/scene.h:14-18`) embeds `entitymanager_t` by
|
||||||
|
value too, and `SCENE_COUNT_MAX = 4` (`scene/scenebase.h:11`) with
|
||||||
|
`SCENE_MANAGER` declared as a plain global (`scene/scene.c:20`) — so this
|
||||||
|
~2.52MB exists as static BSS from process start for **all 4 scene slots**,
|
||||||
|
used or not. **Total: ~10.1MB reserved permanently for entity storage
|
||||||
|
alone** — 16-31% of PSP main RAM — before a single texture, mesh, or audio
|
||||||
|
asset is loaded.
|
||||||
|
|
||||||
|
**Options to fix (roughly increasing effort/risk):**
|
||||||
|
1. Pull `entityrenderablespritebatch_t` out of the component union entirely
|
||||||
|
— store spritebatch entities via a pointer/handle into a separate,
|
||||||
|
smaller fixed pool sized to how many spritebatch-renderable entities
|
||||||
|
actually coexist (almost certainly far fewer than 64 per entity, and
|
||||||
|
far fewer entities need spritebatch at all vs. mesh/material
|
||||||
|
rendering). This alone would shrink the union's dominant variant from
|
||||||
|
~2560 bytes to whatever the next-largest variant is (~300 bytes,
|
||||||
|
`entitytrigger_t`) — an ~88% reduction, dropping the ~10.1MB down to
|
||||||
|
roughly ~1.2MB.
|
||||||
|
2. Reduce `SCENE_COUNT_MAX` from 4 if 4 concurrent scenes were never a
|
||||||
|
deliberate requirement (check with the project owner — this may just
|
||||||
|
be a round-number default nobody revisited), or make scene storage
|
||||||
|
pointer-based/lazily allocated so unused scene slots cost ~0 instead
|
||||||
|
of a full `entitymanager_t`.
|
||||||
|
3. Reduce `ENTITY_COMPONENT_COUNT_MAX` (16) if entities realistically use
|
||||||
|
far fewer distinct component types simultaneously — check actual
|
||||||
|
usage across `entityprefablist.h`/`gameprefablist.h` prefabs.
|
||||||
|
|
||||||
|
Do (1) first — it's the highest-leverage, most contained change (touches
|
||||||
|
`entityrenderable.h`'s data layout and its render/dispose paths, not the
|
||||||
|
general entity/component system), and re-measure via the same startup
|
||||||
|
log line before deciding whether (2)/(3) are still worth doing.
|
||||||
|
|
||||||
|
## Priority 2 — VFPU is completely unused; all vec/mat math is scalar
|
||||||
|
|
||||||
|
The PSP's Allegrex CPU has a **VFPU** (vector floating-point unit) capable
|
||||||
|
of fast SIMD-style 4-wide float ops and hardware-accelerated matrix
|
||||||
|
operations, exposed by pspsdk's `pspvfpu`/GU "Geometry Utility" (`gum_*`)
|
||||||
|
helpers. This engine links `pspvfpu` (`cmake/targets/psp.cmake`) but
|
||||||
|
**never calls into it** — confirmed via a zero-hit grep for
|
||||||
|
`vfpu|pspmath|gum_|vcst|gu_matrix` across `src/duskpsp/` and `src/duskgl/`.
|
||||||
|
|
||||||
|
All vector/matrix math instead goes through cglm (`glm_vec3_*`,
|
||||||
|
`glm_mat4_*`), which auto-detects SIMD only for x86 (`CGLM_SSE2`/`AVX`) or
|
||||||
|
ARM (`CGLM_NEON`) — neither applies to MIPS, and no `CGLM_*` macros are
|
||||||
|
set anywhere in this repo (confirmed zero hits). **Every `glm_mat4_mul`,
|
||||||
|
every position/rotation rebuild, every physics vector op runs on the
|
||||||
|
plain scalar MIPS FPU with the VFPU sitting idle.**
|
||||||
|
|
||||||
|
This is the single largest *available* CPU win identified in this survey
|
||||||
|
— larger than any specific hot-path fix below, because it's a multiplier
|
||||||
|
on all of them. Concretely: route `entityposition.c`'s matrix
|
||||||
|
rebuild/multiply path and `physicsworld.c`'s per-body vector math through
|
||||||
|
`pspvfpu`/`gum_*` on the PSP build specifically (behind `#ifdef DUSK_PSP`,
|
||||||
|
matching the project's existing platform-guard convention), while keeping
|
||||||
|
cglm as the portable fallback for Linux/Knulli/GameCube/Wii. This is a
|
||||||
|
genuinely large effort (new platform-specific math backend, careful
|
||||||
|
correctness verification since VFPU has its own quirks around
|
||||||
|
pipelining/hazards) — scope it as its own project, not a quick pass.
|
||||||
|
|
||||||
|
## Priority 3 — UI/spritebatch rebuilds and redraws every vertex, every frame
|
||||||
|
|
||||||
|
Already flagged in `STATUS.md`/`ROADMAP.md` (item 4/5) as a known open
|
||||||
|
issue; this session's research adds concrete numbers. `meshvertex_t`
|
||||||
|
(`display/mesh/meshvertex.h:14-17`) is `{ float uv[2]; float pos[3]; }` =
|
||||||
|
**20 bytes/vertex**, no packing. `SPRITEBATCH_SPRITES_MAX = 512`,
|
||||||
|
`SPRITEBATCH_FLUSH_COUNT = 16` → 32 sprites/flush = 192 vertices = **3,840
|
||||||
|
bytes rebuilt and redrawn per flush**, with a flush forced on every
|
||||||
|
shader/material change (`spritebatch.c:38-50`) and used unconditionally
|
||||||
|
by every widget except `uiconsole.c` (`uitab.c:56`, `uislider.c:183/199/231`,
|
||||||
|
final flush in `ui.c:51`). A UI screen with ~50-100 sprites and 2-3
|
||||||
|
material changes costs an estimated **8-15KB of vertex rebuild + GU
|
||||||
|
submission every single frame**, regardless of whether the UI changed
|
||||||
|
since the last frame.
|
||||||
|
|
||||||
|
Confirmed on the PSP legacy-GL path specifically: there's no GPU buffer
|
||||||
|
re-upload cost (`meshFlushGL` is a literal no-op comment: "we use the
|
||||||
|
glClientState stuff" — `meshgl.c:91-93`; `meshDrawGL` just points
|
||||||
|
`glVertexPointer` at the CPU-side array each call), so the real cost is
|
||||||
|
(a) the CPU-side per-sprite vertex rewrite every frame and (b) GU
|
||||||
|
re-transforming the full vertex stream from RAM on every draw with no
|
||||||
|
skip for unchanged geometry.
|
||||||
|
|
||||||
|
**Fix direction** (already captured in `ROADMAP.md`'s debt backlog item 5):
|
||||||
|
extend `uiconsole.c`'s cached-mesh, dirty-flag-gated rebuild pattern to
|
||||||
|
the rest of the widget tree. This plan adds one refinement: also consider
|
||||||
|
packing `meshvertex_t` down (next item) since it multiplies this cost.
|
||||||
|
|
||||||
|
## Priority 4 — All-float vertex format wastes bandwidth and T&L cost
|
||||||
|
|
||||||
|
`meshvertex_t` uses two `float` fields (20 bytes) for every mesh and
|
||||||
|
sprite vertex, mesh-wide, with no normal or per-vertex color (color is
|
||||||
|
already handled at the material level, so that part is already optimal).
|
||||||
|
GU natively supports fixed-point 16-bit positions/UVs
|
||||||
|
(`GU_VERTEX_16BIT`/`GU_TEXTURE_16BIT`), which would roughly halve
|
||||||
|
per-vertex size and the corresponding vertex-fetch/transform cost, at the
|
||||||
|
cost of position precision (fine for UI/sprite work and most world
|
||||||
|
geometry at this engine's scale; worth checking against the largest
|
||||||
|
world coordinates actually used before committing). Pair with Priority 3
|
||||||
|
so the packing benefit compounds with the dirty-tracking benefit instead
|
||||||
|
of just reducing the cost of a rebuild that still happens every frame.
|
||||||
|
|
||||||
|
## Priority 5 — Running gameplay logic in JerryScript has a real per-frame cost on PSP
|
||||||
|
|
||||||
|
This is new since last session's work, not a pre-existing issue: `assets/
|
||||||
|
scripts/overworldscene.js`'s `update()` is now called every frame via
|
||||||
|
`scriptManagerCallGlobal("update")` (wired into `engine.c`'s
|
||||||
|
`engineUpdate()`), replacing what used to be a direct `cosf`/`sinf` C
|
||||||
|
callback (`entityUpdateAdd`). Per call, `scriptManagerCallGlobal`
|
||||||
|
(`scriptmanager.c:91-144`) does: a global-object property lookup (key is
|
||||||
|
cached, but the `jerry_object_get` dispatch still runs), full JS
|
||||||
|
interpreter call dispatch (frame setup, argument marshaling) for what's a
|
||||||
|
two-line function, and an unconditional `jerry_value_is_promise` check
|
||||||
|
every call even though `update()` is synchronous. Inside, `Math.cos`/
|
||||||
|
`Math.sin` run as JS builtin calls rather than direct libm calls.
|
||||||
|
|
||||||
|
Additionally: this project's JerryScript fork is already patched to use
|
||||||
|
32-bit float internally (`JERRY_NUMBER_TYPE_FLOAT64=0`,
|
||||||
|
`Findjerryscript.cmake`) — a deliberate, already-correct optimization for
|
||||||
|
this exact concern — but the *public* engine API (`jerry_value_as_number()`)
|
||||||
|
still returns `double`, so every native↔JS boundary crossing (every
|
||||||
|
`moduleBaseArgFloat`, every `moduleBaseVec3ToObject`) still pays a
|
||||||
|
float→double→float round trip. Also note `JERRY_MATH` is off, so
|
||||||
|
`Math.sin`/`cos` fall through to whatever generic libm the platform
|
||||||
|
provides rather than JerryScript's own fdlibm implementation — worth
|
||||||
|
checking whether turning it on changes anything measurable on PSP.
|
||||||
|
|
||||||
|
**This is one `update()` call for one scene-level script today — cheap in
|
||||||
|
absolute terms.** It becomes a real problem only if the pattern scales:
|
||||||
|
giving many individual entities their own per-frame JS update callback
|
||||||
|
would multiply all of the above per entity. **Recommendation: keep
|
||||||
|
high-frequency, hot per-entity logic (movement, camera math, physics
|
||||||
|
response) in native C update callbacks (`entityUpdateAdd`, the existing
|
||||||
|
mechanism), and reserve JS for one-time setup, infrequent/event-driven
|
||||||
|
logic, and coarse-grained per-scene orchestration** — which is exactly
|
||||||
|
what `require()` and the typed component wrappers built this session are
|
||||||
|
suited for, not a per-entity-per-frame hot path. This is a design
|
||||||
|
guideline to apply going forward, not a regression to fix in the current
|
||||||
|
`overworldscene.js` (one scene-level `update()` call per frame is fine).
|
||||||
|
|
||||||
|
## Priority 6 — No pooling/arena allocator; plain malloc/free everywhere
|
||||||
|
|
||||||
|
`memoryAllocate`/`memoryFree` (`util/memory.c`) are direct `malloc`/`free`
|
||||||
|
passthroughs (`memoryAlign`→`memalign`, `memoryReallocate`/`memoryResize`→
|
||||||
|
`realloc`), with the only extra behavior being a global allocation-count
|
||||||
|
tracker used solely for test leak detection. No pool, arena, free-list,
|
||||||
|
or size-class allocator exists anywhere.
|
||||||
|
|
||||||
|
This survey found **no rogue per-frame heap allocations** in the hot
|
||||||
|
paths checked (render dispatch in `entityrenderable.c`, all of
|
||||||
|
`physics/*.c`) — so this is not an active bug today. But it's a
|
||||||
|
structural risk for a no-VM platform over a long play session: any future
|
||||||
|
code that does frequent small alloc/free (dynamic lists, string
|
||||||
|
building, temp buffers) will fragment the 32-64MB heap with no OS-level
|
||||||
|
recovery mechanism. **Recommendation: before adding any new subsystem
|
||||||
|
that allocates/frees frequently at runtime (not just at load time),
|
||||||
|
default to a fixed-size pool or arena for it**, following the same
|
||||||
|
"static, bounded" philosophy already used for `ENTITY_COUNT_MAX`/
|
||||||
|
`SCENE_COUNT_MAX`/`ASSET_ENTRY_COUNT_MAX` elsewhere in the engine, rather
|
||||||
|
than reaching for `memoryAllocate` per-instance.
|
||||||
|
|
||||||
|
## Already correct — don't touch without new evidence
|
||||||
|
|
||||||
|
- **`entityposition_t`'s matrix caching** (the pattern the project owner
|
||||||
|
called out as the reason for writing this plan). Verified: a full
|
||||||
|
"dirty" recompute chain (decompose + rebuild local + rebuild world) is
|
||||||
|
~10 trig calls (`asinf`/`cosf`/`atan2f`) plus at most one 4x4 matrix
|
||||||
|
multiply, and `entityPositionEnsurePRS`/`EnsureLocal`/`EnsureWorld`
|
||||||
|
(`entityposition.c:596-669`) all early-return on a flag check, only
|
||||||
|
doing that work when something actually changed. The cache is correct
|
||||||
|
and clearly worth its ~128-byte-per-entity matrix storage cost.
|
||||||
|
- **Physics narrow-phase sqrt avoidance.** All three `sqrtf` call sites
|
||||||
|
in `physicstest.c`/`physicsshapemesh.c` already sit behind a
|
||||||
|
squared-distance early-reject and only compute the real (linear)
|
||||||
|
distance once, when actually needed for penetration depth — no further
|
||||||
|
"use squared distance instead" opportunity was found here.
|
||||||
|
- **No allocations in the render/physics hot loop** (Priority 6) — this
|
||||||
|
is good and worth preserving as new code is added to those files.
|
||||||
|
- **Asset decompression already runs off the main thread.** Assets are
|
||||||
|
DEFLATE-compressed (not stored) in `dusk.dsk`, so loading pays a real
|
||||||
|
CPU cost for decompression — but every `*LoaderAsync` function
|
||||||
|
(`asset/loader/*/*.c`) runs via `ASSET.loadThread`
|
||||||
|
(`assertNotMainThread` in `asset.c:365`), so this cost is already kept
|
||||||
|
off the main thread. Low priority to change; if load-time CPU cost
|
||||||
|
becomes a measured problem later, revisit stored-vs-compressed as a
|
||||||
|
build-time flag rather than assuming compression is free.
|
||||||
|
|
||||||
|
## Suggested approach
|
||||||
|
|
||||||
|
1. **Measure before changing.** None of the above have been profiled on
|
||||||
|
real PSP hardware in this pass — this is a source-level survey, not a
|
||||||
|
profile. Before investing in Priority 1 or 2 especially, confirm with
|
||||||
|
an actual PSP build/run (or at minimum the existing "Entity manager
|
||||||
|
size" startup log plus a frame-time counter) that these are the real
|
||||||
|
bottlenecks, not just the largest numbers on paper.
|
||||||
|
2. **Priority 1 first** — it's the most contained (one component's data
|
||||||
|
layout), has the clearest before/after metric (the startup log line),
|
||||||
|
and doesn't require new platform-specific code.
|
||||||
|
3. **Priority 3 next** (extend the console's dirty-tracking pattern to
|
||||||
|
the rest of the UI) — already scoped in `ROADMAP.md`, no new design
|
||||||
|
needed, just implementation.
|
||||||
|
4. **Priority 2 (VFPU) as a dedicated project**, not a quick pass — it's
|
||||||
|
the largest potential win but touches core math plumbing and needs
|
||||||
|
careful correctness verification on real hardware.
|
||||||
|
5. Treat Priority 5 as a standing design guideline for all future
|
||||||
|
scripting work, not a one-time fix.
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
# Dusk Roadmap
|
||||||
|
|
||||||
|
Tracking upcoming milestones for the engine. See `PSP_OPTIMIZATION_PLAN.md`
|
||||||
|
for a memory/CPU optimization survey specifically targeting the PSP build
|
||||||
|
(milestone 4 below is its Priority 3).
|
||||||
|
|
||||||
|
## Upcoming milestones
|
||||||
|
|
||||||
|
1. Add a very basic physics engine, moving away from the current
|
||||||
|
tile-based movement.
|
||||||
|
2. Give entities full freedom of movement (no longer locked to tile
|
||||||
|
grid positions).
|
||||||
|
3. Update entity interaction, triggers, chunk management, and other
|
||||||
|
systems that currently assume tile-based positioning so they work
|
||||||
|
with the new 3D positioning/movement code.
|
||||||
|
4. Investigate and fix poor UI rendering performance. First pass done
|
||||||
|
(2026-07-31): slider/tab/frame/textbox now cache their sprite
|
||||||
|
geometry instead of rebuilding it every frame, and the console's
|
||||||
|
vertex buffer is fixed-size instead of alloc/free churn (see
|
||||||
|
`STATUS.md`'s "UI rendering performance" note for detail). Still
|
||||||
|
open: per-widget material/color changes still force a sprite-batch
|
||||||
|
flush each time (color is per-material, not per-vertex), so a row of
|
||||||
|
alternating highlighted widgets still costs one draw call per
|
||||||
|
widget.
|
||||||
|
5. Create UI elements for displaying status indicators, e.g. network
|
||||||
|
connection state and save-in-progress.
|
||||||
|
6. Fully test saving end-to-end on all supported platforms.
|
||||||
|
7. Remove the tile system from chunks in favor of meshes, with
|
||||||
|
dynamic hitboxes per chunk loaded in from the chunk file data.
|
||||||
|
8. Create UI elements for network status: a connecting modal, an
|
||||||
|
error state, and a connected flag. Retire the test HTTP request
|
||||||
|
once these are in place.
|
||||||
|
9. Build the socket server and client implementation, including
|
||||||
|
handlers for the different packet types.
|
||||||
|
10. Add a dedicated multiplayer entity type, `clientplayer`, alongside
|
||||||
|
the existing `npc` and `player` types. Limit to 8 (defined
|
||||||
|
constant) for now.
|
||||||
|
11. Send and receive `clientplayer` position over the network.
|
||||||
|
12. Create a UI menu for creating a server and joining a server. For
|
||||||
|
now, join IPs are hard-coded (testing against a fixed IP of
|
||||||
|
10.0.0.94).
|
||||||
|
13. Create "handshake" packets. For now, just send the username,
|
||||||
|
enforced to be under 10 characters long.
|
||||||
|
14. Server tracks all players' positions and broadcasts them to all
|
||||||
|
connected clients.
|
||||||
|
15. Server sends disconnect packets for users who leave.
|
||||||
|
16. Server assigns each client a UUID; all clients know every other
|
||||||
|
client's UUID (used to reference them across position updates,
|
||||||
|
disconnect packets, etc).
|
||||||
|
17. Server notifies all clients (by UUID) when a user joins, leaves,
|
||||||
|
or is disconnected, so clients can spawn or remove the
|
||||||
|
corresponding `clientplayer` entity in the world.
|
||||||
|
|
||||||
|
## Infrastructure / debt backlog
|
||||||
|
|
||||||
|
Found during a full-codebase inventory pass (2026-07-30, see `STATUS.md`
|
||||||
|
for the full survey). These aren't new feature milestones so much as
|
||||||
|
loose ends worth closing, roughly in order of how cheap/safe they are to
|
||||||
|
fix:
|
||||||
|
|
||||||
|
1. Re-enable `test/item` (currently commented out in
|
||||||
|
`test/CMakeLists.txt`) -- confirm it still passes and turn it back on.
|
||||||
|
2. Restore `itemgive.c/h` in `src/duskrpg/item/CMakeLists.txt` -- the
|
||||||
|
textbox UI dependency it was waiting on (`ui/textbox/`) is already
|
||||||
|
back.
|
||||||
|
3. Decide the fate of the `vita` target: `scripts/build-vita.sh` and
|
||||||
|
`docker/vita/` reference `-DDUSK_TARGET_SYSTEM=vita`, but no
|
||||||
|
`cmake/targets/vita.cmake` exists, so the build is currently broken.
|
||||||
|
Either implement it or remove the dangling scripts/Dockerfile.
|
||||||
|
4. Per-PR CI build coverage for non-Linux targets: first pass added
|
||||||
|
2026-07-31 as `run-tests-gamecube-dolphin`/`run-tests-wii-dolphin`
|
||||||
|
(build the ISO, boot it in Dolphin under Xvfb) and
|
||||||
|
`run-tests-psp-ppsspp` (build the EBOOT, boot it in PPSSPPHeadless)
|
||||||
|
in `.github/workflows/test.yml`, plus matching
|
||||||
|
`scripts/test-*-dolphin.sh`/`scripts/test-psp-ppsspp.sh` (+ `-docker`
|
||||||
|
variants). Marked `continue-on-error: true` since none of these have
|
||||||
|
actually been exercised on a real runner yet -- verify they pass
|
||||||
|
before relying on them, and drop the flag once they do. Knulli still
|
||||||
|
has no per-PR coverage at all.
|
||||||
|
5. Milestone 4 above (poor UI rendering performance) has a first pass
|
||||||
|
in (2026-07-31, see `STATUS.md`) -- remaining concrete lead: sprite
|
||||||
|
batch flushes are keyed on shader+material (including tint color),
|
||||||
|
so per-widget color changes (e.g. focus highlight) still force a
|
||||||
|
flush per widget; moving tint to per-vertex color would let a whole
|
||||||
|
row of widgets batch into one draw call regardless of highlight
|
||||||
|
state.
|
||||||
|
6. Revisit the GameCube/Wii networking static-IP workaround in
|
||||||
|
`networkdolphin.c` -- it's standing in for an unresolved suspected
|
||||||
|
memory-corruption bug in `if_config()`'s DHCP path.
|
||||||
|
7. Decide whether the PSP dialog-based network connect UI needs to come
|
||||||
|
back -- it was removed entirely (not fixed) when the original
|
||||||
|
dialog-tearing bug proved hard to resolve.
|
||||||
|
8. Backfill unit tests for the biggest untested surfaces: `ui/` (whole
|
||||||
|
widget framework), `save/`, `script/` (JerryScript bindings), `event/`.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
- Never trust the network implicitly. Neither side (server or client)
|
||||||
|
should assume the other's packets are well-formed or benign --
|
||||||
|
validate all incoming packet data defensively, since either side
|
||||||
|
may send garbage or malicious data. Use `errorret_t` /
|
||||||
|
`errorThrow()` for these runtime checks, not assert macros --
|
||||||
|
asserts are debug-only and won't guard release builds against
|
||||||
|
malformed or malicious packet data.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Dusk Engine - Status Snapshot
|
||||||
|
|
||||||
|
This is a point-in-time inventory of the codebase's maturity, test coverage,
|
||||||
|
and known gaps. It is not auto-maintained -- re-survey periodically (or
|
||||||
|
whenever picking a new roadmap milestone) rather than trusting it blindly.
|
||||||
|
See `ROADMAP.md` for the ordered feature milestones this status feeds into,
|
||||||
|
and `CLAUDE.md` for coding conventions.
|
||||||
|
|
||||||
|
Last surveyed: 2026-07-31, at commit `8f8fa8f8`.
|
||||||
|
|
||||||
|
## Core engine (`src/dusk/`)
|
||||||
|
|
||||||
|
| Subsystem | Maturity | Notes |
|
||||||
|
|------------|------------------------------------|-------|
|
||||||
|
| asset | Mature, fully wired | Model loading's "async" path is actually synchronous (`assetmodelloader.h`) -- only real stub found in core. |
|
||||||
|
| script | Mature for its current scope | Registered modules: `modulePlatform`, `moduleComponent`/`moduleComponentList` (typed Position/Physics/Renderable wrappers), `moduleEntity`, `moduleScene` (with `Scene.set()` module lifecycle), `moduleMesh`, `moduleTime`, `moduleRequire` (CommonJS-style `require()`/`module.exports`). Entry point is `scripts/init.js`, which does `require('./overworldscene.js')` + `Scene.set()`. Has unit tests (`test/script/`). |
|
||||||
|
| entity | Mature | Engine-level prefab lists are empty sentinels; all real prefabs live in `duskrpg`. |
|
||||||
|
| scene | Mature | Same prefab-delegation pattern as entity. No JSON serialize/deserialize (removed by design). |
|
||||||
|
| save | Mature, but undocumented | Slot-based, yyjson + CRC32, platform stream hooks. Not covered in `CLAUDE.md`, no tests. |
|
||||||
|
| network | Connection-state layer only | HTTP client + connection state machine; no multiplayer/replication protocol (expected -- that's roadmap items 8-17). Not covered in `CLAUDE.md`, has HTTP tests only. |
|
||||||
|
| physics | Mature, recently churned | Recent revert/re-disable of "old ent code" suggests component wiring around physics isn't fully settled. Well tested. |
|
||||||
|
| animation | Early/mid-stage | Keyframes + easing only, no blend trees or state machines. Terse commit history ("ANIM") suggests still iterating. |
|
||||||
|
| display | Most mature/battle-tested | Backbone of the engine; dominated by platform optimization commits. |
|
||||||
|
| ui | Actively churning, perf pass done | Widget framework has had features added and ripped out repeatedly (story/battle UI added then removed). No JS bindings yet (from-scratch surface if that's picked up). Sprite-cache pass landed for slider/tab/frame/textbox (see roadmap item 4 below); still no scripted pointer/hit-testing. Has unit tests for the new caching logic (`test/ui/`), but nothing exercises actual draw calls -- see below. |
|
||||||
|
| console | Small, finished for its scope | Fixed-size cached mesh (no more alloc/free churn), same pattern the rest of `ui/` now follows. |
|
||||||
|
| event | Clean, small, finished | Pub/sub, rebuilt to replace the old input system. |
|
||||||
|
| game | Intentionally header-only | Real implementation lives in `duskrpg/game/game.c`. |
|
||||||
|
|
||||||
|
### UI rendering performance (roadmap item 4)
|
||||||
|
First pass landed 2026-07-31: `uislider`/`uitab` now cache their non-text
|
||||||
|
quads (track/fill/markers, tab background) relative to origin and only
|
||||||
|
rebuild on state change, translating into position at draw time.
|
||||||
|
`uiframe.c` grew `uiFrameDrawCached()`, which skips rebuilding its 9-slice
|
||||||
|
sprites when x/y/width/height match the last call; wired into the
|
||||||
|
confirm dialog, settings panel, and textbox (each owns its own
|
||||||
|
`uiframecache_t`). `uitextbox` no longer does one `spriteBatchBuffer` call
|
||||||
|
per visible glyph -- it builds a per-page glyph cache once and slices a
|
||||||
|
prefix by scroll each frame. `uiconsole` dropped its alloc/free vertex
|
||||||
|
buffer for a fixed 512-glyph array. `uifps` skips its label rebuild when
|
||||||
|
the formatted FPS string hasn't changed.
|
||||||
|
|
||||||
|
Still open: every widget still issues its own `spriteBatchBuffer` call
|
||||||
|
per material/color, so a row of alternating highlighted/plain widgets
|
||||||
|
still forces a GPU flush per widget (color is applied via material, not
|
||||||
|
per-vertex) -- that's the next real win if PSP framerate is still an
|
||||||
|
issue. No scripted pointer/hit-testing exists either; input is still
|
||||||
|
100% gamepad/keyboard directional-focus (`ui/focus/`).
|
||||||
|
|
||||||
|
Test coverage caveat: `test/ui/` and `test/display/test_spritebatchsprite.c`
|
||||||
|
cover the new caching *logic* (geometry math, exercised by constructing
|
||||||
|
widget structs directly) but cannot exercise `uiXxxDraw()`/`uiFrameDraw()`
|
||||||
|
themselves -- those need `FONT_DEFAULT`/`UI_FRAME`'s GL texture, which
|
||||||
|
needs a live GL context this test binary doesn't have (confirmed: calling
|
||||||
|
`fontInitDefault()` in a test asserts in `texturegl.c`). This is true of
|
||||||
|
the whole rendering layer, not something newly introduced -- there's no
|
||||||
|
GL-backed test harness anywhere in the repo yet.
|
||||||
|
|
||||||
|
## Game layer (`src/duskrpg/`)
|
||||||
|
|
||||||
|
Actively maintained, not orphaned (despite an old "remove rpg" commit deep
|
||||||
|
in history) -- last touched the same day as this survey.
|
||||||
|
|
||||||
|
- **cutscene/** -- actively developed, matches `CLAUDE.md`'s documented
|
||||||
|
recipe exactly. 16 registered item types.
|
||||||
|
- **entity/, scene/** -- overworld player/camera/interactable components
|
||||||
|
and prefabs, active.
|
||||||
|
- **item/** -- `item.c`/`inventory.c`/`backpack.c` built via a working
|
||||||
|
`item.json` -> `itemdef.h` codegen pipeline. `itemgive.c/h` exist on
|
||||||
|
disk but are explicitly excluded from the CMake build pending textbox
|
||||||
|
UI restoration -- **that UI (`ui/textbox/`) is already restored**, so
|
||||||
|
this looks like an overdue follow-up, not a real blocker.
|
||||||
|
- **input/** -- headers only, no implementation. Contains the one
|
||||||
|
genuine TODO found in the whole `duskrpg` tree: `// TODO: Wiimote, USB
|
||||||
|
Keyboard, probably more.`
|
||||||
|
- **ui/** -- textbox restored and built; `uitestlabel.c/h` is an
|
||||||
|
intentional smoke-test scaffold, not dead code.
|
||||||
|
|
||||||
|
## Platform layers
|
||||||
|
|
||||||
|
| Platform | Status |
|
||||||
|
|--------------|--------|
|
||||||
|
| duskgl / dusksdl2 | Complete, shared by Linux/Knulli/PSP, no gaps found. |
|
||||||
|
| dusklinux | Complete, well-trodden. |
|
||||||
|
| duskpsp | Complete for current design. The historical dialog/tearing bug (see memory `project_psp_dialog_tearing` etc.) was **not fixed -- the dialog-based connect UI was removed entirely** (commit `d7982599`, "Simplified PSP network") in favor of a silent profile-based connect. Revisit if the dialog UX is still wanted. |
|
||||||
|
| duskdolphin (GameCube/Wii) | Functional, not a stub -- real GX-based display/mesh/shader/texture. Wii input uses only the GameCube `PAD_*` library; no WPAD/Wiimote support (`inputdolphin.h` has `#error "Wii not implemented"` gated behind macros that are never defined, so currently dormant, not a live build break). `networkdolphin.c` hardcodes a static IP as an explicit temporary workaround for a suspected DHCP-related memory-corruption bug in `if_config()` -- root cause still open. |
|
||||||
|
| vita | **Referenced by `scripts/build-vita.sh` and `docker/vita/` but no `cmake/targets/vita.cmake` exists** -- the build target is broken/unfinished at the CMake level. |
|
||||||
|
|
||||||
|
### CI coverage gap
|
||||||
|
`.github/workflows/test.yml` only builds+tests Linux, on PRs to `main`.
|
||||||
|
`build.yml` builds all other platforms (PSP, Knulli, GameCube, Wii +
|
||||||
|
ISO variants) but **only on tag push** (release time). Vita and Dolphin
|
||||||
|
aren't in CI at all. Net effect: regressions on 4+ platforms can land
|
||||||
|
silently until a release tag is cut.
|
||||||
|
|
||||||
|
## Test coverage gaps
|
||||||
|
|
||||||
|
Core `src/dusk/` subsystems with **zero** unit tests: `console`,
|
||||||
|
`engine`, `event`, `game`, `input`, `log`, `save`, `script` (+ all JS
|
||||||
|
module bindings), `system`, `ui` (entire widget framework).
|
||||||
|
|
||||||
|
`src/duskrpg/` has almost no test coverage: `test/item/test_inventory.c`
|
||||||
|
exists but **`add_subdirectory(item)` is commented out in
|
||||||
|
`test/CMakeLists.txt`**, so even that one test never runs. `cutscene`,
|
||||||
|
`entity`, `game`, `input`, `scene`, `ui` under `duskrpg` have no tests at
|
||||||
|
all.
|
||||||
|
|
||||||
|
No stale tests were found referencing deleted systems (old JSON
|
||||||
|
serialize/deserialize, old event/cutscene modules) -- the test suite is
|
||||||
|
internally consistent with current code, just incomplete in coverage.
|
||||||
|
|
||||||
|
## Build/tooling gaps worth knowing about
|
||||||
|
|
||||||
|
- `assetsraw/` -> `assets/` (chunk JSON -> `.dcf`) is a manual/editor-only
|
||||||
|
step (`tools.asset.chunk`), not part of the CMake build -- committed
|
||||||
|
`.dcf` files can silently drift from their raw source.
|
||||||
|
- Several Python tool packages exist but aren't wired into any build:
|
||||||
|
`tools/color/csv/`, `tools/asset/chunk_json/`, `tools/asset/dmf/`,
|
||||||
|
`tools/asset/tiles/`, `tools/input/csv/`. Some may be superseded by
|
||||||
|
`tools/color.py`/`tools/item.py`; worth a pass to confirm which are
|
||||||
|
live vs leftover.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"pause": "DEFAULT",
|
|
||||||
"items": [
|
|
||||||
{ "type": "text", "text": "Test Two." },
|
|
||||||
{ "type": "entityAdd", "entityType": "npc", "position": [4, 4, 0] },
|
|
||||||
{
|
|
||||||
"type": "textMini",
|
|
||||||
"text": "Hello!",
|
|
||||||
"position": [4, 4, 0],
|
|
||||||
"duration": 3.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "emoji",
|
|
||||||
"entityIndex": "lastCreated",
|
|
||||||
"emojiType": "exclamation",
|
|
||||||
"duration": 2.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "entityWalkTo",
|
|
||||||
"entityIndex": "lastCreated",
|
|
||||||
"positions": [[8, 2, 0]]
|
|
||||||
},
|
|
||||||
{ "type": "text", "text": "Done." }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_-1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_4_4.json", "offset": [2.0, 1.0, 0.0]}, {"model": "models/buildings/house_6_3.json", "offset": [1.0, 9.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [12.0, 6.0, 0.0]}]}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_0_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_8_4.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_3_3.json", "offset": [11.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [3.0, 11.0, 0.0]}]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_3_2.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [7.0, 2.0, 0.0]}, {"model": "models/buildings/house_4_2.json", "offset": [1.0, 8.0, 0.0]}, {"model": "models/buildings/house_1_1.json", "offset": [11.0, 10.0, 0.0]}, {"model": "models/buildings/house_5_2.json", "offset": [13.0, 6.0, 0.0]}]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_5_5.json", "offset": [3.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [10.0, 3.0, 0.0]}, {"model": "models/buildings/house_3_1.json", "offset": [1.0, 12.0, 0.0]}]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_2_0_0_0.json", "offset": [0.0, 0.0, 0.0]}]}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Test Map",
|
|
||||||
"entities": [
|
|
||||||
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
|
|
||||||
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 },
|
|
||||||
{
|
|
||||||
"type": "npc",
|
|
||||||
"position": [8, 8, 1],
|
|
||||||
"path": [[4, 4, 0], [10, 10, 1], [4, 4, 0], [10, 10, 1]],
|
|
||||||
"cutscene": "test_npc"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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,85 @@
|
|||||||
|
// 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 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);
|
||||||
|
|
||||||
|
// Player: dynamic capsule body, moved relative to the camera by
|
||||||
|
// PLAYER's own update callback (see entityplayer.c).
|
||||||
|
var player = new Entity();
|
||||||
|
var playerPosition = player.add(POSITION);
|
||||||
|
playerPosition.setLocalPosition(0.0, 2.0, 0.0);
|
||||||
|
|
||||||
|
var playerPhysics = player.add(PHYSICS);
|
||||||
|
playerPhysics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
||||||
|
|
||||||
|
var playerRenderable = player.add(RENDERABLE);
|
||||||
|
playerRenderable.setMesh(0, MESH_CAPSULE);
|
||||||
|
playerRenderable.setColor(0, 0, 255, 255);
|
||||||
|
|
||||||
|
player.add(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;
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
|
||||||
keyframe_t *end;
|
|
||||||
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
|
||||||
keyframe_t *current = anim->keyframes;
|
|
||||||
start = current;
|
|
||||||
|
|
||||||
do {
|
float_t duration = keyframeSetGetDuration(&anim->keyframes);
|
||||||
if(current->time > time) {
|
float_t prevTime = anim->time;
|
||||||
end = current;
|
anim->time += TIME.delta * anim->speed;
|
||||||
break;
|
|
||||||
}
|
|
||||||
start = current;
|
|
||||||
current++;
|
|
||||||
|
|
||||||
if(current > last) {
|
// Loops that overshoot duration wrap back into [0, duration) -- an event
|
||||||
end = start;
|
// near the end must still be checked across that wrap, as two segments:
|
||||||
break;
|
// (prevTime, duration] then [0, newTime].
|
||||||
}
|
bool_t wrapped = anim->loop && duration > 0.0f && anim->time >= duration;
|
||||||
} while(true);
|
|
||||||
|
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
if(anim->loop) {
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
if(duration > 0.0f) anim->time = mathModFloat(anim->time, duration);
|
||||||
}
|
} else if(anim->time >= duration) {
|
||||||
|
anim->time = duration;
|
||||||
|
anim->playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!anim->eventCallback) return;
|
||||||
|
|
||||||
|
bool_t crossed = wrapped
|
||||||
|
? (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
|
||||||
* @param anim The animation to get the value from.
|
* (see keyframeSetGetDuration()). If loop is set, time wraps back into
|
||||||
* @param time The time at which to get the value, in seconds.
|
* [0, duration) on reaching it; otherwise time is clamped there and
|
||||||
* @return The value of the animation at the given time.
|
* 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.
|
||||||
*/
|
*/
|
||||||
float_t animationGetValue(animation_t *anim, const float_t time);
|
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 trackIndex The track to evaluate, in [0, trackCount).
|
||||||
|
* @return The interpolated value of that track at the current 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));
|
||||||
|
}
|
||||||
@@ -10,4 +10,18 @@ typedef struct {
|
|||||||
float_t time;
|
float_t time;
|
||||||
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,220 +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/string.h"
|
|
||||||
#include "asset/loader/assetloading.h"
|
|
||||||
#include "asset/loader/assetentry.h"
|
|
||||||
#include "asset/loader/assetloader.h"
|
|
||||||
#include "asset/loader/json/assetjsonloader.h"
|
|
||||||
#include "asset/asset.h"
|
|
||||||
#include "yyjson.h"
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
|
||||||
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_LOAD_JSON;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_LOAD_JSON: {
|
|
||||||
// Lock the chunk's JSON as a sub-asset. The entry key is prefixed
|
|
||||||
// with "json:" to avoid a type-collision with the chunk entry
|
|
||||||
// itself (both share the same filename). The JSON loader reads
|
|
||||||
// the real file path supplied in the input.
|
|
||||||
char_t jsonKey[ASSET_FILE_NAME_MAX];
|
|
||||||
stringFormat(
|
|
||||||
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
|
|
||||||
);
|
|
||||||
assetloaderinput_t jsonInput;
|
|
||||||
memoryZero(&jsonInput, sizeof(jsonInput));
|
|
||||||
stringCopy(
|
|
||||||
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
|
|
||||||
);
|
|
||||||
assetentry_t *jsonEntry = assetLock(
|
|
||||||
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
|
|
||||||
);
|
|
||||||
errorret_t ret = assetRequireLoaded(jsonEntry);
|
|
||||||
if(errorIsNotOk(ret)) {
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorChain(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
|
||||||
|
|
||||||
yyjson_val *tilesVal = yyjson_obj_get(root, "tiles");
|
|
||||||
if(
|
|
||||||
!tilesVal || !yyjson_is_arr(tilesVal) ||
|
|
||||||
yyjson_arr_size(tilesVal) != CHUNK_TILE_COUNT
|
|
||||||
) {
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorThrow(
|
|
||||||
"Chunk JSON 'tiles' must have exactly %d entries",
|
|
||||||
CHUNK_TILE_COUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
out->tiles = memoryAllocate(CHUNK_TILE_COUNT * sizeof(tile_t));
|
|
||||||
size_t tileIdx, tileMax;
|
|
||||||
yyjson_val *tileVal;
|
|
||||||
yyjson_arr_foreach(tilesVal, tileIdx, tileMax, tileVal) {
|
|
||||||
yyjson_val *shapeVal = yyjson_arr_get(tileVal, 0);
|
|
||||||
yyjson_val *zVal = yyjson_arr_get(tileVal, 1);
|
|
||||||
if(
|
|
||||||
!yyjson_is_arr(tileVal) || yyjson_arr_size(tileVal) != 2 ||
|
|
||||||
!yyjson_is_int(shapeVal) || !yyjson_is_int(zVal)
|
|
||||||
) {
|
|
||||||
memoryFree(out->tiles);
|
|
||||||
out->tiles = NULL;
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorThrow("Chunk JSON tile entries must be [shape, z] arrays");
|
|
||||||
}
|
|
||||||
out->tiles[tileIdx] = (tile_t){
|
|
||||||
.shape = (tileshape_t)yyjson_get_int(shapeVal),
|
|
||||||
.z = (uint8_t)yyjson_get_int(zVal)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
yyjson_val *meshesVal = yyjson_obj_get(root, "meshes");
|
|
||||||
out->meshCount = 0;
|
|
||||||
if(meshesVal && yyjson_is_arr(meshesVal)) {
|
|
||||||
size_t meshCount = yyjson_arr_size(meshesVal);
|
|
||||||
if(meshCount > CHUNK_MESH_COUNT_MAX) {
|
|
||||||
memoryFree(out->tiles);
|
|
||||||
out->tiles = NULL;
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorThrow(
|
|
||||||
"Chunk JSON 'meshes' exceeds CHUNK_MESH_COUNT_MAX (%d)",
|
|
||||||
CHUNK_MESH_COUNT_MAX
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t meshIdx, meshMax;
|
|
||||||
yyjson_val *meshVal;
|
|
||||||
yyjson_arr_foreach(meshesVal, meshIdx, meshMax, meshVal) {
|
|
||||||
yyjson_val *modelVal = yyjson_obj_get(meshVal, "model");
|
|
||||||
if(!modelVal || !yyjson_is_str(modelVal)) {
|
|
||||||
memoryFree(out->tiles);
|
|
||||||
out->tiles = NULL;
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorThrow("Chunk JSON mesh entry missing 'model' string");
|
|
||||||
}
|
|
||||||
const char_t *modelStr = yyjson_get_str(modelVal);
|
|
||||||
size_t modelLen = yyjson_get_len(modelVal);
|
|
||||||
if(modelLen >= CHUNK_MESH_NAME_MAX) {
|
|
||||||
memoryFree(out->tiles);
|
|
||||||
out->tiles = NULL;
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorThrow(
|
|
||||||
"Chunk JSON model path '%s' exceeds max length", modelStr
|
|
||||||
);
|
|
||||||
}
|
|
||||||
memoryCopy(out->modelNames[meshIdx], modelStr, modelLen + 1);
|
|
||||||
|
|
||||||
vec3 offset = { 0.0f, 0.0f, 0.0f };
|
|
||||||
yyjson_val *offsetVal = yyjson_obj_get(meshVal, "offset");
|
|
||||||
if(
|
|
||||||
offsetVal && yyjson_is_arr(offsetVal) &&
|
|
||||||
yyjson_arr_size(offsetVal) == 3
|
|
||||||
) {
|
|
||||||
size_t offIdx, offMax;
|
|
||||||
yyjson_val *offElem;
|
|
||||||
yyjson_arr_foreach(offsetVal, offIdx, offMax, offElem) {
|
|
||||||
if(yyjson_is_num(offElem)) {
|
|
||||||
offset[offIdx] = (float_t)yyjson_get_num(offElem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
glm_vec3_copy(offset, out->meshOffsets[meshIdx]);
|
|
||||||
}
|
|
||||||
out->meshCount = (uint8_t)meshCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
assetUnlockEntry(jsonEntry);
|
|
||||||
|
|
||||||
if(out->meshCount == 0) {
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
|
||||||
loading->loading.chunk.modelIndex = 0;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,77 +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"
|
|
||||||
|
|
||||||
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_LOAD_JSON,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS
|
|
||||||
} assetchunkloadingstate_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
assetchunkloadingstate_t state;
|
|
||||||
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. No-op - the chunk's JSON file is
|
|
||||||
* loaded via a JSON sub-asset in the sync phase (see assetChunkLoaderSync),
|
|
||||||
* which handles its own async file I/O.
|
|
||||||
*
|
|
||||||
* @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. Locks and parses the chunk's JSON
|
|
||||||
* file (tiles plus referenced model paths/offsets), then locks each
|
|
||||||
* referenced model asset before marking the entry loaded.
|
|
||||||
*
|
|
||||||
* Expected JSON shape:
|
|
||||||
* {
|
|
||||||
* "tiles": [ [shape, z], ... ] // exactly CHUNK_TILE_COUNT entries,
|
|
||||||
* // one per (x, y) column, x-major
|
|
||||||
* "meshes": [
|
|
||||||
* { "model": "models/chunks/chunk_0_0_0_0.json",
|
|
||||||
* "offset": [0.0, 0.0, 0.0] }
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* "meshes" is optional; each entry's "offset" defaults to [0, 0, 0] if
|
|
||||||
* omitted. By convention mesh index 0 is the chunk's terrain and the
|
|
||||||
* rest are props (see sceneOverworldDrawChunksBase/Props).
|
|
||||||
*
|
|
||||||
* @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.
|
||||||
*
|
*
|
||||||
|
|||||||
+31
-16
@@ -10,17 +10,20 @@
|
|||||||
#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/item/item.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 "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;
|
||||||
|
|
||||||
@@ -38,14 +41,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(itemInit());
|
|
||||||
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");
|
||||||
|
|
||||||
@@ -55,8 +59,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();
|
||||||
}
|
}
|
||||||
@@ -65,16 +68,27 @@ errorret_t engineUpdate(void) {
|
|||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
|
||||||
consoleUpdate();
|
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
||||||
errorChain(rpgUpdate());
|
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
|
||||||
errorChain(sceneUpdate());
|
inputUpdate();
|
||||||
errorChain(assetUpdate());
|
consoleUpdate();
|
||||||
errorChain(uiUpdate());
|
|
||||||
|
errorChain(gameUpdate());
|
||||||
|
errorChain(moduleSceneUpdateCurrent());
|
||||||
|
errorChain(sceneUpdate());
|
||||||
|
errorChain(assetUpdate());
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,14 +97,15 @@ void engineExit(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
|
errorChain(gameDispose());
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
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
|
||||||
entityitem.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
|
||||||
|
);
|
||||||
@@ -6,5 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
log.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
-3
@@ -6,7 +6,6 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
battle.c
|
entitytrigger.c
|
||||||
battlefighter.c
|
entitytriggerevents.c
|
||||||
party.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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user