1 Commits

Author SHA1 Message Date
YourWishes 71c5756e71 Prog on knulli 2026-03-16 11:45:36 -05:00
704 changed files with 9100 additions and 40235 deletions
-88
View File
@@ -1,88 +0,0 @@
# Animation System
Source: `src/dusk/animation/`
## Overview
The animation system provides time-based keyframe interpolation with
pluggable easing functions. It is intentionally minimal -- no skeleton,
no blending, no state machine. Animations produce a single `float_t`
value at a given time, which callers apply to whatever property they
are animating.
## Keyframes (`keyframe.h`)
```c
typedef struct {
float_t time; // time in seconds this keyframe is at
float_t value; // the value at this keyframe
easingtype_t easing; // easing applied between this frame and the next
} keyframe_t;
```
## Animation (`animation.h`)
```c
typedef struct {
keyframe_t *keyframes; // caller-owned array
uint16_t keyframeCount;
} animation_t;
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
);
float_t animationGetValue(animation_t *anim, float_t time);
// Returns the interpolated value at the given time.
// Before the first keyframe: returns the first keyframe's value.
// After the last keyframe: returns the last keyframe's value.
```
## Easing functions (`easing.h`)
```c
typedef float_t (*easingfn_t)(float_t t); // t in [0, 1], out in [0, 1]
extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT];
float_t easingApply(easingtype_t type, float_t t);
```
Available easing types:
```
EASING_LINEAR
EASING_IN_SINE EASING_OUT_SINE EASING_IN_OUT_SINE
EASING_IN_QUAD EASING_OUT_QUAD EASING_IN_OUT_QUAD
EASING_IN_CUBIC EASING_OUT_CUBIC EASING_IN_OUT_CUBIC
EASING_IN_QUART EASING_OUT_QUART EASING_IN_OUT_QUART
EASING_IN_BACK EASING_OUT_BACK EASING_IN_OUT_BACK
```
## Usage pattern
```c
// Declare keyframes statically (no allocation):
static keyframe_t kfs[] = {
{ .time = 0.0f, .value = 0.0f, .easing = EASING_OUT_CUBIC },
{ .time = 1.0f, .value = 1.0f, .easing = EASING_LINEAR },
};
animation_t anim;
animationInit(&anim, kfs, 2);
// In update loop:
float_t alpha = animationGetValue(&anim, TIME.time);
// Apply alpha to whatever is being animated.
```
## Design notes
- Keyframe arrays are caller-owned and not copied. Use static or
long-lived arrays; do not allocate per-frame.
- The system has no notion of looping -- wrap `time` with `fmodf` if
you need a repeating animation.
- For multi-property animations, use multiple `animation_t` instances
sharing the same time source.
-125
View File
@@ -1,125 +0,0 @@
# Asset System
Source: `src/dusk/asset/`
## Overview
All game assets are packed into a single ZIP archive named `dusk.dsk`
(`ASSET_FILE_NAME`). The asset system loads entries from this archive
asynchronously on a background thread, caches them, and provides
synchronous blocking access when an asset is required immediately.
## Key limits
| Constant | Value | Meaning |
|----------|-------|---------|
| `ASSET_LOADING_COUNT_MAX` | 4 | Concurrent in-flight loads |
| `ASSET_ENTRY_COUNT_MAX` | 128 | Cached entries |
## Top-level API (`asset.h`)
```c
errorret_t assetInit(); // Open dusk.dsk, start background thread
void assetUpdate(); // Dispatch completed-load callbacks (main thread)
errorret_t assetDispose(); // Wait for loads, close archive
assetentry_t *assetGetEntry(
const char_t *path,
assetloadertype_t type,
assetloaderinput_t *input
); // Get (or create) a cache entry; does NOT start loading
errorret_t assetRequireLoaded(assetentry_t *entry);
// Block the calling thread until this entry is fully loaded.
// Only safe to call from the main thread.
void assetLock(assetentry_t *entry);
void assetUnlock(assetentry_t *entry);
// Reference counting. Lock before using loaded data; unlock when done.
// The entry will not be evicted while locked.
```
## Asset entry states
Each cache entry goes through a state machine:
```
IDLE -> QUEUED -> READING (async) -> PROCESSING (sync, main thread) -> LOADED
-> ERROR
```
- **READING** runs on the background loader thread (file I/O).
- **PROCESSING** runs on the main thread (GPU uploads, parsing finalization).
- Once LOADED, data is available in `entry->data`.
## Loader types
Loader types are registered in the `ASSET_LOADER_CALLBACKS[]` table.
Each type implements three callbacks: `loadSync`, `loadAsync`, `dispose`.
| `assetloadertype_t` | Data read | Description |
|---------------------|-----------|-------------|
| `ASSET_LOADER_TYPE_TEXTURE` | STB image | Loads image bytes async, creates GPU texture sync |
| `ASSET_LOADER_TYPE_TILESET` | `.dtf` binary | Custom tile format (magic, version, grid, UVs) |
| `ASSET_LOADER_TYPE_MESH` | `.stl` | STL mesh with configurable axis orientation |
| `ASSET_LOADER_TYPE_JSON` | yyjson | Up to 256 KB; parsed async |
| `ASSET_LOADER_TYPE_LOCALE` | Gettext `.po` | PO parser with plural-form expression evaluation |
| `ASSET_LOADER_TYPE_SCRIPT` | JS source | JerryScript module |
## Adding a new loader type
1. Add an enum value before `_COUNT` in `assetloadertype_t`
(`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 `src/dusk/asset/loader/xxx/`.
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
`src/dusk/asset/loader/assetloader.c`.
5. If user-facing, create a JS module and a `.d.ts` file (see `CLAUDE.md`).
## Asset batch (`assetbatch.h`)
`assetbatch_t` groups multiple asset requests into a single logical
load. All entries in the batch start loading concurrently. The batch
fires a completion callback once every entry has reached LOADED (or
ERROR).
```c
assetbatch_t batch;
assetBatchInit(&batch, entries, count, onComplete, user);
assetBatchStart(&batch);
// ... later, after assetUpdate() fires the callback ...
assetBatchDispose(&batch);
```
## Usage pattern
```c
// 1. Get or create the cache entry (no I/O yet).
assetentry_t *tex = assetGetEntry(
"textures/hero.png",
ASSET_LOADER_TYPE_TEXTURE,
NULL
);
assetLock(tex);
// 2. Option A -- non-blocking: check tex->state each frame.
// Option B -- blocking (main thread only):
errorChain(assetRequireLoaded(tex));
// 3. Use the loaded data.
texture_t *t = &tex->data.texture;
// 4. Release when done.
assetUnlock(tex);
```
## Error macros (inside loader implementations)
```c
assetLoaderErrorThrow("msg %d", val); // errorThrow equivalent
assetLoaderErrorChain(someCall()); // errorChain equivalent
```
Use these instead of the bare error macros inside loader callbacks so
that failures include the loader context in the stack trace.
-116
View File
@@ -1,116 +0,0 @@
# Build System
Dusk uses CMake exclusively. Every source subdirectory owns its own
`CMakeLists.txt`; the root file only wires them together.
## Golden rule
**Never add source files to the root `CMakeLists.txt` directly.**
Every `.c` file is registered in the `CMakeLists.txt` that lives in
the same directory (or a direct parent within the same module):
```cmake
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
myfile.c
)
```
## Configuration variables
| Variable | Purpose |
|------------------------|----------------------------------------------|
| `DUSK_TARGET_SYSTEM` | Selects the platform (see `.claude/platforms.md`) |
| `DUSK_BUILD_TESTS` | Enables the test suite (`ON` / `OFF`) |
| `CMAKE_TOOLCHAIN_FILE` | Cross-compiler toolchain for console targets |
| `CMAKE_BUILD_TYPE` | `Debug` / `Release` / `RelWithDebInfo` |
## Typical configure + build
```sh
# Linux debug build
cmake -B build -DDUSK_TARGET_SYSTEM=linux -DCMAKE_BUILD_TYPE=Debug
cmake --build build
# Linux with tests
cmake -B build \
-DDUSK_TARGET_SYSTEM=linux \
-DDUSK_BUILD_TESTS=ON \
-DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build
```
## Module layout convention
Each logical module under `src/` gets its own directory:
```
src/dusk/ Platform-agnostic core
src/dusk<platform>/ Platform-specific impl (one dir per target)
```
Within a module, subdirectories mirror subsystem boundaries
(`asset/`, `entity/`, `script/`, etc.). Each subdirectory has its own
`CMakeLists.txt` that is `add_subdirectory()`-included by its parent.
## Adding a new source file
1. Create `src/.../myfile.c` (and `myfile.h` if needed).
2. Open the `CMakeLists.txt` in the same directory.
3. Add `myfile.c` to the `target_sources(...)` block.
4. Do **not** touch any parent or root `CMakeLists.txt`.
## Platform-conditional sources
Wrap platform-only files in a generator expression or `if()` block:
```cmake
if(DUSK_TARGET_SYSTEM STREQUAL "psp")
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
mypspfile.c
)
endif()
```
## Embedding JS files (`dusk_embed_js`)
Source: `cmake/modules/duskjs2c.cmake`
The `dusk_embed_js()` CMake function embeds a `.js` source file as a
C string constant in a generated header. It is used to ship script
module code alongside the engine binary without a separate file load.
```cmake
dusk_embed_js(
TARGET ${DUSK_LIBRARY_TARGET_NAME}
JS_FILE path/to/mymodule.js
# NAME is optional; defaults to uppercase stem + "_JS"
# e.g. "mymodule.js" -> "MYMODULE_JS"
NAME MY_CUSTOM_NAME
)
```
The generated header is placed in
`${DUSK_GENERATED_HEADERS_DIR}/<stem>_js.h` and defines:
```c
static const char MY_CUSTOM_NAME[] = "... js source ...";
static const size_t MY_CUSTOM_NAME_SIZE = sizeof(MY_CUSTOM_NAME) - 1;
```
Under the hood it calls `python -m tools.js2c` from the repo root.
The header is generated at build time; include it in the `.c` file
that registers the JS module, then pass `NAME` and `NAME_SIZE` to
`jerry_eval()` (or the equivalent module load helper).
## Tests
- Test files live in `test/` mirroring the `src/dusk/` structure.
- Enable with `-DDUSK_BUILD_TESTS=ON`.
- Uses cmocka; include `dusktest.h` in every test file.
- Every test must assert `memoryGetAllocatedCount() == 0` at teardown
to catch allocator leaks.
- Test function signature: `static void test_something(void **state)`
-92
View File
@@ -1,92 +0,0 @@
# Console
Source: `src/dusk/console/`
---
## Overview
The console is a lightweight in-engine debug overlay. It maintains a
fixed-size ring buffer of text lines and can render them to the screen
as an overlay. On Linux (where `DUSK_CONSOLE_POSIX` is defined), it
also reads commands from stdin on a background thread, allowing
interactive input during development without pausing the game loop.
---
## Limits
| Constant | Value |
|----------|-------|
| `CONSOLE_LINE_MAX` | 512 chars per line |
| `CONSOLE_HISTORY_MAX` | 16 lines in the ring buffer |
| `CONSOLE_EXEC_BUFFER_MAX` | 32 pending execution slots |
---
## Global state
```c
typedef struct {
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; // ring buffer
bool_t visible;
#ifdef DUSK_CONSOLE_POSIX
threadmutex_t printMutex; // guards ring buffer on POSIX targets
#endif
} console_t;
extern console_t CONSOLE;
```
---
## API
```c
void consoleInit(void);
// printf-style print into the ring buffer.
// Thread-safe on POSIX (uses printMutex).
void consolePrint(const char_t *message, ...);
// Process any queued script input lines. Must be called from the
// main thread once per frame.
void consoleUpdate(void);
// Draw the ring buffer as an overlay in UI space.
errorret_t consoleDraw(void);
void consoleDispose(void);
```
---
## POSIX stdin mode (`DUSK_CONSOLE_POSIX`)
On Linux only (`DUSK_CONSOLE_POSIX`), the console launches a background
thread that polls stdin using `poll()` at 75 ms intervals
(`CONSOLE_POSIX_POLL_RATE`). Lines typed at the terminal are queued
and dispatched on the main thread by `consoleUpdate()`.
This allows typing commands or JS expressions into the running game
without blocking the render loop. The ring buffer is protected by
`CONSOLE.printMutex` so `consolePrint` is safe to call from either
thread.
POSIX mode is not available on PSP, Vita, or Dolphin targets.
---
## Notes
- `CONSOLE.visible` controls whether `consoleDraw` renders anything.
Toggle it from a debug keybind or always set it to `true` in
development builds.
- `consolePrint` wraps lines at `CONSOLE_LINE_MAX` -- long messages are
truncated. Use multiple calls for long output.
- The console is distinct from the error system (`errorThrow` /
`errorPrint`). Use `consolePrint` for diagnostic output; use
`errorThrow` for recoverable failures.
- Log calls (`logDebug`, `logError`) go to the platform's debug output
(stdout/stderr), not to the console ring buffer.
-88
View File
@@ -1,88 +0,0 @@
# Display -- Color
Source: `build/generated/display/color.h` (generated at build time)
---
## Overview
`color.h` is a generated header. It is not hand-edited -- the source
lives in the CMake generator that produces it from platform configuration.
Include it via `"display/color.h"`.
---
## Types
```c
typedef float_t colorchannelf_t; // float channel [0.0, 1.0]
typedef uint8_t colorchannel8_t; // 8-bit channel [0, 255]
typedef struct { colorchannelf_t r, g, b; } color3f_t;
typedef struct { colorchannelf_t r, g, b, a; } color4f_t;
typedef struct { colorchannel8_t r, g, b; } color3b_t;
typedef struct { colorchannel8_t r, g, b, a; } color4b_t;
typedef color4b_t color_t; // default: RGBA uint8
```
`color_t` is always `color4b_t` -- four `uint8_t` channels.
Most engine APIs (text rendering, UI, mesh vertices) take `color_t`.
---
## Constructors
```c
color3f(r, g, b) // float RGB
color4f(r, g, b, a) // float RGBA
color3b(r, g, b) // uint8 RGB
color4b(r, g, b, a) // uint8 RGBA
color(r, g, b, a) // alias for color4b
colorHex(0xRRGGBBAA) // unpack 32-bit hex into color4b
```
---
## Predefined constants
Each named colour comes in four variants: `_4B` (uint8 RGBA, default),
`_3B` (uint8 RGB), `_4F` (float RGBA), `_3F` (float RGB). The bare
name (e.g. `COLOR_BLACK`) always resolves to the `_4B` variant.
| Constant | RGBA (uint8) |
|----------|-------------|
| `COLOR_BLACK` | 0, 0, 0, 255 |
| `COLOR_WHITE` | 255, 255, 255, 255 |
| `COLOR_RED` | 255, 0, 0, 255 |
| `COLOR_GREEN` | 0, 255, 0, 255 |
| `COLOR_BLUE` | 0, 0, 255, 255 |
| `COLOR_YELLOW` | 255, 255, 0, 255 |
| `COLOR_CYAN` | 0, 255, 255, 255 |
| `COLOR_MAGENTA` | 255, 0, 255, 255 |
| `COLOR_ORANGE` | 255, 165, 0, 255 |
| `COLOR_PURPLE` | 127, 0, 127, 255 |
| `COLOR_GRAY` | 127, 127, 127, 255 |
| `COLOR_LIGHT_GRAY` | 191, 191, 191, 255 |
| `COLOR_DARK_GRAY` | 63, 63, 63, 255 |
| `COLOR_BROWN` | 153, 102, 51, 255 |
| `COLOR_PINK` | 255, 191, 204, 255 |
| `COLOR_LIME` | 191, 255, 0, 255 |
| `COLOR_NAVY` | 0, 0, 127, 255 |
| `COLOR_TEAL` | 0, 127, 127, 255 |
| `COLOR_CORNFLOWER_BLUE` | 99, 147, 237, 255 |
| `COLOR_TRANSPARENT` | 0, 0, 0, 0 |
| `COLOR_TRANSPARENT_WHITE` | 255, 255, 255, 0 |
| `COLOR_TRANSPARENT_BLACK` | 0, 0, 0, 0 |
---
## Notes
- `color_t` uses premultiplied-friendly `uint8_t` channels for
compatibility with both OpenGL texture uploads and GX on Dolphin.
- For shader uniforms that expect float colours, convert manually:
`(float_t)col.r / 255.0f` etc.
- The `COLOR_SCRIPT` macro is a C string containing the JS Color class
static methods. It is concatenated into the embedded JS runtime
during module init (see `.claude/script.md`).
-81
View File
@@ -1,81 +0,0 @@
# Display -- Screen, Framebuffer, and Size Modes
Source: `src/dusk/display/`
See also: `.claude/display-texture.md`, `.claude/display-shader.md`
---
## Display size modes
Two compile-time configurations exist:
### Fixed size (`DUSK_DISPLAY_WIDTH` + `DUSK_DISPLAY_HEIGHT`)
The render resolution is constant. Set both defines at CMake configure
time. `SCREEN.width` and `SCREEN.height` are compile-time constants.
### Dynamic size (`DUSK_DISPLAY_SIZE_DYNAMIC`)
The window can be resized (desktop targets). Instead of fixed defines,
set `DUSK_DISPLAY_WIDTH_DEFAULT` and `DUSK_DISPLAY_HEIGHT_DEFAULT`.
The screen system renders to an internal framebuffer at a logical
resolution and scales/letterboxes to the actual window.
Screen modes available only with `DUSK_DISPLAY_SIZE_DYNAMIC`:
| Mode | Behaviour |
|------|-----------|
| `SCREEN_MODE_BACKBUFFER` | Render directly to the window backbuffer |
| `SCREEN_MODE_FIXED_SIZE` | Fixed pixel dimensions; letterboxed |
| `SCREEN_MODE_ASPECT_RATIO` | Maintain aspect ratio at all cost |
| `SCREEN_MODE_FIXED_HEIGHT` | Fixed height; width expands/contracts |
| `SCREEN_MODE_FIXED_WIDTH` | Fixed width; height expands/contracts |
| `SCREEN_MODE_FIXED_VIEWPORT_HEIGHT` | Fixed height at higher resolution |
Configure via `SCREEN.mode` and the corresponding union field before
calling `screenInit()`.
---
## Framebuffer (`framebuffer.h`)
```c
extern framebuffer_t FRAMEBUFFER_BACKBUFFER;
extern const framebuffer_t *FRAMEBUFFER_BOUND;
// Bind/unbind:
frameBufferBind(fb);
frameBufferUnbind();
// Clear (pass flag combination):
frameBufferClear(fb, FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH);
// Dimensions of the currently bound framebuffer:
int32_t w = frameBufferGetWidth();
int32_t h = frameBufferGetHeight();
```
`FRAMEBUFFER_BACKBUFFER` is the window surface. Off-screen framebuffers
are used by the screen system when `DUSK_DISPLAY_SIZE_DYNAMIC` is on.
---
## Screen (`screen.h`)
```c
extern screen_t SCREEN;
// SCREEN.width, SCREEN.height -- logical render dimensions
// SCREEN.aspect -- width / height
// SCREEN.background -- clear colour
errorret_t screenInit();
errorret_t screenBind(); // call before rendering game content
errorret_t screenUnbind(); // call after game content, before UI
errorret_t screenRender(); // blit the internal framebuffer to the window
errorret_t screenDispose();
```
`screenBind` / `screenUnbind` / `screenRender` are called by the scene
system automatically each frame. Game code normally does not call them
directly -- use the JS `render()` hook instead.
-240
View File
@@ -1,240 +0,0 @@
# Display -- Mesh
Source: `src/dusk/display/mesh/`
See also: `.claude/display-spritebatch.md`, `.claude/display-shader.md`
---
## Overview
The mesh system wraps platform-specific GPU geometry buffers behind a
common `mesh_t` type. Geometry is described as an array of
`meshvertex_t` values and a primitive type. The platform layer
(`meshplatform.h`) provides the concrete buffer and draw implementation
(VBO on OpenGL, display list or immediate-mode on Dolphin GX).
---
## Vertex format (`meshvertex.h`)
```c
typedef struct {
#if MESH_ENABLE_COLOR
color_t color; // optional per-vertex colour (disabled by default)
#endif
float_t uv[2]; // texture coordinates (U, V)
float_t pos[3]; // position (X, Y, Z)
} meshvertex_t;
```
`MESH_ENABLE_COLOR` is a compile-time flag (default 0). Enable it with
`-DMESH_ENABLE_COLOR=1` at CMake configure time if per-vertex colouring
is needed; be aware this changes the struct size and breaks binary
compatibility with pre-built mesh data.
---
## Core API (`mesh.h`)
```c
// Platform alias -- do not use meshplatform_t directly.
typedef meshplatform_t mesh_t;
typedef meshprimitivetypeplatform_t meshprimitivetype_t;
// Upload vertices to the GPU. Must be called from the main thread.
errorret_t meshInit(
mesh_t *mesh,
const meshprimitivetype_t primitiveType,
const int32_t vertexCount,
const meshvertex_t *vertices
);
// Flush a range of updated vertices to the GPU (modern targets only).
// vertexCount == -1 flushes all vertices.
errorret_t meshFlush(
mesh_t *mesh,
const int32_t vertexOffset,
const int32_t vertexCount
);
// Draw the mesh. vertexCount == -1 draws all vertices.
errorret_t meshDraw(
const mesh_t *mesh,
const int32_t vertexOffset,
const int32_t vertexCount
);
// Compute the axis-aligned bounding box.
void meshGetBounds(const mesh_t *mesh, vec3 outMin, vec3 outMax);
int32_t meshGetVertexCount(const mesh_t *mesh);
errorret_t meshDispose(mesh_t *mesh);
```
On constrained targets (GameCube/Wii) `meshFlush` is a no-op -- the
hardware reads vertices from main memory directly. Always call it on
desktop/mobile targets after modifying vertex data.
---
## Primitive generators
Each generator provides:
- A `*Buffer(vertices, ...)` function that writes into a caller-supplied
`meshvertex_t` array (no allocation).
- A global pre-built `*_MESH_SIMPLE` singleton + `*_MESH_SIMPLE_VERTICES`
array initialised at engine startup (for common one-off uses).
All generators use CCW winding and `MESH_PRIMITIVE_TYPE_TRIANGLES`.
### Quad (`quad.h`)
```c
#define QUAD_VERTEX_COUNT 6 // two triangles
// 2D quad in XY plane:
void quadBuffer(
meshvertex_t *vertices,
const float_t minX, const float_t minY,
const float_t maxX, const float_t maxY,
const float_t u0, const float_t v0,
const float_t u1, const float_t v1
);
// 3D quad using full vec3 min/max:
void quadBuffer3D(
meshvertex_t *vertices,
const vec3 min, const vec3 max,
const vec2 uvMin, const vec2 uvMax
);
extern mesh_t QUAD_MESH_SIMPLE;
```
The SpriteBatch is built on `quadBuffer3D` internally.
### Cube (`cube.h`)
```c
#define CUBE_VERTEX_COUNT 36 // 6 faces x 6 vertices
// Axis-aligned box from min to max:
void cubeBuffer(
meshvertex_t *vertices,
const vec3 min, const vec3 max
);
extern mesh_t CUBE_MESH_SIMPLE; // unit cube (0,0,0) to (1,1,1)
```
### Plane (`plane.h`)
```c
#define PLANE_VERTEX_COUNT 6
typedef enum {
PLANE_AXIS_XY, // flat in XY, normal along +Z (billboard / wall face)
PLANE_AXIS_XZ, // flat in XZ, normal along +Y (ground / floor)
PLANE_AXIS_YZ, // flat in YZ, normal along +X (side wall)
} planeaxis_t;
void planeBuffer(
meshvertex_t *vertices,
const planeaxis_t axis,
const vec3 min, const vec3 max,
const vec2 uvMin, const vec2 uvMax
);
extern mesh_t PLANE_MESH_SIMPLE; // unit XZ plane (0,0,0) to (1,0,1)
```
### Sphere (`sphere.h`)
```c
#define SPHERE_STACKS 8
#define SPHERE_SECTORS 16
#define SPHERE_VERTEX_COUNT (SPHERE_STACKS * SPHERE_SECTORS * 6)
void sphereBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const int32_t stacks,
const int32_t sectors
);
extern mesh_t SPHERE_MESH_SIMPLE; // unit sphere centered at (0,0,0), r=0.5
```
### Capsule (`capsule.h`)
```c
#define CAPSULE_CAP_RINGS 4
#define CAPSULE_SECTORS 16
// Total vertex count = (2 * capRings + 1) * sectors * 6
void capsuleBuffer(
meshvertex_t *vertices,
const vec3 center,
const float_t radius,
const float_t halfHeight, // half-height of the cylindrical section only
const int32_t capRings,
const int32_t sectors
);
extern mesh_t CAPSULE_MESH_SIMPLE; // r=0.5, halfHeight=0.5 (total h=2.0)
```
The long axis is always Y. This mirrors the physics capsule body (see
`.claude/physics.md`).
### Triangular prism (`triprism.h`)
```c
#define TRIPRISM_VERTEX_COUNT 24
// Cross-section triangle defined by three 2D points in XY;
// extruded along Z from minZ to maxZ.
void triPrismBuffer(
meshvertex_t *vertices,
const float_t x0, const float_t y0,
const float_t x1, const float_t y1,
const float_t x2, const float_t y2,
const float_t minZ, const float_t maxZ
);
extern mesh_t TRIPRISM_MESH_SIMPLE;
// Unit prism: triangle (0,0),(1,0),(0.5,1) extruded z=0 to z=1.
```
---
## Custom dynamic mesh
If you need to update geometry each frame (e.g. a procedural mesh):
```c
static meshvertex_t myVerts[MY_VERT_COUNT];
static mesh_t myMesh;
// On init:
// Fill myVerts, then:
errorChain(meshInit(&myMesh, MESH_PRIMITIVE_TYPE_TRIANGLES,
MY_VERT_COUNT, myVerts));
// Each frame (after modifying myVerts):
errorChain(meshFlush(&myMesh, 0, -1));
errorChain(meshDraw(&myMesh, 0, -1));
```
---
## Notes
- `meshInit` must be called on the **main thread** (GPU upload).
- `meshFlush` is required on OpenGL targets when vertices change
after init. It is a no-op on Dolphin.
- All `_MESH_SIMPLE` globals are initialised during engine startup --
do not call `meshInit` on them manually.
-86
View File
@@ -1,86 +0,0 @@
# Display -- Shader, Material, and Display State
Source: `src/dusk/display/`
See also: `.claude/display-core.md`, `.claude/display-texture.md`
---
## Shader (`shader.h` + `shaderlist.h`)
Shaders are platform-abstracted. The current shader list is defined
in `shaderlist.h`. Currently only one shader is implemented:
| Enum | Description |
|------|-------------|
| `SHADER_LIST_SHADER_UNLIT` | Unlit / flat colour + texture shader |
```c
extern shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT];
// SHADER_LIST_DEFS[n].shader is the platform shader object.
// Bind a shader before drawing:
errorret_t shaderBind(shader_t *shader);
// Upload a mat4 uniform by name:
errorret_t shaderSetMatrix(shader_t *shader, const char_t *name, mat4 m);
```
Adding a new shader means adding an entry to `shaderlist.h`, providing
platform-specific vertex/fragment sources, and implementing the
corresponding material type in `shadermaterial_t`.
---
## Shader material (`shadermaterial.h`)
`shadermaterial_t` is a union over per-shader material structs.
Currently contains only the unlit material:
```c
typedef union shadermaterial_u {
shaderunlitmaterial_t unlit;
} shadermaterial_t;
```
`shaderunlitmaterial_t` fields:
```c
typedef struct {
color_t color; // tint colour (multiplied with the texture sample)
texture_t *texture; // NULL uses TEXTURE_WHITE (solid colour draw)
} shaderunlitmaterial_t;
```
The shader exposes uniforms `u_Proj`, `u_View`, `u_Model` (mat4),
`u_Texture` (sampler), and `u_Color` (vec4). They are uploaded via
`shaderUnlitSetMaterial(shader, material)`.
A global singleton `SHADER_UNLIT` is the live shader object;
`SHADER_UNLIT_DEFINITION` is its platform definition descriptor.
To use a shader material on a renderable entity:
1. Set `renderable.type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL`.
2. Set `renderable.data.material.shaderType` to the desired
`shaderlistshadertype_t` value (e.g. `SHADER_LIST_SHADER_UNLIT`).
3. Fill in the corresponding union field:
`renderable.data.material.material.unlit`.
4. Set `renderable.data.material.state.flags` for rasterizer state.
---
## Display state (`displaystate.h`)
`displaystate_t` controls per-draw rasterizer state flags:
```c
DISPLAY_STATE_FLAG_CULL // back-face culling
DISPLAY_STATE_FLAG_DEPTH_TEST // depth testing
DISPLAY_STATE_FLAG_BLEND // alpha blending
```
Set flags via `data.material.state.flags` on the renderable's material.
The default for an uninitialised state is all flags clear (no culling,
no depth test, no blending). Most opaque geometry should set at least
`CULL | DEPTH_TEST`.
-133
View File
@@ -1,133 +0,0 @@
# Display -- SpriteBatch
Source: `src/dusk/display/spritebatch/`
See also: `.claude/display-mesh.md`, `.claude/display-texture.md`
---
## Overview
The SpriteBatch is the primary 2D rendering primitive. It accumulates
axis-aligned quads (sprites) into a shared vertex buffer and draws them
in batches. All 2D rendering in the engine -- UI frames, text, tilemaps,
HUD -- goes through the global `SPRITEBATCH`.
The batch flushes automatically when the per-flush limit is reached, or
explicitly via `spriteBatchFlush()`.
---
## Limits
| Constant | Value | Meaning |
|----------|-------|---------|
| `SPRITEBATCH_SPRITES_MAX` | 512 | Total sprites in the vertex buffer |
| `SPRITEBATCH_FLUSH_COUNT` | 16 | Number of auto-flush segments |
| `SPRITEBATCH_SPRITES_MAX_PER_FLUSH` | 32 | Sprites per auto-flush segment |
| `SPRITEBATCH_VERTEX_COUNT` | 3072 | Total vertices (512 * QUAD_VERTEX_COUNT) |
---
## Sprite structure
```c
typedef struct {
vec3 min; // minimum XYZ corner of the quad in world/screen space
vec3 max; // maximum XYZ corner of the quad in world/screen space
vec2 uvMin; // minimum UV (top-left in [0,1] texture space)
vec2 uvMax; // maximum UV (bottom-right in [0,1] texture space)
} spritebatchsprite_t;
```
Z in `min` and `max` controls draw depth (further from camera = higher Z
in a typical orthographic setup). For flat 2D, set `min.z = max.z = 0`.
---
## SpriteBatch struct
```c
typedef struct {
mesh_t mesh;
int32_t spriteCount;
int32_t spriteFlush;
shader_t *shader;
shadermaterial_t material;
} spritebatch_t;
extern spritebatch_t SPRITEBATCH;
extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT];
```
`SPRITEBATCH_VERTICES` is a separate global (not embedded in the struct)
for platform alignment requirements.
---
## API
```c
errorret_t spriteBatchInit();
errorret_t spriteBatchDispose();
// Clear the buffer and reset state. Call before starting a new batch.
void spriteBatchClear();
// Append sprites to the buffer. Flushes automatically when the per-flush
// segment fills. shader + material are used on the next flush.
errorret_t spriteBatchBuffer(
const spritebatchsprite_t *sprites,
const uint32_t count,
shader_t *shader,
const shadermaterial_t material
);
// Upload and draw all buffered sprites. Binds shader and applies
// material if set. No-op if the buffer is empty.
errorret_t spriteBatchFlush();
```
---
## Typical usage
```c
// Beginning of a 2D render pass:
spriteBatchClear();
// Build sprites (e.g. via tilesetTileGetUV, then fill spritebatchsprite_t):
spritebatchsprite_t s;
glm_vec3_copy((vec3){ x, y, 0 }, s.min);
glm_vec3_copy((vec3){ x + w, y + h, 0 }, s.max);
glm_vec2_copy(uvMin, s.uvMin);
glm_vec2_copy(uvMax, s.uvMax);
shadermaterial_t mat = { .unlit = { .texture = myTexture } };
spriteBatchBuffer(&s, 1, myShader, mat);
// End of pass -- flush remaining sprites:
spriteBatchFlush();
```
---
## Relationship to other systems
- **Text rendering** (`textDraw`) internally calls `spriteBatchBuffer`
for each glyph and requires a final `spriteBatchFlush()` after drawing.
- **UI frames** (`uiFrameDraw`) push 9 quads to the batch without
flushing -- the caller or `uitextboxDraw` is responsible for the flush.
- **ECS renderables** of type `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` are
drawn via the spritebatch in the scene render pipeline.
---
## Notes
- `spriteBatchBuffer` changes the batch's `shader` and `material` fields.
If you mix different shaders or textures in one batch, add an explicit
`spriteBatchFlush()` call between groups to avoid draws with the wrong
material.
- The vertex buffer is a static global -- `SPRITEBATCH_VERTICES` must
not be written from multiple threads.
-115
View File
@@ -1,115 +0,0 @@
# Display -- Text Rendering
Source: `src/dusk/display/text/`
See also: `.claude/display-spritebatch.md`, `.claude/display-texture.md`
---
## Overview
Text rendering is layered on top of the SpriteBatch. Each character
maps to a glyph tile in a bitmap font atlas; `textDraw` builds the
corresponding `spritebatchsprite_t` values and pushes them to the
global `SPRITEBATCH`. The caller is responsible for flushing the batch.
---
## Font type (`font.h`)
```c
typedef struct {
texture_t *texture; // glyph atlas texture
tileset_t *tileset; // grid describing glyph size + UV layout
} font_t;
```
Both pointers are caller-owned. The text system does not allocate or
free them.
```c
extern font_t FONT_DEFAULT;
```
`FONT_DEFAULT` is the engine's built-in bitmap font. It is initialised
during `textInit()` and available for the engine lifetime.
---
## Character range
```c
#define TEXT_CHAR_START '!' // ASCII 33
```
The glyph atlas begins at `'!'` (ASCII 33). Characters below this value
-- space, control characters -- are handled specially:
- `' '` (space) advances the cursor by one tile width without drawing.
- Characters below `TEXT_CHAR_START` other than space are skipped.
---
## API (`text.h`)
```c
// Initialises the text system and FONT_DEFAULT.
errorret_t textInit(void);
// Disposes of the text system.
errorret_t textDispose(void);
// Draw a null-terminated string at (x, y) in screen/world space.
// Pushes sprites to SPRITEBATCH. Caller must call spriteBatchFlush()
// after all text has been drawn.
errorret_t textDraw(
const float_t x,
const float_t y,
const char_t *text,
const color_t color,
font_t *font
);
// Measure the bounding box of a string without drawing it.
void textMeasure(
const char_t *text,
const font_t *font,
int32_t *outWidth,
int32_t *outHeight
);
// Low-level: build a single glyph sprite at position pos.
// Returns a spritebatchsprite_t ready for spriteBatchBuffer.
spritebatchsprite_t textGetSprite(
const vec2 pos,
const char_t c,
const font_t *font
);
```
---
## Typical usage
```c
// Inside a render callback:
errorChain(textDraw(10.0f, 10.0f, "Hello", COLOR_WHITE, &FONT_DEFAULT));
errorChain(spriteBatchFlush());
```
If you are also drawing UI frames or other sprites in the same pass,
batch all the `textDraw` and `spriteBatchBuffer` calls first, then call
`spriteBatchFlush()` once at the end.
---
## Notes
- Text coordinates are in the same space as the scene render (screen
space for UI, or world space if placed in the scene).
- `textMeasure` returns pixel dimensions based on the font's
`tileset.tileWidth` and `tileset.tileHeight`. Use it to centre or
right-align text before drawing.
- For UI-attached text (dialogue, labels), prefer the `uitextbox_t`
system which handles word-wrap and paging automatically
(see `.claude/ui.md`).
-86
View File
@@ -1,86 +0,0 @@
# Display -- Texture, Tileset, and Font
Source: `src/dusk/display/`
See also: `.claude/display-core.md`, `.claude/display-shader.md`
---
## Texture (`texture.h`)
```c
extern texture_t TEXTURE_WHITE; // 4x4 opaque white; always available
errorret_t textureInit(
texture_t *texture,
int32_t width,
int32_t height,
textureformat_t format,
texturedata_t data
);
errorret_t textureDispose(texture_t *texture);
```
`textureformat_t` and `texture_t` are platform aliases
(`textureformatplatform_t`, `textureplatform_t`). On OpenGL targets
the format maps to GL texture format constants.
`texturedata_t` is a union:
- `.paletted.indices` + `.paletted.palette` -- for paletted formats
- `.rgbaColors` -- for RGBA formats
### Texture rules
- Dimensions must be powers of two on PSP and GameCube/Wii. Use
`mathNextPowTwo` from `util/math.h` if needed.
- Texture upload must happen on the main thread. In the asset loader,
this means `loadSync` (not `loadAsync`).
- `TEXTURE_WHITE` is always available without loading; use it as a
placeholder or for untextured geometry.
---
## Tileset (`tileset.h`)
A tileset subdivides a texture into a uniform grid of tiles.
```c
typedef struct {
uint16_t tileWidth, tileHeight;
uint16_t tileCount;
uint16_t columns, rows;
vec2 uv; // UV size per tile (pre-computed from grid dimensions)
} tileset_t;
// Get UV rect for a tile by index:
void tilesetTileGetUV(
const tileset_t *ts, uint16_t tileIndex, vec4 outUV
);
// Get UV rect for a tile by grid position:
void tilesetPositionGetUV(
const tileset_t *ts, uint16_t column, uint16_t row, vec4 outUV
);
```
`outUV` is `{u, v, u2, v2}` in normalised [0, 1] texture space.
Tilesets are loaded from `.dtf` binary files via
`ASSET_LOADER_TYPE_TILESET`. The DTF format stores tile width/height,
grid dimensions, and per-tile UV offsets (magic + version header).
---
## Font (`font.h`)
```c
typedef struct {
texture_t *texture;
tileset_t *tileset;
} font_t;
```
A font is a tileset-backed texture atlas where each tile is a character
glyph. No heap allocation -- both pointers are owned by the caller.
Character lookup is by glyph index into the tileset grid. Rendering is
handled by the spritebatch system using the tileset UV helpers.
-23
View File
@@ -1,23 +0,0 @@
# Display System
Source: `src/dusk/display/`
## Overview
The display system is a platform-abstracted rendering layer. Each
subsystem (texture, shader, framebuffer, screen) is defined by a core
header that requires the platform layer to provide concrete types and
hook macros. The OpenGL implementation lives in `src/duskgl/`; the
Dolphin (GX) implementation in `src/duskdolphin/`.
## Subsystem documentation
| Subsystem | Reference |
|-----------|-----------|
| Screen size modes, framebuffer, screen | `.claude/display-core.md` |
| Texture, tileset, font | `.claude/display-texture.md` |
| Shader, shader material, display state | `.claude/display-shader.md` |
| Mesh, vertex format, primitive generators | `.claude/display-mesh.md` |
| SpriteBatch (2D quad renderer) | `.claude/display-spritebatch.md` |
| Text rendering, font, FONT_DEFAULT | `.claude/display-text.md` |
| Color types, macros, named constants | `.claude/display-color.md` |
-179
View File
@@ -1,179 +0,0 @@
# Entity Component System (ECS)
Source: `src/dusk/entity/`
## Core concepts
- **Entity** (`entityid_t` = `uint8_t`) -- a numeric ID. No data of
its own; just an index into the entity manager pool.
- **Component** -- a plain data struct registered in `componentlist.h`.
Stores state; no behaviour.
- **System** -- functions that query all entities with a given component
type and act on them each tick.
## Hard limits
| Constant | Value |
|----------|-------|
| `ENTITY_COUNT_MAX` | 64 |
| `ENTITY_COMPONENT_COUNT_MAX` | 16 per entity |
| Total component slots | 1024 (64 x 16) |
| Update callbacks per entity | 5 |
| Dispose callbacks per entity | 5 |
`ENTITY_ID_INVALID = 0xFF`, `COMPONENT_ID_INVALID = 0xFF`.
## Global state
```c
extern entitymanager_t ENTITY_MANAGER;
// .entities[64] -- entity structs
// .components[1024] -- all component data (entity * 16 + comp)
// .entitiesWithComponent -- O(1) lookup indexed by [type * 64 + entityId]
```
## Entity lifecycle
```c
entityid_t id = entityManagerAdd(); // reserve first inactive slot
entityInit(id); // zero the entity, mark active
componentid_t posId = entityAddComponent(id, COMPONENT_TYPE_POSITION);
componentid_t rendId = entityAddComponent(id, COMPONENT_TYPE_RENDERABLE);
// Per-frame update (called by entityManagerUpdate):
entityUpdate(id);
// Cleanup:
entityDispose(id); // dispose components, mark inactive
entityDisposeDeep(id); // dispose self + entire position hierarchy
```
## Component registration (X-macro)
All component types are declared in a single table in
`src/dusk/entity/componentlist.h`:
```c
X(NAME, type_t, fieldName, initFn, disposeFn, renderFn)
```
This generates:
- `COMPONENT_TYPE_NAME` enum value
- Union field `fieldName` in `componentdata_t`
- Entry in `COMPONENT_DEFINITIONS[]` with `init` / `dispose` /
`render` function pointers (any may be `NULL`)
Current registered components:
| Enum suffix | Struct | Notes |
|-------------|--------|-------|
| `POSITION` | `entityposition_t` | Transform + parent/child hierarchy |
| `CAMERA` | `entitycamera_t` | View matrix setup |
| `RENDERABLE` | `entityrenderable_t` | Sprite batch, shader material, or custom draw |
| `PHYSICS` | `entityphysics_t` | Physics body (see `.claude/physics.md`) |
| `TRIGGER` | `entitytrigger_t` | Collision trigger zone |
## Accessing component data
```c
void *componentGetData(
entityid_t entityId,
componentid_t componentId,
componenttype_t type
);
// Returns pointer into the preallocated components pool.
// Never NULL for a valid (id, type) pair.
```
Querying all entities with a given type:
```c
entityid_t ids[ENTITY_COUNT_MAX];
componentid_t comps[ENTITY_COUNT_MAX];
entityid_t count = componentGetEntitiesWithComponent(
COMPONENT_TYPE_PHYSICS, ids, comps
);
```
## Adding a new component -- checklist
1. Create `src/dusk/entity/component/<category>/entity<Name>.h/.c`.
- Struct: `entity<Name>_t`
- `entity<Name>Init(entityid_t, componentid_t)` (required)
- `entity<Name>Dispose(entityid_t, componentid_t)` (if needed)
2. `#include` the new header in the header block of `componentlist.h`.
3. Add an `X(...)` row in `componentlist.h`.
4. If JS-facing, add a script module (see `CLAUDE.md`).
## Position component (`entityposition_t`)
The position component implements the transform hierarchy and uses lazy
evaluation with dirty flags to avoid redundant matrix rebuilds.
**Dirty flags:**
| Flag | Meaning |
|------|---------|
| `ENTITY_POSITION_FLAG_PRS_DIRTY` | Cached position/rotation/scale stale vs localTransform |
| `ENTITY_POSITION_FLAG_ROTATION_DIRTY` | Rotation columns of localTransform stale |
| `ENTITY_POSITION_FLAG_POSITION_DIRTY` | Position column of localTransform stale |
| `ENTITY_POSITION_FLAG_WORLD_DIRTY` | World matrix stale |
**Hierarchy:** up to 8 children per entity. `entityPositionSetParent()`
reparents and maintains the child list. `entityDisposeDeep()` /
`entityPositionDisposeDeep()` recursively disposes the entire subtree.
**Key functions:**
```c
// Local space getters/setters (mark local dirty):
entityPositionGetLocalPosition / SetLocalPosition
entityPositionGetLocalRotation / SetLocalRotation
entityPositionGetLocalScale / SetLocalScale
// World space getters/setters (ensure world updated):
entityPositionGetWorldPosition / SetWorldPosition
entityPositionGetWorldRotation / SetWorldRotation
entityPositionGetWorldScale / SetWorldScale
entityPositionSetParent(entityId, parentEntityId, parentComponentId);
entityPositionLookAt(entityId, componentId, eye, target, up);
entityPositionRebuild(pos); // force immediate matrix rebuild
```
## Renderable component (`entityrenderable_t`)
Three rendering modes selected via `entityrenderabletype_t`:
| Mode | Description |
|------|-------------|
| `ENTITY_RENDERABLE_TYPE_CUSTOM` | User-supplied `draw` callback |
| `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` | Up to 64 sprites + texture |
| `ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL` | Up to 8 meshes, shader, material, display state |
Default on init: shader material (white, unlit, depth-tested cube).
`priority` (int8_t) controls render order; 0 = automatic.
## Callback hooks on entities
Entities support up to 5 registered update callbacks and 5 dispose
callbacks. These are used by systems that need per-entity ticks without
building a full query loop every frame:
```c
entityUpdateAdd(entityId, updateFn, componentId, user);
entityUpdateRemove(entityId, updateFn);
entityDisposeAdd(entityId, disposeFn, componentId, user);
entityDisposeRemove(entityId, disposeFn);
```
## Design rules
- Components store **data only**. No logic in a component struct or
its init beyond setting default values.
- Keep components small and focused.
- Cross-component access is fine from a system, but a component must
never hold a pointer to another component -- use entity IDs.
- Systems must not assume component ordering. Use `componentGetEntitiesWithComponent`.
-95
View File
@@ -1,95 +0,0 @@
# Engine, System, and Log
Sources: `src/dusk/engine/`, `src/dusk/system/`, `src/dusk/log/`
---
## Engine (`engine.h`)
The engine owns the top-level init / update / dispose loop. Every
platform's `main()` calls these three functions in order.
```c
extern engine_t ENGINE;
// ENGINE.running -- false causes the main loop to exit
// ENGINE.argc / ENGINE.argv -- passed from main()
// ENGINE.version -- version string
errorret_t engineInit(int32_t argc, const char_t **argv);
errorret_t engineUpdate(void); // called once per tick
errorret_t engineDispose(void);
```
`engineInit` initialises subsystems in order: system, log, assert,
display, time, asset, input, physics, script, etc.
`engineUpdate` steps each subsystem: time, input, physics, script, ECS
entities, rendering, audio, network, asset completion callbacks.
`engineDispose` shuts everything down in reverse order.
**To exit gracefully:** set `ENGINE.running = false` -- the platform
main loop checks this each tick and calls `engineDispose` before
returning.
---
## System (`system.h`)
The system module is initialised very early (before most other
subsystems) and provides two things:
### Platform identity
```c
typedef enum { SYSTEM_PLATFORM_LIST } systemplatform_t;
systemplatform_t systemGetPlatform(void);
```
Platform names come from `systemplatformlist.h` via an X-macro. This
lets game code query the runtime platform when compile-time guards are
not sufficient (e.g. serializing platform name to a log).
### Dialog blocking
Some platforms (PSP, Wii) show OS-level dialogs (Wi-Fi setup, save
management) that block the normal game loop. The system module exposes
the current dialog state so the engine main loop can adjust:
```c
typedef enum {
SYSTEM_DIALOG_TYPE_NONE,
SYSTEM_DIALOG_TYPE_RENDER_BLOCKING, // skip render but still tick
SYSTEM_DIALOG_TYPE_TICK_BLOCKING, // skip both render and tick
} systemdialogtype_t;
systemdialogtype_t systemGetActiveDialogType(void);
```
`engineUpdate` checks this before calling render / update code.
Most platforms always return `SYSTEM_DIALOG_TYPE_NONE`.
---
## Log (`log.h`)
Simple printf-style logging with two levels. Always use these instead
of `printf` / `fprintf`.
```c
void logDebug(const char_t *message, ...);
// Writes to the debug output (stdout on desktop, platform console
// on handhelds). No-op in release builds on some platforms.
void logError(const char_t *message, ...);
// Writes to the error output. On some platforms (PSP) this may
// pause execution to ensure the message is visible before continuing.
```
**Do not** use `logDebug` / `logError` for structured error handling --
that is what `errorThrow` / `errorChain` are for. Log calls are for
human-readable diagnostics only.
The error system calls `logError` internally when printing a caught
error via `errorPrint()`.
-133
View File
@@ -1,133 +0,0 @@
# Error Handling System
Source: `src/dusk/error/`
## Philosophy
Error handling is return-value based. Functions that can fail return
`errorret_t`. There are no exceptions, `errno`, `setjmp`, or global
error codes. Each thread has its own isolated error state.
## Types
```c
typedef uint8_t errorcode_t;
typedef struct {
errorcode_t code;
char_t *message; // allocated; freed by errorCatch
char_t *lines; // call-stack trace; allocated; freed by errorCatch
} errorstate_t;
typedef struct {
errorcode_t code;
errorstate_t *state; // NULL on success
} errorret_t;
```
**Constants:** `ERROR_OK = 0`, `ERROR_NOT_OK = 1`.
Error state is thread-local:
```c
extern THREAD_LOCAL errorstate_t ERROR_STATE;
```
Each thread has its own `ERROR_STATE` so concurrent errors never
interfere.
## Macros
### Throwing an error
```c
errorThrow("message %d", value);
// Throws with ERROR_NOT_OK, captures __FILE__ / __func__ / __LINE__.
errorThrowWithCode(code, "message %d", value);
// Same but with a specific error code.
```
Both macros **return from the current function** with an `errorret_t`.
Do not call them in void functions.
### Propagating up the call stack
```c
errorChain(someCall());
```
If `someCall()` returned an error, appends the current location to the
stack trace and **returns** that error from the current function.
If `someCall()` returned success, execution continues normally.
### Returning success
```c
errorOk();
```
Returns `errorret_t` with `code == ERROR_OK` and asserts the thread's
`ERROR_STATE` is clean (no leftover error). Must be the last statement
in a fallible function.
### Inspecting a result
```c
if(errorIsOk(ret)) { ... }
if(errorIsNotOk(ret)) { ... }
```
### Cleaning up
```c
errorCatch(ret);
```
Frees `ret.state->message` and `ret.state->lines`, resets the thread's
`ERROR_STATE.code` to `ERROR_OK`. Safe to call on a success return
(no-op). **Always call `errorCatch` on errors you are handling** --
otherwise the allocated message and stack-trace leak.
### Logging
```c
errorPrint(ret); // prints code + message + stack trace, returns ret
```
## Typical patterns
### Fallible function
```c
errorret_t myFunction(int_t x) {
if(x < 0) errorThrow("x must be non-negative, got %d", x);
errorChain(someOtherFallibleCall(x));
errorOk();
}
```
### Caller that handles errors
```c
errorret_t ret = myFunction(-1);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
// ... fallback logic ...
}
```
### Stack trace accumulation
Each `errorChain()` call appends a line to `ret.state->lines` in the
format ` at file:line in function\n`. A deeply chained error produces
a full call path readable from `ret.state->lines`.
## What NOT to do
- Do not use raw `errno` for in-engine errors.
- Do not return an error code integer -- always return `errorret_t`.
- Do not ignore an error return without calling `errorCatch` on it if
you are not propagating it.
- Do not mix assertions with error handling. Assertions are for
programmer mistakes; `errorThrow` is for expected failure paths.
-109
View File
@@ -1,109 +0,0 @@
# Event System
Source: `src/dusk/event/`
## Overview
The event system is a simple publish-subscribe mechanism backed by
caller-owned static arrays. There is no heap allocation in the event
system itself -- the caller provides the backing storage.
## API
```c
typedef void (*eventcallback_t)(void *params, void *user);
typedef struct {
eventcallback_t *callbacks;
void **users;
size_t size;
uint32_t count;
} event_t;
```
### Initialise
```c
void eventInit(
event_t *event,
eventcallback_t *callbacks,
void **users,
size_t size
);
```
`callbacks` and `users` are caller-owned arrays of length `size`. Both
are zeroed by `eventInit`. `users` may be `NULL` if no subscriber needs
a user pointer.
### Subscribe / unsubscribe
```c
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
void eventUnsubscribe(event_t *event, eventcallback_t callback);
```
The same `(callback, user)` pair may only be subscribed once --
`eventSubscribe` asserts on a duplicate. `eventUnsubscribe` is a no-op
if the pair is not found. Unsubscribing uses swap-with-last to keep the
array packed; ordering is not preserved.
### Fire
```c
void eventInvoke(const event_t *event, void *params);
```
Calls every subscriber in registration order, passing `params` and
each subscriber's `user` pointer.
## Usage pattern
Declare the backing arrays alongside the event struct, typically as
struct fields or static variables:
```c
#define MY_EVENT_CAPACITY 4
typedef struct {
event_t onComplete;
eventcallback_t _completeCbs[MY_EVENT_CAPACITY];
void *_completeUsers[MY_EVENT_CAPACITY];
} mystate_t;
// Init:
eventInit(
&state.onComplete,
state._completeCbs,
state._completeUsers,
MY_EVENT_CAPACITY
);
// Publish:
eventInvoke(&state.onComplete, &someParams);
// Subscribe from outside:
eventSubscribe(&state.onComplete, myHandler, myUserPtr);
```
## Constraints
- Capacity is fixed at init time. Exceeding it is a runtime assertion.
- Subscriber order is not stable after an unsubscribe.
- `eventInvoke` is synchronous -- all callbacks run on the calling
thread before it returns.
- Do not subscribe or unsubscribe from inside a callback -- the array
may shift during iteration.
## Where events are used
| Subsystem | Event | Fires when |
|-----------|-------|-----------|
| `inputactiondata_t` | `onPressed`, `onReleased` | Action button state changes |
| `uitextbox_t` | `onPageComplete` | Typewriter scroll reveals the full page |
| `uitextbox_t` | `onLastPage` | Last page is fully scrolled |
| `uifullbox_t` | `onTransitionEnd` | Colour transition animation completes |
| `uiloading_t` | `onShow` / `onHide` | Loading indicator fade completes |
| Asset system | `assetbatch_t` callback | All entries in a batch reach LOADED/ERROR |
See `.claude/ui.md` for the UI event details.
-187
View File
@@ -1,187 +0,0 @@
# Input System
Source: `src/dusk/input/`, platform layers in `src/dusk<platform>/input/`
## Architecture
The input system has two layers:
1. **Action layer** (`inputaction_t`) -- named gameplay inputs, e.g.
UP, DOWN, ACCEPT, CANCEL. This is what game code reads.
2. **Button layer** (`inputbutton_t`) -- physical hardware inputs, e.g.
keyboard key, gamepad button, analog axis, mouse axis. Multiple
buttons can bind to the same action (the highest value wins).
The platform layer implements two hooks:
- `inputUpdatePlatform()` -- read hardware state once per frame
- `inputButtonGetValuePlatform()` -- return the analog value [0.0, 1.0]
for a given button
## Defined actions
Actions are defined in `src/dusk/input/input.csv` and code-generated
into the `inputaction_t` enum. Current values:
| Constant | Meaning |
|----------|---------|
| `INPUT_ACTION_NULL` | Invalid / sentinel (0) |
| `INPUT_ACTION_UP` | Up direction |
| `INPUT_ACTION_DOWN` | Down direction |
| `INPUT_ACTION_LEFT` | Left direction |
| `INPUT_ACTION_RIGHT` | Right direction |
| `INPUT_ACTION_ACCEPT` | Confirm / primary action |
| `INPUT_ACTION_CANCEL` | Back / secondary action |
| `INPUT_ACTION_RAGEQUIT` | Quit the application |
| `INPUT_ACTION_CONSOLE` | Toggle debug console |
| `INPUT_ACTION_POINTERX` | Mouse / pointer X axis |
| `INPUT_ACTION_POINTERY` | Mouse / pointer Y axis |
| `INPUT_ACTION_COUNT` | Total count (not a valid action) |
## Global state
```c
extern input_t INPUT;
// INPUT.actions[INPUT_ACTION_COUNT] -- all action states
// INPUT.platform -- platform-specific data
```
## Reading actions (game code)
```c
// Analog value this frame (0.0 - 1.0)
float_t inputGetCurrentValue(inputaction_t action);
// Analog value last frame
float_t inputGetLastValue(inputaction_t action);
// Boolean helpers (built on current/last values)
bool_t inputIsDown(inputaction_t action);
bool_t inputWasDown(inputaction_t action);
bool_t inputPressed(inputaction_t action); // was up, now down
bool_t inputReleased(inputaction_t action); // was down, now up
// Single axis from a neg + pos pair of actions (returns [-1, 1]):
float_t inputAxis(inputaction_t neg, inputaction_t pos);
// 2D axis from four actions (negX/posX/negY/posY):
void inputAxis2D(
inputaction_t negX, inputaction_t posX,
inputaction_t negY, inputaction_t posY,
vec2 result
);
// Same four-action axis, normalized to a unit vector via atan2:
void inputAngle2D(
inputaction_t negX, inputaction_t posX,
inputaction_t negY, inputaction_t posY,
vec2 result
);
// Deadzone filter (applied to raw axis values)
float_t inputDeadzone(float_t value, float_t deadzone);
```
## Binding buttons to actions
```c
void inputBind(inputaction_t action, inputbutton_t button);
```
Each platform's init function calls `inputBind` to wire its hardware
buttons to the standard action IDs. Game code should never need to call
`inputBind` -- it is set up once during platform init.
## Button types
```c
INPUT_BUTTON_TYPE_KEYBOARD // SDL scancode (SDL2 targets only)
INPUT_BUTTON_TYPE_POINTER // Mouse axes: X, Y, Z, WHEEL_X, WHEEL_Y
INPUT_BUTTON_TYPE_TOUCH // Touch (defined, not fully implemented)
INPUT_BUTTON_TYPE_GAMEPAD // Digital gamepad buttons
INPUT_BUTTON_TYPE_GAMEPAD_AXIS // Analog axes (-1.0 to 1.0 internally)
```
## Events
Each action has `onPressed` and `onReleased` event callbacks. Subscribe
via the event system (see `.claude/events.md`):
```c
eventSubscribe(&INPUT.actions[ACTION_ACCEPT].onPressed, myCallback, NULL);
```
## Platform implementations
### SDL2 (`src/dusksdl2/input/`)
Handles Linux, Knulli, and PSP (PSP adds its own button mapping layer
on top of SDL2).
- Keyboard: SDL scancode array from `SDL_GetKeyboardState()`
- Pointer: normalized mouse position (0.0-1.0), scroll axes
- Gamepad: first available `SDL_GameController`; axis values normalized
to [-1.0, 1.0] with deadzone (default 0.2f via `inputGetDeadzoneSDL2`)
### Dolphin -- GameCube / Wii (`src/duskdolphin/input/`)
Uses `libogc` PAD API. No keyboard or pointer input -- trying to use
those button types is a compile-time `#error`.
- Gamepad: `PAD_ScanPads()` + `PAD_ButtonsHeld()` for pad 0
- Axes: left stick X/Y, C-stick X/Y, L/R triggers (6 total)
- Deadzone: hardcoded 0.2f
- Default bindings set at init: D-pad/L-stick = directional actions,
A = ACCEPT, B = CANCEL, X = CONSOLE, Start = RAGEQUIT
### PSP (`src/duskpsp/input/`)
Layered on top of SDL2. `inputInitPSP()` remaps SDL2 controller button
constants to PSP button names, then calls `inputBind` to wire them:
| PSP button | SDL2 constant |
|------------|---------------|
| Cross | `SDL_CONTROLLER_BUTTON_A` |
| Circle | `SDL_CONTROLLER_BUTTON_B` |
| Triangle | `SDL_CONTROLLER_BUTTON_Y` |
| Square | `SDL_CONTROLLER_BUTTON_X` |
| L / R | `SDL_CONTROLLER_BUTTON_LEFTSHOULDER` / `RIGHTSHOULDER` |
| L-Stick | `SDL_CONTROLLER_AXIS_LEFTX/Y` |
### Vita (`src/duskvita/input/`)
Layered on top of SDL2 (via vitaSDL2). Behaviour is similar to PSP --
no keyboard, no pointer, gamepad only.
## JS module (`Input`)
The input system is exposed to JS as the global `Input` object with
static methods. Action constants are pre-defined as numeric properties
on the `Input` object (e.g. `Input.ACCEPT`, `Input.UP`):
```js
// Check if the accept button is held this frame:
if(Input.isDown(Input.ACCEPT)) { ... }
// Was the cancel button just pressed?
if(Input.pressed(Input.CANCEL)) { ... }
// Analog value for the right trigger:
var val = Input.getValue(Input.RIGHT);
// Single axis (-1 to 1) from a neg/pos pair:
var h = Input.axis(Input.LEFT, Input.RIGHT);
```
All `Input.*` action constants match the `INPUT_ACTION_*` enum values
from the C layer (UP, DOWN, LEFT, RIGHT, ACCEPT, CANCEL, RAGEQUIT,
CONSOLE, POINTERX, POINTERY).
## Platform capability notes
| Feature | Linux/Knulli | PSP | Vita | GameCube/Wii |
|---------|-------------|-----|------|--------------|
| Keyboard | Yes (SDL2) | No | No | No |
| Pointer/Mouse | Yes (SDL2) | No | No | No |
| Gamepad | Yes (SDL2) | Yes (SDL2) | Yes (SDL2) | Yes (PAD) |
| Analog axes | Yes | L-Stick only | L-Stick, R-Stick | L-Stick, C-Stick, Triggers |
| Touch | Defined, not implemented | -- | -- | -- |
-90
View File
@@ -1,90 +0,0 @@
# Locale System
Source: `src/dusk/locale/`, asset loader at
`src/dusk/asset/loader/locale/`
## Overview
The locale system loads Gettext PO files from the asset archive and
provides string lookup with plural-form support and printf-style
argument substitution. Locale files live in `locale/` inside `dusk.dsk`.
## Global state
```c
extern localemanager_t LOCALE;
// LOCALE.locale -- currently active localeinfo_t
// LOCALE.entry -- locked assetentry_t for the current PO file
```
## Initialise and switch locale
```c
errorret_t localeManagerInit();
// Defaults to LOCALE_EN_US (locale/en_US.po).
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
// Unlocks the old entry, loads and locks the new one.
// Blocks until the new PO file is fully parsed.
void localeManagerDispose();
```
## Getting a localised string
```c
// Variadic (printf-style args):
localeManagerGetText(id, buffer, bufferSize, plural, ...);
// With a pre-built args array:
localeManagerGetTextArgs(id, buffer, bufferSize, plural, args, argCount);
```
Both are macros that delegate to `assetLocaleGetStringWithVA` /
`assetLocaleGetStringWithArgs`.
- `id` -- message ID string (the English key in the PO file)
- `plural` -- plural index (0 for singular, 1+ per PO plural rules)
- `buffer` -- destination `char_t` array
- `bufferSize` -- size of the destination buffer
- `...` -- format arguments matching `%s`, `%d`, `%f` placeholders
## Locale descriptors (`localeinfo_t`)
```c
typedef struct {
const char_t *name; // e.g. "en-US"
const char_t *file; // path inside dusk.dsk, e.g. "locale/en_US.po"
} localeinfo_t;
```
The built-in descriptor is:
```c
static const localeinfo_t LOCALE_EN_US = {
.name = "en-US",
.file = "locale/en_US.po",
};
```
Add new locales by declaring another `localeinfo_t` constant and
shipping the corresponding `.po` file in the asset archive.
## PO file format notes
The loader (`assetlocaleloader`) parses standard Gettext PO syntax:
- `msgid` / `msgstr` pairs
- `msgid_plural` / `msgstr[n]` for plural forms
- The `Plural-Forms:` header (e.g. `nplurals=2; plural=(n != 1);`)
is parsed and evaluated at lookup time
Argument substitution uses `%s`, `%d`, `%f` placeholders (not
standard Gettext `%1` positional args).
## Adding a new locale
1. Create `locale/<lang_COUNTRY>.po` with a valid `Plural-Forms:`
header and the translated `msgid`/`msgstr` entries.
2. Pack it into `dusk.dsk`.
3. Add a `localeinfo_t` constant in `localeinfo.h`.
4. Call `localeManagerSetLocale()` with the new descriptor to activate.
-132
View File
@@ -1,132 +0,0 @@
# Network System
Source: `src/dusk/network/`, platform layers in
`src/dusk<platform>/network/`
## Overview
The network system provides a platform-agnostic API for detecting and
managing a network connection. Higher-level functionality (HTTP, sockets)
is not yet implemented in any platform. The system handles the
connection lifecycle -- connect, detect disconnect, disconnect -- and
reports the current IP address.
## Implementation status by platform
| Platform | Connection | IP info | HTTP/Requests | Notes |
|----------|-----------|---------|--------------|-------|
| **Linux** | Auto (OS) | IPv4 + IPv6 | Not implemented | `getifaddrs()` |
| **Knulli** | Auto (OS) | IPv4 + IPv6 | Not implemented | same as Linux |
| **PSP** | Manual dialog | IPv4 only | Not implemented | `sceUtilityNetconf`; SSL/HTTP modules commented out |
| **GameCube** | Manual DHCP | IPv4 only | Not implemented | `if_config()` stubbed; `net_init()` commented out |
| **Wii** | Manual DHCP | IPv4 only | Not implemented | blocking `if_config()` via System Menu Wi-Fi settings |
## Connection states
```c
typedef enum {
NETWORK_STATE_DISCONNECTED,
NETWORK_STATE_CONNECTING,
NETWORK_STATE_CONNECTED,
NETWORK_STATE_DISCONNECTING,
} networkstate_t;
```
## Global state
```c
extern network_t NETWORK;
// NETWORK.state -- current connection state
// NETWORK.platform -- platform-specific data
// NETWORK.onDisconnect -- callback fired on unexpected disconnect
```
## Core API (`network.h`)
```c
errorret_t networkInit();
errorret_t networkUpdate(); // call each frame; detects dropped connections
errorret_t networkDispose();
bool_t networkIsConnected();
void networkRequestConnection(
void (*onConnected)(void *user),
void (*onFailed)(errorret_t error, void *user),
void (*onDisconnect)(errorret_t error, void *user),
void *user
);
void networkRequestDisconnection(
void (*onComplete)(void *user),
void *user
);
```
On platforms that manage their own connection (Linux, macOS, Windows),
`networkRequestConnection` immediately calls `onConnected` if a network
interface is up, or `onFailed` if not. No `networkPlatformRequestConnection`
macro is needed.
On platforms that require explicit connection (PSP, Wii), the platform
implements `networkPlatformRequestConnection` and
`networkPlatformRequestDisconnection`.
## Network info
```c
networkinfo_t networkGetInfo();
// Only valid when NETWORK.state == NETWORK_STATE_CONNECTED.
```
```c
typedef struct {
networktype_t type; // NETWORK_TYPE_IPV4 or (ifdef) NETWORK_TYPE_IPV6
union {
networkinfoipv4_t ipv4; // uint8_t ip[4]
networkinfoipv6_t ipv6; // uint8_t ip[16] (requires DUSK_NETWORK_IPV6)
};
} networkinfo_t;
```
IPv6 support requires the `DUSK_NETWORK_IPV6` compile-time define.
## Platform-specific notes
### Linux / Knulli
Fully functional for connection detection and IP querying. No explicit
connect/disconnect step needed -- the OS manages the interface. Uses
`getifaddrs()` to find the first non-loopback running interface.
### PSP
Connection is asynchronous and driven by a state machine in
`networkPSPUpdate()`. The PSP shows the built-in network configuration
dialog (`sceUtilityNetconfInitStart`) to let the user pick a Wi-Fi
access point.
HTTP/SSL modules (`psphttp`, `pspssl`) are loaded in commented-out code
-- the infrastructure for HTTP exists but is disabled.
### GameCube
`net_init()` is commented out. Networking on GameCube is non-functional
in the current build.
### Wii
Uses `if_config()` (DHCP via libogc) to connect using the Wi-Fi settings
stored in Wii System Menu. This call **blocks** the main thread. The
connection only works when `DUSK_WII` is defined; the GameCube path
always fails.
## Adding a new platform implementation
1. Create `src/dusk<platform>/network/network<platform>.h/.c`.
2. Implement the five required functions:
`Init`, `Update`, `Dispose`, `IsConnected`, `GetInfo`.
3. Optionally implement `RequestConnection` and `RequestDisconnection`
if the platform requires an explicit connection step.
4. Create `networkplatform.h` mapping each `networkPlatform*` macro to
your functions, and defining `networkplatform_t`.
-83
View File
@@ -1,83 +0,0 @@
# Optimization Guidelines
Dusk must run well on severely resource-constrained hardware. The PSP
has 32 MB of RAM and a 333 MHz MIPS CPU. The GameCube has 24 MB of RAM
and a 485 MHz PowerPC CPU with no FPU for integer paths. Optimization
is not an afterthought -- it is a first-class design constraint.
## General principles
- **Measure before optimizing.** Don't guess where bottlenecks are.
Profile on the actual target hardware when possible.
- **Data-oriented design.** The ECS exists to enable cache-friendly
iteration over components. Keep hot data tightly packed (SoA over
AoS where it matters).
- **Minimize allocations.** Dynamic allocation at runtime is expensive
and causes fragmentation. Prefer fixed-size pools, arenas, and
pre-allocated arrays.
- **Avoid per-frame allocations.** Anything allocated and freed every
tick is a red flag. Use scratch buffers or static pools.
- **Avoid recursion** on constrained targets -- stack is small.
## Memory
| Platform | Total RAM | Notes |
|------------|-------------|-----------------------------------|
| GameCube | 24 MB | 16 MB main + 8 MB "Aram" (audio) |
| Wii | 88 MB | 24 MB MEM1 + 64 MB MEM2 |
| PSP | 32 MB | 4 MB reserved for OS |
| Vita | 512 MB | Much more headroom |
| Linux | Host RAM | Effectively unlimited |
Treat the GameCube 16 MB main RAM as the worst-case constraint when
designing data structures and budgets.
Always use `memoryAllocate` / `memoryFree` -- never `malloc` / `free`.
The engine allocator tracks usage and can enforce budgets per platform.
## Math
- Prefer integer math over floating-point on platforms without an FPU.
- Use fixed-point arithmetic (`int32_t` with a known scale) for physics
and animation on PSP/GameCube where FPU throughput is limited.
- SIMD / VFPU (PSP) and paired-singles (GameCube) are available but
require platform-guarded code paths under `#ifdef DUSK_PSP` etc.
- Avoid `double` entirely -- use `float_t` (32-bit) throughout.
## Rendering
- Batch draw calls aggressively. Every draw call has overhead on all
platforms; consoles are especially sensitive.
- Minimize state changes (texture binds, shader switches, etc.).
- Use display lists (GameCube/Wii) and vertex buffer objects (OpenGL)
to offload geometry to GPU memory.
- Keep texture sizes powers of two. Non-PoT textures are unsupported
or have penalties on PSP and GameCube.
## Asset loading
- Assets are loaded asynchronously via the asset loader system. Do not
block the game loop waiting for assets.
- Compress textures to the native format for each platform at build
time, not at runtime.
- Stream large assets from the filesystem rather than loading them all
at startup.
## Platform-specific notes
### PSP
- The Media Engine (ME) is a second CPU core -- use it for audio and
background decompression, not general logic.
- VFPU gives 4-wide SIMD floats; use it for matrix and vector math.
- Keep the uncached scratchpad (4 KB at 0x00010000) in mind for hot
temporary data.
### GameCube / Wii
- The GX display list pipeline is the primary rendering path; avoid
immediate-mode GX calls in the hot path.
- Texture Compression (CMPR / S3TC equivalent) halves texture memory.
- Wii: prefer MEM1 for GPU-accessed data; MEM2 for CPU-only buffers.
### PSP / Vita
- OpenGL ES has a subset of desktop OpenGL. Avoid extensions and
features that are not in the ES 1.1 / ES 2.0 core.
-127
View File
@@ -1,127 +0,0 @@
# Physics System
Source: `src/dusk/physics/`, entity component at
`src/dusk/entity/component/physics/entityphysics.h/.c`
## Overview
Dusk uses a lightweight, custom 3D physics simulation with no external
library dependency. It is integrated with the ECS: only entities that
have both a `COMPONENT_TYPE_PHYSICS` and a `COMPONENT_TYPE_POSITION`
component participate in the simulation.
## Shapes
```c
typedef enum {
PHYSICS_SHAPE_CUBE, // Axis-aligned bounding box (AABB)
PHYSICS_SHAPE_SPHERE,
PHYSICS_SHAPE_CAPSULE, // Y-axis aligned; radius + halfHeight
PHYSICS_SHAPE_PLANE, // Infinite plane; normal + distance
} physicshapetype_t;
```
All shape pairs are supported by the collision dispatch
(`physicsTestShapeVsShape`). See `physicstest.h` for the individual
test functions.
## Body types
```c
typedef enum {
PHYSICS_BODY_STATIC, // Never moves; immovable collision surface
PHYSICS_BODY_DYNAMIC, // Driven by gravity, velocity, collisions
PHYSICS_BODY_KINEMATIC, // Moved programmatically; collides but not
// driven by the simulation (e.g. player)
} physicsbodytype_t;
```
## World and gravity
```c
extern physicsworld_t PHYSICS_WORLD;
// PHYSICS_WORLD.gravity -- default {0, -9.81, 0}
```
The simulation step is driven by `physicsManagerUpdate()`, which is
called each fixed-timestep game loop tick. It skips dynamic-timestep
sub-steps (`DUSK_TIME_DYNAMIC`).
## Simulation phases (each step)
1. **Integrate dynamics** -- apply gravity scaled by `gravityScale`,
advance velocity, update position.
2. **Dynamic vs static/kinematic** -- resolve penetration and cancel
the normal velocity component.
3. **Dynamic vs dynamic** -- split penetration 50/50; exchange
relative normal velocity.
4. **Rebuild transforms** -- call `entityPositionRebuild()` for all
affected dynamic bodies.
`PHYSICS_GROUND_THRESHOLD = 0.707f` -- a collision normal with a Y
component above this value sets `onGround = true` on the dynamic body.
## Entity component (`entityphysics_t`)
```c
typedef struct {
physicsbodytype_t type;
physicsshape_t shape;
vec3 velocity;
float_t gravityScale; // default 1.0
bool_t onGround; // set by the solver each step
} entityphysics_t;
```
Default on init: DYNAMIC body, 0.5m half-extents AABB cube,
`gravityScale = 1.0f`.
### Component API
```c
entityphysics_t *entityPhysicsGet(entityid_t, componentid_t);
void entityPhysicsSetShape(entityid_t, componentid_t, physicsshape_t);
physicsshape_t entityPhysicsGetShape(entityid_t, componentid_t);
void entityPhysicsSetVelocity(entityid_t, componentid_t, vec3);
void entityPhysicsGetVelocity(entityid_t, componentid_t, vec3 dest);
void entityPhysicsApplyImpulse(entityid_t, componentid_t, vec3);
// No-op on STATIC bodies.
bool_t entityPhysicsIsOnGround(entityid_t, componentid_t);
void entityPhysicsSetBodyType(entityid_t, componentid_t, physicsbodytype_t);
physicsbodytype_t entityPhysicsGetBodyType(entityid_t, componentid_t);
```
## Collision detection primitives (`physicstest.h`)
Each function returns `true` if overlapping and writes the push-out
normal (pointing from B toward A) and penetration depth.
| Function | Shapes |
|----------|--------|
| `physicsTestAabbVsAabb` | CUBE vs CUBE |
| `physicsTestSphereVsSphere` | SPHERE vs SPHERE |
| `physicsTestSphereVsAabb` | SPHERE vs CUBE |
| `physicsTestSphereVsPlane` | SPHERE vs PLANE |
| `physicsTestAabbVsPlane` | CUBE vs PLANE |
| `physicsTestCapsuleVsSphere` | CAPSULE vs SPHERE |
| `physicsTestCapsuleVsAabb` | CAPSULE vs CUBE |
| `physicsTestCapsuleVsPlane` | CAPSULE vs PLANE |
| `physicsTestCapsuleVsCapsule` | CAPSULE vs CAPSULE |
| `physicsTestShapeVsShape` | Any pair via dispatch |
Capsules are always Y-axis aligned. Planes are infinite (not half-spaces).
## Limitations and known gaps
- No rotation simulation -- bodies do not rotate from collisions.
- No friction or damping model yet.
- No sleeping / deactivation for resting bodies.
- No broad-phase culling: the solver is O(n^2) per phase.
This is acceptable up to the ECS entity limit (64 entities) but must
be revisited if the entity count grows.
- Capsule vs plane uses the bottom/top hemisphere centers as a
degenerate approximation -- accurate for large planes but
not for thin surfaces.
-288
View File
@@ -1,288 +0,0 @@
# Platform -- Dolphin (GameCube and Wii)
`DUSK_TARGET_SYSTEM`: `gamecube` / `wii`
Source layer: `src/duskdolphin/`
Renderer: libogc GX (native Nintendo hardware)
---
## Overview
GameCube and Wii are collectively called the **Dolphin** targets. They
share a single source layer (`src/duskdolphin/`) and a shared CMake base
(`cmake/targets/dolphin.cmake`). Individual targets add `DUSK_GAMECUBE`
or `DUSK_WII` on top.
Both are **big-endian** PowerPC platforms. They do **not** use SDL2 or
OpenGL -- rendering and input go through `libogc` (the open-source
GameCube/Wii SDK) and the GX hardware API directly.
---
## Hardware
| Attribute | GameCube | Wii |
|-----------|---------|-----|
| CPU | IBM PowerPC 750CL (Gekko), 485 MHz | IBM Broadway (Wii CPU), 729 MHz |
| RAM | 24 MB (16 MB MEM1 + 8 MB ARAM) | 88 MB (24 MB MEM1 + 64 MB MEM2) |
| GPU | ATI Flipper (GX) | ATI Hollywood (GX) |
| Display | 640x480 (480p max) | 640x480 (480p/576i), 480p/1080i via component |
| Storage | Memory Card (slots A/B), SD Gecko | SD card, USB, NAND |
| Endian | Big-endian | Big-endian |
Treat the **GameCube 16 MB MEM1** as the worst-case RAM budget for data
structures shared between both targets.
---
## Compile-time macros
| Macro | GameCube | Wii | Notes |
|-------|---------|-----|-------|
| `DUSK_DOLPHIN` | yes | yes | Set by `dolphin.cmake` |
| `DUSK_GAMECUBE` | yes | no | |
| `DUSK_WII` | no | yes | |
| `DUSK_INPUT_GAMEPAD` | yes | yes | |
| `DUSK_DISPLAY_WIDTH` | 640 | 640 | |
| `DUSK_DISPLAY_HEIGHT` | 480 | 480 | |
| `DUSK_THREAD_PTHREAD` | yes | yes | devkitPPC pthreads |
| `DUSK_PLATFORM_ENDIAN_BIG` | yes | yes | Not set by cmake -- apply manually |
| `DOL` | 1 | 1 | Build type token |
| `ISO` | 2 | 2 | Build type token |
| `DUSK_DOLPHIN_BUILD_TYPE` | `DOL` or `ISO` | `DOL` or `ISO` | |
| `DUSK_DOLPHIN_BUILD_ISO` | if ISO mode | if ISO mode | |
No `DUSK_SDL2`, no `DUSK_OPENGL`, no `DUSK_INPUT_KEYBOARD`,
no `DUSK_INPUT_POINTER`, no `DUSK_TIME_DYNAMIC`.
Attempting to use `DUSK_INPUT_KEYBOARD` or `DUSK_INPUT_POINTER` causes
a compile-time `#error` in `inputdolphin.h`.
---
## Endianness
**Both GameCube and Wii are big-endian.** This is the most critical
platform difference from all other targets.
- All binary asset data (`.dtf` tilesets, STL meshes, DTF headers, etc.)
must be byte-swapped when read on Dolphin.
- Use `endianLittleToHost32` / `endianLittleToHost16` etc. from
`util/endian.h` when reading any multi-byte value from a file.
- Save files are stored in little-endian order; the save stream handles
this transparently via the `saveFile*` macros.
- Network data likewise needs endian conversion.
See `.claude/util.md` (Endian section) for the full API.
---
## Display
- Fixed 640x480 resolution, driven by GX (the hardware rasteriser).
- Uses double-buffered framebuffers:
```c
typedef struct {
void *frameBuffer[2]; // double-buffered
int_t whichFrameBuffer;
GXRModeObj *screenMode;
void *fifoBuffer; // GX command FIFO, 256 KB
} displaydolphin_t;
```
- The GX pipeline uses display lists for efficient draw call batching --
avoid immediate-mode GX calls in the hot path.
- `CONF_GetAspectRatio()` returns `CONF_ASPECT_4_3` on GameCube (always)
and the user's setting on Wii. Use `systemGetAspectRatioDolphin()`.
---
## Asset loading
Two modes are selected at CMake configure time via
`DUSK_DOLPHIN_BUILD_TYPE`:
### DOL mode (default -- `DUSK_DOLPHIN_BUILD_TYPE=DOL`)
Assets are loaded from `dusk.dsk` on a FAT filesystem -- SD card on Wii
(via SD slot), or SD Gecko / SD adapter on GameCube. The loader searches
these paths in order:
```c
"/", "/Dusk", "/dusk", "/DUSK",
"/apps", "/apps/Dusk", "/apps/dusk", "/apps/DUSK",
".", "./Dusk", "./dusk", ...
```
Uses `libfat` for filesystem access.
### ISO mode (`DUSK_DOLPHIN_BUILD_TYPE=ISO`)
`dusk.dsk` is read directly off the DVD disc via the libogc DVD driver
(`assetdolphindvd.c`). Reads are 32-byte aligned:
```c
#define ASSET_DOLPHIN_DVD_ALIGN 32u
```
The DVD FST (file-system table) is parsed at init to locate the data
file. All reads go through `assetDolphinDVDRead(offset, size)` which
returns an aligned heap buffer that the caller must free.
Post-build in ISO mode, `makedolphiniso.py` produces **three disc
images** (NTSC-J, NTSC-U, PAL).
---
## Input
Uses libogc `PAD` API. Only GameCube controllers are supported (port 0
by default; up to 4 via `PAD_CHANMAX`).
Available axes (6 total per controller):
| Axis | Enum |
|------|------|
| Left stick X/Y | `INPUT_GAMEPAD_AXIS_LEFT_X/Y` |
| C-stick X/Y | `INPUT_GAMEPAD_AXIS_C_X/Y` |
| L trigger | `INPUT_GAMEPAD_AXIS_TRIGGER_LEFT` |
| R trigger | `INPUT_GAMEPAD_AXIS_TRIGGER_RIGHT` |
Axis values are normalised by dividing the raw 8-bit value by 128.0.
Deadzone: 0.2 (hardcoded).
Default bindings set at init: D-pad/left stick = directional actions,
A = ACCEPT, B = CANCEL, X = CONSOLE, Start = RAGEQUIT.
Wii Remote / Nunchuk / Classic Controller / Pro Controller are not yet
implemented (noted as TODO in `inputdolphin.h`).
---
## Save system
Uses the libogc Memory Card API (`CARD_*`) to read/write save slots.
```c
typedef struct {
card_file cardFile;
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
bool_t mounted;
} savedolphin_t;
```
- Default channel: `CARD_SLOTA` (Memory Card slot A).
Override via `SAVE_DOLPHIN_CHANNEL`.
- Sector size: 8192 bytes (`SAVE_DOLPHIN_SECTOR_SIZE`).
- Buffers must be 32-byte aligned (enforced by `__attribute__((aligned(32)))`).
- Game code: `DUSK` (4 chars, override via `SAVE_DOLPHIN_GAME_CODE`).
- The card must be mounted before any read/write. `saveInitDolphin()`
mounts slot A; failures are treated as "no save present".
- Save stream handles little-endian encoding transparently -- all data
stored little-endian on the card even though the CPU is big-endian.
---
## Network
### GameCube
`net_init()` is commented out. Networking is **non-functional** on
GameCube in the current codebase. The BBA (Broadband Adapter) link
library is in a commented `# bba` in `gamecube.cmake`.
### Wii
Uses `if_config()` from libogc which reads Wi-Fi settings saved in the
Wii System Menu. The call **blocks** the main thread until DHCP
completes or fails. Wii network is available only when `DUSK_WII` is
defined; the GameCube path always fails immediately.
IPv6 is not supported on either Dolphin target.
---
## Time
- No `DUSK_TIME_DYNAMIC`. All ticks are fixed 16 ms steps.
- Tick source: `__SYS_GetSystemTime()` returns PowerPC bus ticks.
- Real time: ticks converted to microseconds via `ticks_to_microsecs()`,
then offset from the GameCube epoch (2000-01-01 00:00:00) to the UNIX
epoch (1970-01-01 00:00:00) by adding **946 684 800 seconds**.
- Timezone: always returned as 0 -- no timezone data without network time.
---
## System
Language and aspect ratio queries:
```c
// Language (used for locale selection):
systemGetLanguageDolphin();
// -> SYS_GetLanguage() on GameCube
// -> CONF_GetLanguage() on Wii
// Aspect ratio:
systemGetAspectRatioDolphin();
// -> CONF_ASPECT_4_3 always on GameCube
// -> CONF_GetAspectRatio() on Wii (4:3 or 16:9)
```
---
## Build and toolchain
Requires [devkitPro](https://devkitpro.org/) with `devkitPPC` and
`libogc` installed.
```sh
# GameCube (SD card / DOL mode)
cmake -B build \
-DDUSK_TARGET_SYSTEM=gamecube \
-DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/GameCube.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
# Wii (SD card / DOL mode)
cmake -B build \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Wii.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
# Either target in ISO mode
cmake -B build \
-DDUSK_TARGET_SYSTEM=gamecube \
-DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/GameCube.cmake \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
```
Post-build outputs (DOL mode): `Dusk.elf` + `Dusk.dol` (generated by
`elf2dol`). Copy `Dusk.dol` and `dusk.dsk` to the SD card.
Post-build outputs (ISO mode): `Dusk.dol` + disc images in
`NTSC-J/`, `NTSC-U/`, `PAL/` subdirectories.
Dependencies: libogc, devkitPPC, `fat` (DOL mode), cglm, zip, bz2,
zstd, z, lzma, m.
---
## Gotchas
- **Big-endian is the most common source of bugs** when porting code
from Linux. Always use `endian.h` utilities for file I/O and network.
- Memory is tight on GameCube -- 16 MB MEM1 must hold code, stack, heap,
framebuffers (2x 640x480x2 bytes), and the GX FIFO (256 KB).
- GX display lists are the correct rendering path; immediate-mode GX
calls carry heavy CPU overhead on the short FIFO pipeline.
- The GameCube has no FPU for integer paths. Avoid `double`; use
`float_t` throughout.
- `consoleInit` is shadowed to `consoleInitDolphin` to avoid conflicts
with the devkitPPC console API.
- On GameCube `CONF_GetAspectRatio()` is always 4:3; the macro is
defined to return `CONF_ASPECT_4_3` unconditionally.
- DVD reads must be 32-byte aligned and padded -- use
`ASSET_DOLPHIN_DVD_ALIGN_UP(n)` when computing read sizes in ISO mode.
-162
View File
@@ -1,162 +0,0 @@
# Platform -- Linux and Knulli
`DUSK_TARGET_SYSTEM`: `linux` / `knulli`
Source layer: `src/dusklinux/`
Renderer: OpenGL (Linux) / OpenGL ES via EGL (Knulli)
---
## Overview
Linux is the primary development target. Knulli is a Linux-based handheld
OS (e.g. Anbernic devices); it shares the `src/dusklinux/` layer entirely
and differs only in the CMake target (OpenGL ES instead of desktop OpenGL,
EGL instead of GLX, and no backtrace support).
Both targets use SDL2 for windowing and input. The window is resizable on
both (`DUSK_DISPLAY_SIZE_DYNAMIC`).
---
## Compile-time macros
| Macro | Linux | Knulli |
|-------|-------|--------|
| `DUSK_LINUX` | yes | yes |
| `DUSK_KNULLI` | no | yes |
| `DUSK_SDL2` | yes | yes |
| `DUSK_OPENGL` | yes | yes |
| `DUSK_OPENGL_ES` | no | yes |
| `DUSK_DISPLAY_SIZE_DYNAMIC` | yes | yes |
| `DUSK_INPUT_KEYBOARD` | yes | yes |
| `DUSK_INPUT_POINTER` | yes | yes |
| `DUSK_INPUT_GAMEPAD` | yes | yes |
| `DUSK_TIME_DYNAMIC` | yes | yes |
| `DUSK_NETWORK_IPV6` | yes | no |
| `DUSK_THREAD_PTHREAD` | yes | yes |
| `DUSK_CONSOLE_POSIX` | yes | no |
---
## Display
- Default logical resolution: **640x480** (`DUSK_DISPLAY_WIDTH_DEFAULT` /
`DUSK_DISPLAY_HEIGHT_DEFAULT`); game content renders at
`DUSK_DISPLAY_SCREEN_HEIGHT=240`.
- Dynamic resize: the window can be resized at any time; the engine
letterboxes/scales the logical framebuffer to fit.
- Screen mode is configurable via `SCREEN.mode` (see
`.claude/display-core.md`).
- Knulli uses OpenGL ES (GLES2) linked via EGL. Avoid any desktop
OpenGL extensions that are not in the ES2 core.
---
## Asset loading
`dusk.dsk` is located by searching a list of paths relative to the
current working directory:
```c
static const char_t *ASSET_LINUX_SEARCH_PATHS[] = {
"%s",
"../%s",
"../../%s",
"data/%s",
"../data/%s",
NULL
};
```
The first path where `dusk.dsk` is found wins. No packaging step is
required on Linux -- run from the build directory or the project root.
---
## Input
All three input types are supported:
- **Keyboard** -- SDL scancode array via `SDL_GetKeyboardState()`.
- **Pointer** -- mouse position normalized to [0, 1], scroll axes.
- **Gamepad** -- first available `SDL_GameController`; axes normalized
to [-1, 1] with a 0.2 deadzone.
See `.claude/input.md` for the full action/button API.
---
## Save system
Save files are plain files written to disk.
- Path: `./saves/save_N.dat` (override `SAVE_LINUX_PATH` to change the
directory at CMake configure time).
- Format: `SAVE_LINUX_FILE_FORMAT = "%s/save_%u.dat"` where `%u` is the
slot index.
- No OS-level dialog blocking -- saves are synchronous filesystem calls.
- Endian: host byte order (little-endian on x86/ARM).
---
## Network
- Connection is detected automatically via `getifaddrs()`. No explicit
connect step is needed.
- `networkRequestConnection` immediately calls `onConnected` if any
non-loopback interface is up, `onFailed` otherwise.
- IPv4 and IPv6 supported (`DUSK_NETWORK_IPV6`).
---
## Time
- Tick source: `SDL_GetTicks64()`.
- Real time: `clock_gettime(CLOCK_REALTIME)`.
- Dynamic timestep enabled (`DUSK_TIME_DYNAMIC`).
---
## Threading
pthreads (`DUSK_THREAD_PTHREAD`). Thread-local storage via `__thread`.
---
## Build and toolchain
No cross-compiler needed -- use the host GCC/Clang.
```sh
# Debug build
cmake -B build -DDUSK_TARGET_SYSTEM=linux -DCMAKE_BUILD_TYPE=Debug
cmake --build build
# Knulli (cross-compile to aarch64)
cmake -B build \
-DDUSK_TARGET_SYSTEM=knulli \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/aarch64-linux-gnu.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
```
Dependencies: `SDL2`, `OpenGL` (Linux) or `GLES2` + `EGL` (Knulli),
`pthread`, `m`.
---
## Endianness
Little-endian. Detected at CMake configure time via `TestBigEndian` and
set as `DUSK_PLATFORM_ENDIAN_LITTLE` or `DUSK_PLATFORM_ENDIAN_BIG`.
---
## Gotchas
- `DUSK_CONSOLE_POSIX` enables POSIX-specific assert backtracing (Linux
only; Knulli does not set it).
- Knulli does not set `DUSK_NETWORK_IPV6` -- IPv6 may not be available
on handheld devices.
- `DUSK_TIME_DYNAMIC` is set, so physics/networking skip dynamic sub-steps
by checking `if(TIME.dynamicUpdate) return;`.
-46
View File
@@ -1,46 +0,0 @@
# Platform -- macOS
`DUSK_TARGET_SYSTEM`: `macos`
Source layer: `src/duskmacos/` (planned, does not exist yet)
Status: **Planned -- not yet implemented**
---
## Overview
macOS desktop is a planned target. No source layer, CMake target file,
or toolchain exists yet. The intended architecture mirrors Linux: SDL2
for windowing/input, OpenGL (or Metal via MoltenVK/SDL2) for rendering.
---
## Expected macros (when implemented)
| Macro | Expected |
|-------|---------|
| `DUSK_MACOS` | yes |
| `DUSK_SDL2` | yes |
| `DUSK_OPENGL` | yes |
| `DUSK_DISPLAY_SIZE_DYNAMIC` | yes |
| `DUSK_INPUT_KEYBOARD` | yes |
| `DUSK_INPUT_POINTER` | yes |
| `DUSK_INPUT_GAMEPAD` | yes |
| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes |
| `DUSK_TIME_DYNAMIC` | yes |
| `DUSK_THREAD_PTHREAD` | yes |
---
## Notes
- Will be little-endian (Apple Silicon and Intel x86-64).
- Apple deprecated OpenGL on macOS in 10.14 (Mojave). The implementation
will need to either target the deprecated OpenGL path or use MoltenVK
(Vulkan-over-Metal) with an SDL2 OpenGL layer. This decision is
pending.
- Save files will likely live in `~/Library/Application Support/`.
- Expected to share `src/dusksdl2/` and `src/duskgl/` with Linux.
- Toolchain: native Clang via Xcode Command Line Tools, or a
cross-compile from Linux with osxcross.
Update this document when the macOS target is implemented.
-200
View File
@@ -1,200 +0,0 @@
# Platform -- Sony PSP
`DUSK_TARGET_SYSTEM`: `psp`
Source layer: `src/duskpsp/`
Renderer: OpenGL ES (legacy, via PSPGL/SDL2)
---
## Overview
The PSP is a 32 MB MIPS-based handheld console running at up to 333 MHz.
It uses SDL2 (ported to PSP) for windowing and OpenGL in legacy/fixed-
function mode. The game binary and all assets are packaged together inside
a `.pbp` file -- the PSP's native executable format.
---
## Hardware
| Attribute | Value |
|-----------|-------|
| CPU | MIPS R4000 (Allegrex), up to 333 MHz |
| RAM | 32 MB (4 MB reserved for OS) |
| Display | 480x272, 16/32-bit colour |
| Storage | Memory Stick (UMD for retail; MS for homebrew) |
| Endian | Little-endian |
---
## Compile-time macros
| Macro | Set |
|-------|-----|
| `DUSK_PSP` | yes |
| `DUSK_SDL2` | yes |
| `DUSK_OPENGL` | yes |
| `DUSK_OPENGL_LEGACY` | yes |
| `DUSK_INPUT_GAMEPAD` | yes |
| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes |
| `DUSK_DISPLAY_WIDTH` | 480 |
| `DUSK_DISPLAY_HEIGHT` | 272 |
| `DUSK_THREAD_PTHREAD` | yes |
No `DUSK_DISPLAY_SIZE_DYNAMIC` -- the resolution is fixed.
No `DUSK_INPUT_KEYBOARD`, no `DUSK_INPUT_POINTER`.
---
## Display
- Fixed 480x272 resolution.
- OpenGL legacy (fixed-function pipeline, `DUSK_OPENGL_LEGACY`).
- Texture dimensions **must** be powers of two (use `mathNextPowTwo`).
- VFPU (4-wide float SIMD) is available -- use it for matrix/vector hot
paths under `#ifdef DUSK_PSP`.
---
## Asset loading
Assets are packed into the **PSAR** section of the `.pbp` file by the
post-build `create_pbp_file()` CMake command. At runtime,
`assetInitPBP()` locates and opens the PSAR from the running executable's
path.
The PBP format header:
```c
typedef struct {
char_t signature[4]; // "\0PBP"
uint32_t version;
uint32_t sfoOffset;
uint32_t icon0Offset;
uint32_t icon1Offset;
uint32_t pic0Offset;
uint32_t pic1Offset;
uint32_t snd0Offset;
uint32_t pspOffset;
uint32_t psarOffset; // dusk.dsk starts here
} assetpbpheader_t;
```
`assetpbp_t` holds the open file handle and parsed header. Asset paths
inside the PSAR are ZIP paths within `dusk.dsk`.
---
## Input
Layered on SDL2. `inputInitPSP()` maps PSP physical buttons to SDL2
`SDL_CONTROLLER_BUTTON_*` constants:
| PSP button | Action |
|------------|--------|
| Cross | Accept |
| Circle | Cancel |
| Triangle | - |
| Square | - |
| L / R | Shoulder buttons |
| D-pad | Directional |
| L-Stick | Analog axes |
No keyboard or pointer input available. Attempting to use
`INPUT_BUTTON_TYPE_KEYBOARD` on PSP is undefined behaviour.
The PSP system setting `PSP_UTILITY_ACCEPT_CROSS` / `ACCEPT_CIRCLE`
swaps the Cross and Circle button roles in OS dialogs -- read this via
`systemPSPGetCrossButtonSetting()` if you need to match the system
convention.
---
## Save system
- Path: `ms0:/PSP/SAVEDATA/<TITLE_ID><slot>/save.dat`
(default title ID `DUSK00001`, configurable via `SAVE_PSP_TITLE_ID`).
- Uses `sceIo` for file I/O -- no extra dialog required for raw reads.
- PSP OS-level save/load dialogs (via `sceUtility`) are separate and
block the main loop when open (`systemGetActiveDialogType()` returns
`SYSTEM_DIALOG_TYPE_TICK_BLOCKING`).
- Do not call save functions directly from game code during a dialog.
---
## Network
Connection requires an explicit user Wi-Fi selection step via the PSP
system network dialog (`sceUtilityNetconfInitStart`).
```
networkRequestConnection(onConnected, onFailed, onDisconnect, user);
// -> shows PSP Wi-Fi selection dialog (blocking dialog type)
// -> calls onConnected or onFailed when the dialog closes
```
HTTP and SSL modules (`psphttp`, `pspssl`, `pspnet_resolver`) are
linked in `psp.cmake` but the HTTP implementation code is commented out.
The infrastructure exists for future use.
---
## Time
- Tick source: `SDL_GetTicks64()`.
- Real time: `sceRtcGetCurrentTick()` (returns microseconds).
- Dynamic timestep is **not** enabled (`DUSK_TIME_DYNAMIC` not set).
Every tick is a fixed 16 ms step.
---
## System dialogs
PSP shows OS-level dialogs for:
- Wi-Fi configuration (`networkRequestConnection`)
- Save management (if using `sceUtility` save dialogs)
Check `systemGetActiveDialogTypePSP()` to know whether the main loop
should skip rendering or ticking.
---
## Build and toolchain
Requires the [PSPDEV toolchain](https://github.com/pspdev/pspdev).
Set `PSPDEV` in your environment before configuring.
```sh
cmake -B build \
-DDUSK_TARGET_SYSTEM=psp \
-DCMAKE_TOOLCHAIN_FILE=${PSPDEV}/lib/cmake/psp.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
```
Post-build output: `Dusk.pbp` (executable + assets combined).
Dependencies: SDL2-PSP, OpenGL-PSP, pspgu, pspctrl, pspdisplay,
pspaudio, pspaudiolib, psputility, pspvfpu, pspvram, pspnet,
pspnet_inet, pspnet_apctl, psphttp, pspssl, pspdebug, psphprm,
mbedtls, mbedcrypto, lzma, zip, bz2, z.
---
## Endianness
Little-endian. `DUSK_PLATFORM_ENDIAN_LITTLE` is set at compile time.
No runtime endian check is needed.
---
## Gotchas
- The PSP has only 28 MB of usable RAM after the OS. Keep asset budgets
tight -- see `.claude/optimization.md`.
- VFPU instructions are not valid on threads other than the main thread
on some firmware versions. Use `assertIsMainThread` on any code that
calls VFPU intrinsics.
- OpenGL legacy mode means no vertex/fragment shaders; rendering uses
the fixed-function pipeline via `pspgl`.
- `DUSK_TIME_DYNAMIC` is absent -- physics always runs at exactly the
fixed step rate.
-173
View File
@@ -1,173 +0,0 @@
# Platform -- PlayStation Vita
`DUSK_TARGET_SYSTEM`: `vita`
Source layer: `src/duskvita/`
Renderer: vitaGL (OpenGL-over-GXM compatibility layer)
---
## Overview
The PlayStation Vita is an ARM-based handheld with 512 MB of RAM and a
960x544 OLED/LCD display. It uses SDL2 (ported to Vita) for input
abstraction, but the graphics layer is vitaGL -- an OpenGL compatibility
shim that translates OpenGL calls to Sony's native GXM API.
The distribution format is a `.vpk` (Vita Package) file containing the
signed executable and `dusk.dsk` bundled as `dusk.dsk` at the package
root, accessible at `app0:/dusk.dsk` at runtime.
---
## Hardware
| Attribute | Value |
|-----------|-------|
| CPU | ARM Cortex-A9 quad-core, ~444 MHz |
| RAM | 512 MB |
| Display | 960x544 |
| Storage | Vita game card / memory card / internal flash |
| Endian | Little-endian |
---
## Compile-time macros
| Macro | Set |
|-------|-----|
| `DUSK_VITA` | yes |
| `DUSK_SDL2` | yes |
| `DUSK_OPENGL` | yes |
| `DUSK_OPENGL_LEGACY` | yes |
| `DUSK_INPUT_GAMEPAD` | yes |
| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes |
| `DUSK_DISPLAY_WIDTH` | 960 |
| `DUSK_DISPLAY_HEIGHT` | 544 |
No `DUSK_DISPLAY_SIZE_DYNAMIC`, no `DUSK_TIME_DYNAMIC`,
no `DUSK_INPUT_KEYBOARD`, no `DUSK_INPUT_POINTER`,
no `DUSK_NETWORK_IPV6`.
---
## Display
- Fixed 960x544 resolution.
- vitaGL translates OpenGL calls to GXM. Some OpenGL calls are stubbed
out in `duskplatform.h` where vitaGL does not support them:
```c
#define glDrawArrays(type, first, count) ((void)0)
#define glDepthFunc(func) ((void)0)
#define glBlendFunc(sfactor, dfactor) ((void)0)
#define glColorTableEXT(...) ((void)0)
```
These stubs mean the Vita uses the fixed-function pipeline through
vitaGL. Do not rely on `glDrawArrays` or depth/blend state changes
being applied -- use the engine's `displaystate_t` flags instead
(see `.claude/display-shader.md`).
- `DUSK_OPENGL_LEGACY` is set. Avoid shader-based features that are
not in the fixed-function ES1 subset.
- Texture dimensions **must** be powers of two.
---
## Asset loading
`dusk.dsk` is bundled inside the `.vpk` and mounted at `app0:/` by the
Vita OS. The asset system opens it at the fixed path:
```c
#define ASSET_VITA_DSK_PATH "app0:/" ASSET_FILE_NAME
```
No path search is needed -- the file is always at that location.
---
## Input
Uses SDL2 with Vita button mapping. Buttons map to SDL2 gamepad
constants:
| Vita button | SDL2 constant |
|-------------|---------------|
| Triangle | `SDL_CONTROLLER_BUTTON_Y` |
| Cross | `SDL_CONTROLLER_BUTTON_A` |
| Circle | `SDL_CONTROLLER_BUTTON_B` |
| Square | `SDL_CONTROLLER_BUTTON_X` |
| Start | `SDL_CONTROLLER_BUTTON_START` |
| Select | `SDL_CONTROLLER_BUTTON_BACK` |
| D-pad | `SDL_CONTROLLER_BUTTON_DPAD_*` |
| L / R | `SDL_CONTROLLER_BUTTON_LEFTSHOULDER / RIGHTSHOULDER` |
| L-Stick | `SDL_CONTROLLER_AXIS_LEFTX/Y` |
Vita also has L2, R2, L3, R3 and touch surfaces -- not currently wired
into the input system.
---
## Save system
Uses `SceIofilemgr` (Vita filesystem API) for file I/O. Save data lives
in the application's sandbox on the memory card. The stream API
(`savestream_t`) handles all serialization with automatic CRC32 and
little-endian encoding (see `.claude/save.md`).
---
## Network
No network implementation exists in `src/duskvita/`. Network
functionality is not currently available on Vita.
---
## Time
No `src/duskvita/time/` directory exists -- the Vita time implementation
falls back to the SDL2 time layer if available, or is not yet
implemented.
---
## Build and toolchain
Requires the [VITASDK](https://vitasdk.org/). Set `VITASDK` in your
environment before configuring.
```sh
cmake -B build \
-DDUSK_TARGET_SYSTEM=vita \
-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
```
Post-build output: `Dusk.self` (signed executable) and `Dusk.vpk`
(installable package containing the SELF + `dusk.dsk`).
Title ID: `DUSK00001` (configurable via `VITA_TITLEID`).
Dependencies: SDL2, vitaGL, mathneon, vitashark, kubridge, SceGxm,
SceCtrl, SceAudio, SceTouch, SceRtc, SceAppUtil, zip, bz2, z, lzma.
---
## Endianness
Little-endian. `DUSK_PLATFORM_ENDIAN_LITTLE` is set at compile time.
---
## Gotchas
- vitaGL stubs several OpenGL calls. Always use the engine display state
API rather than calling `glBlendFunc` / `glDepthFunc` directly.
- `DUSK_TIME_DYNAMIC` is not set -- all ticks are fixed-step 16 ms.
- Threading: `pthread` is linked but `DUSK_THREAD_PTHREAD` is not
explicitly defined in `vita.cmake`. Verify threading behaviour before
relying on it.
- The Vita implementation is less complete than Linux and PSP -- network
and time platform layers are absent. Contributions welcome.
-46
View File
@@ -1,46 +0,0 @@
# Platform -- Windows
`DUSK_TARGET_SYSTEM`: `windows`
Source layer: `src/duskwindows/` (planned, does not exist yet)
Status: **Planned -- not yet implemented**
---
## Overview
Windows desktop is a planned target. No source layer, CMake target file,
or toolchain exists yet. The intended architecture closely mirrors Linux:
SDL2 for windowing/input, desktop OpenGL for rendering, pthreads (via
MinGW or MSVC pthreads shim) for threading.
---
## Expected macros (when implemented)
| Macro | Expected |
|-------|---------|
| `DUSK_WINDOWS` | yes |
| `DUSK_SDL2` | yes |
| `DUSK_OPENGL` | yes |
| `DUSK_DISPLAY_SIZE_DYNAMIC` | yes |
| `DUSK_INPUT_KEYBOARD` | yes |
| `DUSK_INPUT_POINTER` | yes |
| `DUSK_INPUT_GAMEPAD` | yes |
| `DUSK_PLATFORM_ENDIAN_LITTLE` | yes |
| `DUSK_TIME_DYNAMIC` | yes |
| `DUSK_THREAD_PTHREAD` | yes |
---
## Notes
- Will be little-endian (x86-64 Windows).
- Expected to share `src/dusksdl2/` and `src/duskgl/` with Linux and
Knulli; only a thin `src/duskwindows/` layer for OS-specific
functionality (save paths, system dialogs) should be needed.
- Save files will likely live in `%APPDATA%` or a sibling `saves/`
directory.
- No cross-compiler needed; MSVC or MinGW-w64 on Windows or a
cross-compile from Linux.
Update this document when the Windows target is implemented.
-98
View File
@@ -1,98 +0,0 @@
# Platform Support
Dusk targets a wide range of platforms, from modern desktops to classic
handheld and home consoles. New platform targets will be added over time.
## Platform index
| Platform | `DUSK_TARGET_SYSTEM` | Status | Reference |
|----------|----------------------|--------|-----------|
| Linux | `linux` | Supported | `.claude/platform-linux.md` |
| Knulli | `knulli` | Supported | `.claude/platform-linux.md` |
| Windows | `windows` | Planned | `.claude/platform-windows.md` |
| macOS | `macos` | Planned | `.claude/platform-macos.md` |
| Sony PSP | `psp` | Supported | `.claude/platform-psp.md` |
| PlayStation Vita | `vita` | Supported | `.claude/platform-vita.md` |
| Nintendo GameCube | `gamecube` | Supported | `.claude/platform-dolphin.md` |
| Nintendo Wii | `wii` | Supported | `.claude/platform-dolphin.md` |
GameCube and Wii share the `src/duskdolphin/` layer and are collectively
referred to as **Dolphin** targets throughout the codebase.
---
## Layer structure
```
src/dusk/ Core -- platform-agnostic game logic and ECS
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
```
Dolphin is the only target that bypasses SDL2 and OpenGL entirely --
it uses native libogc GX for rendering and PAD for input.
---
## Capability macros
Each target sets a combination of these macros. Do not assume a
capability is present without checking the appropriate macro.
| Macro | Meaning |
|-------|---------|
| `DUSK_SDL2` | SDL2 is available |
| `DUSK_OPENGL` | OpenGL is available |
| `DUSK_OPENGL_ES` | OpenGL ES variant (Knulli) |
| `DUSK_OPENGL_LEGACY` | Fixed-function OpenGL (PSP, Vita) |
| `DUSK_INPUT_GAMEPAD` | Gamepad / controller input |
| `DUSK_INPUT_KEYBOARD` | Keyboard input (Linux, Knulli only) |
| `DUSK_INPUT_POINTER` | Mouse / pointer input (Linux, Knulli only) |
| `DUSK_DISPLAY_SIZE_DYNAMIC` | Window is resizable (Linux, Knulli) |
| `DUSK_TIME_DYNAMIC` | Dynamic timestep available (Linux, Knulli) |
| `DUSK_THREAD_PTHREAD` | pthreads available |
| `DUSK_NETWORK_IPV6` | IPv6 supported (Linux only) |
| `DUSK_PLATFORM_ENDIAN_BIG` | Big-endian byte order |
| `DUSK_PLATFORM_ENDIAN_LITTLE` | Little-endian byte order |
| `DUSK_DOLPHIN` | Any Dolphin target |
| `DUSK_DOLPHIN_BUILD_ISO` | Dolphin DVD-ISO asset mode |
| `DUSK_CONSOLE_POSIX` | POSIX assert backtrace (Linux only) |
---
## Quick comparison
| | Linux | Knulli | PSP | Vita | GameCube | Wii |
|-|-------|--------|-----|------|----------|-----|
| SDL2 | yes | yes | yes | yes | no | no |
| OpenGL | desktop | ES2 | legacy | vitaGL | no | no |
| Endian | little | little | little | little | **big** | **big** |
| Dynamic resize | yes | yes | no | no | no | no |
| Dynamic timestep | yes | yes | no | no | no | no |
| Keyboard | yes | yes | no | no | no | no |
| Pointer/mouse | yes | yes | no | no | no | no |
| Network | full | full | partial | no | no | partial |
| Save storage | file | file | MS/SAVEDATA | SceIo | Mem Card | SD/NAND |
| Asset source | dsk file | dsk file | inside .pbp | inside .vpk | SD or DVD | SD or DVD |
---
## 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 new code under `src/dusk<platform>/` in the matching subsystem
folder.
- Gate any core call-site with `#ifdef DUSK_<PLATFORM>` or the
relevant capability macro.
- Keep `src/dusk/` free of platform `#ifdef`s -- delegate through
the platform header macros instead.
-148
View File
@@ -1,148 +0,0 @@
# Save System
Source: `src/dusk/save/`, platform layers in `src/dusk<platform>/save/`
## Overview
The save system provides multi-slot persistent storage. Each slot
maps to one `savefile_t`. Platform implementations handle the actual
read/write (memory card on GameCube/Wii, EEPROM/flash on PSP,
filesystem on Linux).
## Global state
```c
extern save_t SAVE;
// SAVE.files[SAVE_FILE_COUNT_MAX] -- one per slot
// SAVE.platform -- platform-specific state
```
## API
```c
errorret_t saveInit(void);
errorret_t saveDispose(void);
errorret_t saveLoad(uint8_t slot); // read slot from storage -> SAVE.files[slot]
errorret_t saveWrite(uint8_t slot); // write SAVE.files[slot] -> storage
errorret_t saveDelete(uint8_t slot); // delete slot from storage
bool_t saveExists(uint8_t slot); // true if a save file is present
savefile_t *saveGet(uint8_t slot); // pointer to the in-memory slot data
```
Slot indices are 0-based, range `[0, SAVE_FILE_COUNT_MAX - 1]`.
## Save file structure (`savefile.h`)
`savefile_t` is a plain struct written verbatim to storage. Keep it
small and use fixed-width integer types (`uint8_t`, `int32_t`, etc.)
to ensure cross-platform binary compatibility.
**Endianness:** storage is always written in little-endian byte order.
Use the `endian.h` utilities when reading fields on big-endian targets
(GameCube, Wii). See `.claude/util.md`.
**Versioning:** include a version field at the start of `savefile_t`.
Check it on load and handle mismatches gracefully (reset to defaults
rather than crashing on corrupt data).
## Save stream (`savestream.h`)
`savestream_t` is a cursor-based reader/writer used to serialize
`savefile_t` to/from a raw byte buffer. Platform implementations
use it to abstract the I/O layer.
```c
typedef struct {
bool_t found;
uint32_t checksum;
uint32_t expectedChecksum;
saveplatformstream_t platform;
} savestream_t;
```
### Typed read/write macros
Use the `saveFile*` macros inside `saveFileLoad` and `saveFileWrite`.
All multi-byte values are stored in little-endian order; endian
conversion is handled automatically.
```c
saveFileReadHeader(stream, headerBuf)
saveFileWriteHeader(stream, headerBuf)
saveFileReadVersion(stream, &version)
saveFileWriteVersion(stream, &version)
saveFileReadBool(stream, &boolField)
saveFileWriteBool(stream, &boolField)
saveFileReadInt8(stream, &i8) saveFileWriteInt8(stream, &i8)
saveFileReadUInt8(stream, &u8) saveFileWriteUInt8(stream, &u8)
saveFileReadInt16(stream, &i16) saveFileWriteInt16(stream, &i16)
saveFileReadUInt16(stream, &u16) saveFileWriteUInt16(stream, &u16)
saveFileReadInt32(stream, &i32) saveFileWriteInt32(stream, &i32)
saveFileReadUInt32(stream, &u32) saveFileWriteUInt32(stream, &u32)
saveFileReadInt64(stream, &i64) saveFileWriteInt64(stream, &i64)
saveFileReadUInt64(stream, &u64) saveFileWriteUInt64(stream, &u64)
saveFileReadFloat(stream, &f) saveFileWriteFloat(stream, &f)
saveFileReadString(stream, buf, maxLen)
saveFileWriteString(stream, str, maxLen)
saveFileReadDate(stream, &epoch)
saveFileWriteDate(stream, &epoch)
```
Each macro expands to `errorChain(saveStreamRead/WriteXxxImpl(...))`.
A failing read/write propagates the error up from `saveFileLoad` /
`saveFileWrite`.
### Typical saveFileLoad / saveFileWrite pattern
```c
errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
char_t header[SAVE_FILE_HEADER_SIZE];
saveFileReadHeader(stream, header);
saveFileReadVersion(stream, &file->version);
saveFileReadInt32(stream, &file->score);
// ... remaining fields ...
errorOk();
}
errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
char_t header[SAVE_FILE_HEADER_SIZE] = SAVE_FILE_HEADER;
saveFileWriteHeader(stream, header);
saveFileWriteVersion(stream, &file->version);
saveFileWriteInt32(stream, &file->score);
// ... remaining fields ...
errorOk();
}
```
After `saveFileWrite` completes, the platform layer calls
`saveStreamFinalizeWriteImpl` which seeks back and writes the CRC32.
After `saveFileLoad`, the platform calls `saveStreamVerifyChecksumImpl`
to confirm the CRC matches.
## Platform notes
| Platform | Storage mechanism |
|----------|------------------|
| Linux | File in user home / working directory |
| Knulli | File on filesystem |
| PSP | EEPROM / memory stick via `sceIo` |
| GameCube | Memory Card via libogc `CARD_*` API |
| Wii | NAND filesystem via libogc or SD card |
Platform-specific save implementations go in `src/dusk<platform>/save/`
and are wired in via `save/saveplatform.h` macros.
## PSP note
PSP save dialogs are OS-level UI shown via `sceUtility`. When a dialog
is open, `systemGetActiveDialogType()` returns a blocking type so the
engine pauses the main loop. Never call save functions directly from
game code without going through the engine's dialog guard.
-138
View File
@@ -1,138 +0,0 @@
# Scene System
Source: `src/dusk/scene/`
## Overview
The scene system is the top-level coordinator for a running game state.
It manages one active scene at a time. Scenes are JS scripts -- each
scene is a `.js` asset file that exports an object with lifecycle hooks.
The scene system loads, ticks, and tears down these scripts, while the
C side runs the ECS and render pipeline on each tick.
## Scene lifecycle (C side)
```c
extern scene_t SCENE;
errorret_t sceneInit(void); // initialise the scene manager
errorret_t sceneUpdate(void); // process pending transition, tick active scene
errorret_t sceneRender(void); // render entities + render pipeline + UI
errorret_t sceneDispose(void); // dispose the active scene immediately
```
`sceneUpdate` each tick:
1. Checks for a pending scene transition and performs it (dispose old,
load and init new).
2. Calls the JS scene's `update()` hook.
3. Calls `entityManagerUpdate()` to fire all entity update callbacks.
`sceneRender` each tick:
1. Binds the screen.
2. Calls `sceneRenderPipeline()` -- renders all entities with a
`COMPONENT_TYPE_RENDERABLE` in priority order.
3. Renders UI.
4. Calls the JS scene's `render()` hook (for any custom drawing).
5. Unbinds the screen.
## Scene lifecycle (JS side)
A scene file exports a plain object with these optional hooks:
```js
var scene = {};
scene.init = async function() {
// Load assets, create entities, set up state.
// May be async -- await asset loads here.
};
scene.update = function() {
// Called each fixed-timestep tick.
};
scene.render = function() {
// Called each render tick, after ECS renderables.
};
scene.dispose = function() {
// Clean up entities and state.
};
module.exports = scene;
```
See `CLAUDE.md` -- "JavaScript (asset scripts)" for JS style rules.
## Render pipeline (`scenerenderpipeline.h`)
`sceneRenderPipeline(cameraEntityId)` gathers all active
`COMPONENT_TYPE_RENDERABLE` components, sorts them by effective
priority, and draws each one using its shader.
**Priority rules:**
- `renderable.priority != 0` -- use that value directly.
- `renderable.priority == 0` -- auto-derive: opaque geometry sorts
before transparent geometry; sprite batches sort before shader
materials; etc.
- Lower priority number = drawn first (behind); higher = drawn last
(on top).
The shader used for each renderable:
- `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` and `CUSTOM` default to
`SHADER_LIST_SHADER_UNLIT`.
- `ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL` uses the shader indexed by
`renderable.data.material.shaderType` in `SHADER_LIST_DEFS`.
## Transitioning between scenes
Scene transitions are handled entirely in JS via the `Scene` global.
The `Scene` object is a singleton with:
```js
// Switch to a new scene. Calls dispose() on the current scene, then
// init() on the new one. Both happen synchronously this tick.
Scene.set(newSceneObject);
// The current scene object (may be null):
Scene.current
```
Typical scene-switch pattern:
```js
// Inside a scene's update or event handler:
const nextScene = require("scenes/gameplay.js");
Scene.set(nextScene);
```
`Scene.set` is synchronous -- it calls `dispose` on the old scene and
`init` on the new scene before returning. If `init` needs async work
(loading assets), use an async function and `await` inside `init`:
```js
nextScene.init = async function() {
await batch.load(); // wait for assets before proceeding
};
```
The C side does not defer the transition; the switch happens inside
the current `sceneUpdate` call.
## Relationship to the engine loop
```
engineUpdate()
timeUpdate()
inputUpdate()
physicsManagerUpdate()
scriptUpdate() <- runs JS microjobs
sceneUpdate() <- JS update + ECS entity updates
engineUpdate() -> sceneRender()
screenBind()
sceneRenderPipeline() <- ECS renderables sorted by priority
uiRender()
sceneRender (JS hook)
screenUnbind / screenRender
```
-104
View File
@@ -1,104 +0,0 @@
# Script -- Async Promises (`scriptpromisepend_t`)
Source: `src/dusk/script/scriptpromisepend.h`
See also: `.claude/script.md`
---
## Overview
When a C module needs to resolve a JS `Promise` from an asynchronous
C event (e.g. an asset finishing loading, a network response arriving),
use `scriptpromisepend_t`. The pattern avoids heap allocation by using
a fixed-size pending slot array declared in the module.
---
## Declaring the pending array
```c
#define MY_MODULE_PENDING_MAX 8
static scriptpromisepend_t MY_PENDING[MY_MODULE_PENDING_MAX];
static uint32_t MY_PENDING_COUNT = 0;
```
---
## Add a pending promise
Called from the JS-facing function that returns the Promise:
```c
jerry_value_t promise = jerry_create_promise();
scriptPromisePendAdd(
MY_PENDING, &MY_PENDING_COUNT, MY_MODULE_PENDING_MAX,
key, // opaque void * used to match the resolve/reject later
promise
);
return jerry_acquire_value(promise); // return a copy to the caller
```
The `key` should be a stable pointer that uniquely identifies the
async operation -- e.g. an `assetentry_t *`, a network request handle,
or a pointer to a fixed-size slot in the module.
---
## Resolve or reject
Called when the C event fires, typically from `moduleUpdate` or an
event callback:
```c
// On success:
scriptPromisePendResolve(
MY_PENDING, &MY_PENDING_COUNT,
key, jerry_undefined() // or a result value
);
// On failure:
jerry_value_t err = jerry_create_error(
JERRY_ERROR_COMMON, (const jerry_char_t *)"reason"
);
scriptPromisePendReject(MY_PENDING, &MY_PENDING_COUNT, key, err);
jerry_release_value(err);
```
Both macros remove the slot from the pending array after settling.
---
## Guard against double-submit
```c
if(scriptPromisePendHas(MY_PENDING, MY_PENDING_COUNT, key)) {
// already waiting -- return the existing promise or an error
}
```
---
## Module teardown
Free all pending promises before cleaning up events or other state:
```c
scriptPromisePendFreeAll(MY_PENDING, &MY_PENDING_COUNT);
```
This rejects all still-pending promises and resets the count to 0.
Call it from the module's `Dispose` function, **before** any backing
data (asset entries, event subscriptions) is torn down.
---
## Design notes
- `MY_MODULE_PENDING_MAX` sets a hard cap on concurrent async ops.
Exceeding it is a runtime assertion -- size the array to the maximum
realistic concurrency for the module.
- The key is opaque (`void *`); the system does not dereference it.
A raw integer cast to `void *` is fine if no pointer is available.
- `scriptUpdate()` runs the JerryScript microjob queue each frame,
which is what processes `.then()` chains after a resolve/reject.
-167
View File
@@ -1,167 +0,0 @@
# Script System (JerryScript)
Source: `src/dusk/script/`, modules at `src/dusk/script/module/`
## Overview
The engine embeds **JerryScript** as its scripting runtime. Game scenes
and logic are authored in JavaScript (ES5 subset). The script system
initialises JerryScript, registers all built-in C modules as JS globals,
and runs the event loop each tick.
The full rules for writing JS asset scripts are in `CLAUDE.md` under
"JavaScript (asset scripts)". This doc covers the C-side module system.
## Script lifecycle
```c
errorret_t scriptInit(); // start JerryScript, register all modules
errorret_t scriptUpdate(); // run pending microjobs (call once per frame)
errorret_t scriptDispose(); // shut down JerryScript
errorret_t scriptExecString(const char_t *source);
// Evaluate a JS source string in global scope.
errorret_t scriptExecFile(const char_t *path);
// Load + eval a script from the asset archive. Result cached by asset
// system -- repeated calls with the same path do not re-execute.
```
## Module registration
All C modules are initialised in `src/dusk/script/module/modulelist.c`:
```c
void moduleListInit(void); // called by scriptInit
void moduleListDispose(void); // called by scriptDispose
```
Each module's `Init` is called once. The module registers its
properties and methods on `scriptproto_t` objects (see below), which
become JS globals.
## Writing a C module -- the `scriptproto_t` pattern
A `scriptproto_t` represents a JS class prototype backed by a C struct.
### 1. Declare in the header
```c
// moduleMything.h
extern scriptproto_t MODULE_MYTHING_PROTO;
// Init and dispose for the module itself:
void moduleMyThingInit(void);
void moduleMyThingDispose(void);
```
### 2. Implement
```c
// moduleMything.c
scriptproto_t MODULE_MYTHING_PROTO;
// JS-callable function using the convenience macro:
moduleBaseFunction(myThingDoSomething) {
moduleBaseRequireArgs(1);
moduleBaseRequireNumber(0);
float_t x = moduleBaseArgFloat(0);
// ... do work ...
return jerry_undefined();
}
void moduleMyThingInit(void) {
scriptProtoInit(
&MODULE_MYTHING_PROTO,
"MyThing", // JS global name; NULL to skip registration
sizeof(mything_t),
myThingCtor // constructor handler, or NULL
);
// Instance methods:
scriptProtoDefineFunc(
&MODULE_MYTHING_PROTO, "doSomething", myThingDoSomething
);
// Instance property (get/set):
scriptProtoDefineProp(
&MODULE_MYTHING_PROTO, "x", myThingGetX, myThingSetX
);
// Static method:
scriptProtoDefineStaticFunc(
&MODULE_MYTHING_PROTO, "create", myThingCreate
);
}
```
### 3. Register
In `modulelist.c`: `#include` the header and call `moduleMyThingInit()`
in `moduleListInit()` (and `Dispose` in `moduleListDispose()`).
## `moduleBaseFunction` macro
```c
moduleBaseFunction(myFn) {
// callInfo, args[], argc available
moduleBaseRequireArgs(2);
moduleBaseRequireNumber(0);
moduleBaseRequireString(1);
float_t x = moduleBaseArgFloat(0);
int32_t n = moduleBaseArgInt(0);
bool_t b = moduleBaseArgBool(0);
float_t opt = moduleBaseOptFloat(2, 0.0f); // optional with default
// Error propagation:
errorret_t ret = someCall();
if(errorIsNotOk(ret)) return moduleBaseThrowError(ret);
return jerry_undefined(); // or jerry_boolean(true) etc.
}
```
## Wrapping C values in JS objects
```c
// Create a JS object wrapping a copy of a C value:
jerry_value_t obj = scriptProtoCreateValue(&MY_PROTO, &myValue);
// Unwrap back to C pointer:
mything_t *ptr = scriptProtoGetValue(&MY_PROTO, jsObj);
// ptr is NULL if jsObj is not an instance of MY_PROTO.
```
## Utility helpers (`modulebase.h`)
| Helper | Purpose |
|--------|---------|
| `moduleBaseThrow(msg)` | Return a JS TypeError |
| `moduleBaseThrowError(ret)` | Convert `errorret_t` -> JS error |
| `moduleBaseToString(val, buf, len)` | Jerry value -> C string |
| `moduleBaseGetProp(obj, name)` | Get object property by name |
| `moduleBaseWrapPointer(ptr)` | Wrap a raw pointer in a JS object |
| `moduleBaseUnwrapPointer(val)` | Unwrap a raw pointer |
| `moduleBaseSetValue(name, val)` | Set a global JS variable |
| `moduleBaseSetNumber(name, n)` | Set a global JS number |
| `moduleBaseSetInt(name, n)` | Set a global JS integer |
| `moduleBaseDefineMethod(obj, name, fn)` | Add method to any JS object |
| `moduleBaseDefineGlobalMethod(name, fn)` | Add method to global scope |
## Async JS -- pending promises (`scriptpromisepend.h`)
When a C module needs to resolve a JS `Promise` from an asynchronous
C event, use `scriptpromisepend_t`. Each module declares a fixed-size
pending slot array; the helpers add/resolve/reject by an opaque key.
Full API and design notes: `.claude/script-promises.md`
## Type declarations (`.d.ts`)
Every module that is accessible from JS **must** have a corresponding
TypeScript declaration file in `types/`. The CLAUDE.md checklist
requires updating these whenever a `.c` module file changes.
- Add `types/<category>/mymod.d.ts`
- Add `/// <reference path="..." />` to `types/index.d.ts`
-111
View File
@@ -1,111 +0,0 @@
# Tests and Assertions
## Test infrastructure
Tests live in `test/` and mirror the `src/dusk/` directory structure.
Enable with `-DDUSK_BUILD_TESTS=ON`. The test runner is **cmocka**.
### Entry point
Every test file includes `dusktest.h`, which pulls in `dusk.h` and
`assert/assert.h`. When `DUSK_TEST_ASSERT` is defined, `assert.h`
includes `cmocka.h` and redirects assertion failures through
`mock_assert()` instead of calling `abort()`.
### Test function signature
```c
static void test_something(void **state) {
// ... setup ...
// ... exercise ...
// ... assert ...
assert_int_equal(memoryGetAllocatedCount(), 0); // leak check
}
```
### Registering and running tests
```c
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_errorThrow),
cmocka_unit_test(test_errorOk),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
```
Use `cmocka_unit_test_setup_teardown()` when a test needs per-test
setup or teardown callbacks.
### Assertion mix
Tests use **two** sets of assertion macros:
| Origin | When to use |
|--------|-------------|
| cmocka: `assert_int_equal()`, `assert_non_null()` etc. | Validate results inside test functions |
| Dusk: `assertTrue()`, `assertNotNull()` etc. | Exercise the code under test (these may fire and need catching) |
To assert that a Dusk assertion fires, use cmocka's mock system:
```c
expect_assert_failure(assertTrueImpl(__FILE__, __LINE__, false, "msg"));
```
### Memory leak discipline
Every test function must end by asserting:
```c
assert_int_equal(memoryGetAllocatedCount(), 0);
```
This ensures all allocations from the code under test were freed.
---
## Assertion system
Source: `src/dusk/assert/`
### Runtime vs test mode
| Mode | Trigger | Effect on failure |
|------|---------|-------------------|
| Runtime (default) | Release / non-test builds | Logs the message + backtrace, then calls `abort()` |
| Test (`DUSK_TEST_ASSERT`) | `-DDUSK_TEST_ASSERT` build flag | Routes through cmocka `mock_assert()` for controlled catching |
| Faked (`DUSK_ASSERTIONS_FAKED`) | Defined by platform or test | All macros become no-ops (`((void)0)`) |
### Available macros
| Macro | Description |
|-------|-------------|
| `assertTrue(x, msg)` | Fails if `x` is false |
| `assertFalse(x, msg)` | Fails if `x` is true |
| `assertNotNull(ptr, msg)` | Fails if `ptr` is NULL |
| `assertNull(ptr, msg)` | Fails if `ptr` is not NULL |
| `assertUnreachable(msg)` | Unconditional failure; marks unreachable code |
| `assertDeprecated(msg)` | Marks a code path as deprecated |
| `assertStringEqual(a, b, msg)` | Fails if strings differ |
| `assertStrLenMax(str, len, msg)` | Fails if `strlen(str) >= len` |
| `assertStrLenMin(str, len, msg)` | Fails if `strlen(str) < len` |
| `assertIsMainThread(msg)` | Fails if called from a non-main thread |
| `assertNotMainThread(msg)` | Fails if called from the main thread |
| `assertStructSize(type, size)` | Compile-time size check via `_Static_assert` |
### Thread tracking
`assertInit()` records the main thread ID (pthreads). The main-thread
assertions compare against this stored ID. Call `assertInit()` once at
startup before spawning any threads.
### Usage guidelines
- Prefer the specific macro over a bare `assertTrue` for clarity
(e.g. use `assertNotNull` instead of `assertTrue(ptr != NULL, ...)`).
- Use `assertUnreachable` in `default:` cases of exhaustive switches.
- Use `assertStructSize` to guard struct layouts that must match
a known binary format or a platform ABI.
- Do not use asserts for expected error paths -- use `errorThrow`
instead. Asserts are for programmer mistakes, not runtime errors.
-100
View File
@@ -1,100 +0,0 @@
# Threading System
Source: `src/dusk/thread/`
## Platform support
Threading currently requires **pthreads** (`DUSK_THREAD_PTHREAD`). The
implementation lives in the core thread files and is guarded by that
compile-time flag -- there are no separate per-platform thread
directories.
Thread-local storage uses the `THREAD_LOCAL` macro, which maps to
`__thread` when pthreads is available. This is used by the error system
to give each thread its own `ERROR_STATE`.
## Thread lifecycle
Threads follow a strict state machine:
```
STOPPED -> STARTING -> RUNNING -> STOP_REQUESTED -> STOPPED
```
- `threadStart()` -- blocking: starts the thread and waits until it
reaches RUNNING.
- `threadStop()` -- blocking: requests stop and waits until STOPPED.
- `threadStartRequest()` -- non-blocking equivalent of `threadStart`.
- `threadStopRequest()` -- non-blocking equivalent of `threadStop`.
The thread callback polls `threadShouldStop()` to know when to exit.
Never kill a thread forcefully -- always let it stop cooperatively.
## Thread API
```c
void threadInit(thread_t *thread, errorret_t (*callback)(thread_t *t));
// Initialise; callback is the thread entry point.
errorret_t threadStart(thread_t *thread);
// Start and block until RUNNING.
errorret_t threadStop(thread_t *thread);
// Request stop, block until STOPPED.
void threadStartRequest(thread_t *thread);
void threadStopRequest(thread_t *thread);
// Non-blocking variants.
bool_t threadShouldStop(const thread_t *thread);
// Call from inside the thread callback to know when to exit.
```
## Mutex API (`threadmutex.h`)
Each `threadmutex_t` wraps a pthread mutex and a condition variable.
```c
void threadMutexInit(threadmutex_t *mutex);
void threadMutexDispose(threadmutex_t *mutex);
void threadMutexLock(threadmutex_t *mutex);
void threadMutexUnlock(threadmutex_t *mutex);
bool_t threadMutexTryLock(threadmutex_t *mutex);
// Returns true if the lock was acquired; false if already held.
void threadMutexWaitLock(threadmutex_t *mutex);
// Block until signalled (like pthread_cond_wait).
// Must be called while holding the lock.
void threadMutexSignal(threadmutex_t *mutex);
// Wake one waiter.
```
## Usage example
```c
static errorret_t workerCallback(thread_t *t) {
while(!threadShouldStop(t)) {
// do work
}
errorOk();
}
thread_t worker;
threadInit(&worker, workerCallback);
errorChain(threadStart(&worker));
// ... later ...
errorChain(threadStop(&worker));
```
## Thread safety rules
- The error system (`ERROR_STATE`) is thread-local -- each thread has
its own error state. Do not pass `errorret_t` across thread
boundaries without copying the message and lines strings first.
- Asset loading: the background thread calls `loadAsync`; the main
thread calls `loadSync`. Never call GPU or SDL functions from the
loader background thread.
- Use `assertIsMainThread()` / `assertNotMainThread()` to guard
functions that have thread affinity requirements.
-115
View File
@@ -1,115 +0,0 @@
# Time System
Source: `src/dusk/time/`, platform layers in `src/dusk<platform>/time/`
## Global state
```c
extern dusktime_t TIME;
```
```c
typedef struct {
float_t delta; // Fixed step size in seconds (DUSK_TIME_STEP)
float_t time; // Accumulated game time in seconds
// Only present when DUSK_TIME_DYNAMIC is defined:
float_t lastNonDynamic;
bool_t dynamicUpdate; // true on sub-step ticks
float_t dynamicDelta; // real elapsed seconds this frame
float_t dynamicTime; // accumulated real time
} dusktime_t;
```
## Fixed vs dynamic timestep
### Fixed timestep (default)
`DUSK_TIME_STEP` defaults to `16ms / 1000 = 0.016f` seconds (62.5 Hz).
Every call to `timeUpdate()` advances `TIME.time` by exactly
`DUSK_TIME_STEP` and sets `TIME.delta = DUSK_TIME_STEP`. This is the
safe, deterministic mode for physics and game logic.
### Dynamic timestep (`DUSK_TIME_DYNAMIC`)
When enabled, `timeUpdate()` calls the platform tick hook to measure
actual elapsed time. It fires a "non-dynamic" step (`dynamicUpdate =
false`, `delta = DUSK_TIME_STEP`) once per fixed interval, and
"dynamic" sub-steps (`dynamicUpdate = true`) in between. Systems that
must run on the fixed interval (physics, networking) skip the dynamic
sub-steps by checking:
```c
if(TIME.dynamicUpdate) return;
```
## Platform hooks
Each platform provides three macros in its `time/timeplatform.h`:
| Macro | Purpose |
|-------|---------|
| `timeTickPlatform()` | Sample the hardware timer |
| `timeGetDeltaPlatform()` | Return seconds since last tick |
| `timeGetRealPlatform()` | Return epoch seconds since 1970 |
| `timeGetRealTimeZonePlatform()` | Return local timezone offset (seconds) |
`timeTickPlatform` and `timeGetDeltaPlatform` are only required when
`DUSK_TIME_DYNAMIC` is defined.
## Platform implementations
| Platform | Tick source | Real time source |
|----------|------------|-----------------|
| Linux | `SDL_GetTicks64()` (via SDL2) | `clock_gettime(CLOCK_REALTIME)` |
| Knulli | `SDL_GetTicks64()` (via SDL2) | `clock_gettime(CLOCK_REALTIME)` |
| PSP | `SDL_GetTicks64()` (via SDL2) | `sceRtcGetCurrentTick()` (microseconds) |
| GameCube | none (fixed step only) | `ticks_to_microsecs(__SYS_GetSystemTime())` + 2000->1970 offset |
| Wii | none (fixed step only) | same as GameCube |
GameCube / Wii note: the hardware timer returns ticks since
2000-01-01, so an offset of 946684800 seconds is added to convert to
UNIX epoch. The timezone offset is always returned as 0.0 on Dolphin
(timezone is not available without network time).
## Epoch time (`timeepoch.h`)
```c
typedef struct {
double_t time; // raw UTC seconds since 1970
double_t timeZone; // timezone offset in seconds
double_t offsetTime; // time + timeZone
} dusktimeepoch_t;
dusktimeepoch_t timeGetEpoch(void);
// Returns current time in local timezone.
```
### Epoch helpers
```c
int32_t timeEpochGetYear(epoch);
int32_t timeEpochGetMonth(epoch); // 1-12
int32_t timeEpochGetDayOfMonth(epoch); // 1-31
int32_t timeEpochGetHours(epoch); // 0-23
int32_t timeEpochGetMinutes(epoch); // 0-59
int32_t timeEpochGetSeconds(epoch); // 0-59
bool_t timeEpochIsLeapYear(year);
size_t timeEpochFormat(
dusktimeepoch_t epoch,
const char_t *format, // %Y %m %d %H %M %S
char_t *buffer,
size_t bufferSize
);
```
## Adding a new platform time implementation
1. Create `src/dusk<platform>/time/time<platform>.h/.c`.
2. Implement `timeGetReal<Platform>()` and
`timeGetRealTimeZone<Platform>()`.
3. If `DUSK_TIME_DYNAMIC`: also implement `timeTick<Platform>()` and
`timeGetDelta<Platform>()`.
4. Create `src/dusk<platform>/time/timeplatform.h` with the `#define`
macros pointing to your functions.
-226
View File
@@ -1,226 +0,0 @@
# UI System
Source: `src/dusk/ui/`
See also: `.claude/display-spritebatch.md`, `.claude/display-text.md`,
`.claude/display-color.md`
---
## Overview
The UI system renders overlaid interface elements on top of the scene
each frame. It is called by the engine after the scene render pipeline
and before the screen unbind -- game code does not drive it directly.
All UI elements render through the global SpriteBatch.
---
## Lifecycle
```c
extern ui_t UI;
errorret_t uiInit(void);
void uiUpdate(void);
errorret_t uiRender(void);
void uiDispose(void);
```
`uiUpdate` is called each game tick; `uiRender` is called each render
tick. The engine calls both automatically -- do not call them from
game scripts.
---
## Element registration (`uielement.h`)
```c
typedef struct {
uielementtype_t type; // UI_ELEMENT_TYPE_NULL or UI_ELEMENT_TYPE_NATIVE
errorret_t (*draw)(); // draw callback
} uielement_t;
extern uielement_t UI_ELEMENTS[]; // registered element array
```
`uiRender` iterates `UI_ELEMENTS` and calls each element's `draw`
callback. Elements register themselves during `uiInit`.
---
## Elements
### uiframe_t -- 9-slice border (`uiframe.h`)
A resizable bordered panel rendered using 9-slice (9-patch) technique
from a 3x3 tileset.
```c
typedef struct {
tileset_t tileset;
texture_t *texture;
} uiframe_t;
errorret_t uiFrameInit(uiframe_t *frame);
errorret_t uiFrameDraw(
const uiframe_t *frame,
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
void uiFrameDispose(uiframe_t *frame); // does not dispose texture
```
`uiFrameDraw` pushes 9 quads (corners + edges + centre) to the
SpriteBatch without flushing. The caller must call `spriteBatchFlush()`
after all sprites for a draw pass are buffered.
---
### uitextbox_t -- typewriter dialogue box (`uitextbox.h`)
A global single-instance dialogue box that displays text one character
at a time with word-wrap and multi-page support.
```c
extern uitextbox_t UI_TEXTBOX;
errorret_t uiTextboxInit(void);
void uiTextboxDispose(void);
```
**Setting text:**
```c
uiTextboxSetText("Hello, world!");
// Automatically word-wraps and paginates.
// Resets currentPage and scroll to 0.
```
**Tick update (called from script `update` or uiUpdate):**
```c
uiTextboxUpdate();
// Advances typewriter by UI_TEXTBOX_SCROLL_CHARS_PER_TICK (1) each
// fixed tick. Skipped on dynamic sub-ticks.
```
**Page control:**
```c
bool_t uiTextboxPageIsComplete(void); // scroll revealed full page
bool_t uiTextboxHasNextPage(void); // more pages remain
void uiTextboxNextPage(void); // advance; no-op on last page
int32_t uiTextboxGetPageCharCount(void);
```
**Events:**
```c
// Fires when the current page is fully scrolled into view:
UI_TEXTBOX.onPageComplete
// Fires when the last page has been fully scrolled:
UI_TEXTBOX.onLastPage
```
Subscribe via the event system (see `.claude/events.md`).
**Limits:**
| Constant | Value |
|----------|-------|
| `UI_TEXTBOX_TEXT_MAX` | 1024 chars |
| `UI_TEXTBOX_LINES_MAX` | 64 lines |
| `UI_TEXTBOX_LINES_PER_PAGE_MAX` | 3 lines per page |
| `UI_TEXTBOX_SCROLL_CHARS_PER_TICK` | 1 char per tick |
---
### uifullbox_t -- full-screen colour overlay (`uifullbox.h`)
An animated solid-colour overlay that covers the entire screen. Used
for fade-to-black, scene transitions, and flash effects.
Two global instances are provided:
```c
extern uifullbox_t UI_FULLBOX_UNDER; // draws below scene content
extern uifullbox_t UI_FULLBOX_OVER; // draws above all content
```
```c
void uiFullboxInit(uifullbox_t *fullbox);
// Start a colour-to-colour transition:
void uiFullboxTransition(
uifullbox_t *fullbox,
color_t from,
color_t to,
float_t duration,
easingtype_t easing
);
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta);
errorret_t uiFullboxDraw(uifullbox_t *fullbox);
// Draw is skipped entirely when alpha == 0.
```
`fullbox.onTransitionEnd` event fires once when the transition
completes. Subscribe to it to chain scene transitions:
```c
// Typical fade-out before a scene change:
uiFullboxTransition(
&UI_FULLBOX_OVER,
COLOR_TRANSPARENT, COLOR_BLACK,
0.5f, EASING_OUT_QUAD
);
eventSubscribe(&UI_FULLBOX_OVER.onTransitionEnd, onFadeComplete, NULL);
```
---
### uiloading_t -- loading indicator (`uiloading.h`)
An animated loading indicator with fade-in / fade-out transitions.
Shown while asset batches are loading.
```c
extern uiloading_t UI_LOADING;
void uiLoadingInit(void);
void uiLoadingUpdate(float_t delta);
errorret_t uiLoadingDraw(void);
// Fade in; callback fires when fully visible:
void uiLoadingShow(eventcallback_t callback, void *user);
// Fade out; callback fires when fully hidden:
void uiLoadingHide(eventcallback_t callback, void *user);
```
`UI_LOADING_FADE_DURATION` is 0.5 seconds. `UI_LOADING_MARGIN` is 8px.
`uiLoadingDraw` is a no-op when the indicator is fully transparent.
---
### uifps_t -- FPS counter (`uifps.h`)
Debug FPS display. Measures real elapsed time between ticks and draws
the average frame rate as text in the corner of the screen.
```c
extern uifps_t UIFPS;
errorret_t uiFPSDraw(); // also performs the measurement update
```
Intended for debug builds only. Draw it explicitly from a JS `render`
hook or from a UI element -- it is not registered in `UI_ELEMENTS` by
default.
-191
View File
@@ -1,191 +0,0 @@
# Utility Library
Source: `src/dusk/util/`
All C code in the project must use these utilities instead of their
standard library equivalents. Do not use `malloc`, `free`, `strcmp`,
`strcpy`, `memcpy`, `memset`, etc. directly.
---
## Memory (`memory.h`)
```c
void *memoryAllocate(size_t size);
void *memoryAlign(size_t alignment, size_t size); // aligned alloc
void memoryFree(void *ptr);
void memoryCopy(void *dest, const void *src, size_t size);
void memoryZero(void *dest, size_t size);
errorret_t memoryCompare(const void *a, const void *b, size_t size);
size_t memoryGetAllocatedCount(void);
// Returns the number of live allocations. Must be 0 at test teardown.
void memoryTrack(void *ptr);
// Register a pointer that was malloc'd outside the engine (e.g. by a
// third-party library) so it counts toward the allocation tracker.
```
`MEMORY_POINTERS_IN_USE` is a file-scope static tracking the live count.
It is incremented by `memoryAllocate` / `memoryTrack` and decremented by
`memoryFree`. Tests assert this is 0 at teardown to catch leaks.
---
## String (`string.h`)
Use these instead of `<string.h>` / `<ctype.h>` functions:
```c
void stringCopy(char_t *dest, const char_t *src, size_t destSize);
int stringCompare(const char_t *a, const char_t *b);
bool_t stringEquals(const char_t *a, const char_t *b);
int stringCompareInsensitive(const char_t *a, const char_t *b);
size_t stringLength(const char_t *str);
void stringTrim(char_t *str);
bool_t stringIsWhitespace(char_t c);
bool_t stringStartsWith(const char_t *str, const char_t *prefix);
bool_t stringEndsWith(const char_t *str, const char_t *suffix);
bool_t stringContains(const char_t *haystack, const char_t *needle);
char_t *stringFind(const char_t *haystack, const char_t *needle);
void stringFormat(char_t *dest, size_t destSize, const char_t *fmt, ...);
int32_t stringToInt(const char_t *str);
float_t stringToFloat(const char_t *str);
void stringFromInt(char_t *dest, size_t destSize, int32_t value);
void stringFromFloat(char_t *dest, size_t destSize, float_t value);
```
`destSize` in `stringCopy` / `stringFormat` is the buffer capacity
**excluding** the null terminator.
---
## Math (`math.h`)
```c
#define MATH_PI M_PI
#define mathMax(a, b)
#define mathMin(a, b)
#define mathClamp(x, lower, upper)
#define mathAbs(amt)
uint32_t mathNextPowTwo(uint32_t value);
float_t mathModFloat(float_t x, float_t y); // always non-negative
float_t mathLerp(float_t a, float_t b, float_t t);
// plus additional trig / remap helpers
```
The project uses **cglm** for vector and matrix math (`vec3`, `mat4`,
`glm_vec3_*`, `glm_mat4_*`, etc.). `math.h` provides scalar helpers
that complement cglm.
---
## Endian (`endian.h`)
GameCube and Wii are big-endian. Any binary data format (asset files,
network packets) must use the endian utilities when reading multi-byte
values.
```c
bool_t isHostLittleEndian(void);
uint16_t endianLittleToHost16(uint16_t value);
uint32_t endianLittleToHost32(uint32_t value);
uint64_t endianLittleToHost64(uint64_t value);
float_t endianLittleToHostFloat(float_t value);
```
If neither `DUSK_PLATFORM_ENDIAN_LITTLE` nor `DUSK_PLATFORM_ENDIAN_BIG`
is defined, the implementation falls back to a runtime check
(`ENDIAN_MAGIC` probe). Prefer setting the compile-time macro for new
platform targets.
---
## Reference counting (`ref.h`)
`ref_t` is a generic reference-counted handle with optional lock /
unlock / all-unlocked callbacks.
```c
void refInit(
ref_t *ref,
void *data,
refcallback_t onLock,
refcallback_t onUnlock,
refcallback_t onAllUnlocked // called when count -> 0; do cleanup here
);
void refLock(ref_t *ref); // increment count
bool_t refUnlock(ref_t *ref); // decrement; returns true if count == 0
```
The asset entry system uses `ref_t` internally to track how many
subsystems have locked a loaded asset.
---
## Array (`array.h`)
```c
void arrayReverse(void *array, size_t count, size_t elementSize);
```
Generic in-place reverse using the element stride.
---
## Sort (`sort.h`)
Use these instead of `qsort` for portability across all platforms.
```c
typedef int_t (*sortcompare_t)(const void *, const void *);
void sortBubble(
void *array,
const size_t count,
const size_t size,
const sortcompare_t compare
);
void sortQuick(
void *array,
const size_t count,
const size_t size,
const sortcompare_t compare
);
#define sort sortQuick // preferred; use this in new code
```
Typed convenience helpers for `uint8_t` arrays:
```c
int sortArrayU8Compare(const void *a, const void *b);
void sortArrayU8(uint8_t *array, const size_t count);
```
---
## Crypt (`crypt.h`)
CRC32 checksum for save file integrity. Not cryptographically secure --
do not use for security purposes.
```c
// One-shot checksum:
uint32_t cryptCRC32(const void *data, const size_t size);
// Streaming (incremental) CRC32:
uint32_t cryptCRC32Begin(void);
void cryptCRC32Update(
uint32_t *crc, const void *data, const size_t size
);
uint32_t cryptCRC32End(const uint32_t crc);
```
The streaming API allows computing a checksum across multiple buffers
or while interleaving other reads -- the save system uses this to
verify the whole save slot in a single pass.
+10 -87
View File
@@ -1,8 +1,13 @@
name: Build Dusk
on:
push:
tags:
- '*'
branches:
- main
pull_request:
branches:
- main
jobs:
run-tests:
runs-on: ubuntu-latest
@@ -50,42 +55,6 @@ jobs:
path: ./git-artifcats/Dusk
if-no-files-found: error
# build-vita:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v6
# - name: Set up Docker
# uses: docker/setup-docker-action@v5
# - name: Build Vita
# run: ./scripts/build-vita-docker.sh
# - name: Upload Vita binary
# uses: actions/upload-artifact@v6
# with:
# name: dusk-vita
# path: build-vita/Dusk.vpk
# if-no-files-found: error
build-knulli:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build knulli
run: ./scripts/build-knulli-docker.sh
- name: Move output to Dusk subfolder
run: |
mkdir -p ./git-artifcats/Dusk
cp -r build-knulli/dusk ./git-artifcats/Dusk
- name: Upload knulli binary
uses: actions/upload-artifact@v6
with:
name: dusk-knulli
path: ./git-artifcats/Dusk
if-no-files-found: error
build-gamecube:
runs-on: ubuntu-latest
steps:
@@ -105,29 +74,6 @@ jobs:
with:
name: dusk-gamecube
path: ./git-artifcats/Dusk
if-no-files-found: error
build-gamecube-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build GameCube ISO
run: ./scripts/build-gamecube-iso-docker.sh
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk
cp build-gamecube-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
cp build-gamecube-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
cp build-gamecube-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
- name: Upload GameCube ISO
uses: actions/upload-artifact@v6
with:
name: dusk-gamecube-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii:
runs-on: ubuntu-latest
@@ -141,34 +87,11 @@ jobs:
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/Dusk.dol
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
cp docker/dolphin/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
- name: Upload Wii binary
uses: actions/upload-artifact@v6
with:
name: dusk-wii
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build Wii ISO
run: ./scripts/build-wii-iso-docker.sh
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk
cp build-wii-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
cp build-wii-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
cp build-wii-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
- name: Upload Wii ISO
uses: actions/upload-artifact@v6
with:
name: dusk-wii-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
path: ./git-artifcats/Dusk
+2 -4
View File
@@ -83,6 +83,7 @@ assets/borrowed
.VSCode*
/vita
._*
*~
@@ -103,7 +104,4 @@ yarn.lock
.venv
/build2
/build*
/assets/test
/tools_old
/assets/test.png
/build*
-519
View File
@@ -1,519 +0,0 @@
# Dusk -- Claude Code rules
## About Dusk
Dusk is a pure C game and game engine. There is no C++ anywhere in the
codebase. The engine is built around a data-oriented Entity Component
System (ECS) and is designed for heavy optimization across a wide range
of hardware targets, including platforms with very limited RAM and CPU.
**Current and planned platforms:** Linux, Windows, macOS, Sony PSP,
PlayStation Vita, Nintendo GameCube, Nintendo Wii. Additional platforms
will be added over time. GameCube and Wii are collectively referred to
as the **Dolphin** targets throughout the codebase and docs.
**Build system:** CMake exclusively. Every source subdirectory owns its
own `CMakeLists.txt`.
**Architecture:** Entity Component System (ECS) as the primary pattern.
All game objects are entities; behaviour and state are attached via
components. No inheritance hierarchies -- favour composition.
**Optimization:** Performance is a first-class constraint, not an
afterthought. The engine must run well on hardware as constrained as
the GameCube (16 MB main RAM, 485 MHz PowerPC) and PSP (32 MB RAM,
333 MHz MIPS). Every design and implementation decision should consider
the most constrained target.
**Coding style:** All C code must strictly follow the project style
rules documented in the [Coding style](#coding-style) section below.
Deviations are not acceptable.
### Further reading
Detailed documentation on specific topics lives in `.claude/`:
| Topic | File |
|-------|------|
| Platform overview, capability macros, quick comparison | `.claude/platforms.md` |
| Platform -- Linux and Knulli | `.claude/platform-linux.md` |
| Platform -- Sony PSP | `.claude/platform-psp.md` |
| Platform -- PlayStation Vita | `.claude/platform-vita.md` |
| Platform -- GameCube and Wii (Dolphin) | `.claude/platform-dolphin.md` |
| Platform -- Windows (planned) | `.claude/platform-windows.md` |
| Platform -- macOS (planned) | `.claude/platform-macos.md` |
| ECS architecture and conventions | `.claude/ecs.md` |
| CMake build system and toolchain setup | `.claude/build.md` |
| Optimization guidelines and platform budgets | `.claude/optimization.md` |
| Test infrastructure and assertion macros | `.claude/tests.md` |
| Error handling system (`errorret_t`, macros) | `.claude/errors.md` |
| Asset system (loading, caching, loaders) | `.claude/assets.md` |
| Threading (`thread_t`, mutex, thread-local) | `.claude/threading.md` |
| Input system (actions, buttons, platforms) | `.claude/input.md` |
| Physics simulation and collision shapes | `.claude/physics.md` |
| Event system (pub/sub) | `.claude/events.md` |
| Locale / localisation system | `.claude/locale.md` |
| Time system (fixed/dynamic, epoch) | `.claude/time.md` |
| Network system (per-platform status) | `.claude/network.md` |
| Utility library (memory, string, math, endian, ref) | `.claude/util.md` |
| Script system (JerryScript, module proto API) | `.claude/script.md` |
| Script async promises (`scriptpromisepend_t`) | `.claude/script-promises.md` |
| Engine main loop, system platform API, log | `.claude/engine.md` |
| Save system (multi-slot, platform storage) | `.claude/save.md` |
| Animation (keyframes, easing functions) | `.claude/animation.md` |
| Display (index) | `.claude/display.md` |
| Display -- screen, framebuffer, size modes | `.claude/display-core.md` |
| Display -- texture, tileset, font | `.claude/display-texture.md` |
| Display -- shader, material, display state | `.claude/display-shader.md` |
| Display -- mesh, vertex format, primitives | `.claude/display-mesh.md` |
| Display -- SpriteBatch (2D quad renderer) | `.claude/display-spritebatch.md` |
| Display -- text rendering, FONT_DEFAULT | `.claude/display-text.md` |
| Display -- color types, macros, constants | `.claude/display-color.md` |
| UI system (frame, textbox, fullbox, loading, FPS) | `.claude/ui.md` |
| Console (debug overlay, POSIX stdin mode) | `.claude/console.md` |
| Scene system (lifecycle, render pipeline, JS hooks) | `.claude/scene.md` |
---
## 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
See `.claude/build.md` for extended CMake conventions, platform
toolchain setup, and adding platform-conditional sources.
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
See `.claude/platforms.md` for the full platform table (including
planned Windows / macOS targets), capability macros, toolchain setup,
and endianness notes.
### 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 entity component
See `.claude/ecs.md` for ECS design rules, component categories, and
entity lifecycle details.
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 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 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+0000U+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`.
+2 -19
View File
@@ -4,22 +4,14 @@
# https://opensource.org/licenses/MIT
# Setup
cmake_minimum_required(VERSION 3.13)
cmake_minimum_required(VERSION 3.18)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF)
# Game identity — override these per-project
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YouWish" CACHE STRING "Game author / coder")
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
set(DUSK_GAME_LONG_DESCRIPTION "No description yet." CACHE STRING "Full description")
# Prep cache
set(DUSK_CACHE_TARGET "dusk-target")
@@ -50,7 +42,7 @@ file(MAKE_DIRECTORY ${DUSK_TEMP_DIR})
file(MAKE_DIRECTORY ${DUSK_BUILT_ASSETS_DIR})
# Required build packages
find_package(Python3 COMPONENTS Interpreter REQUIRED)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# Init Project.
project(${DUSK_LIBRARY_TARGET_NAME}
@@ -76,19 +68,10 @@ else()
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET})
endif()
if(NOT DEFINED DUSK_VERSION)
string(TIMESTAMP DUSK_VERSION "debug-%y%m%d%H%M%S")
endif()
# Definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}"
DUSK_GAME_NAME="${DUSK_GAME_NAME}"
DUSK_GAME_AUTHOR="${DUSK_GAME_AUTHOR}"
DUSK_GAME_SHORT_DESCRIPTION="${DUSK_GAME_SHORT_DESCRIPTION}"
DUSK_GAME_LONG_DESCRIPTION="${DUSK_GAME_LONG_DESCRIPTION}"
DUSK_VERSION="${DUSK_VERSION}"
)
# Toolchains
-107
View File
@@ -1,107 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "camera.h"
#include "display/display.h"
#include "assert/assert.h"
#include "display/framebuffer/framebuffer.h"
#include "display/screen/screen.h"
void cameraInit(camera_t *camera) {
cameraInitPerspective(camera);
}
void cameraInitPerspective(camera_t *camera) {
assertNotNull(camera, "Not a camera component");
camera->projType = CAMERA_PROJECTION_TYPE_PERSPECTIVE;
camera->perspective.fov = glm_rad(45.0f);
camera->nearClip = 0.1f;
camera->farClip = 10000.0f;
camera->viewType = CAMERA_VIEW_TYPE_LOOKAT;
glm_vec3_copy((vec3){ 5.0f, 5.0f, 5.0f }, camera->lookat.position);
glm_vec3_copy((vec3){ 0.0f, 1.0f, 0.0f }, camera->lookat.up);
glm_vec3_copy((vec3){ 0.0f, 0.0f, 0.0f }, camera->lookat.target);
}
void cameraInitOrthographic(camera_t *camera) {
assertNotNull(camera, "Not a camera component");
camera->projType = CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC;
camera->orthographic.left = 0.0f;
camera->orthographic.right = SCREEN.width;
camera->orthographic.top = SCREEN.height;
camera->orthographic.bottom = 0.0f;
camera->nearClip = 0.1f;
camera->farClip = 1.0f;
camera->viewType = CAMERA_VIEW_TYPE_2D;
glm_vec2_copy((vec2){ 0.0f, 0.0f }, camera->_2d.position);
camera->_2d.zoom = 1.0f;
}
void cameraGetProjectionMatrix(camera_t *camera, mat4 dest) {
assertNotNull(camera, "Not a camera component");
assertNotNull(dest, "Destination matrix must not be null");
if(
camera->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
camera->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
) {
glm_mat4_identity(dest);
glm_perspective(
camera->perspective.fov,
SCREEN.aspect,
camera->nearClip,
camera->farClip,
dest
);
if(camera->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
dest[1][1] *= -1.0f;
}
} else if(camera->projType == CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
glm_mat4_identity(dest);
glm_ortho(
camera->orthographic.left,
camera->orthographic.right,
camera->orthographic.top,
camera->orthographic.bottom,
camera->nearClip,
camera->farClip,
dest
);
}
}
void cameraGetViewMatrix(camera_t *camera, mat4 dest) {
assertNotNull(camera, "Not a camera component");
assertNotNull(dest, "Destination matrix must not be null");
if(camera->viewType == CAMERA_VIEW_TYPE_MATRIX) {
glm_mat4_ucopy(camera->view, dest);
} else if(camera->viewType == CAMERA_VIEW_TYPE_LOOKAT) {
glm_mat4_identity(dest);
glm_lookat(
camera->lookat.position,
camera->lookat.target,
camera->lookat.up,
dest
);
} else if(camera->viewType == CAMERA_VIEW_TYPE_2D) {
glm_mat4_identity(dest);
glm_lookat(
(vec3){ camera->_2d.position[0], camera->_2d.position[1], 0.5f },
(vec3){ camera->_2d.position[0], camera->_2d.position[1], 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
dest
);
} else if(camera->viewType == CAMERA_VIEW_TYPE_LOOKAT_PIXEL_PERFECT) {
assertUnreachable("LOOKAT_PIXEL_PERFECT view type is not implemented yet");
}
}
+2 -2
View File
@@ -156,7 +156,7 @@ class Map:
newTopLeftChunkY = y // CHUNK_HEIGHT - (MAP_HEIGHT // 2)
newTopLeftChunkZ = z // CHUNK_DEPTH - (MAP_DEPTH // 2)
if(newTopLeftChunkX != self.topLeftX or
if (newTopLeftChunkX != self.topLeftX or
newTopLeftChunkY != self.topLeftY or
newTopLeftChunkZ != self.topLeftZ):
@@ -166,7 +166,7 @@ class Map:
chunkWorldX = chunk.x
chunkWorldY = chunk.y
chunkWorldZ = chunk.z
if(chunkWorldX < newTopLeftChunkX or
if (chunkWorldX < newTopLeftChunkX or
chunkWorldX >= newTopLeftChunkX + MAP_WIDTH or
chunkWorldY < newTopLeftChunkY or
chunkWorldY >= newTopLeftChunkY + MAP_HEIGHT or
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
typedef struct {
void *nothing;
} inventory_t;
-21
View File
@@ -1,21 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli',
[System.PLATFORM_PSP]: 'PSP',
[System.PLATFORM_GAMECUBE]: 'GameCube',
[System.PLATFORM_WII]: 'Wii',
};
Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
UIFullboxOver.setColor(Color.BLACK);
requireAsync('testscene.js').then(Scene.set).catch(err => {
Console.print('Error loading scene: ' + err);
Engine.exit();
});
+77
View File
@@ -0,0 +1,77 @@
module('input')
module('platform')
module('scene')
module('locale')
-- Default Input bindings.
if PSP then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("circle", INPUT_ACTION_CANCEL)
inputBind("cross", INPUT_ACTION_ACCEPT)
inputBind("select", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif DOLPHIN then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("b", INPUT_ACTION_CANCEL)
inputBind("a", INPUT_ACTION_ACCEPT)
inputBind("z", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif LINUX then
if INPUT_KEYBOARD then
inputBind("w", INPUT_ACTION_UP)
inputBind("s", INPUT_ACTION_DOWN)
inputBind("a", INPUT_ACTION_LEFT)
inputBind("d", INPUT_ACTION_RIGHT)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("enter", INPUT_ACTION_ACCEPT)
inputBind("e", INPUT_ACTION_ACCEPT)
inputBind("q", INPUT_ACTION_CANCEL)
inputBind("escape", INPUT_ACTION_RAGEQUIT)
end
if INPUT_GAMEPAD then
inputBind("gamepad_up", INPUT_ACTION_UP)
inputBind("gamepad_down", INPUT_ACTION_DOWN)
inputBind("gamepad_left", INPUT_ACTION_LEFT)
inputBind("gamepad_right", INPUT_ACTION_RIGHT)
inputBind("gamepad_a", INPUT_ACTION_ACCEPT)
inputBind("gamepad_b", INPUT_ACTION_CANCEL)
inputBind("gamepad_back", INPUT_ACTION_RAGEQUIT)
inputBind("gamepad_lstick_up", INPUT_ACTION_UP)
inputBind("gamepad_lstick_down", INPUT_ACTION_DOWN)
inputBind("gamepad_lstick_left", INPUT_ACTION_LEFT)
inputBind("gamepad_lstick_right", INPUT_ACTION_RIGHT)
end
if INPUT_POINTER then
inputBind("mouse_x", INPUT_ACTION_POINTERX)
inputBind("mouse_y", INPUT_ACTION_POINTERY)
end
else
print("Unknown platform, no default input bindings set.")
end
sceneSet('scene/minesweeper.lua')
+5 -56
View File
@@ -1,60 +1,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: en\n"
"Language: en_US\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Welcome"
#: ui/user.c:22
msgid "ui.greeting"
msgstr "Hello, %s!"
#: ui/files.c:40
msgid "ui.file_status"
msgstr "%s has %d files."
#: ui/cart.c:55
msgid "cart.item_count"
msgid_plural "cart.item_count"
msgstr[0] "%d item"
msgstr[1] "%d items (dual)"
msgstr[2] "%d items (few)"
msgstr[3] "%d items (many)"
#: ui/notifications.c:71
msgid ""
"ui.multiline_help"
msgstr ""
"Line one of the help text.\n"
"Line two continues here.\n"
"Line three ends here."
#: ui/errors.c:90
msgid ""
"error.upload_failed.long"
msgstr ""
"Upload failed for file \"%s\".\n"
"Please try again later or contact support."
#: ui/messages.c:110
msgid ""
"user.invite_status"
msgid_plural ""
"user.invite_status"
msgstr[0] ""
"%s invited %d user.\n"
"Please review the request."
msgstr[1] ""
"%s invited %d users (dual).\n"
"Please review the requests."
msgstr[2] ""
"%s invited %d users (few).\n"
"Please review the requests."
msgstr[3] ""
"%s invited %d users (many).\n"
"Please review the requests."
msgid "ui.test"
msgstr "Hello this is a test."
Binary file not shown.
-63
View File
@@ -1,63 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const PLAYER_SPEED = 5.0;
// 1 world unit = 16 pixels.
const PIXEL_SCALE = 1.0 / 16.0;
// Player sprite is 32x32 px (test.png dimensions).
const PLAYER_W = 32 * PIXEL_SCALE;
const PLAYER_H = 32 * PIXEL_SCALE;
var player = {};
player.getAssets = () => {
return [
{ path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA }
];
}
player.init = function(scene) {
var texture = scene.assets.getAssetByPath('test.png');
Console.print('Player init: got texture ' + texture);
_entity = Entity.create();
_position = _entity.add(Component.POSITION);
_physics = _entity.add(Component.PHYSICS);
_physics.bodyType = Physics.DYNAMIC;
_physics.shape = Physics.SHAPE_CUBE;
_physics.gravityScale = 1.0;
var r = _entity.add(Component.RENDERABLE);
r.texture = texture.texture;
r.type = Renderable.SPRITEBATCH;
r.color = new Color(220, 80, 80);
// Upright quad centered on X, bottom-aligned on Y.
r.sprites = [[-PLAYER_W/2, 0, 0, PLAYER_W/2, PLAYER_H, 0, 0, 1, 1, 0]];
_position.localPosition = new Vec3(0, PLAYER_H, 0);
};
player.getPosition = function() {
return _position;
};
player.update = function() {
if(!_physics) return;
var vx = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT) * PLAYER_SPEED;
var vz = Input.axis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) * PLAYER_SPEED;
// Preserve vertical velocity so gravity and landing work correctly.
var vy = _physics.velocity.y;
_physics.velocity = new Vec3(vx, vy, vz);
};
player.dispose = function() {
Entity.dispose(_entity);
_entity = null;
_position = null;
_physics = null;
};
module.exports = player;
+256
View File
@@ -0,0 +1,256 @@
module('spritebatch')
module('camera')
module('color')
module('ui')
module('screen')
module('time')
module('glm')
module('text')
module('tileset')
module('texture')
module('input')
CELL_STATE_DEFAULT = 0
CELL_STATE_HOVER = 1
CELL_STATE_DOWN = 2
CELL_STATE_DISABLED = 3
screenSetBackground(colorCornflowerBlue())
camera = cameraCreate(CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC)
-- tilesetUi = tilesetGetByName("ui")
-- textureUi = textureLoad(tilesetUi.texture)
-- tilesetBorder = tilesetGetByName("border")
-- textureBorder = textureLoad(tilesetBorder.texture)
-- textureGrid = textureLoad("minesweeper/grid_bg.dpi")
-- tilesetCell = tilesetGetByName("cell")
-- textureCell = textureLoad(tilesetCell.texture)
-- cellSliceDefault = tilesetPositionGetUV(tilesetCell, 3, 5)
-- cellSliceHover = tilesetPositionGetUV(tilesetCell, 3, 4)
-- cellSliceDown = tilesetPositionGetUV(tilesetCell, 3, 6)
-- cellSliceDisabled = tilesetPositionGetUV(tilesetCell, 3, 7)
-- sweepwerCols = 10
-- sweeperRows = 14
-- mouseX = -1
-- mouseY = -1
-- centerX = 0
-- centerY = 0
-- boardWidth = sweepwerCols * tilesetCell.tileWidth
-- boardHeight = sweeperRows * tilesetCell.tileHeight
-- i = 0
-- cells = {}
-- for y = 1, sweeperRows do
-- for x = 1, sweepwerCols do
-- cells[i] = CELL_STATE_DEFAULT
-- i = i + 1
-- end
-- end
function cellDraw(x, y, type)
local slice = cellSliceDefault
if type == CELL_STATE_HOVER then
slice = cellSliceHover
elseif type == CELL_STATE_DOWN then
slice = cellSliceDown
elseif type == CELL_STATE_DISABLED then
slice = cellSliceDisabled
end
spriteBatchPush(textureCell,
x, y,
x + tilesetCell.tileWidth, y + tilesetCell.tileHeight,
colorWhite(),
slice.u0, slice.v0,
slice.u1, slice.v1
)
end
function backgroundDraw()
local t = (TIME.time / 40) % 1
local scaleX = screenGetWidth() / textureGrid.width
local scaleY = screenGetHeight() / textureGrid.height
local u0 = t * scaleX
local v0 = t * scaleY
local u1 = scaleX + u0
local v1 = scaleY + v0
spriteBatchPush(textureGrid,
0, 0,
screenGetWidth(), screenGetHeight(),
colorWhite(),
u0, v0,
u1, v1
)
end
function borderDraw(x, y, innerWidth, innerHeight)
-- Top Left
local uv = tilesetPositionGetUV(tilesetBorder, 0, 0)
spriteBatchPush(textureBorder,
x - tilesetBorder.tileWidth, y - tilesetBorder.tileWidth,
x, y,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Top Right
uv = tilesetPositionGetUV(tilesetBorder, 10, 0)
spriteBatchPush(textureBorder,
x + innerWidth, y - tilesetBorder.tileHeight,
x + innerWidth + tilesetBorder.tileWidth, y,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Bottom Left
uv = tilesetPositionGetUV(tilesetBorder, 0, 10)
spriteBatchPush(textureBorder,
x - tilesetBorder.tileWidth, y + innerHeight,
x, y + innerHeight + tilesetBorder.tileHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Bottom Right
uv = tilesetPositionGetUV(tilesetBorder, 10, 10)
spriteBatchPush(textureBorder,
x + innerWidth, y + innerHeight,
x + innerWidth + tilesetBorder.tileWidth, y + innerHeight + tilesetBorder.tileHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Top
uv = tilesetPositionGetUV(tilesetBorder, 1, 0)
spriteBatchPush(textureBorder,
x, y - tilesetBorder.tileHeight,
x + innerWidth, y,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Bottom
uv = tilesetPositionGetUV(tilesetBorder, 1, 10)
spriteBatchPush(textureBorder,
x, y + innerHeight,
x + innerWidth, y + innerHeight + tilesetBorder.tileHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Left
uv = tilesetPositionGetUV(tilesetBorder, 0, 1)
spriteBatchPush(textureBorder,
x - tilesetBorder.tileWidth, y,
x, y + innerHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Right
uv = tilesetPositionGetUV(tilesetBorder, 10, 1)
spriteBatchPush(textureBorder,
x + innerWidth, y,
x + innerWidth + tilesetBorder.tileWidth, y + innerHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
end
x = 0
y = 0
function sceneDispose()
end
function sceneUpdate()
x = x + inputAxis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT)
y = y + inputAxis(INPUT_ACTION_UP, INPUT_ACTION_DOWN)
end
function sceneRender()
-- Update camera
cameraPushMatrix(camera)
camera.bottom = screenGetHeight()
camera.right = screenGetWidth()
spriteBatchPush(
nil,
x, y, x + 32, y + 32,
colorBlue()
)
-- Update mouse position
-- if INPUT_POINTER then
-- mouseX = inputGetValue(INPUT_ACTION_POINTERX) * screenGetWidth()
-- mouseY = inputGetValue(INPUT_ACTION_POINTERY) * screenGetHeight()
-- -- Draw cursor
-- spriteBatchPush(
-- nil,
-- mouseX - 2, mouseY - 2,
-- mouseX + 2, mouseY + 2,
-- colorRed(),
-- 0, 0,
-- 1, 1
-- )
-- end
-- textDraw(10, 10, "Hello World")
-- centerX = math.floor(screenGetWidth() / 2)
-- centerY = math.floor(screenGetHeight() / 2)
-- Draw elements
-- backgroundDraw()
-- borderDraw(
-- centerX - (boardWidth / 2), centerY - (boardHeight / 2),
-- boardWidth, boardHeight
-- )
-- i = 0
-- -- Foreach cell
-- local offX = centerX - (boardWidth / 2)
-- local offY = centerY - (boardHeight / 2)
-- for y = 0, sweeperRows - 1 do
-- for x = 0, sweepwerCols - 1 do
-- i = y * sweepwerCols + x
-- -- Hovered
-- if
-- cells[i] == CELL_STATE_DEFAULT and
-- mouseX >= x * tilesetCell.tileWidth + offX and mouseX < (x + 1) * tilesetCell.tileWidth + offX and
-- mouseY >= y * tilesetCell.tileHeight + offY and mouseY < (y + 1) * tilesetCell.tileHeight + offY
-- then
-- cells[i] = CELL_STATE_HOVER
-- else
-- cells[i] = CELL_STATE_DEFAULT
-- end
-- cellDraw(
-- x * tilesetCell.tileWidth + offX,
-- y * tilesetCell.tileHeight + offY,
-- cells[i]
-- )
-- end
-- end
spriteBatchFlush()
cameraPopMatrix()
end
-42
View File
@@ -1,42 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
var scene = {};
// Pokemon DS-style camera: ~34 degrees elevation (atan(6/9)).
// CAM_HEIGHT / CAM_DIST ratio controls the tilt - keep it under 0.7 for
// the characteristically shallow DS angle.
const CAM_HEIGHT = 6;
const CAM_DIST = 9;
scene.init = async function() {
// Camera
scene.cam = Entity.create();
var camPos = scene.cam.add(Component.POSITION);
var cam = scene.cam.add(Component.CAMERA);
camPos.localPosition = new Vec3(3, 3, 3);
camPos.lookAt(new Vec3(0, 0, 0));
// Floor - large flat slab, no texture needed.
scene.floor = Entity.create();
var floorPos = scene.floor.add(Component.POSITION);
var floorR = scene.floor.add(Component.RENDERABLE);
floorR.type = Renderable.SHADER_MATERIAL;
floorR.color = Color.BLUE;
// floorPos.localScale = new Vec3(16, 0.2, 16);
// floorPos.localPosition = new Vec3(0, -0.1, 0);
await UIFullboxOver.transition(Color.BLACK, Color.TRANSPARENT, 1.0);
};
scene.update = function() {
};
scene.dispose = function() {
Entity.dispose(scene.floor);
Entity.dispose(scene.cam);
};
module.exports = scene;
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

-6
View File
@@ -1,6 +0,0 @@
module = {
render() {
Text.draw(0, 0, "Hello World");
SpriteBatch.flush();
}
};
-96
View File
@@ -1,96 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Turn things off we don't need
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
set(JERRY_EXT ON CACHE BOOL "" FORCE)
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
# Fetch Jerry
include(FetchContent)
FetchContent_Declare(
jerryscript
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
GIT_TAG float32-fix
)
FetchContent_MakeAvailable(jerryscript)
# Mark found
set(jerryscript_FOUND ON)
# Define targets
if(TARGET jerryscript-core)
set(JERRY_CORE_TARGET jerryscript-core)
elseif(TARGET jerry-core)
set(JERRY_CORE_TARGET jerry-core)
endif()
if(TARGET jerryscript-ext)
set(JERRY_EXT_TARGET jerryscript-ext)
elseif(TARGET jerry-ext)
set(JERRY_EXT_TARGET jerry-ext)
endif()
if(TARGET jerryscript-port-default)
set(JERRY_PORT_TARGET jerryscript-port-default)
elseif(TARGET jerry-port-default)
set(JERRY_PORT_TARGET jerry-port-default)
elseif(TARGET jerryscript-port)
set(JERRY_PORT_TARGET jerryscript-port)
elseif(TARGET jerry-port)
set(JERRY_PORT_TARGET jerry-port)
endif()
if(NOT JERRY_CORE_TARGET)
message(FATAL_ERROR "JerryScript core target not found")
endif()
if(NOT JERRY_EXT_TARGET)
message(FATAL_ERROR "JerryScript ext target not found")
endif()
if(NOT JERRY_PORT_TARGET)
message(FATAL_ERROR "JerryScript port target not found")
endif()
foreach(tgt IN ITEMS
${JERRY_CORE_TARGET}
${JERRY_EXT_TARGET}
${JERRY_PORT_TARGET}
)
if(TARGET ${tgt})
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
JERRY_NUMBER_TYPE_FLOAT64=0
JERRY_BUILTIN_DATE=0
)
endif()
endforeach()
# Export include dirs through the targets
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-core/include
)
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-ext/include
)
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-port/default/include
)
# Suppress JerryScript-only warning
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
-Wno-error
)
endif()
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
-31
View File
@@ -1,31 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(FetchContent)
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
)
# Fetch stb if not already done
FetchContent_MakeAvailable(stb)
# Find the stb_image.h header
set(STB_INCLUDE_DIR "${stb_SOURCE_DIR}")
set(STB_IMAGE_HEADER "${stb_SOURCE_DIR}/stb_image.h")
if(EXISTS "${STB_IMAGE_HEADER}")
add_library(stb_image INTERFACE)
target_include_directories(stb_image INTERFACE "${STB_INCLUDE_DIR}")
set(STB_IMAGE_FOUND TRUE)
else()
set(STB_IMAGE_FOUND FALSE)
endif()
# Standard CMake variables
set(STB_IMAGE_INCLUDE_DIRS "${STB_INCLUDE_DIR}")
set(STB_IMAGE_LIBRARIES stb_image)
mark_as_advanced(STB_IMAGE_INCLUDE_DIRS STB_IMAGE_LIBRARIES STB_IMAGE_FOUND)
-18
View File
@@ -1,18 +0,0 @@
include(FetchContent)
if(NOT TARGET yyjson)
FetchContent_Declare(
yyjson
GIT_REPOSITORY https://github.com/ibireme/yyjson.git
GIT_TAG 0.12.0
)
FetchContent_MakeAvailable(yyjson)
endif()
# Provide an imported target if not already available
if(NOT TARGET yyjson::yyjson)
add_library(yyjson::yyjson ALIAS yyjson)
endif()
# Mark yyjson as found for find_package compatibility
set(yyjson_FOUND TRUE)
-43
View File
@@ -1,43 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# dusk_embed_js(TARGET JS_FILE [NAME identifier])
#
# Converts a JS file into a C string header in DUSK_GENERATED_HEADERS_DIR.
# The generated header defines:
# static const char <NAME>[] = "...";
# static const size_t <NAME>_SIZE = sizeof(<NAME>) - 1;
#
# NAME defaults to the uppercase stem + "_JS" (e.g. scene.js -> SCENE_JS).
function(dusk_embed_js TARGET JS_FILE)
cmake_parse_arguments(ARG "" "NAME" "" ${ARGN})
get_filename_component(JS_ABS "${JS_FILE}" ABSOLUTE)
get_filename_component(JS_STEM "${JS_FILE}" NAME_WE)
set(OUTPUT_HEADER "${DUSK_GENERATED_HEADERS_DIR}/${JS_STEM}_js.h")
set(NAME_ARG "")
if(ARG_NAME)
set(NAME_ARG "--name" "${ARG_NAME}")
endif()
add_custom_command(
OUTPUT "${OUTPUT_HEADER}"
COMMAND ${Python3_EXECUTABLE} -m tools.js2c
--input "${JS_ABS}"
--output "${OUTPUT_HEADER}"
${NAME_ARG}
WORKING_DIRECTORY "${DUSK_ROOT_DIR}"
DEPENDS "${JS_ABS}"
COMMENT "js2c: ${JS_STEM}.js -> ${JS_STEM}_js.h"
VERBATIM
)
file(RELATIVE_PATH JS_REL "${DUSK_ROOT_DIR}" "${JS_ABS}")
string(MAKE_C_IDENTIFIER "dusk_js2c_${JS_REL}" JS_TARGET)
add_custom_target(${JS_TARGET} DEPENDS "${OUTPUT_HEADER}")
add_dependencies(${TARGET} ${JS_TARGET})
endfunction()
+34 -46
View File
@@ -1,73 +1,61 @@
# Build type: DOL (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "DOL" CACHE STRING "Dolphin asset source: DOL (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "DOL" "ISO")
# Numeric tokens so #if DUSK_DOLPHIN_BUILD_TYPE == DOL works in C.
# DUSK_DOLPHIN_BUILD_TYPE is passed without quotes so it expands to the identifier.
# Target definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
)
# Custom compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
# Need PkgConfig
find_package(PkgConfig REQUIRED)
pkg_check_modules(zip IMPORTED_TARGET libzip)
# Disable all warnings
target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w)
# cglm: fetched at source level via Findcglm.cmake (FetchContent, headers only)
# Custom flags for cglm
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Pre-create ZLIB::ZLIB so any downstream cmake module that resolves it
# (FindZLIB, pkg-config IMPORTED_TARGET Requires processing) gets plain -lz
# rather than an unresolvable IMPORTED target that the PPC linker rejects.
if(NOT TARGET ZLIB::ZLIB)
add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL)
set_target_properties(ZLIB::ZLIB PROPERTIES INTERFACE_LINK_LIBRARIES "z")
endif()
# Compile lua
include(FetchContent)
FetchContent_Declare(
liblua
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz
)
FetchContent_MakeAvailable(liblua)
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
# Mark libzip as found so src/dusk/CMakeLists.txt skips Findlibzip.cmake.
# Findlibzip.cmake calls find_package(ZLIB) which can recreate a broken
# ZLIB::ZLIB IMPORTED target, bypassing the shim above.
set(libzip_FOUND TRUE CACHE BOOL "libzip found (devkitpro portlibs)" FORCE)
# Locate zip.h in the devkitpro sysroot (respects CMAKE_FIND_ROOT_PATH).
find_path(_dusk_zip_inc NAMES zip.h)
if(_dusk_zip_inc)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE "${_dusk_zip_inc}")
endif()
# Link libraries.
# zip/z/lzma use target_link_options (raw flags) to bypass cmake target
# resolution — pkg-config-generated targets for these carry ZLIB::ZLIB in
# INTERFACE_LINK_LIBRARIES which breaks the PPC link step.
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
cglm
liblua
m
zip
bz2
zstd
z
lzma
fat
PkgConfig::zip
)
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN_BUILD_ISO)
else()
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE fat)
endif()
# Postbuild: ELF -> DOL
# Postbuild
set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND elf2dol
"$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>"
"${DUSK_BINARY_TARGET_NAME_DOL}"
COMMENT "Generating ${DUSK_BINARY_TARGET_NAME_DOL} from ${DUSK_BINARY_TARGET_NAME}"
)
)
+1 -20
View File
@@ -2,23 +2,4 @@ include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_GAMECUBE
)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
# bba
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"GCN"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building GameCube ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
)
-46
View File
@@ -1,46 +0,0 @@
# Find link platform-specific libraries
set(OpenGL_GL_PREFERENCE LEGACY)
find_package(SDL2 REQUIRED)
find_library(EGL_LIB EGL REQUIRED)
find_library(GL_LIB GL REQUIRED)
find_package(OpenGL REQUIRED)
# Setup endianess at compile time to optimize.
include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_BIG
)
else()
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_LITTLE
)
endif()
# Link required libraries.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
SDL2
pthread
OpenGL::GLES2
${GL_LIB}
${EGL_LIB}
m
)
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_OPENGL_ES
DUSK_LINUX
DUSK_KNULLI
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
)
+1 -9
View File
@@ -1,7 +1,6 @@
# Find link platform-specific libraries
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
# find_package(CURL REQUIRED)
# Setup endianess at compile time to optimize.
include(TestBigEndian)
@@ -17,23 +16,18 @@ else()
endif()
# Link required libraries.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
SDL2
pthread
OpenGL::GL
GL
m
# CURL::libcurl
)
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_CONSOLE_POSIX
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
@@ -43,6 +37,4 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
DUSK_NETWORK_IPV6
DUSK_THREAD_PTHREAD
)
+3 -20
View File
@@ -1,16 +1,6 @@
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_ARCHIVE_CREATE "$ENV{PSPDEV}/bin/psp-ar qc <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_APPEND "$ENV{PSPDEV}/bin/psp-ar q <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_FINISH "$ENV{PSPDEV}/bin/psp-ranlib <TARGET>")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE)
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
SDL2
pthread
@@ -34,27 +24,20 @@ target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
pspvfpu
pspvram
psphprm
pspnet
pspnet_inet
pspnet_apctl
psphttp
pspssl
)
target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_PSP
DUSK_INPUT_GAMEPAD
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=480
DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD
)
# Postbuild, create .pbp file for PSP.
-88
View File
@@ -1,88 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
if(NOT DEFINED ENV{VITASDK})
message(FATAL_ERROR "VITASDK environment variable is not set.")
endif()
include("$ENV{VITASDK}/share/vita.cmake" REQUIRED)
set(VITA_APP_NAME "Dusk")
set(VITA_TITLEID "DUSK00001")
set(VITA_VERSION "01.00")
find_package(SDL2 REQUIRED)
# Custom flags for cglm
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
cglm
SDL2
SDL2main
zip
bz2
z
zstd
crypto
lzma
m
pthread
stdc++
vitaGL
mathneon
vitashark
kubridge_stub
SceAppMgr_stub
SceAudio_stub
SceCtrl_stub
SceCommonDialog_stub
SceDisplay_stub
SceKernelDmacMgr_stub
SceGxm_stub
SceShaccCg_stub
SceSysmodule_stub
ScePower_stub
SceTouch_stub
SceVshBridge_stub
SceIofilemgr_stub
SceShaccCgExt
libtaihen_stub.a
# SceKernel_stub
SceAppUtil_stub
SceHid_stub
SceRtc_stub
)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_VITA
DUSK_INPUT_GAMEPAD
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=960
DUSK_DISPLAY_HEIGHT=544
)
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE)
# Post-build: package SELF + assets into a .vpk installable on the Vita
vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self
VERSION ${VITA_VERSION}
NAME ${VITA_APP_NAME}
FILE ${DUSK_ASSETS_ZIP} dusk.dsk
)
+1 -23
View File
@@ -2,26 +2,4 @@ include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_WII
)
# Generate Homebrew Channel meta.xml from project identity variables
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
configure_file(
"${CMAKE_SOURCE_DIR}/docker/dolphin/meta.xml.in"
"${DUSK_BUILD_DIR}/meta.xml"
@ONLY
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"WII"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building Wii ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
)
-29
View File
@@ -1,29 +0,0 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_ASM_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_SYSROOT /)
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH
/usr/aarch64-linux-gnu
/usr/lib/aarch64-linux-gnu
/usr/include/aarch64-linux-gnu
)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_LIBDIR} "/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "/usr/lib/aarch64-linux-gnu/pkgconfig")
set(CMAKE_PREFIX_PATH "/usr/aarch64-linux-gnu;/usr/lib/aarch64-linux-gnu")
# Optional: helps some Find modules
set(SDL2_DIR "/usr/lib/aarch64-linux-gnu/cmake/SDL2" CACHE PATH "")
@@ -0,0 +1,16 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-buildroot-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-buildroot-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/aarch64-buildroot-linux-gnu_sdk-buildroot/aarch64-buildroot-linux-gnu/sysroot)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT})
set(ENV{PKG_CONFIG_LIBDIR} "${CMAKE_SYSROOT}/usr/lib/pkgconfig:${CMAKE_SYSROOT}/usr/share/pkgconfig")
+3 -4
View File
@@ -1,7 +1,6 @@
FROM ghcr.io/extremscorner/libogc2
FROM devkitpro/devkitppc
WORKDIR /workdir
RUN apt update && \
dkp-pacman -Syu --noconfirm && \
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip
VOLUME ["/workdir"]
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>Dusk</name>
<version>1.00</version>
<release_date></release_date>
<coder>YouWish</coder>
<short_description>Dusk game</short_description>
<long_description>No description yet.</long_description>
<ahb_access/>
</app>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>@DUSK_GAME_NAME@</name>
<version>@PROJECT_VERSION@</version>
<release_date>@DUSK_BUILD_DATE@</release_date>
<coder>@DUSK_GAME_AUTHOR@</coder>
<short_description>@DUSK_GAME_SHORT_DESCRIPTION@</short_description>
<long_description>@DUSK_GAME_LONG_DESCRIPTION@</long_description>
<ahb_access/>
</app>
+38 -33
View File
@@ -1,35 +1,40 @@
FROM debian:trixie
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /workdir
RUN dpkg --add-architecture arm64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
crossbuild-essential-arm64 \
ca-certificates \
pkg-config \
cmake \
make \
ninja-build \
git \
file \
python3 \
python3-pip \
python3-polib \
python3-pil \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.4-dev:arm64 \
xz-utils:arm64 \
libbz2-dev:arm64 \
zlib1g-dev:arm64 \
libzip-dev:arm64 \
libssl-dev:arm64 \
libsdl2-dev:arm64 \
liblzma-dev:arm64 \
libopengl0:arm64 \
libgl1:arm64 \
libegl1:arm64 \
libgles2:arm64 \
libgl1-mesa-dev:arm64 && \
rm -rf /var/lib/apt/lists/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get -y update && apt-get -y install \
python3-dotenv \
bc \
bison \
build-essential \
bzip2 \
bzr \
cmake \
cmake-curses-gui \
cpio \
device-tree-compiler \
flex \
git \
imagemagick \
libncurses5-dev \
locales \
make \
nano \
p7zip-full \
rsync \
sharutils \
scons \
tree \
unzip \
vim \
wget \
zip \
python3 \
python3-pip \
python3-polib \
python3-pil \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
&& apt clean
VOLUME ["/workdir"]
CMD ["/bin/bash"]
+1 -1
View File
@@ -14,8 +14,8 @@ RUN apt-get install -y \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.3-dev \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
libzip-dev \
-13
View File
@@ -1,13 +0,0 @@
FROM vitasdk/vitasdk:latest
WORKDIR /workdir
# Install vitaGL and its dependencies (vitashark, SceShaccCg) via vdpm
RUN which vdpm
# Install Python (needed for Dusk code generation tools)
RUN apk add --no-cache \
python3 \
py3-pip \
py3-dotenv
VOLUME ["/workdir"]
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
if [ -z "$DEVKITPRO" ]; then
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
exit 1
fi
mkdir -p build-gamecube-iso
cmake -S. -Bbuild-gamecube-iso \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake"
cd build-gamecube-iso
make -j$(nproc) VERBOSE=1
+1 -5
View File
@@ -5,10 +5,6 @@ if [ -z "$DEVKITPRO" ]; then
fi
mkdir -p build-gamecube
cmake -S. -Bbuild-gamecube \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \
-DDKP_OGC_PLATFORM_LIBRARY=libogc2
cmake -S. -Bbuild-gamecube -DDUSK_TARGET_SYSTEM=gamecube -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake"
cd build-gamecube
make -j$(nproc) VERBOSE=1
+2 -2
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
docker build --no-cache -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+13 -22
View File
@@ -1,24 +1,15 @@
#!/bin/bash
cmake -S . -B build-knulli -G Ninja \
-DDUSK_BUILD_TESTS=ON \
-DDUSK_TARGET_SYSTEM=knulli \
-DCMAKE_TOOLCHAIN_FILE=./cmake/toolchains/aarch64-linux-gnu.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-knulli -- -j$(nproc)
git clone --depth 1 --branch SDL2 https://github.com/libsdl-org/SDL.git /tmp/SDL2 && \
cmake -S /tmp/SDL2 -B /tmp/SDL2/build \
-DCMAKE_TOOLCHAIN_FILE=/workdir/toolchain-aarch64-buildroot.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$SYSROOT/usr" && \
cmake --build /tmp/SDL2/build -j"$(nproc)" && \
cmake --install /tmp/SDL2/build
# Copy necessary libs out
mkdir -p ./build-knulli/dusk
cp ./build-knulli/Dusk ./build-knulli/dusk/Dusk
cp ./build-knulli/dusk.dsk ./build-knulli/dusk/dusk.dsk
echo '#!/bin/bash' > build-knulli/dusk/Dusk.sh
echo 'cd "$(dirname "$(readlink -f "$0")")"' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(dirname "$(readlink -f "$0")")' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/gl4es' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib' >> build-knulli/dusk/Dusk.sh
echo '$(dirname "$(readlink -f "$0")")/Dusk' >> build-knulli/dusk/Dusk.sh
chmod +x build-knulli/dusk/Dusk.sh
cp /usr/lib/aarch64-linux-gnu/liblua5.4.so.0 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libGL.so.1 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libEGL.so.1 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libGLESv2.so.2 build-knulli/dusk/
cmake -S . \
-B build-knulli \
-DDUSK_BUILD_TESTS=ON \
-DDUSK_TARGET_SYSTEM=linux \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/toolchain-aarch64-buildroot.cmake
cmake --build build-knulli -- -j$(nproc)
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/build-linux.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-psp -f docker/psp/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
docker run --rm -v $(pwd):/workdir dusk-psp /bin/bash -c "./scripts/build-psp.sh"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
if [ -z "$VITASDK" ]; then
echo "VITASDK environment variable is not set. Please set it to the path of your VitaSDK installation."
exit 1
fi
mkdir -p build-vita
cd build-vita
cmake \
-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake \
-DDUSK_TARGET_SYSTEM=vita \
..
make -j$(nproc)
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
if [ -z "$DEVKITPRO" ]; then
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
exit 1
fi
mkdir -p build-wii-iso
cmake -S. -Bbuild-wii-iso \
-DDUSK_TARGET_SYSTEM=wii \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cd build-wii-iso
make -j$(nproc) VERBOSE=1
+2 -6
View File
@@ -5,10 +5,6 @@ if [ -z "$DEVKITPRO" ]; then
fi
mkdir -p build-wii
cmake -S. -Bbuild-wii \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL
cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cd build-wii
make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
make -j$(nproc) VERBOSE=1
+1 -5
View File
@@ -1,7 +1,3 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run \
--rm \
-v "${GITHUB_WORKSPACE}:/workdir" \
dusk-linux \
/bin/bash -c "./scripts/test-linux.sh"
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/test-linux.sh"
-2
View File
@@ -1,6 +1,4 @@
#!/bin/bash
set -e
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j$(nproc)
ctest --output-on-failure --test-dir build-tests
+2 -7
View File
@@ -5,7 +5,7 @@
add_subdirectory(dusk)
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
add_subdirectory(dusklinux)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
@@ -15,12 +15,7 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
add_subdirectory(duskvita)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
add_subdirectory(duskdolphin)
endif()
+23 -45
View File
@@ -14,38 +14,18 @@ if(NOT libzip_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip)
endif()
if(NOT stb_image_FOUND)
find_package(stb REQUIRED)
if(STB_IMAGE_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image)
else()
message(FATAL_ERROR "stb_image not found. Please ensure stb is correctly fetched.")
if(NOT Lua_FOUND)
find_package(Lua REQUIRED)
if(Lua_FOUND AND NOT TARGET Lua::Lua)
add_library(Lua::Lua INTERFACE IMPORTED)
set_target_properties(
Lua::Lua
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
)
endif()
endif()
if(NOT yyjson_FOUND)
find_package(yyjson REQUIRED)
if(yyjson_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson)
else()
message(FATAL_ERROR "yyjson not found. Please ensure yyjson is correctly fetched.")
endif()
endif()
if(NOT jerryscript_FOUND)
find_package(jerryscript REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
jerryscript::core
jerryscript::ext
jerryscript::port
)
endif()
if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_BACKTRACE
)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Lua::Lua)
endif()
# Includes
@@ -60,30 +40,28 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
main.c
)
# Defs
dusk_env_to_h(duskdefs.env duskdefs.h)
# Subdirs
add_subdirectory(animation)
add_subdirectory(event)
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(cutscene)
add_subdirectory(item)
add_subdirectory(story)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(log)
add_subdirectory(display)
add_subdirectory(engine)
add_subdirectory(entity)
add_subdirectory(error)
add_subdirectory(event)
add_subdirectory(input)
add_subdirectory(item)
add_subdirectory(locale)
add_subdirectory(physics)
add_subdirectory(map)
add_subdirectory(scene)
add_subdirectory(script)
add_subdirectory(system)
add_subdirectory(story)
add_subdirectory(time)
add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(overworld)
add_subdirectory(save)
add_subdirectory(util)
add_subdirectory(thread)
# if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "psp")
# add_subdirectory(thread)
# endif()
-10
View File
@@ -1,10 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
)
-52
View File
@@ -1,52 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "animation.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
}
float_t animationGetValue(animation_t *anim, const float_t time) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
if(current > last) {
end = start;
break;
}
} while(true);
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "keyframe.h"
typedef struct {
keyframe_t *keyframes;
uint16_t keyframeCount;
} animation_t;
/**
* Initializes an animation.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
*/
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
);
/**
* Gets the value of the animation at a given time.
*
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
*/
float_t animationGetValue(animation_t *anim, const float_t time);
-111
View File
@@ -1,111 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "easing.h"
#include "assert/assert.h"
#include "util/math.h"
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear,
easingInSine,
easingOutSine,
easingInOutSine,
easingInQuad,
easingOutQuad,
easingInOutQuad,
easingInCubic,
easingOutCubic,
easingInOutCubic,
easingInQuart,
easingOutQuart,
easingInOutQuart,
easingInBack,
easingOutBack,
easingInOutBack,
};
float_t easingApply(const easingtype_t type, const float_t t) {
assertTrue(type < EASING_COUNT, "Invalid easing type");
return EASING_FUNCTIONS[type](t);
}
float_t easingLinear(const float_t t) {
return t;
}
float_t easingInSine(const float_t t) {
return 1.0f - cosf(t * MATH_PI * 0.5f);
}
float_t easingOutSine(const float_t t) {
return sinf(t * MATH_PI * 0.5f);
}
float_t easingInOutSine(const float_t t) {
return -(cosf(MATH_PI * t) - 1.0f) * 0.5f;
}
float_t easingInQuad(const float_t t) {
return t * t;
}
float_t easingOutQuad(const float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u;
}
float_t easingInOutQuad(const float_t t) {
if(t < 0.5f) return 2.0f * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * 0.5f;
}
float_t easingInCubic(const float_t t) {
return t * t * t;
}
float_t easingOutCubic(const float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u * u;
}
float_t easingInOutCubic(const float_t t) {
if(t < 0.5f) return 4.0f * t * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * u * 0.5f;
}
float_t easingInQuart(const float_t t) {
return t * t * t * t;
}
float_t easingOutQuart(const float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u * u * u;
}
float_t easingInOutQuart(const float_t t) {
if(t < 0.5f) return 8.0f * t * t * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * u * u * 0.5f;
}
float_t easingInBack(const float_t t) {
return EASING_C3 * t * t * t - EASING_C1 * t * t;
}
float_t easingOutBack(const float_t t) {
float_t u = t - 1.0f;
return 1.0f + EASING_C3 * u * u * u + EASING_C1 * u * u;
}
float_t easingInOutBack(const float_t t) {
if(t < 0.5f) {
float_t u = 2.0f * t;
return u * u * ((EASING_C2 + 1.0f) * u - EASING_C2) * 0.5f;
}
float_t u = 2.0f * t - 2.0f;
return (u * u * ((EASING_C2 + 1.0f) * u + EASING_C2) + 2.0f) * 0.5f;
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "dusk.h"
#define EASING_PI 3.14159265358979323846f
#define EASING_C1 1.70158f
#define EASING_C2 (EASING_C1 * 1.525f)
#define EASING_C3 (EASING_C1 + 1.0f)
typedef enum {
EASING_LINEAR,
EASING_IN_SINE,
EASING_OUT_SINE,
EASING_IN_OUT_SINE,
EASING_IN_QUAD,
EASING_OUT_QUAD,
EASING_IN_OUT_QUAD,
EASING_IN_CUBIC,
EASING_OUT_CUBIC,
EASING_IN_OUT_CUBIC,
EASING_IN_QUART,
EASING_OUT_QUART,
EASING_IN_OUT_QUART,
EASING_IN_BACK,
EASING_OUT_BACK,
EASING_IN_OUT_BACK,
EASING_COUNT
} easingtype_t;
typedef float_t (*easingfn_t)(const float_t t);
extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT];
/**
* Applies the specified easing function to t.
*
* @param type The easing type to apply.
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingApply(const easingtype_t type, const float_t t);
float_t easingLinear(const float_t t);
float_t easingInSine(const float_t t);
float_t easingOutSine(const float_t t);
float_t easingInOutSine(const float_t t);
float_t easingInQuad(const float_t t);
float_t easingOutQuad(const float_t t);
float_t easingInOutQuad(const float_t t);
float_t easingInCubic(const float_t t);
float_t easingOutCubic(const float_t t);
float_t easingInOutCubic(const float_t t);
float_t easingInQuart(const float_t t);
float_t easingOutQuart(const float_t t);
float_t easingInOutQuart(const float_t t);
float_t easingInBack(const float_t t);
float_t easingOutBack(const float_t t);
float_t easingInOutBack(const float_t t);
-13
View File
@@ -1,13 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "easing.h"
typedef struct {
float_t time;
float_t value;
easingtype_t easing;
} keyframe_t;
+3 -72
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026 Dominic Masters
* Copyright (c) 2023 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -7,20 +7,8 @@
#include "assert.h"
#include "log/log.h"
#include "util/string.h"
#include "util/memory.h"
#ifdef DUSK_THREAD_PTHREAD
pthread_t ASSERT_MAIN_THREAD_ID = 0;
#endif
#ifndef DUSK_ASSERTIONS_FAKED
void assertInit(void) {
#ifdef DUSK_THREAD_PTHREAD
ASSERT_MAIN_THREAD_ID = pthread_self();
#endif
}
#ifdef DUSK_TEST_ASSERT
void assertTrueImpl(
const char *file,
@@ -36,25 +24,6 @@
);
}
#else
#ifdef DUSK_BACKTRACE
#include <execinfo.h>
#include <stdlib.h>
static void assertLogBacktrace(void) {
void *frames[64];
int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count);
memoryTrack(symbols);
logError("Stack trace:\n");
if(symbols) {
for(int i = 0; i < count; i++) {
logError(" %s\n", symbols[i]);
}
memoryFree(symbols);
}
}
#endif
void assertTrueImpl(
const char *file,
const int32_t line,
@@ -63,14 +32,11 @@
) {
if(x != true) {
logError(
"Assertion Failed in %s:%i\n\n%s\n\n",
"Assertion Failed in %s:%i\n\n%s\n",
file,
line,
message
);
#ifdef DUSK_BACKTRACE
assertLogBacktrace();
#endif
abort();
}
}
@@ -132,39 +98,4 @@
) {
assertUnreachableImpl(file, line, message);
}
void assertStringEqualImpl(
const char *file,
const int32_t line,
const char *a,
const char *b,
const char *message
) {
assertTrueImpl(file, line, stringCompare(a, b) == 0, message);
}
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() == ASSERT_MAIN_THREAD_ID, message
);
#endif
}
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() != ASSERT_MAIN_THREAD_ID, message
);
#endif
}
#endif
#endif
+2 -91
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026 Dominic Masters
* Copyright (c) 2023 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -16,18 +16,7 @@
#endif
#endif
#ifdef DUSK_THREAD_PTHREAD
#include "thread/thread.h"
extern pthread_t ASSERT_MAIN_THREAD_ID;
#endif
#ifndef DUSK_ASSERTIONS_FAKED
/**
* Initializes the assert system. Must be the very first call in engine
* startup.
*/
void assertInit(void);
/**
* Assert a given value to be true.
*
@@ -115,45 +104,6 @@
const char *message
);
/**
* Asserts two strings to be equal.
*
* @param file File that the assertion is being made from.
* @param line Line that the assertion is being made from.
* @param a First string to compare.
* @param b Second string to compare.
* @param message Message to throw against assertion failure.
*/
void assertStringEqualImpl(
const char *file,
const int32_t line,
const char *a,
const char *b,
const char *message
);
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts a given value to be true.
*
@@ -228,38 +178,8 @@
#define assertStrLenMin(str, len, message) \
assertTrue(strlen(str) >= len, message)
/**
* Asserts two strings to be equal.
*
* @param a First string to compare.
* @param b Second string to compare.
* @param message Message to throw against assertion failure.
*/
#define assertStringEqual(a, b, message) \
assertStringEqualImpl(__FILE__, __LINE__, a, b, message)
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertIsMainThread(message) \
assertIsMainThreadImpl(__FILE__, __LINE__, message)
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertNotMainThread(message) \
assertNotMainThreadImpl(__FILE__, __LINE__, message)
#else
// If assertions are faked, we define the macros to do nothing.
#define assertInit() ((void)0)
#define assertMainThreadInit() ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#define assertTrue(x, message) ((void)0)
#define assertFalse(x, message) ((void)0)
#define assertUnreachable(message) ((void)0)
@@ -268,14 +188,5 @@
#define assertDeprecated(message) ((void)0)
#define assertStrLenMax(str, len, message) ((void)0)
#define assertStrLenMin(str, len, message) ((void)0)
#define assertStringEqual(a, b, message) ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#endif
// Static Assertions
#define assertStructSize(struct, size) \
_Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
// EOF
#endif

Some files were not shown because too many files have changed in this diff Show More