# 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/` in the matching subsystem folder. - Gate any core call-site with the appropriate `#ifdef DUSK_` 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//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//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/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/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//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//mymod.d.ts` and add a `/// ` 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__4B color4b(r8, g8, b8, a8) // default alias target COLOR__3B color3b(r8, g8, b8) COLOR__3F color3f(rf, gf, bf) COLOR__4F color4f(rf, gf, bf, af) COLOR_ COLOR__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`.