12 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, andexternglobal. - Write a JSDoc block (
/** … */) above every declaration explaining purpose,@params, and@returns. - Only include headers that the
.hfile itself strictly requires for the types it exposes. Move everything else to the.cfile. Do not use forward declarations as a workaround — use the real include in the.cfile instead.
Implementation files (.c)
- Contain function bodies only; no declarations.
- Pull in whatever additional includes the implementation needs.
- Do not use
staticorinlineon functions. Every function, including internal helpers, must be declared in the matching.hand defined in the.cfile. Internal helpers belong near the bottom of the.cfile, not at the top with astaticqualifier.staticandinlineon functions are only appropriate when the function body is written directly inside a.hfile.staticon 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
- Add an enum value to
assetloadertype_t(before_COUNT) insrc/dusk/asset/loader/assetloader.h. - Add fields to the input/loading/output unions in
assetloader.h. - Implement
assetXxxLoaderSync,assetXxxLoaderAsync, andassetXxxDisposein a newsrc/dusk/asset/loader/xxx/directory. - Register the three callbacks in
ASSET_LOADER_CALLBACKS[]insrc/dusk/asset/loader/assetloader.c. - If user-facing, create a JS module (see below) and a
.d.tsfile.
Adding a new entity component
- Create
src/dusk/entity/component/<category>/entityMyComp.h/.cwith structentityMyComp_t,entityMyCompInit(), and optionallyentityMyCompDispose(). - Add the include to
src/dusk/entity/componentlist.hheader block. - Add a row to
src/dusk/entity/componentlist.h:This auto-generates the enum, union field, and definition entry.X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL) - If JS-facing, create the script module and
.d.ts(see below).
Adding a new script (JS) module
- 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()withscriptProtoDefineProp/scriptProtoDefineFunc/scriptProtoDefineStaticFunc.
- Declare
#includethe header insrc/dusk/script/module/modulelist.cand callmoduleMyModInit()inmoduleListInit()(andDisposeinmoduleListDispose()).- For component modules also register in
src/dusk/script/module/entity/component/modulecomponentlist.csoentity.add()returns the typed wrapper. - Create
types/<category>/mymod.d.tsand add a/// <reference path="..." />line totypes/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
varfor module-level state;constfor 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 functionandawait.
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)xor*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.cfunction 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/mirroringsrc/dusk/structure. - Use cmocka; include
dusktest.h. - Test functions:
static void test_something(void **state). - After each test, assert
memoryGetAllocatedCount() == 0to catch leaks. - Build with
-DDUSK_BUILD_TESTS=ON.