383 lines
8.6 KiB
Markdown
383 lines
8.6 KiB
Markdown
# Coding Style
|
|
|
|
All source is C11. Everything below is derived from the existing codebase — match it exactly.
|
|
|
|
---
|
|
|
|
## File structure
|
|
|
|
### Headers (`.h`)
|
|
|
|
```c
|
|
/**
|
|
* 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 once` always, never `#ifndef` guards.
|
|
- No blank line between the license block and `#pragma once`.
|
|
- One blank line between `#pragma once` and the first `#include`.
|
|
|
|
### Sources (`.c`)
|
|
|
|
```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 `.h` for this `.c` file.
|
|
- Remaining includes follow with no separator unless logically grouped (then one blank line between groups — see [Include order](#include-order)).
|
|
|
|
---
|
|
|
|
## Include order
|
|
|
|
In `.c` files, include in this order with a blank line between each group:
|
|
|
|
1. The matching header (e.g. `#include "entity.h"`)
|
|
2. Core utilities (`assert/assert.h`, `util/memory.h`, `util/math.h`, etc.)
|
|
3. Engine subsystems (`display/...`, `input/...`, etc.)
|
|
4. 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:
|
|
```c
|
|
#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 `{`:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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 `(`:
|
|
```c
|
|
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:
|
|
```c
|
|
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 `.c` files.
|
|
- 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
|
|
|
|
```c
|
|
typedef struct {
|
|
uint8_t id;
|
|
entitytype_t type;
|
|
} entity_t;
|
|
```
|
|
|
|
Use a named tag (`struct entity_s`) only when forward declaration is required:
|
|
|
|
```c
|
|
typedef struct entity_s {
|
|
// ...
|
|
} entity_t;
|
|
```
|
|
|
|
### Unions
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
#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:
|
|
|
|
```c
|
|
#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:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
// 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:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
// 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:
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
|
|
[ENTITY_TYPE_NULL] = { NULL },
|
|
|
|
[ENTITY_TYPE_PLAYER] = {
|
|
.init = playerInit,
|
|
.movement = playerInput
|
|
},
|
|
};
|
|
```
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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.
|
|
|
|
```c
|
|
// Walking up a ramp — only the direction the ramp faces is valid.
|
|
if(tileIsRamp(tileCurrent) && ...) {
|
|
```
|
|
|
|
Section labels inside long functions are acceptable:
|
|
|
|
```c
|
|
// Chunks
|
|
{
|
|
...
|
|
}
|
|
|
|
// Entities
|
|
{
|
|
...
|
|
}
|
|
```
|
|
|
|
Doc comments on public functions use Javadoc style with `@param` / `@return`:
|
|
|
|
```c
|
|
/**
|
|
* 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`:
|
|
|
|
```c
|
|
#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.
|