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