Files
dusk/CLAUDE.md
T

12 KiB
Raw Blame History

Dusk — Claude Code rules

File headers

Every C, H, and JS file starts with:

/**
 * 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, @params, 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:

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:

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():

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:

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
vita DUSK_VITA PlayStation Vita
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, Vita)
src/dusksdl2/      SDL2 window + input (Linux, Knulli, PSP, Vita)
src/dusklinux/     Linux + Knulli platform impl
src/duskpsp/       PSP platform impl
src/duskvita/      Vita 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:

#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().
  2. Add the include to src/dusk/entity/componentlist.h header block.
  3. Add a row to src/dusk/entity/componentlist.h:
    X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
    
    This auto-generates the enum, union field, and definition entry.
  4. If JS-facing, create the script module and .d.ts (see below).

Adding a new script (JS) module

  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.
    • 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 component modules also register in src/dusk/script/module/entity/component/modulecomponentlist.c so entity.add() returns the typed wrapper.
  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+0000U+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:

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:

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:

void assetEntryLock(assetentry_t *entry) {
  ...
}

if(dirty) {
  ...
} else {
  ...
}

Guard returns

Short guards go on one line with no braces:

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:

assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;

Casts

Space between cast and operand:

(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v

Return

No parentheses around the return value:

return ptr;
return MEMORY_POINTERS_IN_USE;

switch / case

case indented 2 spaces from switch; body indented 2 more from case:

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:

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:

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:

jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };

Ternary operator

Spaces around ? and ::

const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;

const placement

const before the type, * attached to the variable:

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:
    // 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.


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.