Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5184064a26 | |||
| 6a43363539 | |||
| b9195fbbad | |||
| f715ad2176 |
@@ -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.
|
||||
@@ -33,74 +33,3 @@ jobs:
|
||||
libssl-dev
|
||||
- name: Run tests
|
||||
run: ./scripts/test-linux.sh
|
||||
|
||||
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
|
||||
# confirm the emulator doesn't crash. Not yet verified against a real
|
||||
# runner (no CI run has exercised these) -- continue-on-error so a
|
||||
# flaky/broken emulator step doesn't block the required Linux test job.
|
||||
run-tests-gamecube-dolphin:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ghcr.io/extremscorner/libogc2:latest
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
run: apt-get update && apt-get install -y nodejs
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install additional dependencies
|
||||
run: |
|
||||
apt-get install -y \
|
||||
python3-pip python3-polib python3-pil \
|
||||
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||
dolphin-emu xvfb
|
||||
dkp-pacman -Syu --noconfirm
|
||||
dkp-pacman -S --needed --noconfirm \
|
||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||
- name: Build GameCube ISO and boot it in Dolphin
|
||||
run: ./scripts/test-gamecube-dolphin.sh
|
||||
|
||||
run-tests-wii-dolphin:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ghcr.io/extremscorner/libogc2:latest
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
run: apt-get update && apt-get install -y nodejs
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install additional dependencies
|
||||
run: |
|
||||
apt-get install -y \
|
||||
python3-pip python3-polib python3-pil \
|
||||
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||
dolphin-emu xvfb
|
||||
dkp-pacman -Syu --noconfirm
|
||||
dkp-pacman -S --needed --noconfirm \
|
||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||
- name: Build Wii ISO and boot it in Dolphin
|
||||
run: ./scripts/test-wii-dolphin.sh
|
||||
|
||||
run-tests-psp-ppsspp:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pspdev
|
||||
uses: ./.github/actions/setup-pspdev
|
||||
- name: Install PPSSPPHeadless build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
|
||||
- name: Build PPSSPPHeadless
|
||||
run: |
|
||||
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
|
||||
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
|
||||
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
|
||||
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
|
||||
run: ./scripts/test-psp-ppsspp.sh
|
||||
|
||||
@@ -1,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.
|
||||
@@ -1,5 +1,6 @@
|
||||
# Dusk
|
||||
RPG Game Project, small and able to run on a PSP.
|
||||
# Documentation
|
||||
- [Scripting](docs/SCRIPTING.md) — writing gameplay logic in JavaScript.
|
||||
- [UI](docs/UI.md) — building buttons/menus/etc from engine/game C code.
|
||||
|
||||
# Building
|
||||
Each build target has different requirements. You can take a look at the git
|
||||
|
||||
-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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
var Actions;
|
||||
var camera, cameraPosition;
|
||||
var cube, cubePosition, cubeRenderable, cubeMesh;
|
||||
var ground, groundPosition, groundRenderable;
|
||||
|
||||
// init() is called via scriptManagerCallGlobal(), which pumps the asset
|
||||
// system + job queue until any promise it returns settles - so it's safe
|
||||
// to await include() here even though this runs before the main loop.
|
||||
async function init() {
|
||||
Actions = await include("input.js");
|
||||
|
||||
camera = new Entity();
|
||||
cameraPosition = camera.add(POSITION);
|
||||
camera.add(CAMERA);
|
||||
cameraPosition.position = new Vec3(3, 3, -6);
|
||||
cameraPosition.lookAt(new Vec3(0, 0, 0));
|
||||
|
||||
cube = new Entity();
|
||||
cubePosition = cube.add(POSITION);
|
||||
cubeRenderable = cube.add(RENDERABLE);
|
||||
cubeMesh = Mesh.createCube();
|
||||
cubeRenderable.mesh = cubeMesh;
|
||||
cubeRenderable.color = Color.red();
|
||||
|
||||
ground = new Entity();
|
||||
groundPosition = ground.add(POSITION);
|
||||
groundRenderable = ground.add(RENDERABLE);
|
||||
groundRenderable.mesh = Mesh.createCube();
|
||||
groundRenderable.color = Color.dark_gray();
|
||||
}
|
||||
|
||||
// Runs every frame (including dynamic/interpolation frames) - use for
|
||||
// smooth, purely presentational animation.
|
||||
function update() {
|
||||
cubePosition.rotation.y += TIME.delta * 1.5;
|
||||
cubePosition.rotation.x += TIME.delta * 0.7;
|
||||
}
|
||||
|
||||
// Runs once per fixed timestep only - use for gameplay logic that should
|
||||
// be deterministic and independent of display refresh rate.
|
||||
function fixedUpdate() {
|
||||
}
|
||||
|
||||
function deinit() {
|
||||
cube.dispose();
|
||||
camera.dispose();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Binds physical buttons to abstract actions, then exports the action
|
||||
// constants so other scripts don't need to know raw INPUT_ACTION_* names.
|
||||
Input.bind("w", INPUT_ACTION_UP);
|
||||
Input.bind("s", INPUT_ACTION_DOWN);
|
||||
Input.bind("a", INPUT_ACTION_LEFT);
|
||||
Input.bind("d", INPUT_ACTION_RIGHT);
|
||||
Input.bind("space", INPUT_ACTION_ACCEPT);
|
||||
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
|
||||
|
||||
if(typeof INPUT_GAMEPAD !== "undefined") {
|
||||
Input.bind("gamepad_up", INPUT_ACTION_UP);
|
||||
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
|
||||
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
|
||||
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
|
||||
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
|
||||
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
|
||||
}
|
||||
|
||||
module = {
|
||||
UP: INPUT_ACTION_UP,
|
||||
DOWN: INPUT_ACTION_DOWN,
|
||||
LEFT: INPUT_ACTION_LEFT,
|
||||
RIGHT: INPUT_ACTION_RIGHT,
|
||||
ACCEPT: INPUT_ACTION_ACCEPT,
|
||||
CANCEL: INPUT_ACTION_CANCEL,
|
||||
RAGEQUIT: INPUT_ACTION_RAGEQUIT
|
||||
};
|
||||
+2
-66
@@ -1,66 +1,2 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: en\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Welcome"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "Input"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "Display"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "Deadzone"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "Language"
|
||||
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "Takes effect after restarting the application."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "Apply"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "Discard unsaved changes?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Characters"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Items"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
msgid "item.potato.name"
|
||||
msgstr "Potato"
|
||||
|
||||
msgid "item.apple.name"
|
||||
msgstr "Apple"
|
||||
msgid "test.string"
|
||||
msgstr "This is a test string"
|
||||
@@ -1,70 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: es\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Bienvenido"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "Pantalla"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "Deadzone"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "Se aplica después de reiniciar la aplicación."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "¿Descartar los cambios no guardados?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Personajes"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Objetos"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Configuración"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "Poción"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "Papa"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "Manzana"
|
||||
@@ -1,70 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: ja\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"歓迎"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "一般"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "入力"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "表示"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "オーディオ"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "デッドゾーン"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "言語"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "アプリケーションを再起動すると適用されます。"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "適用"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "未保存の変更を破棄しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "キャラクター"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "アイテム"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "ポーション"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "ジャガイモ"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "リンゴ"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_5.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_6.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_7.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_1_8.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_1.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_2.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_3.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"mesh": "meshes/buildings/house_2_4.dmf",
|
||||
"color": [255, 0, 255, 255]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user