Files
dusk/CLAUDE.md
T
2026-06-07 19:51:54 -05:00

11 KiB

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.

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

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

  • Block comments that describe a section use the divider style:
    /* ---- Public API ---- */
    
  • 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.