433 lines
12 KiB
Markdown
433 lines
12 KiB
Markdown
# Dusk — Claude Code rules
|
||
|
||
## 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 |
|
||
| `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:
|
||
|
||
```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<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`:
|
||
```c
|
||
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+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.
|
||
|
||
---
|
||
|
||
## 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`.
|