.MD remove
This commit is contained in:
@@ -1,858 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,549 +0,0 @@
|
||||
# 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
|
||||
Every C, H, and JS file starts with:
|
||||
|
||||
```c
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
```
|
||||
|
||||
JS files use `//` comment style instead.
|
||||
|
||||
---
|
||||
|
||||
## C conventions
|
||||
|
||||
### Types
|
||||
Always use the project-defined aliases instead of bare C primitives:
|
||||
|
||||
| Use | Not |
|
||||
|-----------|--------------|
|
||||
| `bool_t` | `bool` |
|
||||
| `int_t` | `int` |
|
||||
| `float_t` | `float` |
|
||||
| `char_t` | `char` |
|
||||
|
||||
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
|
||||
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
|
||||
|
||||
### Naming
|
||||
- **Functions** — snake_case, prefixed with their module:
|
||||
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
|
||||
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
|
||||
- **Macros / constants** — UPPER_SNAKE_CASE:
|
||||
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
|
||||
- **Files** — snake_case matching the primary type: `entityposition.c`,
|
||||
`moduleassetbatch.c`
|
||||
|
||||
### Header files (`.h`)
|
||||
- Use `#pragma once` — no include guards.
|
||||
- Declare every public function, `#define`, and `extern` global.
|
||||
- Write a JSDoc block (`/** … */`) above every declaration explaining
|
||||
purpose, `@param`s, and `@returns`.
|
||||
- Only include headers that the `.h` file itself strictly requires for
|
||||
the types it exposes. Move everything else to the `.c` file.
|
||||
Do not use forward declarations as a workaround — use the real
|
||||
include in the `.c` file instead.
|
||||
|
||||
### Implementation files (`.c`)
|
||||
- Contain function bodies only; no declarations.
|
||||
- Pull in whatever additional includes the implementation needs.
|
||||
- Do not use `static` or `inline` on **functions**. Every function,
|
||||
including internal helpers, must be declared in the matching `.h` and
|
||||
defined in the `.c` file. Internal helpers belong near the bottom of
|
||||
the `.c` file, not at the top with a `static` qualifier.
|
||||
`static` and `inline` on functions are only appropriate when the
|
||||
function body is written directly inside a `.h` file.
|
||||
`static` on **variables** (file-scope state) is fine and expected.
|
||||
|
||||
### Formatting
|
||||
- Hard-wrap all lines at **80 characters**.
|
||||
|
||||
### Error handling
|
||||
Return `errorret_t` from fallible functions. Use these macros:
|
||||
|
||||
```c
|
||||
errorOk(); // return success
|
||||
errorThrow("msg %d", val); // return failure with message
|
||||
errorChain(someCall()); // propagate failure, continue on success
|
||||
errorIsOk(ret) / errorIsNotOk(ret) // test a result
|
||||
errorCatch(ret); // handle + free an error
|
||||
```
|
||||
|
||||
Never return raw error codes or use `errno` for in-engine errors.
|
||||
|
||||
### Memory
|
||||
Use the project allocator — never raw `malloc`/`free`:
|
||||
|
||||
```c
|
||||
memoryAllocate(size) // allocate
|
||||
memoryFree(ptr) // free
|
||||
memoryZero(dest, size) // zero a block
|
||||
memoryCopy(dest, src, size) // copy
|
||||
```
|
||||
|
||||
### Asserts
|
||||
Prefer specific assert macros over bare `assert()`:
|
||||
|
||||
```c
|
||||
assertNotNull(ptr, "msg");
|
||||
assertTrue(cond, "msg");
|
||||
assertFalse(cond, "msg");
|
||||
assertUnreachable("msg");
|
||||
assertIsMainThread("msg");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build system
|
||||
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
|
||||
|
||||
```cmake
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
myfile.c
|
||||
)
|
||||
```
|
||||
|
||||
Never add source files to the root `CMakeLists.txt` directly.
|
||||
|
||||
---
|
||||
|
||||
## Platform support
|
||||
|
||||
### Targets
|
||||
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
||||
|
||||
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|
||||
|----------------------|-------------------|------------------|
|
||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||
|
||||
### Layer structure
|
||||
```
|
||||
src/dusk/ core, platform-agnostic game logic
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP)
|
||||
src/dusklinux/ Linux + Knulli platform impl
|
||||
src/duskpsp/ PSP platform impl
|
||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||
```
|
||||
|
||||
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
||||
it uses native GameCube/Wii rendering and input APIs.
|
||||
|
||||
### Platform guards
|
||||
Use the compile-time macros for platform-specific code:
|
||||
|
||||
```c
|
||||
#ifdef DUSK_PSP
|
||||
// PSP-only path
|
||||
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
||||
// GameCube / Wii path
|
||||
#else
|
||||
// Generic / Linux fallback
|
||||
#endif
|
||||
```
|
||||
|
||||
Additional capability macros set per-target:
|
||||
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
||||
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
||||
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
||||
|
||||
### Abstraction pattern
|
||||
Platform-specific implementations are wired in via `#define` macros in
|
||||
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
||||
the core calls through. Functions that a platform does not support are
|
||||
simply left undefined — the core guards calls with `#ifdef`.
|
||||
|
||||
### Adding platform-specific code
|
||||
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
||||
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
||||
or capability macro.
|
||||
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
||||
the platform header macros instead.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new asset loader type
|
||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
||||
`src/dusk/asset/loader/assetloader.h`.
|
||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
||||
`src/dusk/asset/loader/assetloader.c`.
|
||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new entity component
|
||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
||||
`entityMyCompDispose()`, `entityMyCompRender()`.
|
||||
2. Add the include to `src/dusk/entity/componentlist.h` header block
|
||||
(or `src/duskrpg/entity/gamecomponentlist.h` for a game-specific
|
||||
component, appended after the engine's inbuilt ones).
|
||||
3. Add a row:
|
||||
```c
|
||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
||||
```
|
||||
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).
|
||||
|
||||
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
|
||||
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`.
|
||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
||||
- 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
|
||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
||||
`scriptProtoDefineStaticFunc`.
|
||||
2. `#include` the header in
|
||||
`src/dusk/script/module/modulelist.c` and call
|
||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
||||
`moduleListDispose()`).
|
||||
3. For a component module that adds a *typed* wrapper for a specific
|
||||
component type, create
|
||||
`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
|
||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Script module type declarations
|
||||
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
||||
check whether the corresponding `types/**/*.d.ts` needs updating and
|
||||
apply any changes before finishing the task.
|
||||
|
||||
---
|
||||
|
||||
## JavaScript (asset scripts)
|
||||
- Use `var` for module-level state; `const` for values that never
|
||||
change.
|
||||
- Always use semicolons.
|
||||
- Scene objects are plain objects (`var scene = {}`) with assigned
|
||||
methods.
|
||||
- Export via `module.exports = scene`.
|
||||
- Async scene init should use `async function` and `await`.
|
||||
|
||||
---
|
||||
|
||||
## Coding style
|
||||
|
||||
### ASCII only
|
||||
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
||||
Non-ASCII characters are banned even in comments and string literals.
|
||||
Use ASCII-only substitutes instead:
|
||||
- `--` or `-` instead of `—` (em dash)
|
||||
- `->` instead of `→` (arrow)
|
||||
- `x` or `*` instead of `×` (multiplication)
|
||||
|
||||
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
||||
|
||||
### Indentation
|
||||
2 spaces. No tabs.
|
||||
|
||||
### Keyword and operator spacing
|
||||
No space between a keyword or function name and its opening parenthesis:
|
||||
|
||||
```c
|
||||
if(!ptr) return;
|
||||
for(uint8_t i = 0; i < count; i++) {
|
||||
while(entry->state != DONE) {
|
||||
switch(type) {
|
||||
sizeof(assetbatch_t)
|
||||
memoryZero(ptr, size)
|
||||
```
|
||||
|
||||
Spaces around all binary operators and after every comma:
|
||||
|
||||
```c
|
||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||
(size_t)end - (size_t)start
|
||||
foo(a, b, c)
|
||||
```
|
||||
|
||||
### Braces
|
||||
Opening brace on the **same line** as the statement (K&R style) for all
|
||||
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
||||
|
||||
```c
|
||||
void assetEntryLock(assetentry_t *entry) {
|
||||
...
|
||||
}
|
||||
|
||||
if(dirty) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Guard returns
|
||||
Short guards go on one line with no braces:
|
||||
|
||||
```c
|
||||
if(!ptr) return;
|
||||
if(!b || !b->batch) return jerry_undefined();
|
||||
if(!(flags & DIRTY)) return;
|
||||
```
|
||||
|
||||
### Blank lines
|
||||
- One blank line between functions; no blank line at the start or end of
|
||||
a function body.
|
||||
- One blank line between logical blocks inside a function body.
|
||||
- No trailing blank lines at the end of a file.
|
||||
|
||||
### Pointer placement
|
||||
`*` is attached to the variable name, not the type:
|
||||
|
||||
```c
|
||||
assetentry_t *entry
|
||||
const char_t *name
|
||||
void *ptr
|
||||
uint8_t *d = (uint8_t *)dest;
|
||||
```
|
||||
|
||||
### Casts
|
||||
Space between cast and operand:
|
||||
|
||||
```c
|
||||
(assetbatch_t *)user
|
||||
(uint8_t *)dest
|
||||
(textureformat_t)v
|
||||
```
|
||||
|
||||
### Return
|
||||
No parentheses around the return value:
|
||||
|
||||
```c
|
||||
return ptr;
|
||||
return MEMORY_POINTERS_IN_USE;
|
||||
```
|
||||
|
||||
### switch / case
|
||||
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
||||
|
||||
```c
|
||||
switch(type) {
|
||||
case ASSET_LOADER_TYPE_TEXTURE:
|
||||
descs[i].input.texture = (textureformat_t)v;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-line function signatures
|
||||
When parameters don't fit on one line, put each on its own line indented
|
||||
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
||||
its own line at column 0:
|
||||
|
||||
```c
|
||||
void assetEntryInit(
|
||||
assetentry_t *entry,
|
||||
const char_t *name,
|
||||
const assetloadertype_t type,
|
||||
assetloaderinput_t *input
|
||||
) {
|
||||
|
||||
errorret_t memoryCompare(
|
||||
const void *a,
|
||||
const void *b,
|
||||
const size_t size
|
||||
);
|
||||
```
|
||||
|
||||
### Structs and enums
|
||||
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
||||
brace and name on the same line:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
errorcode_t code;
|
||||
char_t *message;
|
||||
} errorstate_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
```
|
||||
|
||||
### Designated initialisers
|
||||
Spaces inside braces; `.field = value`:
|
||||
|
||||
```c
|
||||
jsassetentry_t e = { .entry = entry };
|
||||
assetbatchloadedpend_t init = { .batch = batch };
|
||||
```
|
||||
|
||||
### Ternary operator
|
||||
Spaces around `?` and `:`:
|
||||
|
||||
```c
|
||||
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
||||
```
|
||||
|
||||
### const placement
|
||||
`const` before the type, `*` attached to the variable:
|
||||
|
||||
```c
|
||||
const char_t *name
|
||||
const void *src
|
||||
const size_t size
|
||||
```
|
||||
|
||||
### Comments in `.c` files
|
||||
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
||||
functions follow one another with a single blank line between them.
|
||||
- Multi-line explanatory comments inside function bodies use `//` lines:
|
||||
```c
|
||||
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
||||
// so their finalizers fire before assetDispose() checks ref counts.
|
||||
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
||||
```
|
||||
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
||||
function bodies.
|
||||
|
||||
### Comments in `.h` files
|
||||
Every public declaration gets a Javadoc block (`/** … */`) with
|
||||
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
||||
above the declaration with no blank line in between.
|
||||
|
||||
---
|
||||
|
||||
## Color system
|
||||
|
||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
||||
|
||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
||||
The script emits four `#define` variants per color plus a bare alias:
|
||||
|
||||
```
|
||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
||||
COLOR_<NAME> COLOR_<NAME>_4B
|
||||
```
|
||||
|
||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
||||
|
||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
||||
hand-edit the generated header.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
- Tests live in `test/` mirroring `src/dusk/` structure.
|
||||
- Use cmocka; include `dusktest.h`.
|
||||
- Test functions: `static void test_something(void **state)`.
|
||||
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
||||
leaks.
|
||||
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
||||
@@ -1,260 +0,0 @@
|
||||
# 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
@@ -1,105 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,120 +0,0 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user