550 lines
18 KiB
Markdown
550 lines
18 KiB
Markdown
# 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`.
|