8.6 KiB
Coding Style
All source is C11. Everything below is derived from the existing codebase — match it exactly.
File structure
Headers (.h)
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "direct/dependency.h"
#pragma oncealways, never#ifndefguards.- No blank line between the license block and
#pragma once. - One blank line between
#pragma onceand the first#include.
Sources (.c)
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "thisfile.h"
#include "assert/assert.h"
#include "util/memory.h"
- First include is always the matching
.hfor this.cfile. - Remaining includes follow with no separator unless logically grouped (then one blank line between groups — see Include order).
Include order
In .c files, include in this order with a blank line between each group:
- The matching header (e.g.
#include "entity.h") - Core utilities (
assert/assert.h,util/memory.h,util/math.h, etc.) - Engine subsystems (
display/...,input/..., etc.) - Domain subsystems (
rpg/...,scene/..., etc.)
In .h files, only include what the header directly requires. Never include more than necessary to make the type definitions in that header compile.
All include paths are relative to src/dusk/ (the root include directory). Use the full path:
#include "rpg/overworld/map.h" // correct
#include "map.h" // wrong
Line length
80-character limit. Break before it, not after.
Multi-parameter function signatures break one param per line, with 2-space indent, closing ) on its own line before {:
errorret_t textureInit(
texture_t *texture,
const int32_t width,
const int32_t height,
const textureformat_t format,
const texturedata_t data
) {
Same rule for calls that don't fit on one line:
assertTrue(
data.paletted.palette->count ==
mathNextPowTwo(data.paletted.palette->count),
"Palette color count must be a power of 2"
);
Indentation and spacing
- 2 spaces per indent level. No tabs.
- No space between a control keyword and its
(:if(x) { // correct if (x) { // wrong - No space between a function name and its
(in either declarations or calls. - Opening brace on the same line:
void entityUpdate(entity_t *entity) { if(x) { for(int i = 0; i < n; i++) { - Closing brace always on its own line, except
} else {and} while(...). - One blank line between function definitions in
.cfiles. - No trailing whitespace.
Naming
| Kind | Convention | Example |
|---|---|---|
| Types (struct/union/typedef) | snake_case_t |
entity_t, worldpos_t |
| Struct tags | struct name_s |
struct entity_s |
| Union tags | union name_u |
union texturedata_u |
| Enum tags (when typedef'd separately) | name_enum_t |
entitytype_enum_t |
| Functions | subsystemVerb (camelCase, noun-first) |
entityInit, mapGetTile |
| Macro constants | UPPER_SNAKE_CASE |
CHUNK_WIDTH, FIXED_ONE |
| Function-like macros | camelCase (same as functions) |
errorThrow, assertNotNull |
| Global subsystem instances | UPPER_SNAKE_CASE |
ENGINE, MAP, ENTITIES |
| Local variables | camelCase |
tileNew, spriteCount |
| Parameters | camelCase |
texture, worldPos |
Subsystem prefix always comes first in function names: textureInit, shaderBind, spriteBatchFlush. The verb describes the action: Init, Update, Dispose, Get, Set, Is, etc.
Typedefs
Structs
typedef struct {
uint8_t id;
entitytype_t type;
} entity_t;
Use a named tag (struct entity_s) only when forward declaration is required:
typedef struct entity_s {
// ...
} entity_t;
Unions
typedef union texturedata_u {
struct {
uint8_t *indices;
palette_t *palette;
} paletted;
color_t *rgbaColors;
} texturedata_t;
Enums
When the enum values need to be a compact integer (common for arrays and flags), declare the enum separately and typedef an integer type:
typedef enum {
ENTITY_TYPE_NULL,
ENTITY_TYPE_PLAYER,
ENTITY_TYPE_NPC,
ENTITY_TYPE_COUNT
} entitytype_enum_t;
typedef uint8_t entitytype_t; // actual type used everywhere
Always include _NULL as the first value (zero) and _COUNT as the last value.
#define constants
All-caps, underscores. Wrap multi-token expressions in parentheses:
#define CHUNK_WIDTH 16
#define CHUNK_HEIGHT CHUNK_WIDTH
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH)
Multi-line macros: backslash continuation, body indented 2 spaces, closing line has no backslash:
#define errorThrow(message, ...) \
return errorThrowImpl(\
&ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \
##__VA_ARGS__ \
)
const usage
Mark every pointer and value parameter const unless the function modifies it:
void entityTurn(entity_t *entity, const entitydir_t direction);
errorret_t textureInit(texture_t *texture, const int32_t width, ...);
entity_t *entity is non-const because the function writes to it; direction is const because it is read-only.
void in no-argument functions
Use (void) in definitions and declarations of zero-parameter functions:
errorret_t engineUpdate(void);
errorret_t spriteBatchFlush(void);
Global subsystem state
Each subsystem exposes a single global instance declared extern in the header and defined (once) in the .c file:
// entity.h
extern entity_t ENTITIES[ENTITY_COUNT];
// entity.c
entity_t ENTITIES[ENTITY_COUNT];
Never define a subsystem global as static in a header.
Assertions
Place assertions at the very top of a function, before any logic:
void entityInit(entity_t *entity, const entitytype_t type) {
assertNotNull(entity, "Entity pointer cannot be NULL");
assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type");
// ... actual logic
}
Available assertion macros (from assert/assert.h):
assertNotNull(ptr, msg)assertNull(ptr, msg)assertTrue(expr, msg)assertFalse(expr, msg)assertUnreachable(msg)assertStringEqual(a, b, msg)assertIsMainThread(msg)/assertNotMainThread(msg)
Error handling style
Functions that can fail return errorret_t. Three patterns:
// Propagate a child call's failure and return from this function:
errorChain(someCall());
// Return success:
errorOk();
// Return failure:
errorThrow("Descriptive message %s", variable);
errorChain is used inline — do not capture the result first:
errorChain(textureInitPlatform(texture, width, height, format, data)); // correct
errorret_t r = textureInitPlatform(...); errorChain(r); // wrong
Struct initialization
Use C99 designated initializers for any struct literal with more than one field:
static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
[ENTITY_TYPE_NULL] = { NULL },
[ENTITY_TYPE_PLAYER] = {
.init = playerInit,
.movement = playerInput
},
};
shadermaterial_t material = {
.unlit = {
.color = COLOR_WHITE,
.texture = NULL
}
};
Fixed-size array iteration
Prefer pointer arithmetic with do/while over index loops for iterating through fixed global arrays:
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) continue;
// ...
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
Use for loops when an index variable is actually needed.
Comments
Comments explain why, not what. One short inline comment is fine; multi-line block comments for non-obvious invariants only.
// Walking up a ramp — only the direction the ramp faces is valid.
if(tileIsRamp(tileCurrent) && ...) {
Section labels inside long functions are acceptable:
// Chunks
{
...
}
// Entities
{
...
}
Doc comments on public functions use Javadoc style with @param / @return:
/**
* Gets the tile at the given world position.
*
* @param position The world position.
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
*/
tile_t mapGetTile(const worldpos_t position);
Platform-conditional code
Use the DUSK_* compile-definition macros set by cmake/targets/<target>.cmake:
#ifdef DUSK_THREAD_PTHREAD
#include "thread/thread.h"
extern pthread_t ASSERT_MAIN_THREAD_ID;
#endif
Never use #ifdef __linux__, #ifdef _WIN32, etc. directly — go through the engine macros.