Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42cb84b610 | |||
| f8607d114c | |||
| 56230dd340 | |||
| df9fdf26c8 | |||
| 8f8fa8f8d1 | |||
| 07f98c119a | |||
| d49194fc4d | |||
| e61914bad4 | |||
| 015d90519f | |||
| 7b4f0abfb8 | |||
| 0667a44e14 | |||
| 0852b8463b | |||
| 1436fda858 | |||
| 0826908068 | |||
| 7ceb9e571d | |||
| 38c2f6b6f9 | |||
| b9f06fef7a | |||
| 373f1c1010 | |||
| e80d693d85 | |||
| ef60ddf6a8 | |||
| de0ff2e263 | |||
| 13a435e9c1 | |||
| 725338cd5a | |||
| acd14f46a8 | |||
| ef78f023ed | |||
| 85b6109792 | |||
| 6c5738c863 | |||
| 1174e471f6 | |||
| a618ff46fe | |||
| 1fa5cd316e | |||
| 11e5d37563 | |||
| 9f86c20129 | |||
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 | |||
| ca02ee0352 | |||
| fbaa54145e | |||
| 28754ffbf2 | |||
| 470c0eba7a | |||
| 7098dcec43 | |||
| 07137f57af | |||
| 8b7491a3d3 | |||
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 | |||
| a73f55beb0 | |||
| 0bd2491ab7 | |||
| 860c797c9c | |||
| 38c5080f9f | |||
| fae191d8fe | |||
| d83a953e2d | |||
| 2dcf0d0f0d | |||
| 6dee37d8c1 | |||
| 3bad03afb3 | |||
| 6a6d8448f7 | |||
| 189babd2cf | |||
| 988a0f2294 | |||
| 85bf455731 | |||
| 589e4224f3 | |||
| bd7fa154b4 | |||
| a292f1992b | |||
| 88ddf429b7 | |||
| 7a1f6662df | |||
| afc68bccc6 |
@@ -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.
|
||||||
@@ -4,36 +4,6 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
python3 \
|
|
||||||
python3-pip \
|
|
||||||
python3-polib \
|
|
||||||
python3-pil \
|
|
||||||
libsdl2-dev \
|
|
||||||
libgl1-mesa-dev \
|
|
||||||
libzip-dev \
|
|
||||||
python3-dotenv \
|
|
||||||
python3-pyqt5 \
|
|
||||||
python3-opengl \
|
|
||||||
xz-utils \
|
|
||||||
liblzma-dev \
|
|
||||||
libbz2-dev \
|
|
||||||
zlib1g-dev \
|
|
||||||
git \
|
|
||||||
libssl-dev
|
|
||||||
- name: Run tests
|
|
||||||
run: ./scripts/test-linux.sh
|
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
name: Test Dusk
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
cmake \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-polib \
|
||||||
|
python3-pil \
|
||||||
|
libsdl2-dev \
|
||||||
|
libgl1-mesa-dev \
|
||||||
|
libzip-dev \
|
||||||
|
python3-dotenv \
|
||||||
|
python3-pyqt5 \
|
||||||
|
python3-opengl \
|
||||||
|
xz-utils \
|
||||||
|
liblzma-dev \
|
||||||
|
libbz2-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
git \
|
||||||
|
libssl-dev
|
||||||
|
- name: Run tests
|
||||||
|
run: ./scripts/test-linux.sh
|
||||||
|
|
||||||
|
# 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.
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
# 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 16 (defined
|
||||||
|
constant, see SERVER_CLIENT_COUNT_MAX/CLIENT_COUNT_MAX in
|
||||||
|
src/dusk/network/socket/) 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.
+49
-43
@@ -10,51 +10,57 @@ msgid "ui.title"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Welcome"
|
"Welcome"
|
||||||
|
|
||||||
#: ui/user.c:22
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "ui.greeting"
|
msgid "ui.settings.tabs.general"
|
||||||
msgstr "Hello, %s!"
|
msgstr "General"
|
||||||
|
|
||||||
#: ui/files.c:40
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "ui.file_status"
|
msgid "ui.settings.tabs.input"
|
||||||
msgstr "%s has %d files."
|
msgstr "Input"
|
||||||
|
|
||||||
#: ui/cart.c:55
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "cart.item_count"
|
msgid "ui.settings.tabs.display"
|
||||||
msgid_plural "cart.item_count"
|
msgstr "Display"
|
||||||
msgstr[0] "%d item"
|
|
||||||
msgstr[1] "%d items (dual)"
|
|
||||||
msgstr[2] "%d items (few)"
|
|
||||||
msgstr[3] "%d items (many)"
|
|
||||||
|
|
||||||
#: ui/notifications.c:71
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid ""
|
msgid "ui.settings.tabs.audio"
|
||||||
"ui.multiline_help"
|
msgstr "Audio"
|
||||||
msgstr ""
|
|
||||||
"Line one of the help text.\n"
|
|
||||||
"Line two continues here.\n"
|
|
||||||
"Line three ends here."
|
|
||||||
|
|
||||||
#: ui/errors.c:90
|
msgid "ui.settings.input.deadzone"
|
||||||
msgid ""
|
msgstr "Deadzone"
|
||||||
"error.upload_failed.long"
|
|
||||||
msgstr ""
|
|
||||||
"Upload failed for file \"%s\".\n"
|
|
||||||
"Please try again later or contact support."
|
|
||||||
|
|
||||||
#: ui/messages.c:110
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
msgid ""
|
msgid "ui.settings.general.language"
|
||||||
"user.invite_status"
|
msgstr "Language"
|
||||||
msgid_plural ""
|
|
||||||
"user.invite_status"
|
msgid "ui.settings.general.language_detail"
|
||||||
msgstr[0] ""
|
msgstr "Takes effect after restarting the application."
|
||||||
"%s invited %d user.\n"
|
|
||||||
"Please review the request."
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgstr[1] ""
|
msgid "ui.settings.apply"
|
||||||
"%s invited %d users (dual).\n"
|
msgstr "Apply"
|
||||||
"Please review the requests."
|
|
||||||
msgstr[2] ""
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
"%s invited %d users (few).\n"
|
msgid "ui.confirm.discard_changes"
|
||||||
"Please review the requests."
|
msgstr "Discard unsaved changes?"
|
||||||
msgstr[3] ""
|
|
||||||
"%s invited %d users (many).\n"
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
"Please review the requests."
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "Characters"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "Items"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "Settings"
|
||||||
|
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "Potion"
|
||||||
|
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "Potato"
|
||||||
|
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "Apple"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ExampleApp 1.0\n"
|
||||||
|
"Language: es\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||||
|
|
||||||
|
#: ui/menu.c:10
|
||||||
|
msgid "ui.title"
|
||||||
|
msgstr ""
|
||||||
|
"Bienvenido"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.general"
|
||||||
|
msgstr "General"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.input"
|
||||||
|
msgstr "Entrada"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.display"
|
||||||
|
msgstr "Pantalla"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.audio"
|
||||||
|
msgstr "Audio"
|
||||||
|
|
||||||
|
msgid "ui.settings.input.deadzone"
|
||||||
|
msgstr "Deadzone"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language"
|
||||||
|
msgstr "Idioma"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language_detail"
|
||||||
|
msgstr "Se aplica después de reiniciar la aplicación."
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.apply"
|
||||||
|
msgstr "Aplicar"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
|
msgid "ui.confirm.discard_changes"
|
||||||
|
msgstr "¿Descartar los cambios no guardados?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "Personajes"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "Objetos"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "Configuración"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "Poción"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "Papa"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "Manzana"
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: ExampleApp 1.0\n"
|
||||||
|
"Language: ja\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||||
|
|
||||||
|
#: ui/menu.c:10
|
||||||
|
msgid "ui.title"
|
||||||
|
msgstr ""
|
||||||
|
"歓迎"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.general"
|
||||||
|
msgstr "一般"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.input"
|
||||||
|
msgstr "入力"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.display"
|
||||||
|
msgstr "表示"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.tabs.audio"
|
||||||
|
msgstr "オーディオ"
|
||||||
|
|
||||||
|
msgid "ui.settings.input.deadzone"
|
||||||
|
msgstr "デッドゾーン"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language"
|
||||||
|
msgstr "言語"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||||
|
msgid "ui.settings.general.language_detail"
|
||||||
|
msgstr "アプリケーションを再起動すると適用されます。"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
|
msgid "ui.settings.apply"
|
||||||
|
msgstr "適用"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
|
msgid "ui.confirm.discard_changes"
|
||||||
|
msgstr "未保存の変更を破棄しますか?"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.characters"
|
||||||
|
msgstr "キャラクター"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.items"
|
||||||
|
msgstr "アイテム"
|
||||||
|
|
||||||
|
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||||
|
msgid "ui.game_menu.settings"
|
||||||
|
msgstr "設定"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potion.name"
|
||||||
|
msgstr "ポーション"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.potato.name"
|
||||||
|
msgstr "ジャガイモ"
|
||||||
|
|
||||||
|
#: src/dusk/rpg/item/item.json
|
||||||
|
msgid "item.apple.name"
|
||||||
|
msgstr "リンゴ"
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Player: dynamic capsule body, moved relative to the camera by PLAYER's
|
||||||
|
// own update callback (see entityplayer.c).
|
||||||
|
class Player {
|
||||||
|
constructor() {
|
||||||
|
this.entity = new Entity();
|
||||||
|
|
||||||
|
this.position = this.entity.add(POSITION);
|
||||||
|
this.position.setLocalPosition(0.0, 2.0, 0.0);
|
||||||
|
|
||||||
|
this.physics = this.entity.add(PHYSICS);
|
||||||
|
this.physics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
||||||
|
|
||||||
|
this.renderable = this.entity.add(RENDERABLE);
|
||||||
|
this.renderable.setMesh(0, MESH_CAPSULE);
|
||||||
|
this.renderable.setColor(0, 0, 255, 255);
|
||||||
|
|
||||||
|
this.entity.add(PLAYER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Player;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Entry point run once at startup (see game.c). Swap which scene module
|
||||||
|
// gets passed to Scene.set() here to change what the game boots into.
|
||||||
|
var overworldScene = require('./overworldscene.js');
|
||||||
|
Scene.set(overworldScene);
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (c) 2026 Dominic Masters
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
|
||||||
|
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
|
||||||
|
// puts parts of the floor's own geometry near/behind the camera's view
|
||||||
|
// direction, which the PSP's legacy GU pipeline can't clip properly and
|
||||||
|
// drops the whole triangle instead of clipping it.
|
||||||
|
var CAMERA_ORBIT_RADIUS = 18.0;
|
||||||
|
var CAMERA_ORBIT_HEIGHT = 10.0;
|
||||||
|
var CAMERA_ORBIT_SPEED = 0.5;
|
||||||
|
|
||||||
|
var Player = require('./Player.js');
|
||||||
|
|
||||||
|
var cameraOrbitAngle = 0.0;
|
||||||
|
var cameraPosition = null;
|
||||||
|
|
||||||
|
// Orbits the camera around the world origin at a fixed radius/height/
|
||||||
|
// speed, always looking back at the origin. Called once up front (so the
|
||||||
|
// very first rendered frame is already positioned correctly) and then
|
||||||
|
// once per frame via update() below.
|
||||||
|
function updateCameraOrbit() {
|
||||||
|
cameraOrbitAngle += Time.delta * CAMERA_ORBIT_SPEED;
|
||||||
|
|
||||||
|
var eyeX = Math.cos(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
|
||||||
|
var eyeY = CAMERA_ORBIT_HEIGHT;
|
||||||
|
var eyeZ = Math.sin(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
|
||||||
|
|
||||||
|
cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Called once by Scene.set(), right after it creates and activates the
|
||||||
|
// scene this module owns.
|
||||||
|
init: function() {
|
||||||
|
// Camera, orbiting the origin (see updateCameraOrbit above).
|
||||||
|
var camera = new Entity();
|
||||||
|
cameraPosition = camera.add(POSITION);
|
||||||
|
camera.add(CAMERA);
|
||||||
|
updateCameraOrbit();
|
||||||
|
|
||||||
|
// Static ground plane. Physics ignores the entity's position
|
||||||
|
// component -- the shape's own normal/distance fully define the
|
||||||
|
// plane in world space.
|
||||||
|
var plane = new Entity();
|
||||||
|
var planePosition = plane.add(POSITION);
|
||||||
|
planePosition.setLocalPosition(-10.0, 0.0, -10.0);
|
||||||
|
planePosition.setLocalScale(20.0, 1.0, 20.0);
|
||||||
|
|
||||||
|
var planePhysics = plane.add(PHYSICS);
|
||||||
|
planePhysics.setBodyType(PHYSICS_BODY_STATIC);
|
||||||
|
planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
var planeRenderable = plane.add(RENDERABLE);
|
||||||
|
planeRenderable.setMesh(0, MESH_PLANE);
|
||||||
|
planeRenderable.setColor(128, 128, 128, 255);
|
||||||
|
|
||||||
|
new Player();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called once per engine frame while this module is the active scene
|
||||||
|
// (see Scene.set(), engineUpdate() -> moduleSceneUpdateCurrent()).
|
||||||
|
update: function() {
|
||||||
|
if(cameraPosition) updateCameraOrbit();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called once by Scene.set() when this module is replaced by another,
|
||||||
|
// right before the scene it owns is destroyed.
|
||||||
|
dispose: function() {
|
||||||
|
cameraPosition = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Turn things off we don't need
|
||||||
|
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JERRY_EXT ON CACHE BOOL "" FORCE)
|
||||||
|
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
|
||||||
|
|
||||||
|
# Fetch Jerry
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
jerryscript
|
||||||
|
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
|
||||||
|
GIT_TAG float32-fix
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(jerryscript)
|
||||||
|
|
||||||
|
# Mark found
|
||||||
|
set(jerryscript_FOUND ON)
|
||||||
|
|
||||||
|
# Define targets
|
||||||
|
if(TARGET jerryscript-core)
|
||||||
|
set(JERRY_CORE_TARGET jerryscript-core)
|
||||||
|
elseif(TARGET jerry-core)
|
||||||
|
set(JERRY_CORE_TARGET jerry-core)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET jerryscript-ext)
|
||||||
|
set(JERRY_EXT_TARGET jerryscript-ext)
|
||||||
|
elseif(TARGET jerry-ext)
|
||||||
|
set(JERRY_EXT_TARGET jerry-ext)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET jerryscript-port-default)
|
||||||
|
set(JERRY_PORT_TARGET jerryscript-port-default)
|
||||||
|
elseif(TARGET jerry-port-default)
|
||||||
|
set(JERRY_PORT_TARGET jerry-port-default)
|
||||||
|
elseif(TARGET jerryscript-port)
|
||||||
|
set(JERRY_PORT_TARGET jerryscript-port)
|
||||||
|
elseif(TARGET jerry-port)
|
||||||
|
set(JERRY_PORT_TARGET jerry-port)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT JERRY_CORE_TARGET)
|
||||||
|
message(FATAL_ERROR "JerryScript core target not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT JERRY_EXT_TARGET)
|
||||||
|
message(FATAL_ERROR "JerryScript ext target not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT JERRY_PORT_TARGET)
|
||||||
|
message(FATAL_ERROR "JerryScript port target not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach(tgt IN ITEMS
|
||||||
|
${JERRY_CORE_TARGET}
|
||||||
|
${JERRY_EXT_TARGET}
|
||||||
|
${JERRY_PORT_TARGET}
|
||||||
|
)
|
||||||
|
if(TARGET ${tgt})
|
||||||
|
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
|
||||||
|
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
|
||||||
|
JERRY_NUMBER_TYPE_FLOAT64=0
|
||||||
|
JERRY_BUILTIN_DATE=0
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# Export include dirs through the targets
|
||||||
|
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
|
||||||
|
${jerryscript_SOURCE_DIR}/jerry-core/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
|
||||||
|
${jerryscript_SOURCE_DIR}/jerry-ext/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
|
||||||
|
${jerryscript_SOURCE_DIR}/jerry-port/default/include
|
||||||
|
)
|
||||||
|
|
||||||
|
# Suppress JerryScript-only warning
|
||||||
|
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
|
||||||
|
-Wno-error
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
|
||||||
|
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
|
||||||
|
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
|
||||||
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DOL=1
|
DOL=1
|
||||||
ISO=2
|
ISO=2
|
||||||
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
|
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
|
||||||
|
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
|
||||||
|
# like every other target instead of relying on endian.h's runtime probe.
|
||||||
|
DUSK_PLATFORM_ENDIAN_BIG
|
||||||
)
|
)
|
||||||
|
|
||||||
# Custom compiler flags
|
# Custom compiler flags
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
# DUSK_OPENGL_LEGACY
|
# DUSK_OPENGL_LEGACY
|
||||||
DUSK_LINUX
|
DUSK_LINUX
|
||||||
DUSK_DISPLAY_SIZE_DYNAMIC
|
DUSK_DISPLAY_SIZE_DYNAMIC
|
||||||
DUSK_DISPLAY_WIDTH_DEFAULT=640
|
DUSK_DISPLAY_WIDTH_DEFAULT=854
|
||||||
DUSK_DISPLAY_HEIGHT_DEFAULT=480
|
DUSK_DISPLAY_HEIGHT_DEFAULT=480
|
||||||
DUSK_DISPLAY_SCREEN_HEIGHT=240
|
DUSK_DISPLAY_SCREEN_HEIGHT=240
|
||||||
DUSK_INPUT_KEYBOARD
|
DUSK_INPUT_KEYBOARD
|
||||||
|
|||||||
@@ -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,15 +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
|
||||||
)
|
)
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
+48
-57
@@ -59,20 +59,28 @@ assetentry_t * assetGetEntry(
|
|||||||
entry++;
|
entry++;
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||||
|
|
||||||
// We did not find one existing, Find first available slot.
|
// We did not find one existing. Find first available slot, reaping
|
||||||
entry = ASSET.entries;
|
// zero-ref entries to make room if none are immediately available.
|
||||||
do {
|
bool_t reaped = false;
|
||||||
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
for(;;) {
|
||||||
entry++;
|
entry = ASSET.entries;
|
||||||
continue;
|
do {
|
||||||
}
|
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
||||||
|
entry++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
||||||
assetEntryInit(entry, name, type, input);
|
assetEntryInit(entry, name, type, input);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
entry++;
|
entry++;
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||||
|
|
||||||
|
if(reaped) break;
|
||||||
|
reaped = true;
|
||||||
|
errorCatch(assetReapUnused());
|
||||||
|
}
|
||||||
|
|
||||||
assertUnreachable("No available asset entry slots.");
|
assertUnreachable("No available asset entry slots.");
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
|
|||||||
assetEntryUnlock(entry);
|
assetEntryUnlock(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t assetReapUnused(void) {
|
||||||
|
assertIsMainThread("assetReapUnused must be called from the main thread.");
|
||||||
|
|
||||||
|
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
|
||||||
|
// This handles dependency chains where an entry (e.g. a model) holds refs
|
||||||
|
// on child entries (mesh, texture): dispose parents first so child ref
|
||||||
|
// counts drop to zero, then pick up the children on the next pass. Without
|
||||||
|
// this, a forward-only scan fails when a shared child entry appears before
|
||||||
|
// a parent that still holds a ref to it.
|
||||||
|
bool_t any;
|
||||||
|
do {
|
||||||
|
any = false;
|
||||||
|
assetentry_t *entry = ASSET.entries;
|
||||||
|
do {
|
||||||
|
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
|
||||||
|
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
|
||||||
|
if(entry->refs.count > 0) { entry++; continue; }
|
||||||
|
errorChain(assetEntryDispose(entry));
|
||||||
|
any = true;
|
||||||
|
entry++;
|
||||||
|
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||||
|
} while(any);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t assetUpdate(void) {
|
errorret_t assetUpdate(void) {
|
||||||
assertIsMainThread("assetUpdate must be called from the main thread.");
|
assertIsMainThread("assetUpdate must be called from the main thread.");
|
||||||
|
|
||||||
@@ -324,30 +358,6 @@ errorret_t assetUpdate(void) {
|
|||||||
}
|
}
|
||||||
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
||||||
|
|
||||||
|
|
||||||
// Reap unused entries.
|
|
||||||
entry = ASSET.entries;
|
|
||||||
do {
|
|
||||||
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
entry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
|
||||||
entry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(entry->refs.count > 0) {
|
|
||||||
entry++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// consolePrint("Reaping asset %s", entry->name);
|
|
||||||
errorChain(assetEntryDispose(entry));
|
|
||||||
entry++;
|
|
||||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,26 +423,7 @@ errorret_t assetDispose(void) {
|
|||||||
assertIsMainThread("Must be called from the main thread.");
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
threadStop(&ASSET.loadThread);
|
threadStop(&ASSET.loadThread);
|
||||||
|
|
||||||
// Drain-dispose: repeatedly find and dispose zero-ref LOADED entries
|
errorChain(assetReapUnused());
|
||||||
// until none remain. This handles dependency chains where an entry
|
|
||||||
// (e.g. a model) holds refs on child entries (mesh, texture): dispose
|
|
||||||
// parents first so child ref counts drop to zero, then pick up the
|
|
||||||
// children on the next pass. Without this, a forward-only scan fails
|
|
||||||
// when a shared child entry appears before a parent that still holds a
|
|
||||||
// ref to it.
|
|
||||||
bool_t any;
|
|
||||||
do {
|
|
||||||
any = false;
|
|
||||||
assetentry_t *e = ASSET.entries;
|
|
||||||
do {
|
|
||||||
if(e->type == ASSET_LOADER_TYPE_NULL) { e++; continue; }
|
|
||||||
if(e->state != ASSET_ENTRY_STATE_LOADED) { e++; continue; }
|
|
||||||
if(e->refs.count > 0) { e++; continue; }
|
|
||||||
errorChain(assetEntryDispose(e));
|
|
||||||
any = true;
|
|
||||||
e++;
|
|
||||||
} while(e < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
|
||||||
} while(any);
|
|
||||||
|
|
||||||
// Cleanup zip file.
|
// Cleanup zip file.
|
||||||
if(ASSET.zip != NULL) {
|
if(ASSET.zip != NULL) {
|
||||||
|
|||||||
+12
-2
@@ -23,8 +23,8 @@
|
|||||||
#define ASSET_FILE_NAME "dusk.dsk"
|
#define ASSET_FILE_NAME "dusk.dsk"
|
||||||
#define ASSET_HEADER_SIZE 3
|
#define ASSET_HEADER_SIZE 3
|
||||||
|
|
||||||
#define ASSET_LOADING_COUNT_MAX 20
|
#define ASSET_LOADING_COUNT_MAX 10
|
||||||
#define ASSET_ENTRY_COUNT_MAX 128
|
#define ASSET_ENTRY_COUNT_MAX 64
|
||||||
|
|
||||||
typedef struct asset_s {
|
typedef struct asset_s {
|
||||||
zip_t *zip;
|
zip_t *zip;
|
||||||
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
|
|||||||
*/
|
*/
|
||||||
void assetUnlockEntry(assetentry_t *entry);
|
void assetUnlockEntry(assetentry_t *entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
|
||||||
|
* until a full pass frees nothing further, since disposing a parent entry
|
||||||
|
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
|
||||||
|
* making it eligible for reaping too.
|
||||||
|
*
|
||||||
|
* @return An error code if any entry could not be disposed properly.
|
||||||
|
*/
|
||||||
|
errorret_t assetReapUnused(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requires an asset entry to be loaded. This will block until the asset entry
|
* Requires an asset entry to be loaded. This will block until the asset entry
|
||||||
* is fully loaded.
|
* is fully loaded.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
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,166 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "assetchunkloader.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/endian.h"
|
|
||||||
#include "asset/loader/assetloading.h"
|
|
||||||
#include "asset/loader/assetentry.h"
|
|
||||||
#include "asset/loader/assetloader.h"
|
|
||||||
#include "asset/asset.h"
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertNotMainThread("Should be called from an async thread.");
|
|
||||||
|
|
||||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
|
||||||
|
|
||||||
assetfile_t *file = &loading->loading.chunk.file;
|
|
||||||
assetLoaderErrorChain(loading,
|
|
||||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
|
||||||
);
|
|
||||||
|
|
||||||
uint8_t *data = memoryAllocate(file->size);
|
|
||||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
|
||||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
|
||||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
|
||||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
|
||||||
assertTrue(
|
|
||||||
file->lastRead == file->size,
|
|
||||||
"Failed to read entire chunk file."
|
|
||||||
);
|
|
||||||
|
|
||||||
loading->loading.chunk.data = data;
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
|
||||||
assertIsMainThread("Must be called from the main thread.");
|
|
||||||
|
|
||||||
assetchunkoutput_t *out = &loading->entry->data.chunk;
|
|
||||||
|
|
||||||
switch(loading->loading.chunk.state) {
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
|
||||||
errorOk();
|
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
|
||||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
|
||||||
uint8_t m = loading->loading.chunk.modelIndex;
|
|
||||||
if(out->modelEntries[m] == NULL) {
|
|
||||||
out->modelEntries[m] = assetLock(
|
|
||||||
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
|
|
||||||
);
|
|
||||||
assertNotNull(
|
|
||||||
out->modelEntries[m], "Failed to lock model."
|
|
||||||
);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
if(out->modelEntries[m]->state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
assetLoaderErrorThrow(
|
|
||||||
loading, "Model failed to load: %s", out->modelNames[m]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if(out->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
loading->loading.chunk.modelIndex++;
|
|
||||||
}
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
||||||
errorOk();
|
|
||||||
|
|
||||||
default:
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t *data = loading->loading.chunk.data;
|
|
||||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
|
||||||
|
|
||||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
|
||||||
memoryFree(data);
|
|
||||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
|
||||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
|
||||||
memoryFree(data);
|
|
||||||
assetLoaderErrorThrow(
|
|
||||||
loading, "Unsupported chunk file version %u", version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t offset = 8;
|
|
||||||
|
|
||||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
|
||||||
memoryCopy(out->tiles, data + offset, tileSize);
|
|
||||||
offset += tileSize;
|
|
||||||
|
|
||||||
out->meshCount = data[offset];
|
|
||||||
offset += sizeof(uint8_t);
|
|
||||||
assertTrue(
|
|
||||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
|
||||||
"Chunk mesh count exceeds maximum."
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
|
||||||
uint8_t nameLen = 0;
|
|
||||||
while(
|
|
||||||
data[offset + nameLen] != '\0' &&
|
|
||||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
|
||||||
) {
|
|
||||||
nameLen++;
|
|
||||||
}
|
|
||||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
|
||||||
out->modelNames[m][nameLen] = '\0';
|
|
||||||
offset += nameLen + 1;
|
|
||||||
|
|
||||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
|
||||||
offset += sizeof(vec3);
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryFree(data);
|
|
||||||
loading->loading.chunk.data = NULL;
|
|
||||||
|
|
||||||
if(out->meshCount == 0) {
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
|
||||||
loading->loading.chunk.modelIndex = 0;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
|
||||||
assertNotNull(entry, "Entry cannot be NULL");
|
|
||||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
|
||||||
assertIsMainThread("Must be called from the main thread.");
|
|
||||||
|
|
||||||
assetchunkoutput_t *out = &entry->data.chunk;
|
|
||||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
|
||||||
if(out->modelEntries[m] == NULL) continue;
|
|
||||||
assetUnlockEntry(out->modelEntries[m]);
|
|
||||||
out->modelEntries[m] = NULL;
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "asset/assetfile.h"
|
|
||||||
#include "rpg/overworld/chunk.h"
|
|
||||||
|
|
||||||
#define ASSET_CHUNK_FILE_VERSION 3
|
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
void *nothing;
|
|
||||||
} assetchunkloaderinput_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
|
||||||
ASSET_CHUNK_LOADING_STATE_DONE
|
|
||||||
} assetchunkloadingstate_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
assetfile_t file;
|
|
||||||
assetchunkloadingstate_t state;
|
|
||||||
uint8_t *data;
|
|
||||||
uint8_t modelIndex;
|
|
||||||
} assetchunkloaderloading_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
tile_t tiles[CHUNK_TILE_COUNT];
|
|
||||||
uint8_t meshCount;
|
|
||||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
|
||||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
|
||||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
|
||||||
} assetchunkoutput_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
|
||||||
* the loading buffer so the sync phase can parse without blocking the
|
|
||||||
* main thread on I/O.
|
|
||||||
*
|
|
||||||
* @param loading Loading information for the asset being loaded.
|
|
||||||
* @return Error code indicating success or failure of the load operation.
|
|
||||||
*/
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
|
||||||
* read by the async phase and populates the output assetchunkoutput_t with
|
|
||||||
* tile data and model paths.
|
|
||||||
*
|
|
||||||
* @param loading Loading information for the asset being loaded.
|
|
||||||
* @return Error code indicating success or failure of the load operation.
|
|
||||||
*/
|
|
||||||
errorret_t assetChunkLoaderSync(assetloading_t *loading);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposer for chunk assets.
|
|
||||||
*
|
|
||||||
* @param entry Asset entry containing the chunk data to dispose.
|
|
||||||
* @return Error code indicating success or failure of the dispose operation.
|
|
||||||
*/
|
|
||||||
errorret_t assetChunkDispose(assetentry_t *entry);
|
|
||||||
@@ -48,8 +48,20 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
|
|||||||
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
|
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
|
||||||
meshvertex_t *vertices = NULL;
|
meshvertex_t *vertices = NULL;
|
||||||
if(vertCount > 0) {
|
if(vertCount > 0) {
|
||||||
vertices = memoryAllocate(vertCount * sizeof(meshvertex_t));
|
// 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
|
||||||
|
// require this for the DMA'd vertex data to actually reach the GPU
|
||||||
|
// coherently. Static compiled-in vertex arrays happen to get this from
|
||||||
|
// the linker; a plain memoryAllocate here would not.
|
||||||
|
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
|
||||||
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
|
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
|
||||||
|
|
||||||
|
for(uint32_t v = 0; v < vertCount; v++) {
|
||||||
|
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
|
||||||
|
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
|
||||||
|
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
|
||||||
|
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
|
||||||
|
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
memoryFree(raw);
|
memoryFree(raw);
|
||||||
|
|
||||||
@@ -103,22 +115,18 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
errorChain(ret);
|
||||||
}
|
}
|
||||||
|
out->meshInitialized = true;
|
||||||
|
|
||||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||||
if(errorIsNotOk(ret)) {
|
if(errorIsNotOk(ret)) {
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
meshDispose(&out->mesh);
|
meshDispose(&out->mesh);
|
||||||
|
out->meshInitialized = false;
|
||||||
memoryFree(out->vertices);
|
memoryFree(out->vertices);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
errorChain(ret);
|
errorChain(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef DUSK_OPENGL_LEGACY
|
|
||||||
// VBO owns the data now; CPU copy is no longer needed.
|
|
||||||
memoryFree(out->vertices);
|
|
||||||
out->vertices = NULL;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -129,8 +137,11 @@ errorret_t assetMeshDispose(assetentry_t *entry) {
|
|||||||
assertIsMainThread("Must be called from the main thread.");
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
assetmeshoutput_t *out = &entry->data.mesh;
|
assetmeshoutput_t *out = &entry->data.mesh;
|
||||||
if(out->vertices != NULL) {
|
if(out->meshInitialized) {
|
||||||
errorChain(meshDispose(&out->mesh));
|
errorChain(meshDispose(&out->mesh));
|
||||||
|
out->meshInitialized = false;
|
||||||
|
}
|
||||||
|
if(out->vertices != NULL) {
|
||||||
memoryFree(out->vertices);
|
memoryFree(out->vertices);
|
||||||
out->vertices = NULL;
|
out->vertices = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ typedef struct {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
mesh_t mesh;
|
mesh_t mesh;
|
||||||
|
bool_t meshInitialized;
|
||||||
meshvertex_t *vertices;
|
meshvertex_t *vertices;
|
||||||
} assetmeshoutput_t;
|
} assetmeshoutput_t;
|
||||||
|
|
||||||
|
|||||||
@@ -506,12 +506,17 @@ errorret_t assetLocaleGetString(
|
|||||||
sizeof(lineBuffer)
|
sizeof(lineBuffer)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Prime the reader with the first line before scanning; outBuffer holds
|
||||||
|
// uninitialized memory until the first Next() call fills it.
|
||||||
|
errorChain(assetFileLineReaderNext(&reader));
|
||||||
|
|
||||||
// Skip blanks, comments, etc and start looking for msgid's
|
// Skip blanks, comments, etc and start looking for msgid's
|
||||||
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
||||||
|
|
||||||
while(!reader.eof) {
|
while(true) {
|
||||||
// Is this msgid?
|
// Is this msgid?
|
||||||
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
|
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
|
||||||
|
if(reader.eof) break;
|
||||||
errorChain(assetFileLineReaderNext(&reader));
|
errorChain(assetFileLineReaderNext(&reader));
|
||||||
msgidBuffer[0] = '\0';
|
msgidBuffer[0] = '\0';
|
||||||
continue;
|
continue;
|
||||||
@@ -535,7 +540,7 @@ errorret_t assetLocaleGetString(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We are either going to see a msgstr or a msgid_plural
|
// We are either going to see a msgstr or a msgid_plural
|
||||||
while(!reader.eof) {
|
while(true) {
|
||||||
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
||||||
|
|
||||||
// Is msgid_plural?
|
// Is msgid_plural?
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ console_t CONSOLE;
|
|||||||
|
|
||||||
void consoleInit(void) {
|
void consoleInit(void) {
|
||||||
memoryZero(&CONSOLE, sizeof(console_t));
|
memoryZero(&CONSOLE, sizeof(console_t));
|
||||||
CONSOLE.visible = true;
|
CONSOLE.visible = false;
|
||||||
|
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexInit(&CONSOLE.printMutex);
|
threadMutexInit(&CONSOLE.printMutex);
|
||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
+41
-14
@@ -10,16 +10,23 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "rpg/rpg.h"
|
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
#include "script/scriptmanager.h"
|
||||||
|
#include "script/module/scene/modulescene.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
|
#include "network/socket/client/client.h"
|
||||||
|
#include "network/socket/client/clientroster.h"
|
||||||
|
#include "network/socket/server/serveronline.h"
|
||||||
|
#include "game/game.h"
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "save/savesettings.h"
|
||||||
|
#include "log/log.h"
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -37,13 +44,15 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(systemInit());
|
errorChain(systemInit());
|
||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
// errorChain(saveInit());
|
errorChain(scriptManagerInit());
|
||||||
|
errorChain(saveInit());
|
||||||
|
errorChain(saveSettingsLoad());
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
errorChain(rpgInit());
|
|
||||||
errorChain(networkInit());
|
errorChain(networkInit());
|
||||||
errorChain(sceneInit());
|
errorChain(sceneInit());
|
||||||
|
errorChain(gameInit());
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
|
|
||||||
@@ -53,8 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
consolePrint("Assertions real");
|
consolePrint("Assertions real");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
// sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -62,17 +70,30 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
|
errorChain(clientUpdate());
|
||||||
|
errorChain(serverOnlineUpdate());
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,14 +102,20 @@ void engineExit(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
|
errorChain(gameDispose());
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
|
|
||||||
|
errorChain(serverOnlineStop());
|
||||||
|
client_t *localClient = clientRosterFindLocal();
|
||||||
|
if(localClient) localClientDisconnect(&localClient->local);
|
||||||
|
|
||||||
errorChain(networkDispose());
|
errorChain(networkDispose());
|
||||||
errorChain(rpgDispose());
|
|
||||||
localeManagerDispose();
|
localeManagerDispose();
|
||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
// errorChain(saveDispose());
|
errorChain(saveDispose());
|
||||||
|
errorChain(scriptManagerDispose());
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ errorret_t engineUpdate(void);
|
|||||||
* Shuts down the engine.
|
* Shuts down the engine.
|
||||||
*/
|
*/
|
||||||
errorret_t engineDispose(void);
|
errorret_t engineDispose(void);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
entity.c
|
||||||
|
entitymanager.c
|
||||||
|
component.c
|
||||||
|
entityprefab.c
|
||||||
|
)
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(component)
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitymanager.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
||||||
|
[COMPONENT_TYPE_NULL] = { 0 },
|
||||||
|
|
||||||
|
#define X(enm, type, field, iMethod, dMethod, rMethod) \
|
||||||
|
[COMPONENT_TYPE_##enm] = { \
|
||||||
|
.enumName = #enm, \
|
||||||
|
.name = #field, \
|
||||||
|
.init = iMethod, \
|
||||||
|
.dispose = dMethod, \
|
||||||
|
.render = rMethod \
|
||||||
|
},
|
||||||
|
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
[COMPONENT_TYPE_COUNT] = { 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
void componentInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &mgr->components[index];
|
||||||
|
memoryZero(cmp, sizeof(component_t));
|
||||||
|
|
||||||
|
cmp->type = type;
|
||||||
|
if(COMPONENT_DEFINITIONS[type].init) {
|
||||||
|
COMPONENT_DEFINITIONS[type].init(mgr, entityId, componentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void * componentGetData(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &mgr->components[index];
|
||||||
|
assertTrue(cmp->type == type, "Component type mismatch");
|
||||||
|
|
||||||
|
return &cmp->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentindex_t componentGetIndex(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const componenttype_t type,
|
||||||
|
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||||
|
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
|
||||||
|
assertNotNull(outEntities, "Output entities array cannot be null");
|
||||||
|
assertNotNull(outComponents, "Output components array cannot be null");
|
||||||
|
|
||||||
|
entityid_t written = 0;
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
componentid_t used = mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + i
|
||||||
|
];
|
||||||
|
if(used == COMPONENT_ID_INVALID) continue;
|
||||||
|
assertTrue(
|
||||||
|
mgr->components[componentGetIndex(i, used)].type == type,
|
||||||
|
"Component type mismatch in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
(mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0,
|
||||||
|
"Inactive entity in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
used < ENTITY_COMPONENT_COUNT_MAX,
|
||||||
|
"Component ID OOB in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
|
||||||
|
"Component index OOB in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
outComponents[written] = used;
|
||||||
|
outEntities[written++] = i;
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t componentRenderAll(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
for(entityid_t eid = 0; eid < ENTITY_COUNT_MAX; eid++) {
|
||||||
|
if(!(mgr->entities[eid].state & ENTITY_STATE_ACTIVE)) continue;
|
||||||
|
for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) {
|
||||||
|
component_t *cmp = &mgr->components[componentGetIndex(eid, cid)];
|
||||||
|
if(cmp->type == COMPONENT_TYPE_NULL) continue;
|
||||||
|
if(!COMPONENT_DEFINITIONS[cmp->type].render) continue;
|
||||||
|
errorChain(COMPONENT_DEFINITIONS[cmp->type].render(mgr, eid, cid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void componentDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &mgr->components[index];
|
||||||
|
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
||||||
|
|
||||||
|
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
||||||
|
COMPONENT_DEFINITIONS[cmp->type].dispose(mgr, entityId, componentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp->type = COMPONENT_TYPE_NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define X(enumName, type, field, init, dispose, render) \
|
||||||
|
// do nothing
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
#define X(enumName, type, field, init, dispose, render) type field;
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
} componentdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback signature for a component's init/dispose hooks.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
typedef void (*componentcallback_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback signature for a component's render hook.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
typedef errorret_t (*componentcallbackerror_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char_t *enumName;
|
||||||
|
const char_t *name;
|
||||||
|
componentcallback_t init;
|
||||||
|
componentcallback_t dispose;
|
||||||
|
componentcallbackerror_t render;
|
||||||
|
} componentdefinition_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
COMPONENT_TYPE_NULL,
|
||||||
|
|
||||||
|
#define X(enumName, type, field, init, dispose, render) \
|
||||||
|
COMPONENT_TYPE_##enumName,
|
||||||
|
#include "componentlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
COMPONENT_TYPE_COUNT
|
||||||
|
} componenttype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
componenttype_t type;
|
||||||
|
componentdata_t data;
|
||||||
|
} component_t;
|
||||||
|
|
||||||
|
extern componentdefinition_t COMPONENT_DEFINITIONS[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a component of the given type for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param type The type of the component to initialize.
|
||||||
|
*/
|
||||||
|
void componentInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the pointer to the data of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param type The type of the component to get, only used for assertion.
|
||||||
|
* @return A pointer to the component data.
|
||||||
|
*/
|
||||||
|
void * componentGetData(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The index of the component in the component array.
|
||||||
|
*/
|
||||||
|
componentindex_t componentGetIndex(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity IDs of all entities with a component of the given type.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to search.
|
||||||
|
* @param type The type of the component to get entities for.
|
||||||
|
* @param outEntities An array to write the entity IDs to, must be at least
|
||||||
|
* ENTITY_COUNT_MAX in size.
|
||||||
|
* @param outComponents An array to write the component IDs to.
|
||||||
|
* @return The number of entity IDs written to outEntities.
|
||||||
|
*/
|
||||||
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const componenttype_t type,
|
||||||
|
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||||
|
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void componentDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the render callback on every active component that defines one.
|
||||||
|
* Iterates all active entities and all their component slots. No-op for
|
||||||
|
* components whose definition has render == NULL.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to render.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t componentRenderAll(entitymanager_t *mgr);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(display)
|
||||||
|
add_subdirectory(physics)
|
||||||
|
add_subdirectory(trigger)
|
||||||
|
add_subdirectory(animation)
|
||||||
+1
-1
@@ -6,5 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
assetvita.c
|
entityanimation.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityanimation.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
void entityAnimationInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
memoryZero(animComp, sizeof(entityanimation_t));
|
||||||
|
|
||||||
|
entityUpdateAdd(mgr, entityId, entityAnimationUpdate, componentId, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityanimation_t *entityAnimationGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_ANIMATION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationSetKeyframes(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
keyframe_t **channelTracks,
|
||||||
|
uint16_t *channelTrackCounts,
|
||||||
|
const uint16_t channelCount
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animationInit(
|
||||||
|
&animComp->anim, channelTracks, channelTrackCounts, channelCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationPlay(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.time = 0.0f;
|
||||||
|
animComp->anim.playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationStop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t entityAnimationIsPlaying(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
return animComp->anim.playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationSetLoop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const bool_t loop
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationSetSpeed(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const float_t speed
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animComp->anim.speed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
float_t entityAnimationGetValue(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint16_t channelIndex
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
return animationGetValue(&animComp->anim, channelIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityAnimationUpdate(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
||||||
|
animationUpdate(&animComp->anim);
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "animation/animation.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
animation_t anim;
|
||||||
|
} entityanimation_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the animation component: no keyframes set, and registers
|
||||||
|
* entityAnimationUpdate as an update callback. Call
|
||||||
|
* entityAnimationSetKeyframes() before entityAnimationPlay().
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityAnimationInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying animation structure (temporarily) for the given
|
||||||
|
* entity. Prefer the dedicated getters/setters where possible.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The animation component data for the given entity and
|
||||||
|
* component ID.
|
||||||
|
*/
|
||||||
|
entityanimation_t *entityAnimationGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the entity's keyframes, stopped, at speed 1.0, non-looping. See
|
||||||
|
* keyframeSetInit() -- channelTracks/channelTrackCounts and the
|
||||||
|
* keyframe_t arrays they point to are not copied, and must outlive this
|
||||||
|
* component (e.g. static/const arrays owned by the caller).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param channelTracks Array of channelCount keyframe_t arrays -- one per
|
||||||
|
* animated channel (e.g. position.x/y/z), sharing this animation's
|
||||||
|
* timeline.
|
||||||
|
* @param channelTrackCounts Array of channelCount keyframe counts,
|
||||||
|
* matching channelTracks.
|
||||||
|
* @param channelCount The number of channels.
|
||||||
|
*/
|
||||||
|
void entityAnimationSetKeyframes(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
keyframe_t **channelTracks,
|
||||||
|
uint16_t *channelTrackCounts,
|
||||||
|
const uint16_t channelCount
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts (or restarts) playback from time 0.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityAnimationPlay(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops playback without resetting the current time.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityAnimationStop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the animation is currently playing.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return True if playing.
|
||||||
|
*/
|
||||||
|
bool_t entityAnimationIsPlaying(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the animation loops on reaching its final keyframe, rather
|
||||||
|
* than stopping there.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param loop True to loop, false to stop at the end.
|
||||||
|
*/
|
||||||
|
void entityAnimationSetLoop(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const bool_t loop
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the animation's playback rate multiplier.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param speed The new playback rate; 1.0 = normal speed.
|
||||||
|
*/
|
||||||
|
void entityAnimationSetSpeed(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const float_t speed
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates one of the animation's channels at its current playback
|
||||||
|
* time, regardless of whether it's currently playing.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param channelIndex The channel to evaluate, in [0, channelCount) as
|
||||||
|
* passed to entityAnimationSetKeyframes().
|
||||||
|
* @return That channel's value at the current time.
|
||||||
|
*/
|
||||||
|
float_t entityAnimationGetValue(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint16_t channelIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-tick update for the animation component: calls animationUpdate()
|
||||||
|
* (a no-op if not playing). Registered automatically as an update
|
||||||
|
* callback by entityAnimationInit.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void entityAnimationUpdate(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
+3
-4
@@ -6,8 +6,7 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
npc.c
|
entityposition.c
|
||||||
npcturn.c
|
entitycamera.c
|
||||||
npcwalk.c
|
entityrenderable.c
|
||||||
npcpath.c
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "entity/entity.h"
|
||||||
|
#include "entity/component/display/entityposition.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
|
||||||
|
void entityCameraInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
cam->nearClip = 0.1f;
|
||||||
|
cam->farClip = 5000.0f;
|
||||||
|
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||||
|
cam->perspective.fov = glm_rad(45.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetProjection(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 out
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
|
||||||
|
if(
|
||||||
|
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
|
||||||
|
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
|
||||||
|
) {
|
||||||
|
glm_mat4_identity(out);
|
||||||
|
glm_perspective(
|
||||||
|
cam->perspective.fov,
|
||||||
|
SCREEN.aspect,
|
||||||
|
cam->nearClip,
|
||||||
|
cam->farClip,
|
||||||
|
out
|
||||||
|
);
|
||||||
|
|
||||||
|
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
|
||||||
|
out[1][1] *= -1.0f;
|
||||||
|
}
|
||||||
|
} else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||||
|
glm_mat4_identity(out);
|
||||||
|
glm_ortho(
|
||||||
|
cam->orthographic.left,
|
||||||
|
cam->orthographic.right,
|
||||||
|
cam->orthographic.top,
|
||||||
|
cam->orthographic.bottom,
|
||||||
|
cam->nearClip,
|
||||||
|
cam->farClip,
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityCameraGetCurrent(entitymanager_t *mgr) {
|
||||||
|
entityid_t camEnts[ENTITY_COUNT_MAX];
|
||||||
|
componentid_t camComps[ENTITY_COUNT_MAX];
|
||||||
|
entityid_t count = componentGetEntitiesWithComponent(
|
||||||
|
mgr, COMPONENT_TYPE_CAMERA, camEnts, camComps
|
||||||
|
);
|
||||||
|
if(count == 0) return ENTITY_ID_INVALID;
|
||||||
|
return camEnts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetForward(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
) {
|
||||||
|
componentid_t posComp = entityGetComponent(
|
||||||
|
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
mat4 transform;
|
||||||
|
entityPositionGetTransform(mgr, entityId, posComp, transform);
|
||||||
|
// transform is an object->world matrix; column 2 is the entity's local Z
|
||||||
|
// axis expressed in world space. Cameras look down their local -Z.
|
||||||
|
float_t fx = -transform[2][0];
|
||||||
|
float_t fz = -transform[2][2];
|
||||||
|
float_t len = sqrtf(fx * fx + fz * fz);
|
||||||
|
if(len > 1e-6f) { fx /= len; fz /= len; }
|
||||||
|
out[0] = fx;
|
||||||
|
out[1] = fz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetRight(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
) {
|
||||||
|
componentid_t posComp = entityGetComponent(
|
||||||
|
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
mat4 transform;
|
||||||
|
entityPositionGetTransform(mgr, entityId, posComp, transform);
|
||||||
|
// transform is an object->world matrix; column 0 is the entity's local X
|
||||||
|
// (right) axis expressed in world space.
|
||||||
|
float_t rx = transform[0][0];
|
||||||
|
float_t rz = transform[0][2];
|
||||||
|
float_t len = sqrtf(rx * rx + rz * rz);
|
||||||
|
if(len > 1e-6f) { rx /= len; rz /= len; }
|
||||||
|
out[0] = rx;
|
||||||
|
out[1] = rz;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraLookAtPixelPerfect(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t ent,
|
||||||
|
const componentid_t posComp,
|
||||||
|
const componentid_t camComp,
|
||||||
|
const vec3 point,
|
||||||
|
const vec3 eyeOffset,
|
||||||
|
const float_t scale
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
mgr, ent, camComp, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
float_t dist = (
|
||||||
|
(float_t)SCREEN.height / (2.0f * scale * tanf(cam->perspective.fov * 0.5f))
|
||||||
|
);
|
||||||
|
|
||||||
|
vec3 eye = {
|
||||||
|
point[0] + eyeOffset[0],
|
||||||
|
point[1] + dist + eyeOffset[1],
|
||||||
|
point[2] + eyeOffset[2]
|
||||||
|
};
|
||||||
|
vec3 up = { 0.0f, 0.0f, -1.0f };
|
||||||
|
entityPositionLookAt(mgr, ent, posComp, eye, (float_t *)point, up);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
||||||
|
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED,
|
||||||
|
ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC
|
||||||
|
} entitycameraprojectiontype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float_t fov;
|
||||||
|
} perspective;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float_t left;
|
||||||
|
float_t right;
|
||||||
|
float_t top;
|
||||||
|
float_t bottom;
|
||||||
|
} orthographic;
|
||||||
|
};
|
||||||
|
|
||||||
|
float_t nearClip;
|
||||||
|
float_t farClip;
|
||||||
|
entitycameraprojectiontype_t projType;
|
||||||
|
} entitycamera_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity camera component.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityCameraInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders out the projection matrix for the given camera.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param out The output projection matrix.
|
||||||
|
*/
|
||||||
|
void entityCameraGetProjection(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 out
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the entity ID of the first active camera, or ENTITY_ID_INVALID if
|
||||||
|
* none are active.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to search.
|
||||||
|
*/
|
||||||
|
entityid_t entityCameraGetCurrent(entitymanager_t *mgr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the camera's horizontal forward direction (XZ plane) from its
|
||||||
|
* position component. Automatically finds the position component on the
|
||||||
|
* entity.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The camera entity ID.
|
||||||
|
* @param out Output vec2: {forwardX, forwardZ} normalized.
|
||||||
|
*/
|
||||||
|
void entityCameraGetForward(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the camera's horizontal right direction (XZ plane) from its position
|
||||||
|
* component. Automatically finds the position component on the entity.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The camera entity ID.
|
||||||
|
* @param out Output vec2: {rightX, rightZ} normalized.
|
||||||
|
*/
|
||||||
|
void entityCameraGetRight(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
vec2 out
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions the camera to look at a 3D point at a pixel-perfect distance
|
||||||
|
* derived from the camera's FOV and screen height.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param ent The camera entity ID.
|
||||||
|
* @param posComp The position component ID.
|
||||||
|
* @param camComp The camera component ID.
|
||||||
|
* @param point World position to look at.
|
||||||
|
* @param eyeOffset Offset added to the eye position only (not the target).
|
||||||
|
* @param scale Pixels per world unit. 1.0 = pixel perfect, 2.0 = 2px per unit.
|
||||||
|
*/
|
||||||
|
void entityCameraLookAtPixelPerfect(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t ent,
|
||||||
|
const componentid_t posComp,
|
||||||
|
const componentid_t camComp,
|
||||||
|
const vec3 point,
|
||||||
|
const vec3 eyeOffset,
|
||||||
|
const float_t scale
|
||||||
|
);
|
||||||
@@ -0,0 +1,669 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityPositionInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
|
||||||
|
pos->flags = 0;
|
||||||
|
pos->parentEntityId = ENTITY_ID_INVALID;
|
||||||
|
pos->parentComponentId = COMPONENT_ID_INVALID;
|
||||||
|
pos->childCount = 0;
|
||||||
|
glm_vec3_zero(pos->position);
|
||||||
|
glm_vec3_zero(pos->rotation);
|
||||||
|
glm_vec3_one(pos->scale);
|
||||||
|
glm_mat4_identity(pos->localTransform);
|
||||||
|
glm_mat4_identity(pos->worldTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionLookAt(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 eye,
|
||||||
|
vec3 target,
|
||||||
|
vec3 up
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
// glm_lookat() produces a view matrix (world -> eye space). Every other
|
||||||
|
// setter treats localTransform as this entity's placement in world space
|
||||||
|
// (eye -> world), so invert it here to keep that meaning consistent --
|
||||||
|
// callers that need a view matrix (e.g. scene rendering) invert it back.
|
||||||
|
mat4 view;
|
||||||
|
glm_lookat(eye, target, up, view);
|
||||||
|
glm_mat4_inv(view, pos->localTransform);
|
||||||
|
// localTransform is now authoritative; PRS cache is stale.
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY)
|
||||||
|
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
||||||
|
ENTITY_POSITION_FLAG_POSITION_DIRTY);
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
glm_mat4_copy(
|
||||||
|
pos->parentEntityId == ENTITY_ID_INVALID
|
||||||
|
? pos->localTransform : pos->worldTransform,
|
||||||
|
dest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureLocal(pos);
|
||||||
|
glm_mat4_copy(pos->localTransform, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->position, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->position, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
dest[0] = pos->worldTransform[3][0];
|
||||||
|
dest[1] = pos->worldTransform[3][1];
|
||||||
|
dest[2] = pos->worldTransform[3][2];
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_vec3_copy(position, pos->position);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
mat4 invParent;
|
||||||
|
glm_mat4_inv(parent->worldTransform, invParent);
|
||||||
|
vec3 localPos;
|
||||||
|
glm_mat4_mulv3(invParent, position, 1.0f, localPos);
|
||||||
|
glm_vec3_copy(localPos, pos->position);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_vec3_copy(position, pos->position);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->rotation, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->rotation, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
const float_t (*wt)[4] = pos->worldTransform;
|
||||||
|
const float_t sx = sqrtf(
|
||||||
|
wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]
|
||||||
|
);
|
||||||
|
const float_t sy = sqrtf(
|
||||||
|
wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]
|
||||||
|
);
|
||||||
|
const float_t sz = sqrtf(
|
||||||
|
wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]
|
||||||
|
);
|
||||||
|
const float_t r00 = sx > 0.0f ? wt[0][0]/sx : 0.0f;
|
||||||
|
const float_t r10 = sy > 0.0f ? wt[1][0]/sy : 0.0f;
|
||||||
|
const float_t r20 = sz > 0.0f ? wt[2][0]/sz : 0.0f;
|
||||||
|
const float_t r01 = sx > 0.0f ? wt[0][1]/sx : 0.0f;
|
||||||
|
const float_t r11 = sy > 0.0f ? wt[1][1]/sy : 0.0f;
|
||||||
|
const float_t r21 = sz > 0.0f ? wt[2][1]/sz : 0.0f;
|
||||||
|
const float_t r22 = sz > 0.0f ? wt[2][2]/sz : 0.0f;
|
||||||
|
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
|
||||||
|
dest[1] = asinf(sinBeta);
|
||||||
|
const float_t cosBeta = cosf(dest[1]);
|
||||||
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
|
dest[0] = atan2f(-r21, r22);
|
||||||
|
dest[2] = atan2f(-r10, r00);
|
||||||
|
} else {
|
||||||
|
dest[2] = 0.0f;
|
||||||
|
dest[0] = (sinBeta > 0.0f) ? atan2f(r01, r11) : -atan2f(r01, r11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_vec3_copy(rotation, pos->rotation);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_vec3_copy(rotation, pos->rotation);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
|
||||||
|
// Build target world rotation matrix (unit scale) from XYZ euler.
|
||||||
|
const float_t c0 = cosf(rotation[0]), s0 = sinf(rotation[0]);
|
||||||
|
const float_t c1 = cosf(rotation[1]), s1 = sinf(rotation[1]);
|
||||||
|
const float_t c2 = cosf(rotation[2]), s2 = sinf(rotation[2]);
|
||||||
|
const float_t s0s1 = s0*s1, c0s1 = c0*s1;
|
||||||
|
// Named wr[col_stored][row_stored] matching cglm column-major layout.
|
||||||
|
const float_t wr00 = c1*c2;
|
||||||
|
const float_t wr01 = c0*s2 + s0s1*c2;
|
||||||
|
const float_t wr02 = s0*s2 - c0s1*c2;
|
||||||
|
const float_t wr10 = -c1*s2;
|
||||||
|
const float_t wr11 = c0*c2 - s0s1*s2;
|
||||||
|
const float_t wr12 = s0*c2 + c0s1*s2;
|
||||||
|
const float_t wr20 = s1;
|
||||||
|
const float_t wr21 = -s0*c1;
|
||||||
|
const float_t wr22 = c0*c1;
|
||||||
|
|
||||||
|
// Normalize parent world columns to extract pure rotation.
|
||||||
|
const float_t (*pt)[4] = parent->worldTransform;
|
||||||
|
const float_t psx = sqrtf(
|
||||||
|
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
|
||||||
|
);
|
||||||
|
const float_t psy = sqrtf(
|
||||||
|
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
|
||||||
|
);
|
||||||
|
const float_t psz = sqrtf(
|
||||||
|
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
|
||||||
|
);
|
||||||
|
const float_t pr00 = psx > 0.f ? pt[0][0]/psx : 0.f;
|
||||||
|
const float_t pr01 = psx > 0.f ? pt[0][1]/psx : 0.f;
|
||||||
|
const float_t pr02 = psx > 0.f ? pt[0][2]/psx : 0.f;
|
||||||
|
const float_t pr10 = psy > 0.f ? pt[1][0]/psy : 0.f;
|
||||||
|
const float_t pr11 = psy > 0.f ? pt[1][1]/psy : 0.f;
|
||||||
|
const float_t pr12 = psy > 0.f ? pt[1][2]/psy : 0.f;
|
||||||
|
const float_t pr20 = psz > 0.f ? pt[2][0]/psz : 0.f;
|
||||||
|
const float_t pr21 = psz > 0.f ? pt[2][1]/psz : 0.f;
|
||||||
|
const float_t pr22 = psz > 0.f ? pt[2][2]/psz : 0.f;
|
||||||
|
|
||||||
|
// local_R = parent_R^T * world_R (R^-1 == R^T for orthogonal matrices).
|
||||||
|
// Compute only the 7 entries of the local rotation matrix needed for XYZ
|
||||||
|
// euler extraction (stored column-major: [col][row] = math [row][col]).
|
||||||
|
// sinBeta = stored[2][0] = math[0][2]
|
||||||
|
// r21/r22 = stored[2][1..2] = math[1..2][2]
|
||||||
|
// r10/r00 = stored[1][0], stored[0][0] = math[0][1], math[0][0]
|
||||||
|
// gimbal = stored[0][1], stored[1][1] = math[1][0], math[1][1]
|
||||||
|
const float_t lr00 = pr00*wr00 + pr01*wr10 + pr02*wr20; // math[0][0]
|
||||||
|
const float_t lr10 = pr00*wr01 + pr01*wr11 + pr02*wr21; // math[0][1]
|
||||||
|
const float_t lr20 = pr00*wr02 + pr01*wr12 + pr02*wr22; // [0][2] -> sinBeta
|
||||||
|
const float_t lr01 = pr10*wr00 + pr11*wr10 + pr12*wr20; // math[1][0]
|
||||||
|
const float_t lr11 = pr10*wr01 + pr11*wr11 + pr12*wr21; // math[1][1]
|
||||||
|
const float_t lr21 = pr10*wr02 + pr11*wr12 + pr12*wr22; // [1][2] -> r21
|
||||||
|
const float_t lr22 = pr20*wr02 + pr21*wr12 + pr22*wr22; // [2][2] -> r22
|
||||||
|
|
||||||
|
const float_t sinBeta = glm_clamp(lr20, -1.0f, 1.0f);
|
||||||
|
pos->rotation[1] = asinf(sinBeta);
|
||||||
|
const float_t cosBeta = cosf(pos->rotation[1]);
|
||||||
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
|
pos->rotation[0] = atan2f(-lr21, lr22);
|
||||||
|
pos->rotation[2] = atan2f(-lr10, lr00);
|
||||||
|
} else {
|
||||||
|
pos->rotation[2] = 0.0f;
|
||||||
|
pos->rotation[0] = (sinBeta > 0.0f)
|
||||||
|
? atan2f(lr01, lr11) : -atan2f(lr01, lr11);
|
||||||
|
}
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->scale, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionGetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
entityPositionEnsurePRS(pos);
|
||||||
|
glm_vec3_copy(pos->scale, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityPositionEnsureWorld(mgr, pos);
|
||||||
|
const float_t (*wt)[4] = pos->worldTransform;
|
||||||
|
dest[0] = sqrtf(wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]);
|
||||||
|
dest[1] = sqrtf(wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]);
|
||||||
|
dest[2] = sqrtf(wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_vec3_copy(scale, pos->scale);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_vec3_copy(scale, pos->scale);
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
const float_t (*pt)[4] = parent->worldTransform;
|
||||||
|
const float_t psx = sqrtf(
|
||||||
|
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
|
||||||
|
);
|
||||||
|
const float_t psy = sqrtf(
|
||||||
|
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
|
||||||
|
);
|
||||||
|
const float_t psz = sqrtf(
|
||||||
|
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
|
||||||
|
);
|
||||||
|
pos->scale[0] = psx > 0.0f ? scale[0] / psx : scale[0];
|
||||||
|
pos->scale[1] = psy > 0.0f ? scale[1] / psy : scale[1];
|
||||||
|
pos->scale[2] = psz > 0.0f ? scale[2] / psz : scale[2];
|
||||||
|
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||||
|
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionSetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t parentEntityId,
|
||||||
|
const componentid_t parentComponentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove from old parent's child list.
|
||||||
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
entityposition_t *oldParent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId,
|
||||||
|
COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < oldParent->childCount; i++) {
|
||||||
|
if(
|
||||||
|
oldParent->childEntityIds[i] == entityId &&
|
||||||
|
oldParent->childComponentIds[i] == componentId
|
||||||
|
) {
|
||||||
|
oldParent->childCount--;
|
||||||
|
for(uint8_t j = i; j < oldParent->childCount; j++) {
|
||||||
|
oldParent->childEntityIds[j] = oldParent->childEntityIds[j + 1];
|
||||||
|
oldParent->childComponentIds[j] = oldParent->childComponentIds[j + 1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos->parentEntityId = parentEntityId;
|
||||||
|
pos->parentComponentId = parentComponentId;
|
||||||
|
|
||||||
|
// Register with new parent.
|
||||||
|
if(parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
|
||||||
|
parent->childEntityIds[parent->childCount] = entityId;
|
||||||
|
parent->childComponentIds[parent->childCount] = componentId;
|
||||||
|
parent->childCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityPositionGetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
return pos->parentEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityposition_t *entityPositionGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos) {
|
||||||
|
pos->flags = (
|
||||||
|
pos->flags |
|
||||||
|
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
||||||
|
ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
) & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
entityPositionMarkDirty(mgr, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos) {
|
||||||
|
if(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY) return;
|
||||||
|
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||||
|
for(uint8_t i = 0; i < pos->childCount; i++) {
|
||||||
|
entityposition_t *child = componentGetData(
|
||||||
|
mgr, pos->childEntityIds[i], pos->childComponentIds[i],
|
||||||
|
COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionMarkDirty(mgr, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionDisposeDeep(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = entityPositionGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
// Detach from parent so the parent's child list stays consistent.
|
||||||
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
entityPositionSetParent(
|
||||||
|
mgr, entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the child list before disposing self (entityDispose invalidates
|
||||||
|
// pos).
|
||||||
|
uint8_t childCount = pos->childCount;
|
||||||
|
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
for(uint8_t i = 0; i < childCount; i++) {
|
||||||
|
childEntityIds[i] = pos->childEntityIds[i];
|
||||||
|
childComponentIds[i] = pos->childComponentIds[i];
|
||||||
|
// Sever child's parent link so it won't try to modify our disposed
|
||||||
|
// data.
|
||||||
|
entityposition_t *child = entityPositionGet(
|
||||||
|
mgr, childEntityIds[i], childComponentIds[i]
|
||||||
|
);
|
||||||
|
child->parentEntityId = ENTITY_ID_INVALID;
|
||||||
|
child->parentComponentId = COMPONENT_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityDispose(mgr, entityId);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < childCount; i++) {
|
||||||
|
entityPositionDisposeDeep(mgr, childEntityIds[i], childComponentIds[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionDecompose(entityposition_t *pos) {
|
||||||
|
// Translation: column 3
|
||||||
|
pos->position[0] = pos->localTransform[3][0];
|
||||||
|
pos->position[1] = pos->localTransform[3][1];
|
||||||
|
pos->position[2] = pos->localTransform[3][2];
|
||||||
|
|
||||||
|
// Scale: length of each basis column (xyz only)
|
||||||
|
pos->scale[0] = sqrtf(
|
||||||
|
pos->localTransform[0][0] * pos->localTransform[0][0] +
|
||||||
|
pos->localTransform[0][1] * pos->localTransform[0][1] +
|
||||||
|
pos->localTransform[0][2] * pos->localTransform[0][2]
|
||||||
|
);
|
||||||
|
pos->scale[1] = sqrtf(
|
||||||
|
pos->localTransform[1][0] * pos->localTransform[1][0] +
|
||||||
|
pos->localTransform[1][1] * pos->localTransform[1][1] +
|
||||||
|
pos->localTransform[1][2] * pos->localTransform[1][2]
|
||||||
|
);
|
||||||
|
pos->scale[2] = sqrtf(
|
||||||
|
pos->localTransform[2][0] * pos->localTransform[2][0] +
|
||||||
|
pos->localTransform[2][1] * pos->localTransform[2][1] +
|
||||||
|
pos->localTransform[2][2] * pos->localTransform[2][2]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Normalize columns to isolate the rotation matrix (no mat4 needed).
|
||||||
|
const float_t invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
|
||||||
|
const float_t invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
|
||||||
|
const float_t invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
|
||||||
|
|
||||||
|
const float_t r00 = pos->localTransform[0][0] * invS0;
|
||||||
|
const float_t r01 = pos->localTransform[0][1] * invS0;
|
||||||
|
const float_t r02 = pos->localTransform[0][2] * invS0;
|
||||||
|
const float_t r10 = pos->localTransform[1][0] * invS1;
|
||||||
|
const float_t r11 = pos->localTransform[1][1] * invS1;
|
||||||
|
const float_t r20 = pos->localTransform[2][0] * invS2;
|
||||||
|
const float_t r21 = pos->localTransform[2][1] * invS2;
|
||||||
|
const float_t r22 = pos->localTransform[2][2] * invS2;
|
||||||
|
|
||||||
|
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
|
||||||
|
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
|
||||||
|
pos->rotation[1] = asinf(sinBeta);
|
||||||
|
const float_t cosBeta = cosf(pos->rotation[1]);
|
||||||
|
|
||||||
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
|
pos->rotation[0] = atan2f(-r21, r22);
|
||||||
|
pos->rotation[2] = atan2f(-r10, r00);
|
||||||
|
} else {
|
||||||
|
// Gimbal lock: pin Z to 0, recover X.
|
||||||
|
pos->rotation[2] = 0.0f;
|
||||||
|
pos->rotation[0] = (sinBeta > 0.0f)
|
||||||
|
? atan2f(r01, r11)
|
||||||
|
: -atan2f(r01, r11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionEnsurePRS(entityposition_t *pos) {
|
||||||
|
if(!(pos->flags & ENTITY_POSITION_FLAG_PRS_DIRTY)) return;
|
||||||
|
entityPositionDecompose(pos);
|
||||||
|
pos->flags &= ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionEnsureLocal(entityposition_t *pos) {
|
||||||
|
const uint8_t dirty = pos->flags & (
|
||||||
|
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
);
|
||||||
|
if(!dirty) return;
|
||||||
|
|
||||||
|
if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
|
||||||
|
// Rotation or scale changed: rebuild cols 0-2 analytically (XYZ euler).
|
||||||
|
const float_t c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
|
||||||
|
const float_t c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
|
||||||
|
const float_t c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
|
||||||
|
const float_t s0s1 = s0 * s1;
|
||||||
|
const float_t c0s1 = c0 * s1;
|
||||||
|
|
||||||
|
pos->localTransform[0][0] = c1 * c2 * pos->scale[0];
|
||||||
|
pos->localTransform[0][1] = (c0 * s2 + s0s1 * c2) * pos->scale[0];
|
||||||
|
pos->localTransform[0][2] = (s0 * s2 - c0s1 * c2) * pos->scale[0];
|
||||||
|
pos->localTransform[0][3] = 0.0f;
|
||||||
|
|
||||||
|
pos->localTransform[1][0] = -c1 * s2 * pos->scale[1];
|
||||||
|
pos->localTransform[1][1] = (c0 * c2 - s0s1 * s2) * pos->scale[1];
|
||||||
|
pos->localTransform[1][2] = (s0 * c2 + c0s1 * s2) * pos->scale[1];
|
||||||
|
pos->localTransform[1][3] = 0.0f;
|
||||||
|
|
||||||
|
pos->localTransform[2][0] = s1 * pos->scale[2];
|
||||||
|
pos->localTransform[2][1] = -s0 * c1 * pos->scale[2];
|
||||||
|
pos->localTransform[2][2] = c0 * c1 * pos->scale[2];
|
||||||
|
pos->localTransform[2][3] = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dirty & ENTITY_POSITION_FLAG_POSITION_DIRTY) {
|
||||||
|
// Only position changed: update column 3 only (no trig needed).
|
||||||
|
pos->localTransform[3][0] = pos->position[0];
|
||||||
|
pos->localTransform[3][1] = pos->position[1];
|
||||||
|
pos->localTransform[3][2] = pos->position[2];
|
||||||
|
pos->localTransform[3][3] = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos->flags &= ~(
|
||||||
|
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos) {
|
||||||
|
if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return;
|
||||||
|
entityPositionEnsureLocal(pos);
|
||||||
|
|
||||||
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
|
// Parented: world = parent.world x local. worldTransform must be
|
||||||
|
// written because children (and this node's getters) read it.
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
mgr, pos->parentEntityId, pos->parentComponentId,
|
||||||
|
COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionEnsureWorld(mgr, parent);
|
||||||
|
glm_mat4_mul(
|
||||||
|
parent->worldTransform, pos->localTransform, pos->worldTransform
|
||||||
|
);
|
||||||
|
} else if(pos->childCount > 0) {
|
||||||
|
// Parentless root with children: children need a valid worldTransform
|
||||||
|
// to multiply against, but world == local, so just copy.
|
||||||
|
glm_mat4_copy(pos->localTransform, pos->worldTransform);
|
||||||
|
}
|
||||||
|
// Parentless leaf: world == local. Getters read localTransform directly;
|
||||||
|
// no copy needed.
|
||||||
|
|
||||||
|
pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/** Maximum number of child position components this node can track. */
|
||||||
|
#define ENTITY_POSITION_CHILDREN_MAX 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PRS cache is stale. localTransform was written directly (e.g. lookAt) and
|
||||||
|
* position/rotation/scale need to be decomposed before they can be read.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_PRS_DIRTY (1 << 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Columns 0-2 of localTransform are stale. Rotation or scale changed; the
|
||||||
|
* basis vectors need to be rebuilt analytically before the matrix can be used.
|
||||||
|
* Does not imply column 3 (translation) is stale.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_ROTATION_DIRTY (1 << 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column 3 of localTransform is stale. Position changed; only the
|
||||||
|
* translation column needs to be written. Does not imply columns 0-2 are
|
||||||
|
* stale.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_POSITION_DIRTY (1 << 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* worldTransform is stale. Either the local matrix changed or an ancestor
|
||||||
|
* moved; worldTransform must be recomputed before world data can be read.
|
||||||
|
*/
|
||||||
|
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 3)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/*
|
||||||
|
* Hot fields - flag checks, parent/child traversal (markDirty, ensureWorld)
|
||||||
|
* only touch these. Kept at the front so they share the first cache line.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** ENTITY_POSITION_FLAG_* bitmask; describes which caches are stale. */
|
||||||
|
uint8_t flags;
|
||||||
|
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */
|
||||||
|
entityid_t parentEntityId;
|
||||||
|
/** Component ID of the parent position, or COMPONENT_ID_INVALID if none. */
|
||||||
|
componentid_t parentComponentId;
|
||||||
|
/** Number of currently registered children. */
|
||||||
|
uint8_t childCount;
|
||||||
|
/** Entity IDs of child nodes. */
|
||||||
|
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
/** Component IDs of child position components. */
|
||||||
|
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Warm fields - read/written by PRS getters/setters.
|
||||||
|
* Accessed more often than the matrices but less often than flags.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Cached local position (XYZ). Stale when PRS_DIRTY is set. */
|
||||||
|
vec3 position;
|
||||||
|
/** Cached local rotation (XYZ euler, radians). Stale when PRS_DIRTY. */
|
||||||
|
vec3 rotation;
|
||||||
|
/** Cached local scale (XYZ). Stale when PRS_DIRTY is set. */
|
||||||
|
vec3 scale;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cold fields - only touched when actually rebuilding transforms.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
|
||||||
|
mat4 localTransform;
|
||||||
|
/** World transform matrix, recomputed lazily from the parent chain. */
|
||||||
|
mat4 worldTransform;
|
||||||
|
} entityposition_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the entity position component, setting identity transforms and
|
||||||
|
* zeroing all parent/child state.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityPositionInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions and orients the entity at eye, facing target. Stores this as
|
||||||
|
* the entity's normal world-space placement (consistent with every other
|
||||||
|
* setter), not as a view matrix -- invert entityPositionGetTransform()'s
|
||||||
|
* result to get a view matrix for rendering.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param eye The eye/camera position.
|
||||||
|
* @param target The target point to look at.
|
||||||
|
* @param up The up vector.
|
||||||
|
*/
|
||||||
|
void entityPositionLookAt(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 eye,
|
||||||
|
vec3 target,
|
||||||
|
vec3 up
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space transform matrix, recomputing it lazily if dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination matrix.
|
||||||
|
*/
|
||||||
|
void entityPositionGetTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the local transform matrix (does not include parent transforms).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination matrix.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalTransform(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mat4 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached local position (XYZ). Decomposes localTransform into PRS
|
||||||
|
* first if ENTITY_POSITION_FLAG_PRS_DIRTY is set; never triggers a matrix
|
||||||
|
* rebuild or world-transform update.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space position. For parentless entities this is the same as
|
||||||
|
* the local position.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the world-space position. For parentless entities this is equivalent
|
||||||
|
* to entityPositionSetLocalPosition. For parented entities the position is
|
||||||
|
* converted to local space via the inverted parent world transform.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param position The desired world-space position.
|
||||||
|
*/
|
||||||
|
void entityPositionSetWorldPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local position, marks localTransform and worldTransform (self +
|
||||||
|
* descendants) dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param position The new local position.
|
||||||
|
*/
|
||||||
|
void entityPositionSetLocalPosition(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 position
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached local euler rotation (XYZ, radians). Decomposes
|
||||||
|
* localTransform first if ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space euler rotation (XYZ, radians) by decomposing the
|
||||||
|
* world transform. For parentless entities this is the same as local
|
||||||
|
* rotation.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local euler rotation (XYZ, radians) and marks transforms dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param rotation The new local rotation.
|
||||||
|
*/
|
||||||
|
void entityPositionSetLocalRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the world-space euler rotation (XYZ, radians). For parentless
|
||||||
|
* entities this is equivalent to entityPositionSetLocalRotation. For
|
||||||
|
* parented entities the rotation is converted to local space by removing
|
||||||
|
* the parent world rotation.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param rotation The desired world-space euler rotation.
|
||||||
|
*/
|
||||||
|
void entityPositionSetWorldRotation(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 rotation
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached local scale. Decomposes localTransform first if
|
||||||
|
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the world-space scale by extracting column lengths from the world
|
||||||
|
* transform. For parentless entities this is the same as local scale.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest Destination vector.
|
||||||
|
*/
|
||||||
|
void entityPositionGetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the local scale and marks transforms dirty.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param scale The new local scale.
|
||||||
|
*/
|
||||||
|
void entityPositionSetLocalScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the world-space scale. For parentless entities this is equivalent to
|
||||||
|
* entityPositionSetLocalScale. For parented entities the scale is converted
|
||||||
|
* to local space by dividing by the parent world scale (assumes no shear).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param scale The desired world-space scale.
|
||||||
|
*/
|
||||||
|
void entityPositionSetWorldScale(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 scale
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the parent of this entity's position component.
|
||||||
|
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param entityId The child entity ID.
|
||||||
|
* @param componentId The child component ID.
|
||||||
|
* @param parentEntityId The parent entity ID.
|
||||||
|
* @param parentComponentId The parent component ID.
|
||||||
|
*/
|
||||||
|
void entityPositionSetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t parentEntityId,
|
||||||
|
const componentid_t parentComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity ID of this position component's parent.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The parent entity ID, or ENTITY_ID_INVALID if unparented.
|
||||||
|
*/
|
||||||
|
entityid_t entityPositionGetParent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a direct pointer to the entity position component data.
|
||||||
|
* After modifying localTransform directly, call entityPositionMarkDirty() to
|
||||||
|
* set ENTITY_POSITION_FLAG_WORLD_DIRTY on self and descendants. After
|
||||||
|
* modifying PRS directly, call entityPositionRebuild() instead.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return Pointer to the component data.
|
||||||
|
*/
|
||||||
|
entityposition_t *entityPositionGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals that the PRS cache was modified externally. Sets both
|
||||||
|
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||||
|
* so all of localTransform is rebuilt lazily on the next read, clears
|
||||||
|
* ENTITY_POSITION_FLAG_PRS_DIRTY, propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
|
||||||
|
* to self and all descendants.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns pos and its descendants.
|
||||||
|
* @param pos The position component whose PRS was modified.
|
||||||
|
*/
|
||||||
|
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets ENTITY_POSITION_FLAG_WORLD_DIRTY on this node and all descendants,
|
||||||
|
* indicating that worldTransform must be recomputed before it is read.
|
||||||
|
* Call this after modifying localTransform directly.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns pos and its descendants.
|
||||||
|
* @param pos The position component to mark dirty.
|
||||||
|
*/
|
||||||
|
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes this entity and all of its position-component descendants
|
||||||
|
* recursively. Detaches from any parent before destroying.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The root entity ID.
|
||||||
|
* @param componentId The root position component ID.
|
||||||
|
*/
|
||||||
|
void entityPositionDisposeDeep(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decomposes the local transform matrix back into the position, rotation
|
||||||
|
* (XYZ euler, radians), and scale cache fields.
|
||||||
|
*
|
||||||
|
* @param pos The position component to decompose.
|
||||||
|
*/
|
||||||
|
void entityPositionDecompose(entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Decomposes localTransform into the PRS cache if
|
||||||
|
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param pos The position component to update.
|
||||||
|
*/
|
||||||
|
void entityPositionEnsurePRS(entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Rebuilds localTransform from the PRS cache, touching only the
|
||||||
|
* columns flagged as stale (ROTATION_DIRTY and/or POSITION_DIRTY).
|
||||||
|
*
|
||||||
|
* @param pos The position component to update.
|
||||||
|
*/
|
||||||
|
void entityPositionEnsureLocal(entityposition_t *pos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Recomputes worldTransform from the parent chain if
|
||||||
|
* ENTITY_POSITION_FLAG_WORLD_DIRTY is set.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns pos and its ancestors.
|
||||||
|
* @param pos The position component to update.
|
||||||
|
*/
|
||||||
|
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos);
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityrenderable.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "display/shader/shadermaterial.h"
|
||||||
|
#include "display/shader/shaderunlit.h"
|
||||||
|
#include "display/display.h"
|
||||||
|
#include "display/mesh/cube.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityRenderableInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
memoryZero(r, sizeof(entityrenderable_t));
|
||||||
|
r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL;
|
||||||
|
r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT;
|
||||||
|
r->data.material.material.unlit.color = COLOR_WHITE;
|
||||||
|
r->data.material.meshes[0] = &CUBE_MESH_SIMPLE;
|
||||||
|
r->data.material.meshOffsets[0] = 0;
|
||||||
|
r->data.material.meshCounts[0] = -1;
|
||||||
|
r->data.material.meshCount = 1;
|
||||||
|
r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderabletype_t type
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetPriority(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const int8_t priority
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetColor(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const color_t color
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||||
|
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set color"
|
||||||
|
);
|
||||||
|
r->data.material.material.unlit.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetMesh(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t slot,
|
||||||
|
mesh_t *mesh
|
||||||
|
) {
|
||||||
|
assertNotNull(mesh, "Mesh cannot be null");
|
||||||
|
assertTrue(slot < ENTITY_RENDERABLE_MESHES_MAX, "Mesh slot out of bounds");
|
||||||
|
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||||
|
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set a mesh"
|
||||||
|
);
|
||||||
|
|
||||||
|
r->data.material.meshes[slot] = mesh;
|
||||||
|
r->data.material.meshOffsets[slot] = 0;
|
||||||
|
r->data.material.meshCounts[slot] = -1;
|
||||||
|
if(slot >= r->data.material.meshCount) {
|
||||||
|
r->data.material.meshCount = slot + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
errorret_t (*draw)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
),
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(draw, "Draw callback cannot be null");
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->type = ENTITY_RENDERABLE_TYPE_CUSTOM;
|
||||||
|
r->data.custom.draw = draw;
|
||||||
|
r->data.custom.drawUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
switch(r->type) {
|
||||||
|
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
|
||||||
|
return entityRenderableDrawSpritebatch(&r->data.spritebatch);
|
||||||
|
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
|
||||||
|
return entityRenderableDrawMaterial(&r->data.material);
|
||||||
|
case ENTITY_RENDERABLE_TYPE_CUSTOM:
|
||||||
|
return entityRenderableDrawCustom(
|
||||||
|
mgr, entityId, componentId, &r->data.custom
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
assertUnreachable("Invalid renderable type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDrawSpritebatch(
|
||||||
|
const entityrenderablespritebatch_t *sb
|
||||||
|
) {
|
||||||
|
if(sb->spriteCount == 0) errorOk();
|
||||||
|
|
||||||
|
errorChain(displaySetState((displaystate_t){
|
||||||
|
.flags = DISPLAY_STATE_FLAG_BLEND
|
||||||
|
}));
|
||||||
|
|
||||||
|
spriteBatchClear();
|
||||||
|
shadermaterial_t mat;
|
||||||
|
memoryZero(&mat, sizeof(shadermaterial_t));
|
||||||
|
mat.unlit.texture = sb->texture;
|
||||||
|
mat.unlit.color = COLOR_WHITE;
|
||||||
|
errorChain(spriteBatchBuffer(
|
||||||
|
sb->sprites, sb->spriteCount,
|
||||||
|
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
|
||||||
|
));
|
||||||
|
return spriteBatchFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m) {
|
||||||
|
errorChain(displaySetState(m->state));
|
||||||
|
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
|
||||||
|
assertNotNull(shader, "Shader cannot be null for material type");
|
||||||
|
errorChain(shaderBind(shader));
|
||||||
|
errorChain(shaderSetMaterial(shader, &m->material));
|
||||||
|
for(uint8_t i = 0; i < m->meshCount; i++) {
|
||||||
|
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t entityRenderableDrawCustom(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderablecustom_t *custom
|
||||||
|
) {
|
||||||
|
return custom->draw(mgr, entityId, componentId, custom->drawUser);
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "display/mesh/mesh.h"
|
||||||
|
#include "display/shader/shadermaterial.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/displaystate.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
||||||
|
#define ENTITY_RENDERABLE_MESHES_MAX 8
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ENTITY_RENDERABLE_TYPE_CUSTOM = 0,
|
||||||
|
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
|
||||||
|
ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||||
|
} entityrenderabletype_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
|
||||||
|
uint32_t spriteCount;
|
||||||
|
texture_t *texture;
|
||||||
|
} entityrenderablespritebatch_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
mesh_t *meshes[ENTITY_RENDERABLE_MESHES_MAX];
|
||||||
|
int32_t meshOffsets[ENTITY_RENDERABLE_MESHES_MAX];
|
||||||
|
int32_t meshCounts[ENTITY_RENDERABLE_MESHES_MAX];
|
||||||
|
uint8_t meshCount;
|
||||||
|
shaderlistshader_t shaderType;
|
||||||
|
shadermaterial_t material;
|
||||||
|
displaystate_t state;
|
||||||
|
} entityrenderablematerial_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
errorret_t (*draw)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
void *drawUser;
|
||||||
|
} entityrenderablecustom_t;
|
||||||
|
|
||||||
|
typedef union entityrenderabledata_u {
|
||||||
|
entityrenderablespritebatch_t spritebatch;
|
||||||
|
entityrenderablematerial_t material;
|
||||||
|
entityrenderablecustom_t custom;
|
||||||
|
} entityrenderabledata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
entityrenderabletype_t type;
|
||||||
|
entityrenderabledata_t data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render priority. 0 = auto (derived from type/flags). Higher values
|
||||||
|
* render later (on top of lower values). Range: [-128..127] with 0 auto.
|
||||||
|
*/
|
||||||
|
int8_t priority;
|
||||||
|
} entityrenderable_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the entity renderable component. Defaults to
|
||||||
|
* ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL with the unlit shader, a white
|
||||||
|
* cube, and depth-test enabled.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to initialize the component for.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
*/
|
||||||
|
void entityRenderableInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes the entity renderable component.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to dispose the component for.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
*/
|
||||||
|
void entityRenderableDispose(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the rendering type for the renderable component. Resets
|
||||||
|
* type-specific data to zero.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param type The rendering type to use.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderabletype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the render priority. 0 = auto (derived from type/flags). Higher
|
||||||
|
* values render later (on top). Use non-zero to force ordering.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param priority The priority value, or 0 for auto.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetPriority(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const int8_t priority
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the unlit material color. Only meaningful when the renderable is
|
||||||
|
* (or defaults to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param color The color to tint the material with.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetColor(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const color_t color
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets one of the material renderable's meshes, drawn in full (no offset/
|
||||||
|
* count override). Only meaningful when the renderable is (or defaults
|
||||||
|
* to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts otherwise.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component.
|
||||||
|
* @param slot Index into the material's meshes array (0 to
|
||||||
|
* ENTITY_RENDERABLE_MESHES_MAX - 1).
|
||||||
|
* @param mesh The mesh to draw in that slot.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetMesh(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t slot,
|
||||||
|
mesh_t *mesh
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the draw callback, switching the type to
|
||||||
|
* ENTITY_RENDERABLE_TYPE_CUSTOM.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to configure.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
* @param draw The draw callback to assign.
|
||||||
|
* @param user Userdata passed to the callback.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
errorret_t (*draw)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
),
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the entity using its renderable component data.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to draw.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
* @return Any error state that happened.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDraw(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Draws a spritebatch-type renderable.
|
||||||
|
*
|
||||||
|
* @param sb The spritebatch data to draw.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDrawSpritebatch(
|
||||||
|
const entityrenderablespritebatch_t *sb
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Draws a shader-material-type renderable.
|
||||||
|
*
|
||||||
|
* @param m The material data to draw.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes a custom-type renderable's draw callback.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity being drawn.
|
||||||
|
* @param componentId The renderable component of the entity.
|
||||||
|
* @param custom The custom draw data.
|
||||||
|
* @return Error state.
|
||||||
|
*/
|
||||||
|
errorret_t entityRenderableDrawCustom(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityrenderablecustom_t *custom
|
||||||
|
);
|
||||||
+1
-1
@@ -6,5 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
inputvita.c
|
entityphysics.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entityphysics.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
void entityPhysicsInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
memoryZero(phys, sizeof(entityphysics_t));
|
||||||
|
|
||||||
|
// Default to cube
|
||||||
|
phys->type = PHYSICS_BODY_DYNAMIC;
|
||||||
|
phys->shape.type = PHYSICS_SHAPE_CUBE;
|
||||||
|
phys->shape.data.cube.halfExtents[0] = 0.5f;
|
||||||
|
phys->shape.data.cube.halfExtents[1] = 0.5f;
|
||||||
|
phys->shape.data.cube.halfExtents[2] = 0.5f;
|
||||||
|
phys->gravityScale = 1.0f;
|
||||||
|
phys->onGround = false;
|
||||||
|
phys->collideMask = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityphysics_t *entityPhysicsGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_PHYSICS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
phys->shape = shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicsshape_t entityPhysicsGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsGetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
glm_vec3_copy(phys->velocity, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 velocity
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
glm_vec3_copy(velocity, phys->velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsApplyImpulse(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 impulse
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
if(phys->type == PHYSICS_BODY_STATIC) return;
|
||||||
|
glm_vec3_add(phys->velocity, impulse, phys->velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t entityPhysicsIsOnGround(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->onGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsbodytype_t type
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
phys->type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicsbodytype_t entityPhysicsGetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPhysicsSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
phys->collideMask = mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t entityPhysicsGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||||
|
return phys->collideMask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "physics/physicsshape.h"
|
||||||
|
#include "physics/physicsbodytype.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
physicsbodytype_t type;
|
||||||
|
physicsshape_t shape;
|
||||||
|
vec3 velocity;
|
||||||
|
float_t gravityScale;
|
||||||
|
bool_t onGround;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitmask of collision layers/categories this body belongs to. Two
|
||||||
|
* bodies only collide with each other if (a.collideMask & b.collideMask)
|
||||||
|
* is non-zero -- i.e. they share at least one set bit. Defaults to 0x1
|
||||||
|
* (only bit 0 set).
|
||||||
|
*/
|
||||||
|
uint32_t collideMask;
|
||||||
|
} entityphysics_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the physics component: defaults to a dynamic 1x1x1 cube
|
||||||
|
* body, zero velocity, unit gravity scale, and onGround false.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityPhysicsInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying physics structure (temporarily) for the given entity.
|
||||||
|
* This is really just intended for doing operations faster than using the
|
||||||
|
* getters and setters, but it is preferred that you use those.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The physics component data for the given entity and component ID.
|
||||||
|
*/
|
||||||
|
entityphysics_t *entityPhysicsGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the shape of the entity's physics body. This will not reset the body
|
||||||
|
* state, so if you change from a cube to a sphere, it will keep the same
|
||||||
|
* velocity and onGround state.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param shape The new shape to set on the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shape of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The shape of the physics body.
|
||||||
|
*/
|
||||||
|
physicsshape_t entityPhysicsGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the velocity of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param dest The destination vec3 to write the velocity to.
|
||||||
|
*/
|
||||||
|
void entityPhysicsGetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 dest
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the velocity of the entity's physics body. This is not an impulse, so
|
||||||
|
* it will be affected by mass and drag.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param velocity The new velocity to set on the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetVelocity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 velocity
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies an impulse to the entity's physics body. This is an immediate
|
||||||
|
* velocity change that is not affected by mass or drag. No-op on STATIC
|
||||||
|
* bodies.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param impulse The impulse to apply to the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsApplyImpulse(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 impulse
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the entity's physics body rested on a surface during the
|
||||||
|
* last physicsWorldStep.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return True if the body is on the ground, false otherwise.
|
||||||
|
*/
|
||||||
|
bool_t entityPhysicsIsOnGround(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the body type of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param type The body type to set.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsbodytype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the body type of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The body type of the physics body.
|
||||||
|
*/
|
||||||
|
physicsbodytype_t entityPhysicsGetBodyType(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the collision mask of the entity's physics body. Two bodies only
|
||||||
|
* collide if their masks share at least one set bit.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param mask The new collision mask to set on the physics body.
|
||||||
|
*/
|
||||||
|
void entityPhysicsSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the collision mask of the entity's physics body.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The collision mask of the physics body.
|
||||||
|
*/
|
||||||
|
uint32_t entityPhysicsGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
+2
-1
@@ -6,5 +6,6 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
assetchunkloader.c
|
entitytrigger.c
|
||||||
|
entitytriggerevents.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitytrigger.h"
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
void entityTriggerInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
|
||||||
|
memoryZero(trig, sizeof(entitytrigger_t));
|
||||||
|
|
||||||
|
trig->shape.type = PHYSICS_SHAPE_CUBE;
|
||||||
|
trig->shape.data.cube.halfExtents[0] = 0.5f;
|
||||||
|
trig->shape.data.cube.halfExtents[1] = 0.5f;
|
||||||
|
trig->shape.data.cube.halfExtents[2] = 0.5f;
|
||||||
|
trig->collideMask = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
entitytrigger_t *entityTriggerGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_TRIGGER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
trig->shape = shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicsshape_t entityTriggerGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
return trig->shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
trig->collideMask = mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t entityTriggerGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
return trig->collideMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t entityTriggerGetOccupantCount(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
return trig->occupantCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityTriggerGetOccupantEntityId(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t index
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
if(index >= trig->occupantCount) return ENTITY_ID_INVALID;
|
||||||
|
return trig->occupants[index].entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t entityTriggerIsOccupyingEntity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t otherEntityId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->occupantCount; i++) {
|
||||||
|
if(trig->occupants[i].entityId == otherEntityId) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "physics/physicsshape.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/** Maximum number of entities a single trigger can track at once. */
|
||||||
|
#define ENTITY_TRIGGER_OCCUPANTS_MAX 8
|
||||||
|
|
||||||
|
/** Maximum number of subscribers per trigger event (onEnter/onActive/...). */
|
||||||
|
#define ENTITY_TRIGGER_CALLBACK_COUNT_MAX 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked for a trigger's onEnter/onActive/onMove/onLeave events.
|
||||||
|
* See entityTriggerOnEnterAdd() and its onActive/onMove/onLeave siblings.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that entered/is inside/left the
|
||||||
|
* trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
* @param user The user pointer passed to the matching *Add() call.
|
||||||
|
*/
|
||||||
|
typedef void (*triggercallback_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Tracks one entity currently overlapping a trigger volume. */
|
||||||
|
typedef struct {
|
||||||
|
/** The occupying entity's ID. */
|
||||||
|
entityid_t entityId;
|
||||||
|
/** The occupying entity's POSITION component ID. */
|
||||||
|
componentid_t componentId;
|
||||||
|
/** The occupying entity's local position as of the last step, used to
|
||||||
|
* detect movement (see entityTriggerOnMoveAdd()). */
|
||||||
|
vec3 lastPosition;
|
||||||
|
} entitytriggeroccupant_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
/** The trigger volume's shape, in the owning entity's local space. */
|
||||||
|
physicsshape_t shape;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitmask of collision layers/categories this trigger detects. An
|
||||||
|
* entity's PHYSICS body is only tested against this trigger if
|
||||||
|
* (trigger.collideMask & body.collideMask) is non-zero -- i.e. they
|
||||||
|
* share at least one set bit. Defaults to 0x1 (only bit 0 set). See
|
||||||
|
* entityPhysicsSetCollideMask().
|
||||||
|
*/
|
||||||
|
uint32_t collideMask;
|
||||||
|
|
||||||
|
/** Number of entities currently tracked in occupants. */
|
||||||
|
uint8_t occupantCount;
|
||||||
|
/** Entities currently overlapping this trigger. */
|
||||||
|
entitytriggeroccupant_t occupants[ENTITY_TRIGGER_OCCUPANTS_MAX];
|
||||||
|
|
||||||
|
/** Fired the step an entity begins overlapping this trigger. */
|
||||||
|
uint8_t onEnterCount;
|
||||||
|
triggercallback_t onEnter[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onEnterUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
|
||||||
|
/** Fired every step an entity continues overlapping this trigger,
|
||||||
|
* starting the step after onEnter fired. */
|
||||||
|
uint8_t onActiveCount;
|
||||||
|
triggercallback_t onActive[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onActiveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
|
||||||
|
/** Fired alongside onActive on any step where the entity's position
|
||||||
|
* changed since the previous step. */
|
||||||
|
uint8_t onMoveCount;
|
||||||
|
triggercallback_t onMove[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onMoveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
|
||||||
|
/** Fired the step an entity stops overlapping this trigger. */
|
||||||
|
uint8_t onLeaveCount;
|
||||||
|
triggercallback_t onLeave[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
void *onLeaveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||||
|
} entitytrigger_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the trigger component: defaults to a 1x1x1 cube volume, no
|
||||||
|
* tracked occupants, and no event subscribers.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerInit(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying trigger structure (temporarily) for the given
|
||||||
|
* entity. Prefer the dedicated getters/setters where possible.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The trigger component data for the given entity and component ID.
|
||||||
|
*/
|
||||||
|
entitytrigger_t *entityTriggerGet(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the shape of the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param shape The new shape to set on the trigger volume.
|
||||||
|
*/
|
||||||
|
void entityTriggerSetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const physicsshape_t shape
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shape of the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The shape of the trigger volume.
|
||||||
|
*/
|
||||||
|
physicsshape_t entityTriggerGetShape(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the collision mask of the trigger volume. An entity's PHYSICS body
|
||||||
|
* is only detected if their masks share at least one set bit.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param mask The new collision mask to set on the trigger volume.
|
||||||
|
*/
|
||||||
|
void entityTriggerSetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint32_t mask
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the collision mask of the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The collision mask of the trigger volume.
|
||||||
|
*/
|
||||||
|
uint32_t entityTriggerGetCollideMask(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of entities currently overlapping this trigger.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The number of currently tracked occupants.
|
||||||
|
*/
|
||||||
|
uint8_t entityTriggerGetOccupantCount(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity ID of the occupant at the given index, in [0,
|
||||||
|
* entityTriggerGetOccupantCount()). Order is not stable across steps.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param index The occupant index.
|
||||||
|
* @return The occupying entity's ID, or ENTITY_ID_INVALID if index is out
|
||||||
|
* of range.
|
||||||
|
*/
|
||||||
|
entityid_t entityTriggerGetOccupantEntityId(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const uint8_t index
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a specific entity is currently overlapping this trigger.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param otherEntityId The entity ID to check for.
|
||||||
|
* @return True if otherEntityId is a currently tracked occupant.
|
||||||
|
*/
|
||||||
|
bool_t entityTriggerIsOccupyingEntity(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const entityid_t otherEntityId
|
||||||
|
);
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitytriggerevents.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityTriggerOnEnterAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onEnterCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onEnter callback slots full"
|
||||||
|
);
|
||||||
|
trig->onEnter[trig->onEnterCount] = callback;
|
||||||
|
trig->onEnterUser[trig->onEnterCount] = user;
|
||||||
|
trig->onEnterCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnEnterRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
|
||||||
|
if(trig->onEnter[i] != callback) continue;
|
||||||
|
trig->onEnterCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onEnterCount; j++) {
|
||||||
|
trig->onEnter[j] = trig->onEnter[j + 1];
|
||||||
|
trig->onEnterUser[j] = trig->onEnterUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnActiveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onActiveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onActive callback slots full"
|
||||||
|
);
|
||||||
|
trig->onActive[trig->onActiveCount] = callback;
|
||||||
|
trig->onActiveUser[trig->onActiveCount] = user;
|
||||||
|
trig->onActiveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnActiveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
|
||||||
|
if(trig->onActive[i] != callback) continue;
|
||||||
|
trig->onActiveCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onActiveCount; j++) {
|
||||||
|
trig->onActive[j] = trig->onActive[j + 1];
|
||||||
|
trig->onActiveUser[j] = trig->onActiveUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnMoveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onMoveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onMove callback slots full"
|
||||||
|
);
|
||||||
|
trig->onMove[trig->onMoveCount] = callback;
|
||||||
|
trig->onMoveUser[trig->onMoveCount] = user;
|
||||||
|
trig->onMoveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnMoveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
|
||||||
|
if(trig->onMove[i] != callback) continue;
|
||||||
|
trig->onMoveCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onMoveCount; j++) {
|
||||||
|
trig->onMove[j] = trig->onMove[j + 1];
|
||||||
|
trig->onMoveUser[j] = trig->onMoveUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnLeaveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
assertTrue(
|
||||||
|
trig->onLeaveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||||
|
"Trigger onLeave callback slots full"
|
||||||
|
);
|
||||||
|
trig->onLeave[trig->onLeaveCount] = callback;
|
||||||
|
trig->onLeaveUser[trig->onLeaveCount] = user;
|
||||||
|
trig->onLeaveCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerOnLeaveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||||
|
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
|
||||||
|
if(trig->onLeave[i] != callback) continue;
|
||||||
|
trig->onLeaveCount--;
|
||||||
|
for(uint8_t j = i; j < trig->onLeaveCount; j++) {
|
||||||
|
trig->onLeave[j] = trig->onLeave[j + 1];
|
||||||
|
trig->onLeaveUser[j] = trig->onLeaveUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireEnter(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
|
||||||
|
trig->onEnter[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onEnterUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireActive(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
|
||||||
|
trig->onActive[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onActiveUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireMove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
|
||||||
|
trig->onMove[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onMoveUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityTriggerFireLeave(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
) {
|
||||||
|
entitytrigger_t *trig = entityTriggerGet(
|
||||||
|
mgr, triggerEntityId, triggerComponentId
|
||||||
|
);
|
||||||
|
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
|
||||||
|
trig->onLeave[i](
|
||||||
|
mgr, triggerEntityId, triggerComponentId,
|
||||||
|
otherEntityId, otherComponentId, trig->onLeaveUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entitytrigger.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onEnter event, fired the step an
|
||||||
|
* entity begins overlapping the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnEnterAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onEnter callback. No-op if not
|
||||||
|
* currently subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnEnterAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnEnterRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onActive event, fired every step an
|
||||||
|
* entity continues overlapping the trigger volume (starting the step
|
||||||
|
* after onEnter fired).
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnActiveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onActive callback. No-op if not
|
||||||
|
* currently subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnActiveAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnActiveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onMove event, fired alongside
|
||||||
|
* onActive on any step where the overlapping entity's position changed
|
||||||
|
* since the previous step.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnMoveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onMove callback. No-op if not currently
|
||||||
|
* subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnMoveAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnMoveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a callback to a trigger's onLeave event, fired the step an
|
||||||
|
* entity stops overlapping the trigger volume.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback to invoke.
|
||||||
|
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void entityTriggerOnLeaveAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes a previously added onLeave callback. No-op if not
|
||||||
|
* currently subscribed.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param callback The callback that was passed to entityTriggerOnLeaveAdd().
|
||||||
|
*/
|
||||||
|
void entityTriggerOnLeaveRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const triggercallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onEnter callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() when an entity begins overlapping.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that entered the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireEnter(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onActive callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() for every entity still overlapping.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID still inside the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireActive(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onMove callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() when an overlapping entity's position
|
||||||
|
* changed since the previous step.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that moved within the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireMove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Invokes every subscribed onLeave callback for a trigger.
|
||||||
|
* Called by triggerSystemStep() when an entity stops overlapping.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns both entities.
|
||||||
|
* @param triggerEntityId The entity ID of the trigger volume.
|
||||||
|
* @param triggerComponentId The TRIGGER component ID.
|
||||||
|
* @param otherEntityId The entity ID that left the trigger volume.
|
||||||
|
* @param otherComponentId The other entity's POSITION component ID.
|
||||||
|
*/
|
||||||
|
void entityTriggerFireLeave(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t triggerEntityId,
|
||||||
|
const componentid_t triggerComponentId,
|
||||||
|
const entityid_t otherEntityId,
|
||||||
|
const componentid_t otherComponentId
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/component/display/entityposition.h"
|
||||||
|
#include "entity/component/display/entitycamera.h"
|
||||||
|
#include "entity/component/display/entityrenderable.h"
|
||||||
|
#include "entity/component/physics/entityphysics.h"
|
||||||
|
#include "entity/component/trigger/entitytrigger.h"
|
||||||
|
#include "entity/component/animation/entityanimation.h"
|
||||||
|
|
||||||
|
// Name (Uppercase)
|
||||||
|
// Structure
|
||||||
|
// Field name (lowercase)
|
||||||
|
// Init function (optional)
|
||||||
|
// Dispose function (optional)
|
||||||
|
// Render function (optional)
|
||||||
|
|
||||||
|
X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL)
|
||||||
|
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL)
|
||||||
|
X(RENDERABLE, entityrenderable_t, renderable,
|
||||||
|
entityRenderableInit, entityRenderableDispose, NULL)
|
||||||
|
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, NULL, NULL)
|
||||||
|
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL)
|
||||||
|
X(ANIMATION, entityanimation_t, animation, entityAnimationInit, NULL, NULL)
|
||||||
|
|
||||||
|
// Game-specific components
|
||||||
|
#include "entity/gamecomponentlist.h"
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitymanager.h"
|
||||||
|
#include "component/display/entityposition.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
void entityInit(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
|
||||||
|
memoryZero(ent, sizeof(entity_t));
|
||||||
|
|
||||||
|
// Mark all component types not using this entity.
|
||||||
|
for(
|
||||||
|
componenttype_t compType = 0;
|
||||||
|
compType < COMPONENT_TYPE_COUNT;
|
||||||
|
compType++
|
||||||
|
) {
|
||||||
|
mgr->entitiesWithComponent[
|
||||||
|
compType * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = COMPONENT_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
ent->state |= ENTITY_STATE_ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentid_t entityAddComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
componentindex_t compInd;
|
||||||
|
|
||||||
|
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||||
|
compInd = componentGetIndex(entityId, i);
|
||||||
|
if(mgr->components[compInd].type != COMPONENT_TYPE_NULL) {
|
||||||
|
assertTrue(
|
||||||
|
mgr->components[compInd].type != type,
|
||||||
|
"Entity already has component of this type"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
componentInit(mgr, entityId, i, type);
|
||||||
|
mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = i;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertUnreachable("Entity has no more component slots available");
|
||||||
|
return COMPONENT_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentid_t entityGetComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
componentid_t compId = mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
];
|
||||||
|
if(compId == COMPONENT_ID_INVALID) return compId;
|
||||||
|
assertTrue(
|
||||||
|
mgr->components[componentGetIndex(entityId, compId)].type == type,
|
||||||
|
"Component type mismatch"
|
||||||
|
);
|
||||||
|
return compId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entitySetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
stringCopy(mgr->entities[entityId].name, name, ENTITY_NAME_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t *entityGetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId
|
||||||
|
) {
|
||||||
|
return mgr->entities[entityId].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityFindByName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const char_t *name
|
||||||
|
) {
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
if(!(mgr->entities[i].state & ENTITY_STATE_ACTIVE)) continue;
|
||||||
|
if(stringEquals(mgr->entities[i].name, name)) return i;
|
||||||
|
}
|
||||||
|
return ENTITY_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
componentid_t posComp = entityGetComponent(
|
||||||
|
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
if(posComp != COMPONENT_ID_INVALID) {
|
||||||
|
entityPositionDisposeDeep(mgr, entityId, posComp);
|
||||||
|
} else {
|
||||||
|
entityDispose(mgr, entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityUpdate(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
for(uint8_t i = 0; i < ent->updateCount; i++) {
|
||||||
|
ent->onUpdate[i](
|
||||||
|
mgr, entityId, ent->updateComponentId[i], ent->updateUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDispose(entitymanager_t *mgr, const entityid_t entityId) {
|
||||||
|
componentindex_t compInd;
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < ent->disposeCount; i++) {
|
||||||
|
ent->onDispose[i](
|
||||||
|
mgr, entityId, ent->disposeComponentId[i], ent->disposeUser[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||||
|
compInd = componentGetIndex(entityId, i);
|
||||||
|
componenttype_t type = mgr->components[compInd].type;
|
||||||
|
if(type == COMPONENT_TYPE_NULL) continue;
|
||||||
|
mgr->entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = COMPONENT_ID_INVALID;
|
||||||
|
componentDispose(mgr, entityId, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
ent->state = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityUpdateAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
assertTrue(
|
||||||
|
ent->updateCount < ENTITY_UPDATE_CALLBACK_COUNT_MAX,
|
||||||
|
"Entity update callback slots full"
|
||||||
|
);
|
||||||
|
ent->onUpdate[ent->updateCount] = callback;
|
||||||
|
ent->updateComponentId[ent->updateCount] = componentId;
|
||||||
|
ent->updateUser[ent->updateCount] = user;
|
||||||
|
ent->updateCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityUpdateRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
for(uint8_t i = 0; i < ent->updateCount; i++) {
|
||||||
|
if(ent->onUpdate[i] != callback) continue;
|
||||||
|
ent->updateCount--;
|
||||||
|
for(uint8_t j = i; j < ent->updateCount; j++) {
|
||||||
|
ent->onUpdate[j] = ent->onUpdate[j + 1];
|
||||||
|
ent->updateComponentId[j] = ent->updateComponentId[j + 1];
|
||||||
|
ent->updateUser[j] = ent->updateUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDisposeAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
assertTrue(
|
||||||
|
ent->disposeCount < ENTITY_DISPOSE_CALLBACK_COUNT_MAX,
|
||||||
|
"Entity dispose callback slots full"
|
||||||
|
);
|
||||||
|
ent->onDispose[ent->disposeCount] = callback;
|
||||||
|
ent->disposeComponentId[ent->disposeCount] = componentId;
|
||||||
|
ent->disposeUser[ent->disposeCount] = user;
|
||||||
|
ent->disposeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityDisposeRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
) {
|
||||||
|
entity_t *ent = &mgr->entities[entityId];
|
||||||
|
for(uint8_t i = 0; i < ent->disposeCount; i++) {
|
||||||
|
if(ent->onDispose[i] != callback) continue;
|
||||||
|
ent->disposeCount--;
|
||||||
|
for(uint8_t j = i; j < ent->disposeCount; j++) {
|
||||||
|
ent->onDispose[j] = ent->onDispose[j + 1];
|
||||||
|
ent->disposeComponentId[j] = ent->disposeComponentId[j + 1];
|
||||||
|
ent->disposeUser[j] = ent->disposeUser[j + 1];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "component.h"
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
#define ENTITY_STATE_ACTIVE (1 << 0)
|
||||||
|
|
||||||
|
#define ENTITY_UPDATE_CALLBACK_COUNT_MAX 5
|
||||||
|
#define ENTITY_DISPOSE_CALLBACK_COUNT_MAX 5
|
||||||
|
#define ENTITY_NAME_MAX 32
|
||||||
|
|
||||||
|
typedef void (*entitycallback_t)(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t name[ENTITY_NAME_MAX];
|
||||||
|
uint8_t state;
|
||||||
|
uint8_t updateCount;
|
||||||
|
uint8_t disposeCount;
|
||||||
|
entitycallback_t onUpdate[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
|
||||||
|
componentid_t updateComponentId[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
|
||||||
|
void *updateUser[ENTITY_UPDATE_CALLBACK_COUNT_MAX];
|
||||||
|
entitycallback_t onDispose[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
|
||||||
|
componentid_t disposeComponentId[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
|
||||||
|
void *disposeUser[ENTITY_DISPOSE_CALLBACK_COUNT_MAX];
|
||||||
|
} entity_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity with the given ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to initialize.
|
||||||
|
*/
|
||||||
|
void entityInit(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a component of the given type to the entity with the given ID.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to add the component to.
|
||||||
|
* @param type The type of the component to add.
|
||||||
|
* @return The ID of the entity with component.
|
||||||
|
*/
|
||||||
|
componentid_t entityAddComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ID of the component of the given type on the entity with the
|
||||||
|
* given ID, or COMPONENT_ID_INVALID if the entity lacks the component.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to get the component from.
|
||||||
|
* @param type The type of the component to get.
|
||||||
|
* @return The ID of the component.
|
||||||
|
*/
|
||||||
|
componentid_t entityGetComponent(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an entity's name. Names are optional -- an entity's name is empty
|
||||||
|
* by default (entityInit zeroes it) -- but are required to make the
|
||||||
|
* entity referenceable by name, e.g. as a parent via
|
||||||
|
* entityPositionSetParent().
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to name.
|
||||||
|
* @param name The name to give the entity. Must be shorter than
|
||||||
|
* ENTITY_NAME_MAX.
|
||||||
|
*/
|
||||||
|
void entitySetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an entity's name, or an empty string if it has none.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to get the name of.
|
||||||
|
* @return The entity's name.
|
||||||
|
*/
|
||||||
|
const char_t *entityGetName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first active entity with the given name.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager to search.
|
||||||
|
* @param name The name to search for.
|
||||||
|
* @return The entity ID, or ENTITY_ID_INVALID if no active entity matches.
|
||||||
|
*/
|
||||||
|
entityid_t entityFindByName(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const char_t *name
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs all registered update callbacks for the entity.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to update.
|
||||||
|
*/
|
||||||
|
void entityUpdate(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of an entity with the given ID. Fires all dispose callbacks
|
||||||
|
* before cleaning up components and state.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The ID of the entity to dispose of.
|
||||||
|
*/
|
||||||
|
void entityDispose(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of an entity and all of its position-component descendants
|
||||||
|
* recursively. If the entity has no position component, behaves like
|
||||||
|
* entityDispose.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The root entity ID.
|
||||||
|
*/
|
||||||
|
void entityDisposeDeep(entitymanager_t *mgr, const entityid_t entityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an update callback, invoked each time entityUpdate is called.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to register on.
|
||||||
|
* @param callback The function to call.
|
||||||
|
* @param componentId The component ID passed back to the callback.
|
||||||
|
* @param user Userdata passed back to the callback.
|
||||||
|
*/
|
||||||
|
void entityUpdateAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously registered update callback.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to remove from.
|
||||||
|
* @param callback The function to remove.
|
||||||
|
*/
|
||||||
|
void entityUpdateRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a dispose callback, invoked at the start of entityDispose
|
||||||
|
* before any component or state cleanup.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to register on.
|
||||||
|
* @param callback The function to call.
|
||||||
|
* @param componentId The component ID passed back to the callback.
|
||||||
|
* @param user Userdata passed back to the callback.
|
||||||
|
*/
|
||||||
|
void entityDisposeAdd(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback,
|
||||||
|
const componentid_t componentId,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a previously registered dispose callback.
|
||||||
|
*
|
||||||
|
* @param mgr The entity manager that owns the entity.
|
||||||
|
* @param entityId The entity to remove from.
|
||||||
|
* @param callback The function to remove.
|
||||||
|
*/
|
||||||
|
void entityDisposeRemove(
|
||||||
|
entitymanager_t *mgr,
|
||||||
|
const entityid_t entityId,
|
||||||
|
const entitycallback_t callback
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
#define ENTITY_COUNT_MAX 64
|
||||||
|
#define ENTITY_COMPONENT_COUNT_MAX 16
|
||||||
|
|
||||||
|
#define ENTITY_ID_INVALID 0xFF
|
||||||
|
#define COMPONENT_ID_INVALID 0xFF
|
||||||
|
|
||||||
|
typedef uint8_t entityid_t;
|
||||||
|
typedef uint8_t componentid_t;
|
||||||
|
typedef uint16_t componentindex_t;
|
||||||
|
|
||||||
|
// Forward declared here (rather than in component.h) so every entity/
|
||||||
|
// component header can reference entitymanager_t* regardless of include
|
||||||
|
// order, without needing a full entitymanager.h include.
|
||||||
|
typedef struct entitymanager_t entitymanager_t;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entitymanager.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
|
||||||
|
void entityManagerInit(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
memoryZero(mgr, sizeof(entitymanager_t));
|
||||||
|
memorySet(
|
||||||
|
mgr->entitiesWithComponent, COMPONENT_ID_INVALID,
|
||||||
|
sizeof(componentid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
consolePrint(
|
||||||
|
"Entity manager size: %zu bytes (%.2f KB)",
|
||||||
|
sizeof(entitymanager_t),
|
||||||
|
sizeof(entitymanager_t) / 1024.0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
entityid_t entityManagerAdd(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0) continue;
|
||||||
|
entityInit(mgr, i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
assertUnreachable("No more entity IDs available");
|
||||||
|
return ENTITY_ID_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityManagerUpdate(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
entityid_t i = 0;
|
||||||
|
while(i < ENTITY_COUNT_MAX) {
|
||||||
|
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0) {
|
||||||
|
entityUpdate(mgr, i);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityManagerDispose(entitymanager_t *mgr) {
|
||||||
|
assertNotNull(mgr, "Entity manager cannot be null");
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
if((mgr->entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue;
|
||||||
|
entityDispose(mgr, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user