56 Commits

Author SHA1 Message Date
YourWishes ed5c60ac30 Add claude docs 2026-06-16 10:15:59 -05:00
YourWishes 8131bcd4d4 Build on github tag 2026-06-09 15:43:31 -05:00
YourWishes 4ba11e3363 Test
Build Dusk / run-tests (push) Failing after 15m50s
2026-06-08 18:01:19 -05:00
YourWishes acf2be3f66 Try set
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 17:04:07 -05:00
YourWishes f5df0195e2 Docker compose
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 17:02:58 -05:00
YourWishes d26995b48d pwd
Build Dusk / run-tests (push) Failing after 6s
2026-06-08 16:59:39 -05:00
YourWishes 617f8120ae Docker exec
Build Dusk / run-tests (push) Failing after 14s
2026-06-08 16:48:17 -05:00
YourWishes 06c517c9aa chmod
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 16:47:45 -05:00
YourWishes 593ed6408c test2
Build Dusk / run-tests (push) Successful in 7s
2026-06-08 16:47:03 -05:00
YourWishes fb7d3ed122 LS stuff
Build Dusk / run-tests (push) Successful in 7s
2026-06-08 16:45:59 -05:00
YourWishes 19b88ec858 Test linux?
Build Dusk / run-tests (push) Failing after 6s
2026-06-08 16:08:24 -05:00
YourWishes 160e65be7f Run on ubuntu-latest
Build Dusk / run-tests (push) Failing after 16s
2026-06-08 15:34:31 -05:00
YourWishes 079b0d2cf6 Update test script back to what it was
Build Dusk / run-tests (push) Has been cancelled
2026-06-08 15:18:40 -05:00
YourWishes 78f1310f41 Update test script
Build Dusk / run-tests (push) Failing after 7s
2026-06-08 15:14:53 -05:00
YourWishes eb1974c113 Update to docker-host
Build Dusk / run-tests (push) Failing after 6m15s
2026-06-08 14:55:31 -05:00
YourWishes 551409a023 Force run again
Build Dusk / run-tests (push) Failing after 9m15s
2026-06-08 13:58:11 -05:00
YourWishes a11e14daac Try without setup docker
Build Dusk / run-tests (push) Failing after 8m28s
2026-06-08 13:15:15 -05:00
YourWishes 7441e15e76 Run on ubuntu-latest
Build Dusk / run-tests (push) Failing after 3m15s
2026-06-08 13:09:53 -05:00
YourWishes 46506228a6 One thing at a time
Build Dusk / run-tests (push) Has been cancelled
2026-06-08 12:46:38 -05:00
YourWishes 17c49c74cf Try tests updated
Build Dusk / build-linux (push) Failing after 29s
Build Dusk / build-psp (push) Failing after 1m13s
Build Dusk / run-tests (push) Has been cancelled
Build Dusk / build-gamecube (push) Has been cancelled
Build Dusk / build-gamecube-iso (push) Has been cancelled
Build Dusk / build-knulli (push) Has been cancelled
Build Dusk / build-wii (push) Has been cancelled
Build Dusk / build-wii-iso (push) Has been cancelled
2026-06-08 12:44:22 -05:00
YourWishes 3f8024d4db Workflows
Build Dusk / run-tests (push) Failing after 58s
Build Dusk / build-linux (push) Failing after 1m11s
Build Dusk / build-psp (push) Failing after 46s
Build Dusk / build-knulli (push) Failing after 53s
Build Dusk / build-gamecube (push) Failing after 47s
Build Dusk / build-gamecube-iso (push) Failing after 49s
Build Dusk / build-wii (push) Failing after 49s
Build Dusk / build-wii-iso (push) Failing after 1m14s
2026-06-08 12:25:48 -05:00
YourWishes 8675e44d28 GitTea actions 2026-06-08 12:24:09 -05:00
YourWishes 1301d9a718 idk why I btoher with github actions 2026-06-08 12:23:18 -05:00
YourWishes da3db50ca8 Want to test this in PSP 2026-06-08 11:32:59 -05:00
YourWishes 2ca6780305 Just trying to fix things now 2026-06-08 09:39:09 -05:00
YourWishes be68fe5a35 Emdashless 2026-06-07 21:27:59 -05:00
YourWishes dc41c0e302 Cleanup 2026-06-07 21:16:46 -05:00
YourWishes 51388c90d5 we ball I guess 2026-06-07 19:51:54 -05:00
YourWishes f8c9d33df2 Fix some script bugs 2026-06-07 18:47:34 -05:00
YourWishes ed0420fdce Cleanup of script modules. 2026-06-07 12:59:17 -05:00
YourWishes 47a6f396fa Fix typedefs 2026-06-06 19:06:52 -05:00
YourWishes 9edb2aa0c1 Example scene working 2026-06-06 18:46:08 -05:00
YourWishes 003b647d83 Fix tests 2026-06-06 17:36:13 -05:00
YourWishes 2849ff8844 Fix linux? 2026-06-06 17:14:42 -05:00
YourWishes 9f3089742a Fixed ISO builds. 2026-06-06 17:07:30 -05:00
YourWishes b286a9bbcd Fixed wii build 2026-06-06 16:53:06 -05:00
YourWishes 6204e745ba Fixed gamecube building. 2026-06-06 16:47:07 -05:00
YourWishes bbe0e48d23 Builds again? 2026-06-06 16:42:12 -05:00
YourWishes 79054080c0 Cleanup a bit. 2026-06-06 16:39:27 -05:00
YourWishes 81024c4c09 Require async 2026-06-06 10:55:10 -05:00
YourWishes 9068d96130 Add timeouts 2026-06-06 10:38:10 -05:00
YourWishes 6f47543720 Update jerryscript each frame. 2026-06-06 10:30:22 -05:00
YourWishes 5a08384ae1 start async 2026-06-05 19:42:24 -05:00
YourWishes 45d8fda0e4 require() working as I like 2026-06-05 18:49:10 -05:00
YourWishes a9e664492f First round of asset refactoring 2026-06-05 13:18:08 -05:00
YourWishes 3c8b6cb2cc Scene script code 2026-06-04 13:36:35 -05:00
YourWishes 2b3abbe13b ABout to try scene and script merger 2026-06-02 16:46:39 -05:00
YourWishes 241a52b94a nuke unused overworld code 2026-06-02 13:23:11 -05:00
YourWishes 82c300b077 Add some script modules 2026-06-02 12:55:32 -05:00
YourWishes 0f8b629e20 Add logging to Wii 2026-06-02 11:01:54 -05:00
YourWishes 36f6ac65f2 Builds and works on Gamecube 2026-06-02 09:53:56 -05:00
YourWishes a25871a849 Test sprite from script 2026-06-02 09:32:07 -05:00
YourWishes 57766a9104 Merge branch 'main' into scriptentity 2026-06-02 07:36:44 -05:00
YourWishes 3770ae1645 Fix tests? 2026-06-02 07:35:28 -05:00
YourWishes d73edb403f Example Camera 2026-06-01 23:04:55 -05:00
YourWishes b14196ff0d Basic entity script 2026-06-01 22:56:37 -05:00
331 changed files with 14697 additions and 2860 deletions
+88
View File
@@ -0,0 +1,88 @@
# 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
@@ -0,0 +1,125 @@
# 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.
+85
View File
@@ -0,0 +1,85 @@
# 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()
```
## 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)`
+81
View File
@@ -0,0 +1,81 @@
# 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.
+73
View File
@@ -0,0 +1,73 @@
# 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` typically holds a `texture_t *` and a
tint colour. Check `shaderunlitmaterial.h` for the exact fields.
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`.
+86
View File
@@ -0,0 +1,86 @@
# 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.
+19
View File
@@ -0,0 +1,19 @@
# 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` |
+179
View File
@@ -0,0 +1,179 @@
# 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
@@ -0,0 +1,95 @@
# 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
@@ -0,0 +1,133 @@
# 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.
+102
View File
@@ -0,0 +1,102 @@
# 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
- `inputactiondata_t`: `onPressed`, `onReleased` per action
- Any engine subsystem that exposes hooks (network connect/disconnect,
asset batch completion, etc.)
+130
View File
@@ -0,0 +1,130 @@
# 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
## 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
// 2D axis helpers
void inputAxis2D(
inputaction_t horiz,
inputaction_t vert,
vec2 out
);
float_t inputAngle2D(inputaction_t horiz, inputaction_t vert);
void inputAxis(inputaction_t action, float_t *out);
// 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` |
## Platform capability notes
| Feature | Linux/Knulli | PSP | GameCube/Wii |
|---------|-------------|-----|--------------|
| Keyboard | Yes (SDL2) | No | No |
| Pointer/Mouse | Yes (SDL2) | No | No |
| Gamepad | Yes (SDL2) | Yes (SDL2) | Yes (PAD) |
| Analog axes | Yes | L-Stick only | L-Stick, C-Stick, Triggers |
| Touch | Defined, not implemented | -- | -- |
+90
View File
@@ -0,0 +1,90 @@
# 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
@@ -0,0 +1,132 @@
# 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
@@ -0,0 +1,83 @@
# 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
@@ -0,0 +1,127 @@
# 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
@@ -0,0 +1,288 @@
# 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
@@ -0,0 +1,162 @@
# 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
@@ -0,0 +1,46 @@
# 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
@@ -0,0 +1,200 @@
# 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
@@ -0,0 +1,173 @@
# 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
@@ -0,0 +1,46 @@
# 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
@@ -0,0 +1,98 @@
# 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
@@ -0,0 +1,148 @@
# 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.
+110
View File
@@ -0,0 +1,110 @@
# 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
To move to a new scene from JS, call the scene module's transition
function (exact API in the `scene` JS module). The C side defers the
actual switch to the start of the next `sceneUpdate` call so the
current tick completes cleanly before any dispose runs.
## 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
@@ -0,0 +1,104 @@
# 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
@@ -0,0 +1,167 @@
# 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
@@ -0,0 +1,111 @@
# 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
@@ -0,0 +1,100 @@
# 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
@@ -0,0 +1,115 @@
# 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.
+191
View File
@@ -0,0 +1,191 @@
# 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.
+3 -6
View File
@@ -1,11 +1,8 @@
name: Build Dusk
on:
push:
branches:
- main
pull_request:
branches:
- main
tags:
- '*'
jobs:
run-tests:
runs-on: ubuntu-latest
@@ -144,7 +141,7 @@ jobs:
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.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
- name: Upload Wii binary
+2 -1
View File
@@ -105,4 +105,5 @@ yarn.lock
/build2
/build*
/assets/test
/tools_old
/tools_old
/assets/test.png
+513
View File
@@ -0,0 +1,513 @@
# 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` |
| 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`.
+12 -3
View File
@@ -1,4 +1,7 @@
Console.print('This is called from JavaScript');
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
@@ -8,5 +11,11 @@ const platformNames = {
[System.PLATFORM_WII]: 'Wii',
};
const platformName = platformNames[System.platform] || 'Unknown';
Console.print('Platform: ' + platformName);
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();
});
+63
View File
@@ -0,0 +1,63 @@
// 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;
+42
View File
@@ -0,0 +1,42 @@
// 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;
+43
View File
@@ -0,0 +1,43 @@
# 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()
+8 -5
View File
@@ -1,15 +1,18 @@
# Build type: FAT (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "FAT" CACHE STRING "Dolphin asset source: FAT (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "FAT" "ISO")
# 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")
# Target definitions
# 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_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD
DUSK_DOLPHIN_BUILD_TYPE="${DUSK_DOLPHIN_BUILD_TYPE}"
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
)
# Custom compiler flags
+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"
+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-iso.sh"
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
+1
View File
@@ -7,6 +7,7 @@ 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
cd build-gamecube
+1 -1
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 run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+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"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/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"
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
+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"
+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-iso.sh"
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
+6 -2
View File
@@ -5,6 +5,10 @@ if [ -z "$DEVKITPRO" ]; then
fi
mkdir -p build-wii
cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cmake -S. -Bbuild-wii \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL
cd build-wii
make -j$(nproc) VERBOSE=1
make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
+5 -1
View File
@@ -1,3 +1,7 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/test-linux.sh"
docker run \
--rm \
-v "${GITHUB_WORKSPACE}:/workdir" \
dusk-linux \
/bin/bash -c "./scripts/test-linux.sh"
+4 -4
View File
@@ -5,7 +5,7 @@
#include "easing.h"
#include "assert/assert.h"
#include <math.h>
#include "util/math.h"
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear,
@@ -36,15 +36,15 @@ float_t easingLinear(const float_t t) {
}
float_t easingInSine(const float_t t) {
return 1.0f - cosf(t * EASING_PI * 0.5f);
return 1.0f - cosf(t * MATH_PI * 0.5f);
}
float_t easingOutSine(const float_t t) {
return sinf(t * EASING_PI * 0.5f);
return sinf(t * MATH_PI * 0.5f);
}
float_t easingInOutSine(const float_t t) {
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f;
return -(cosf(MATH_PI * t) - 1.0f) * 0.5f;
}
float_t easingInQuad(const float_t t) {
+37 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -10,7 +10,17 @@
#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,
@@ -132,4 +142,29 @@
) {
assertTrueImpl(file, line, stringCompare(a, b) == 0, message);
}
#endif
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
+57 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -16,7 +16,18 @@
#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.
*
@@ -121,6 +132,28 @@
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.
*
@@ -205,8 +238,28 @@
#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)
@@ -215,11 +268,13 @@
#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)
+118 -18
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -78,21 +78,83 @@ assetentry_t * assetGetEntry(
return NULL;
}
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
) {
assertNotNull(outEntries, "Output entries cannot be NULL.");
uint32_t count = 0;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == type) {
outEntries[count++] = entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
return count;
}
errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
// Already loaded?
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
}
// Not loaded, just spin the wheel
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000);
errorChain(assetUpdate());
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
errorret_t assetRequireDisposed(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->type == ASSET_LOADER_TYPE_NULL) {
errorOk();
}
if(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
entry->state == ASSET_ENTRY_STATE_ERROR
) {
errorOk();
}
assertTrue(
entry->refs.count == 0,
"Cannot require disposal of an entry with active references."
);
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->type != ASSET_LOADER_TYPE_NULL) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
@@ -108,14 +170,19 @@ assetentry_t * assetLock(
void assetUnlock(const char_t *name) {
assertNotNull(name, "Name cannot be NULL.");
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL && stringEquals(entry->name, name)) {
if(
entry->type != ASSET_LOADER_TYPE_NULL &&
stringEquals(entry->name, name)
) {
assetEntryUnlock(entry);
return;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("Asset entry not found for unlock.");
}
@@ -125,6 +192,8 @@ void assetUnlockEntry(assetentry_t *entry) {
}
errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread.");
// Determine how many available loading slots we have.
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
uint8_t availableLoadingCount = 0;
@@ -191,8 +260,12 @@ errorret_t assetUpdate(void) {
switch(loading->entry->state) {
// This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC:
// Perform sync load.
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
// Unlock before calling loadSync. The sync loader may re-enter
// assetUpdate (e.g. a script loading another asset), and the async
// thread never touches LOADING_SYNC entries, so this is safe.
threadMutexUnlock(&loading->mutex);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
);
@@ -206,25 +279,25 @@ errorret_t assetUpdate(void) {
"Loader did not set entry state to loaded or error on finished load."
);
// If an error occured these things need to be true, basically just
// ensuring the sync loader is setting the error correctly.
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
eventInvoke(&loading->entry->onLoaded, loading->entry);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
assertUnreachable(
"Entry is in a pending sync state still?"
);
break;
// A re-entrant assetUpdate call (e.g. from a script loading another
// asset) will see this entry mid-sync-load. Skip it.
threadMutexUnlock(&loading->mutex);
loading++;
continue;
// Done loading, we can just free it up.
case ASSET_ENTRY_STATE_LOADED:
@@ -233,10 +306,14 @@ errorret_t assetUpdate(void) {
loading++;
break;
case ASSET_ENTRY_STATE_ERROR:
case ASSET_ENTRY_STATE_ERROR: {
assetentry_t *errEntry = loading->entry;
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
eventInvoke(&errEntry->onError, errEntry);
errorThrow("Failed to load asset asynchronously.");
break;
}
default:
threadMutexUnlock(&loading->mutex);
@@ -246,9 +323,7 @@ errorret_t assetUpdate(void) {
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap entries that have no external locks (refs.count == 1 means only the
// system hold remains). Only safe to reap LOADED and NOT_STARTED states —
// mid-load entries are left for the next cycle.
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
@@ -275,6 +350,8 @@ errorret_t assetUpdate(void) {
}
void assetUpdateAsync(thread_t *thread) {
assertNotMainThread("assetUpdateAsync must not run on the main thread.");
while(!threadShouldStop(thread)) {
// Walk over each asset
assetloading_t *loading;
@@ -331,12 +408,35 @@ void assetUpdateAsync(thread_t *thread) {
}
errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// Free any script read-buffers left behind by an in-flight async load
// that was interrupted before the sync eval phase ran.
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexDispose(&ASSET.loading[i].mutex);
assetloading_t *loading = &ASSET.loading[i];
if(
loading->entry != NULL &&
loading->type == ASSET_LOADER_TYPE_SCRIPT &&
loading->loading.script.buffer != NULL
) {
memoryFree(loading->loading.script.buffer);
loading->loading.script.buffer = NULL;
}
threadMutexDispose(&loading->mutex);
}
// Dispose every non-null entry so type-specific dispose callbacks
// (e.g. assetScriptDispose freeing jerry values) run before the
// scripting engine is torn down.
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
errorChain(assetEntryDispose(entry));
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// Cleanup zip file.
if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) {
+23 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -56,7 +56,7 @@ errorret_t assetInit(void);
bool_t assetFileExists(const char_t *filename);
/**
* Gets, or creates, a new asset entry. Internal prefer assetLock.
* Gets, or creates, a new asset entry. Internal - prefer assetLock.
*
* @param name Filename of the asset.
* @param type Type of the asset.
@@ -68,6 +68,18 @@ assetentry_t * assetGetEntry(
assetloaderinput_t *input
);
/**
* Gets all asset entries of a given type.
*
* @param outEntries Output array to write the entries to.
* @param type Type of the asset entries to get.
* @return The number of entries written to outEntries.
*/
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
);
/**
* Gets, creates, and locks an asset entry. The asset will begin loading on
* the next assetUpdate. Call assetUnlock when done to allow the entry to be
@@ -109,6 +121,15 @@ void assetUnlockEntry(assetentry_t *entry);
*/
errorret_t assetRequireLoaded(assetentry_t *entry);
/**
* Requires an asset entry to be disposed. This will block until the asset entry
* is fully disposed.
*
* @param entry The asset entry to require disposal of.
* @return An error code if the asset entry could not be disposed properly.
*/
errorret_t assetRequireDisposed(assetentry_t *entry);
/**
* Updates the asset system.
*
+75 -4
View File
@@ -19,15 +19,53 @@ void assetBatchInit(
assertNotNull(batch, "Batch cannot be NULL.");
assertNotNull(descs, "Descs cannot be NULL.");
assertTrue(count > 0, "Count must be greater than 0.");
assertTrue(count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX.");
assertTrue(
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
);
memoryZero(batch, sizeof(assetbatch_t));
batch->count = count;
eventInit(
&batch->onLoaded,
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryLoaded,
batch->onEntryLoadedCallbacks,
batch->onEntryLoadedUsers,
ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onError,
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
);
eventInit(
&batch->onEntryError,
batch->onEntryErrorCallbacks,
batch->onEntryErrorUsers,
ASSET_BATCH_EVENT_MAX
);
for(uint16_t i = 0; i < count; i++) {
// Copy input into batch-owned storage so the descriptor need not persist.
batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(descs[i].path, descs[i].type, &batch->inputs[i]);
batch->entries[i] = assetLock(
descs[i].path, descs[i].type, &batch->inputs[i]
);
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
// Already loaded (cached) - count it now, no subscription needed.
batch->loadedCount++;
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
batch->errorCount++;
} else {
eventSubscribe(
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
);
eventSubscribe(
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
);
}
}
}
@@ -88,7 +126,40 @@ errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetUnlockEntry(batch->entries[i]);
if(batch->entries[i]) {
// Unsubscribe while we still hold a lock so the entry is live.
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
assetUnlockEntry(batch->entries[i]);
}
}
memoryZero(batch, sizeof(assetbatch_t));
}
void assetBatchEntryOnLoadedCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->loadedCount++;
eventInvoke(&batch->onEntryLoaded, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
if(batch->errorCount == 0) {
eventInvoke(&batch->onLoaded, batch);
} else {
eventInvoke(&batch->onError, batch);
}
}
}
void assetBatchEntryOnErrorCb(void *params, void *user) {
assetentry_t *entry = (assetentry_t *)params;
assetbatch_t *batch = (assetbatch_t *)user;
batch->errorCount++;
eventInvoke(&batch->onEntryError, entry);
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
eventInvoke(&batch->onError, batch);
}
}
+43 -1
View File
@@ -8,8 +8,10 @@
#pragma once
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "event/event.h"
#define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_COUNT_MAX 64
#define ASSET_BATCH_EVENT_MAX 4
typedef struct {
const char_t *path;
@@ -21,6 +23,28 @@ typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
uint16_t loadedCount;
uint16_t errorCount;
/** Fires once when every entry loaded. params = assetbatch_t * */
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry loads. params = assetentry_t * */
event_t onEntryLoaded;
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
/** Fires each time a single entry errors. params = assetentry_t * */
event_t onEntryError;
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
} assetbatch_t;
/**
@@ -80,3 +104,21 @@ errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
* @param batch Batch to dispose.
*/
void assetBatchDispose(assetbatch_t *batch);
/**
* Event trampoline invoked when a batch entry finishes loading.
* Increments the loaded counter and fires batch-level events.
*
* @param params The loaded assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnLoadedCb(void *params, void *user);
/**
* Event trampoline invoked when a batch entry fails to load.
* Increments the error counter and fires batch-level events.
*
* @param params The errored assetentry_t pointer.
* @param user The owning assetbatch_t pointer.
*/
void assetBatchEntryOnErrorCb(void *params, void *user);
+45
View File
@@ -117,6 +117,51 @@ errorret_t assetFileDispose(assetfile_t *file) {
errorOk();
}
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(outBuffer, "outBuffer cannot be NULL.");
assertNotNull(outSize, "outSize cannot be NULL.");
assertTrue(
file->size > 0,
"Asset file has no size; call assetFileInit first."
);
// File should be closed currently.
assertNull(file->zipFile, "Asset file must be closed before reading entire.");
// Open file
errorret_t ret = assetFileOpen(file);
if(errorIsNotOk(ret)) {
errorChain(ret);
}
// Set output.
size_t size = (size_t)file->size;
uint8_t *buffer = (uint8_t *)memoryAllocate(size);
// Read entire file.
ret = assetFileRead(file, buffer, size);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
// Close the file.
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
*outBuffer = buffer;
*outSize = size;
errorOk();
}
// Line Reader;
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
+16 -1
View File
@@ -86,12 +86,27 @@ errorret_t assetFileClose(assetfile_t *file);
/**
* Disposes the asset file structure, closing any open handles and zeroing
* out the structure.
*
*
* @param file The asset file to dispose.
* @return An error code if the file could not be disposed properly.
*/
errorret_t assetFileDispose(assetfile_t *file);
/**
* Reads the entire contents of the asset file into a newly allocated buffer.
* The caller is responsible for freeing the buffer with memoryFree.
*
* @param file The asset file to read. Must be initialized but not open.
* @param outBuffer Receives a pointer to the allocated buffer.
* @param outSize Receives the number of bytes written to the buffer.
* @return An error code if the file could not be read.
*/
errorret_t assetFileReadEntire(
assetfile_t *file,
uint8_t **outBuffer,
size_t *outSize
);
typedef struct {
assetfile_t *file;
uint8_t *readBuffer;
+36 -5
View File
@@ -1,6 +1,6 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
@@ -21,24 +21,49 @@ void assetEntryInit(
assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long");
assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
entry->input = input;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
if(input) {
entry->inputData = *input;
entry->input = &entry->inputData;
} else {
memoryZero(&entry->inputData, sizeof(assetloaderinput_t));
entry->input = NULL;
}
refInit(&entry->refs, entry, NULL, NULL, NULL);
eventInit(
&entry->onLoaded,
entry->onLoadedCallbacks, entry->onLoadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onUnloaded,
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
ASSET_ENTRY_EVENT_MAX
);
eventInit(
&entry->onError,
entry->onErrorCallbacks, entry->onErrorUsers,
ASSET_ENTRY_EVENT_MAX
);
}
void assetEntryLock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refLock(&entry->refs);
}
void assetEntryUnlock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refUnlock(&entry->refs);
}
@@ -53,6 +78,7 @@ void assetEntryStartLoading(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED,
"Can only start loading from NOT_STARTED state."
);
assertIsMainThread("Must be called from the main thread.");
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
memoryZero(&loading->loading, sizeof(assetloaderloading_t));
@@ -63,11 +89,16 @@ void assetEntryStartLoading(
errorret_t assetEntryDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
assertTrue(
entry->refs.count == 0,
"Asset entry still refed at dispose time."
);
eventInvoke(&entry->onUnloaded, entry);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t));
errorOk();
}
}
+42 -16
View File
@@ -1,12 +1,13 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetloading.h"
#include "event/event.h"
#include "util/ref.h"
typedef enum {
@@ -19,25 +20,49 @@ typedef enum {
ASSET_ENTRY_STATE_ERROR
} assetentrystate_t;
typedef struct assetentry_s {
// Filename and cache key
/** Maximum number of subscribers for each per-entry event. */
#define ASSET_ENTRY_EVENT_MAX 2
typedef struct assetentry_s assetentry_t;
struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX];
// What type of asset is this?
assetloadertype_t type;
// Data
assetloaderoutput_t data;
// What state is this asset entry in currently?
assetentrystate_t state;
// What is referencing this asset entry.
ref_t refs;
// Data that will be passed to the loader about how it should load.
assetloaderinput_t *input;
} assetentry_t;
assetloaderinput_t inputData;
/**
* Fired once when loading completes successfully (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onLoaded;
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
* The asset data is still accessible when the callback runs.
* Always invoked on the main thread.
*/
event_t onUnloaded;
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
/**
* Fired once when loading fails (params = assetentry_t *).
* Always invoked on the main thread.
*/
event_t onError;
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
};
/**
* Initializes an asset entry with the given name and type. This does not load
* the asset.
*
*
* @param entry The asset entry to initialize.
* @param name The name of the asset, used as a key for loading and caching.
* @param type The type of asset this entry represents.
@@ -52,14 +77,14 @@ void assetEntryInit(
/**
* Locks an asset entry, preventing it from being freed until it is unlocked.
*
*
* @param entry The asset entry to lock.
*/
void assetEntryLock(assetentry_t *entry);
/**
* Unlocks an asset entry, allowing it to be freed if there are no more locks.
*
*
* @param entry The asset entry to unlock.
*/
void assetEntryUnlock(assetentry_t *entry);
@@ -68,9 +93,9 @@ void assetEntryUnlock(assetentry_t *entry);
* Starts loading the given asset entry using an assetloading slot. This will
* be called by the asset manager when it deems it's a good time to begin the
* loading of this asset entry.
*
*
* Currently we return the error but in future this will not be returned.
*
*
* @param entry The asset entry to start loading.
* @param loading The assetloading slot to use for loading this asset entry.
* @return Any error that occurs during loading.
@@ -79,8 +104,9 @@ void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading);
/**
* Disposes an asset entry, freeing any resources it holds.
*
* Fires the onUnloaded event before releasing asset data.
*
* @param entry The asset entry to dispose.
* @return Any error that occurs during disposal.
*/
errorret_t assetEntryDispose(assetentry_t *entry);
errorret_t assetEntryDispose(assetentry_t *entry);
-4
View File
@@ -13,13 +13,9 @@
typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s {
// Protects entry pointer and entry->state from concurrent access.
threadmutex_t mutex;
// What type of asset is being loaded.
assetloadertype_t type;
// Referral back to the asset entry that will be kept alive after load done.
assetentry_t *entry;
// Information used during the load operation only.
assetloaderloading_t loading;
} assetloading_t;
@@ -21,9 +21,11 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assetmeshoutput_t *out = &loading->entry->data.mesh;
assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->input->mesh;
assetmeshinputaxis_t axis = loading->entry->inputData.mesh;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
assetLoaderErrorChain(loading, assetFileOpen(file));
// Skip the 80-byte STL header.
@@ -33,7 +35,9 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
}
uint32_t triangleCount;
assetLoaderErrorChain(loading, assetFileRead(file, &triangleCount, sizeof(uint32_t)));
assetLoaderErrorChain(loading,
assetFileRead(file, &triangleCount, sizeof(uint32_t))
);
if(file->lastRead != sizeof(uint32_t)) {
assetLoaderErrorThrow(loading, "Failed to read tri count");
}
@@ -75,7 +79,9 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
verts[i * 3 + j].uv[1] = 0.0f;
for(uint8_t k = 0; k < 3; k++) {
verts[i * 3 + j].pos[k] = endianLittleToHostFloat(triData.positions[j][k]);
verts[i * 3 + j].pos[k] = endianLittleToHostFloat(
triData.positions[j][k]
);
}
switch(axis) {
@@ -54,6 +54,7 @@ int assetTextureEOF(void *user) {
errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
// Only care about loading pixels.
if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){
@@ -76,7 +77,7 @@ errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
// Determine channels
int channelsDesired;
switch(loading->entry->input->texture) {
switch(loading->entry->inputData.texture) {
case TEXTURE_FORMAT_RGBA:
channelsDesired = 4;
break;
@@ -102,7 +103,9 @@ errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
// Ensure we loaded correctly.
if(loading->loading.texture.data == NULL) {
const char_t *errorStr = stbi_failure_reason();
assetLoaderErrorThrow(loading, "Failed to load texture from file %s.", errorStr);
assetLoaderErrorThrow(
loading, "Failed to load texture from file %s.", errorStr
);
}
// Fixes a specific bug probably with Dolphin but for now just assuming endian
@@ -122,6 +125,7 @@ errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.texture.state) {
case ASSET_TEXTURE_LOADING_STATE_INITIAL:
@@ -146,7 +150,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
(texture_t*)&loading->entry->data.texture,
loading->loading.texture.width,
loading->loading.texture.height,
loading->entry->input->texture,
loading->entry->inputData.texture,
(texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data
}
@@ -161,5 +165,7 @@ errorret_t assetTextureLoaderSync(assetloading_t *loading) {
errorret_t assetTextureDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
return textureDispose(&entry->data.texture);
}
@@ -14,6 +14,7 @@
errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
errorOk();
@@ -22,14 +23,19 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNull(loading->loading.tileset.data, "Data already defined?");
assetfile_t *file = &loading->loading.tileset.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
assertTrue(file->lastRead == file->size, "Failed to read entire tileset file.");
assertTrue(
file->lastRead == file->size,
"Failed to read entire tileset file."
);
loading->loading.tileset.data = data;
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE;
@@ -40,6 +46,7 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.tileset.state) {
case ASSET_TILESET_LOADING_STATE_INITIAL:
@@ -111,5 +118,7 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk();
}
+8 -1
View File
@@ -13,6 +13,7 @@
errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) {
errorOk();
@@ -21,7 +22,9 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNull(loading->loading.json.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
@@ -45,6 +48,7 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
errorret_t assetJsonLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.json.state) {
case ASSET_JSON_LOADING_STATE_INITIAL:
@@ -82,7 +86,10 @@ errorret_t assetJsonLoaderSync(assetloading_t *loading) {
errorret_t assetJsonDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
yyjson_doc_free(entry->data.json);
entry->data.json = NULL;
errorOk();
}
@@ -16,6 +16,7 @@
errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) {
errorOk();
@@ -23,12 +24,18 @@ errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
assetlocalefile_t *localeFile = &loading->entry->data.locale;
memoryZero(localeFile, sizeof(assetlocalefile_t));
assetLoaderErrorChain(loading, assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileInit(
&localeFile->file, loading->entry->name, NULL, NULL
));
assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file));
char_t buffer[1024];
assetLoaderErrorChain(loading, assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
assetLoaderErrorChain(loading, assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
assetLoaderErrorChain(loading, assetLocaleGetString(
localeFile, "", 0, buffer, sizeof(buffer)
));
assetLoaderErrorChain(loading, assetLocaleParseHeader(
localeFile, buffer, sizeof(buffer)
));
loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_DONE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
@@ -38,6 +45,7 @@ errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.locale.state) {
case ASSET_LOCALE_LOADER_STATE_INITIAL:
@@ -60,11 +68,14 @@ errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
errorret_t assetLocaleDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetlocalefile_t *localeFile = &entry->data.locale;
errorChain(assetFileClose(&localeFile->file));
return assetFileDispose(&localeFile->file);
}
// These functions probably need some cleaning;
errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile,
char_t *headerBuffer,
@@ -11,7 +11,7 @@
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
/** Input passed to the locale loader currently unused. */
/** Input passed to the locale loader - currently unused. */
typedef struct { void *nothing; } assetlocaleloaderinput_t;
typedef enum {
@@ -100,7 +100,7 @@ typedef struct {
uint8_t pluralDefaultIndex;
} assetlocalefile_t;
/** Convenience alias the loaded output type of a locale asset entry. */
/** Convenience alias - the loaded output type of a locale asset entry. */
typedef assetlocalefile_t assetlocaleoutput_t;
/**
@@ -189,7 +189,7 @@ errorret_t assetLocaleLineSkipBlanks(
*
* @param reader Line reader positioned at the line containing the opening
* quote (e.g. `msgstr "..."`).
* @param lineBuffer Buffer the reader fills on each @ref assetFileLineReaderNext
* @param lineBuffer Buffer filled on each @ref assetFileLineReaderNext
* call; also used to detect continuation lines.
* @param stringBuffer Destination for the unescaped string content.
* @param stringBufferSize Capacity of `stringBuffer` in bytes.
@@ -9,12 +9,14 @@
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "script/module/require/modulerequire.h"
#include "util/memory.h"
#include "assert/assert.h"
#include <jerryscript.h>
errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Async loader should not be on main thread.");
if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) {
errorOk();
@@ -23,33 +25,21 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNull(loading->loading.script.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.script.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(
loading, assetFileInit(file, loading->entry->name, NULL, NULL)
);
size_t capacity = ASSET_SCRIPT_CHUNK_SIZE;
uint8_t *buffer = memoryAllocate(capacity + 1);
size_t offset = 0;
while(1) {
if(offset + ASSET_SCRIPT_CHUNK_SIZE > capacity) {
size_t oldCapacity = capacity + 1;
capacity += ASSET_SCRIPT_CHUNK_SIZE;
memoryResize((void **)&buffer, oldCapacity, capacity + 1);
}
assetLoaderErrorChain(loading, assetFileRead(
file, buffer + offset, ASSET_SCRIPT_CHUNK_SIZE
));
size_t chunk = (size_t)file->lastRead;
offset += chunk;
if(chunk == 0) break;
}
buffer[offset] = '\0';
assetLoaderErrorChain(loading, assetFileClose(file));
uint8_t *buffer = NULL;
size_t size = 0;
assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
assetLoaderErrorChain(loading, assetFileDispose(file));
// Null-terminate for jerry_eval.
memoryResize((void **)&buffer, size, size + 1);
buffer[size] = '\0';
loading->loading.script.buffer = buffer;
loading->loading.script.size = offset;
loading->loading.script.size = size;
loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_EXEC;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
@@ -58,6 +48,7 @@ errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
errorret_t assetScriptLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.script.state) {
case ASSET_SCRIPT_LOADING_STATE_INITIAL:
@@ -73,34 +64,58 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) {
errorOk();
}
// Get read buffer
uint8_t *buffer = loading->loading.script.buffer;
assertNotNull(buffer, "Script buffer should have been loaded by now.");
// Get the current global script realm
jerry_value_t global = jerry_current_realm();
// Replace globalThis.module with a new `module = {}`
jerry_value_t oldModule = jerry_object_get_sz(global, "module");
jerry_value_t module = jerry_object();
jerry_object_set_sz(global, "module", module);
// Eval the script, we handle failure later down the code.
jerry_value_t result = jerry_eval(
(const jerry_char_t *)buffer,
buffer,
loading->loading.script.size,
JERRY_PARSE_NO_OPTS
);
// Free the read buffer
memoryFree(buffer);
loading->loading.script.buffer = NULL;
// Restore globalThis.module
jerry_object_set_sz(global, "module", oldModule);
jerry_value_free(oldModule);
jerry_value_free(global);
if(jerry_value_is_exception(result)) {
jerry_value_t errVal = jerry_exception_value(result, false);
jerry_value_t errStr = jerry_value_to_string(errVal);
jerry_value_free(module);
loading->entry->data.script.exports = jerry_undefined();
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
// Get error string
char_t buf[256];
jerry_size_t len = jerry_string_to_buffer(
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
);
buf[len] = '\0';
jerry_value_free(errStr);
jerry_value_free(errVal);
moduleBaseExceptionMessage(result, buf, sizeof(buf));
jerry_value_free(result);
assetLoaderErrorThrow(loading, "Script error in '%s': %s",
loading->entry->name, buf
assetLoaderErrorThrow(
loading,
"Script execution failed: %s: %s", loading->entry->name, buf
);
}
// Get module.exports
jerry_value_t exports = jerry_object_get_sz(module, "exports");
jerry_value_free(result);
jerry_value_free(module);
loading->entry->data.script = (assetscriptoutput_t)result;
// Store the exports.
loading->entry->data.script.exports = exports;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
@@ -108,9 +123,15 @@ errorret_t assetScriptLoaderSync(assetloading_t *loading) {
errorret_t assetScriptDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
if(entry->data.script != 0) {
jerry_value_free((jerry_value_t)entry->data.script);
entry->data.script = 0;
assertIsMainThread("Must be called from the main thread.");
if(
entry->data.script.exports != 0 &&
!jerry_value_is_undefined(entry->data.script.exports)
) {
jerry_value_free(entry->data.script.exports);
entry->data.script.exports = 0;
}
errorOk();
}
@@ -7,14 +7,18 @@
#pragma once
#include "asset/assetfile.h"
#include "script/scriptmodule.h"
#define ASSET_SCRIPT_CHUNK_SIZE 1024
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetscriptloaderinput_t;
typedef uint32_t assetscriptoutput_t;
typedef struct {
void *nothing;
} assetscriptloaderinput_t;
typedef scriptmodule_t assetscriptoutput_t;
typedef enum {
ASSET_SCRIPT_LOADING_STATE_INITIAL,
@@ -30,6 +34,27 @@ typedef struct {
size_t size;
} assetscriptloaderloading_t;
/**
* Asynchronous loader for a script asset/module.
*
* @param loading The loading context.
* @returns An error code and state.
*/
errorret_t assetScriptLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for a script asset/module. This executes the script after
* it has been loaded by the async loader.
*
* @param loading The loading context.
* @returns An error code and state.
*/
errorret_t assetScriptLoaderSync(assetloading_t *loading);
/**
* Disposes of a loaded script asset/module.
*
* @param entry The asset entry to dispose.
* @returns An error code and state.
*/
errorret_t assetScriptDispose(assetentry_t *entry);
+1
View File
@@ -20,6 +20,7 @@ console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
CONSOLE.visible = true;
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Dominic Masters
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
+1 -1
View File
@@ -74,7 +74,7 @@ void planeBuffer(
const float_t u0 = uvMin[0], u1 = uvMax[0];
const float_t v0 = uvMin[1], v1 = uvMax[1];
switch (axis) {
switch(axis) {
case PLANE_AXIS_XY: {
/* Flat in XY at z = min[2]; spans X and Y. */
const float_t z = min[2];
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -47,7 +47,7 @@ errorret_t spriteBatchInit();
/**
* Lowest-level buffer function. Writes sprites into the internal vertex buffer.
* Flushes automatically when the per-flush capacity is reached. Does not
* modify material state call spriteBatchSetState or use a high-level push
* modify material state - call spriteBatchSetState or use a high-level push
* function before buffering.
*
* @param sprites Pointer to the sprite array.
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+7 -7
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -30,6 +30,7 @@
engine_t ENGINE;
errorret_t engineInit(const int32_t argc, const char_t **argv) {
assertInit();
memoryZero(&ENGINE, sizeof(engine_t));
ENGINE.running = true;
ENGINE.argc = argc;
@@ -47,18 +48,16 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(displayInit());
errorChain(uiInit());
errorChain(uiTextboxInit());
errorChain(cutsceneInit());
errorChain(sceneInit());
entityManagerInit();
backpackInit();
physicsManagerInit();
errorChain(networkInit());
errorChain(scriptInit());
errorChain(scriptExecFile("init.js"));
errorChain(sceneInit());
consolePrint("Engine initialized");
errorChain(scriptExecFile("init.js"));
errorOk();
}
@@ -77,6 +76,7 @@ errorret_t engineUpdate(void) {
errorChain(cutsceneUpdate());
errorChain(sceneUpdate());
errorChain(assetUpdate());
errorChain(scriptUpdate());
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
errorOk();
@@ -89,7 +89,8 @@ void engineExit(void) {
errorret_t engineDispose(void) {
uiTextboxDispose();
cutsceneDispose();
sceneDispose();
errorChain(sceneDispose());
errorChain(scriptDispose());
errorChain(networkDispose());
entityManagerDispose();
localeManagerDispose();
@@ -98,7 +99,6 @@ errorret_t engineDispose(void) {
errorChain(displayDispose());
// errorChain(saveDispose());
errorChain(assetDispose());
errorChain(scriptDispose());
errorOk();
}
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2025 Dominic Masters
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
+3 -1
View File
@@ -119,7 +119,9 @@ errorret_t componentRenderAll(void) {
for(entityid_t eid = 0; eid < ENTITY_COUNT_MAX; eid++) {
if(!(ENTITY_MANAGER.entities[eid].state & ENTITY_STATE_ACTIVE)) continue;
for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) {
component_t *cmp = &ENTITY_MANAGER.components[componentGetIndex(eid, cid)];
component_t *cmp = &ENTITY_MANAGER.components[
componentGetIndex(eid, cid)
];
if(cmp->type == COMPONENT_TYPE_NULL) continue;
if(!COMPONENT_DEFINITIONS[cmp->type].render) continue;
errorChain(COMPONENT_DEFINITIONS[cmp->type].render(eid, cid));
-2
View File
@@ -4,7 +4,5 @@
# https://opensource.org/licenses/MIT
add_subdirectory(display)
add_subdirectory(overworld)
add_subdirectory(physics)
add_subdirectory(script)
add_subdirectory(trigger)
@@ -22,12 +22,12 @@ static void entityPositionEnsureLocal(entityposition_t *pos) {
if(!dirty) return;
if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
// Rotation or scale changed: rebuild columns 0-2 analytically (XYZ euler order).
const float c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
const float c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
const float c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
const float s0s1 = s0 * s1;
const float c0s1 = c0 * s1;
// Rotation or scale changed: rebuild cols 0-2 analytically (XYZ euler).
const float_t c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
const float_t c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
const float_t c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
const float_t s0s1 = s0 * s1;
const float_t c0s1 = c0 * s1;
pos->localTransform[0][0] = c1 * c2 * pos->scale[0];
pos->localTransform[0][1] = (c0 * s2 + s0s1 * c2) * pos->scale[0];
@@ -53,7 +53,9 @@ static void entityPositionEnsureLocal(entityposition_t *pos) {
pos->localTransform[3][3] = 1.0f;
}
pos->flags &= ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY);
pos->flags &= ~(
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
);
}
// Recompute worldTransform from the parent chain. Only called when WORLD_DIRTY.
@@ -62,13 +64,15 @@ static void entityPositionEnsureWorld(entityposition_t *pos) {
entityPositionEnsureLocal(pos);
if(pos->parentEntityId != ENTITY_ID_INVALID) {
// Parented: world = parent.world × local. worldTransform must be written
// Parented: world = parent.world x local. worldTransform must be written
// because children (and this node's getters) read it.
entityposition_t *parent = componentGetData(
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(parent);
glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform);
glm_mat4_mul(
parent->worldTransform, pos->localTransform, pos->worldTransform
);
} else if(pos->childCount > 0) {
// Parentless root with children: children need a valid worldTransform to
// multiply against, but world == local, so just copy.
@@ -123,7 +127,8 @@ void entityPositionLookAt(
glm_lookat(eye, target, up, pos->localTransform);
// localTransform is now authoritative; PRS cache is stale.
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY)
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY);
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY |
ENTITY_POSITION_FLAG_POSITION_DIRTY);
entityPositionMarkDirty(pos);
}
@@ -137,7 +142,8 @@ void entityPositionGetTransform(
);
entityPositionEnsureWorld(pos);
glm_mat4_copy(
pos->parentEntityId == ENTITY_ID_INVALID ? pos->localTransform : pos->worldTransform,
pos->parentEntityId == ENTITY_ID_INVALID
? pos->localTransform : pos->worldTransform,
dest
);
}
@@ -254,20 +260,26 @@ void entityPositionGetWorldRotation(
return;
}
entityPositionEnsureWorld(pos);
const float (*wt)[4] = pos->worldTransform;
const float sx = sqrtf(wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]);
const float sy = sqrtf(wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]);
const float sz = sqrtf(wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]);
const float r00 = sx > 0.0f ? wt[0][0]/sx : 0.0f;
const float r10 = sy > 0.0f ? wt[1][0]/sy : 0.0f;
const float r20 = sz > 0.0f ? wt[2][0]/sz : 0.0f;
const float r01 = sx > 0.0f ? wt[0][1]/sx : 0.0f;
const float r11 = sy > 0.0f ? wt[1][1]/sy : 0.0f;
const float r21 = sz > 0.0f ? wt[2][1]/sz : 0.0f;
const float r22 = sz > 0.0f ? wt[2][2]/sz : 0.0f;
const float sinBeta = glm_clamp(r20, -1.0f, 1.0f);
const float_t (*wt)[4] = pos->worldTransform;
const float_t sx = sqrtf(
wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]
);
const float_t sy = sqrtf(
wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]
);
const float_t sz = sqrtf(
wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]
);
const float_t r00 = sx > 0.0f ? wt[0][0]/sx : 0.0f;
const float_t r10 = sy > 0.0f ? wt[1][0]/sy : 0.0f;
const float_t r20 = sz > 0.0f ? wt[2][0]/sz : 0.0f;
const float_t r01 = sx > 0.0f ? wt[0][1]/sx : 0.0f;
const float_t r11 = sy > 0.0f ? wt[1][1]/sy : 0.0f;
const float_t r21 = sz > 0.0f ? wt[2][1]/sz : 0.0f;
const float_t r22 = sz > 0.0f ? wt[2][2]/sz : 0.0f;
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
dest[1] = asinf(sinBeta);
const float cosBeta = cosf(dest[1]);
const float_t cosBeta = cosf(dest[1]);
if(fabsf(cosBeta) > 1e-6f) {
dest[0] = atan2f(-r21, r22);
dest[2] = atan2f(-r10, r00);
@@ -312,29 +324,41 @@ void entityPositionSetWorldRotation(
entityPositionEnsureWorld(parent);
// Build target world rotation matrix (unit scale) from XYZ euler.
const float c0 = cosf(rotation[0]), s0 = sinf(rotation[0]);
const float c1 = cosf(rotation[1]), s1 = sinf(rotation[1]);
const float c2 = cosf(rotation[2]), s2 = sinf(rotation[2]);
const float s0s1 = s0*s1, c0s1 = c0*s1;
const float_t c0 = cosf(rotation[0]), s0 = sinf(rotation[0]);
const float_t c1 = cosf(rotation[1]), s1 = sinf(rotation[1]);
const float_t c2 = cosf(rotation[2]), s2 = sinf(rotation[2]);
const float_t s0s1 = s0*s1, c0s1 = c0*s1;
// Named wr[col_stored][row_stored] matching cglm column-major layout.
const float wr00 = c1*c2, wr01 = c0*s2 + s0s1*c2, wr02 = s0*s2 - c0s1*c2;
const float wr10 = -c1*s2, wr11 = c0*c2 - s0s1*s2, wr12 = s0*c2 + c0s1*s2;
const float wr20 = s1, wr21 = -s0*c1, wr22 = c0*c1;
const float_t wr00 = c1*c2;
const float_t wr01 = c0*s2 + s0s1*c2;
const float_t wr02 = s0*s2 - c0s1*c2;
const float_t wr10 = -c1*s2;
const float_t wr11 = c0*c2 - s0s1*s2;
const float_t wr12 = s0*c2 + c0s1*s2;
const float_t wr20 = s1;
const float_t wr21 = -s0*c1;
const float_t wr22 = c0*c1;
// Normalize parent world columns to extract pure rotation.
const float (*pt)[4] = parent->worldTransform;
const float psx = sqrtf(pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]);
const float psy = sqrtf(pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]);
const float psz = sqrtf(pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]);
const float pr00 = psx > 0.f ? pt[0][0]/psx : 0.f;
const float pr01 = psx > 0.f ? pt[0][1]/psx : 0.f;
const float pr02 = psx > 0.f ? pt[0][2]/psx : 0.f;
const float pr10 = psy > 0.f ? pt[1][0]/psy : 0.f;
const float pr11 = psy > 0.f ? pt[1][1]/psy : 0.f;
const float pr12 = psy > 0.f ? pt[1][2]/psy : 0.f;
const float pr20 = psz > 0.f ? pt[2][0]/psz : 0.f;
const float pr21 = psz > 0.f ? pt[2][1]/psz : 0.f;
const float pr22 = psz > 0.f ? pt[2][2]/psz : 0.f;
const float_t (*pt)[4] = parent->worldTransform;
const float_t psx = sqrtf(
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
);
const float_t psy = sqrtf(
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
);
const float_t psz = sqrtf(
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
);
const float_t pr00 = psx > 0.f ? pt[0][0]/psx : 0.f;
const float_t pr01 = psx > 0.f ? pt[0][1]/psx : 0.f;
const float_t pr02 = psx > 0.f ? pt[0][2]/psx : 0.f;
const float_t pr10 = psy > 0.f ? pt[1][0]/psy : 0.f;
const float_t pr11 = psy > 0.f ? pt[1][1]/psy : 0.f;
const float_t pr12 = psy > 0.f ? pt[1][2]/psy : 0.f;
const float_t pr20 = psz > 0.f ? pt[2][0]/psz : 0.f;
const float_t pr21 = psz > 0.f ? pt[2][1]/psz : 0.f;
const float_t pr22 = psz > 0.f ? pt[2][2]/psz : 0.f;
// local_R = parent_R^T * world_R (R^-1 == R^T for orthogonal matrices).
// Compute only the 7 entries of the local rotation matrix needed for XYZ
@@ -343,23 +367,24 @@ void entityPositionSetWorldRotation(
// r21/r22 = stored[2][1..2] = math[1..2][2]
// r10/r00 = stored[1][0], stored[0][0] = math[0][1], math[0][0]
// gimbal = stored[0][1], stored[1][1] = math[1][0], math[1][1]
const float lr00 = pr00*wr00 + pr01*wr10 + pr02*wr20; // math[0][0]
const float lr10 = pr00*wr01 + pr01*wr11 + pr02*wr21; // math[0][1]
const float lr20 = pr00*wr02 + pr01*wr12 + pr02*wr22; // math[0][2] sinBeta
const float lr01 = pr10*wr00 + pr11*wr10 + pr12*wr20; // math[1][0]
const float lr11 = pr10*wr01 + pr11*wr11 + pr12*wr21; // math[1][1]
const float lr21 = pr10*wr02 + pr11*wr12 + pr12*wr22; // math[1][2] r21
const float lr22 = pr20*wr02 + pr21*wr12 + pr22*wr22; // math[2][2] r22
const float_t lr00 = pr00*wr00 + pr01*wr10 + pr02*wr20; // math[0][0]
const float_t lr10 = pr00*wr01 + pr01*wr11 + pr02*wr21; // math[0][1]
const float_t lr20 = pr00*wr02 + pr01*wr12 + pr02*wr22; // [0][2] -> sinBeta
const float_t lr01 = pr10*wr00 + pr11*wr10 + pr12*wr20; // math[1][0]
const float_t lr11 = pr10*wr01 + pr11*wr11 + pr12*wr21; // math[1][1]
const float_t lr21 = pr10*wr02 + pr11*wr12 + pr12*wr22; // [1][2] -> r21
const float_t lr22 = pr20*wr02 + pr21*wr12 + pr22*wr22; // [2][2] -> r22
const float sinBeta = glm_clamp(lr20, -1.0f, 1.0f);
const float_t sinBeta = glm_clamp(lr20, -1.0f, 1.0f);
pos->rotation[1] = asinf(sinBeta);
const float cosBeta = cosf(pos->rotation[1]);
const float_t cosBeta = cosf(pos->rotation[1]);
if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-lr21, lr22);
pos->rotation[2] = atan2f(-lr10, lr00);
} else {
pos->rotation[2] = 0.0f;
pos->rotation[0] = (sinBeta > 0.0f) ? atan2f(lr01, lr11) : -atan2f(lr01, lr11);
pos->rotation[0] = (sinBeta > 0.0f)
? atan2f(lr01, lr11) : -atan2f(lr01, lr11);
}
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
@@ -392,7 +417,7 @@ void entityPositionGetWorldScale(
return;
}
entityPositionEnsureWorld(pos);
const float (*wt)[4] = pos->worldTransform;
const float_t (*wt)[4] = pos->worldTransform;
dest[0] = sqrtf(wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]);
dest[1] = sqrtf(wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]);
dest[2] = sqrtf(wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]);
@@ -431,10 +456,16 @@ void entityPositionSetWorldScale(
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
);
entityPositionEnsureWorld(parent);
const float (*pt)[4] = parent->worldTransform;
const float psx = sqrtf(pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]);
const float psy = sqrtf(pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]);
const float psz = sqrtf(pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]);
const float_t (*pt)[4] = parent->worldTransform;
const float_t psx = sqrtf(
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
);
const float_t psy = sqrtf(
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
);
const float_t psz = sqrtf(
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
);
pos->scale[0] = psx > 0.0f ? scale[0] / psx : scale[0];
pos->scale[1] = psy > 0.0f ? scale[1] / psy : scale[1];
pos->scale[2] = psz > 0.0f ? scale[2] / psz : scale[2];
@@ -501,8 +532,11 @@ entityposition_t *entityPositionGet(
}
void entityPositionRebuild(entityposition_t *pos) {
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY)
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
pos->flags = (
pos->flags |
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
ENTITY_POSITION_FLAG_POSITION_DIRTY
) & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
entityPositionMarkDirty(pos);
}
@@ -514,7 +548,9 @@ void entityPositionDisposeDeep(
// Detach from parent so the parent's child list stays consistent.
if(pos->parentEntityId != ENTITY_ID_INVALID) {
entityPositionSetParent(entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID);
entityPositionSetParent(
entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
);
}
// Copy the child list before disposing self (entityDispose invalidates pos).
@@ -524,8 +560,10 @@ void entityPositionDisposeDeep(
for(uint8_t i = 0; i < childCount; i++) {
childEntityIds[i] = pos->childEntityIds[i];
childComponentIds[i] = pos->childComponentIds[i];
// Sever the child's parent link so it won't try to modify our disposed data.
entityposition_t *child = entityPositionGet(childEntityIds[i], childComponentIds[i]);
// Sever child's parent link so it won't try to modify our disposed data.
entityposition_t *child = entityPositionGet(
childEntityIds[i], childComponentIds[i]
);
child->parentEntityId = ENTITY_ID_INVALID;
child->parentComponentId = COMPONENT_ID_INVALID;
}
@@ -560,24 +598,24 @@ void entityPositionDecompose(entityposition_t *pos) {
pos->localTransform[2][2] * pos->localTransform[2][2]
);
// Normalize columns to isolate the rotation matrix (9 floats, no mat4 needed).
const float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
const float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
const float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
// Normalize columns to isolate the rotation matrix (no mat4 needed).
const float_t invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
const float_t invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
const float_t invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
const float r00 = pos->localTransform[0][0] * invS0;
const float r01 = pos->localTransform[0][1] * invS0;
const float r02 = pos->localTransform[0][2] * invS0;
const float r10 = pos->localTransform[1][0] * invS1;
const float r11 = pos->localTransform[1][1] * invS1;
const float r20 = pos->localTransform[2][0] * invS2;
const float r21 = pos->localTransform[2][1] * invS2;
const float r22 = pos->localTransform[2][2] * invS2;
const float_t r00 = pos->localTransform[0][0] * invS0;
const float_t r01 = pos->localTransform[0][1] * invS0;
const float_t r02 = pos->localTransform[0][2] * invS0;
const float_t r10 = pos->localTransform[1][0] * invS1;
const float_t r11 = pos->localTransform[1][1] * invS1;
const float_t r20 = pos->localTransform[2][0] * invS2;
const float_t r21 = pos->localTransform[2][1] * invS2;
const float_t r22 = pos->localTransform[2][2] * invS2;
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
const float sinBeta = glm_clamp(r20, -1.0f, 1.0f);
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
pos->rotation[1] = asinf(sinBeta);
const float cosBeta = cosf(pos->rotation[1]);
const float_t cosBeta = cosf(pos->rotation[1]);
if(fabsf(cosBeta) > 1e-6f) {
pos->rotation[0] = atan2f(-r21, r22);
@@ -32,21 +32,21 @@
/**
* worldTransform is stale. Either the local matrix changed or an ancestor
* moved; the full parent-chain multiply must be rerun before world data is read.
* moved; worldTransform must be recomputed before world data can be read.
*/
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 3)
typedef struct {
/*
* Hot fields flag checks and parent/child traversal (markDirty, ensureWorld)
* Hot fields - flag checks, parent/child traversal (markDirty, ensureWorld)
* only touch these. Kept at the front so they share the first cache line.
*/
/** Bitmask of ENTITY_POSITION_FLAG_* values describing which caches are stale. */
/** ENTITY_POSITION_FLAG_* bitmask; describes which caches are stale. */
uint8_t flags;
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */
entityid_t parentEntityId;
/** Component ID of the parent position component, or COMPONENT_ID_INVALID if none. */
/** Component ID of the parent position, or COMPONENT_ID_INVALID if none. */
componentid_t parentComponentId;
/** Number of currently registered children. */
uint8_t childCount;
@@ -56,19 +56,19 @@ typedef struct {
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
/*
* Warm fields read/written by PRS getters/setters.
* Warm fields - read/written by PRS getters/setters.
* Accessed more often than the matrices but less often than flags.
*/
/** Cached local position (XYZ). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
/** Cached local position (XYZ). Stale when PRS_DIRTY is set. */
vec3 position;
/** Cached local euler rotation (XYZ, radians). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
/** Cached local rotation (XYZ euler, radians). Stale when PRS_DIRTY. */
vec3 rotation;
/** Cached local scale (XYZ). May be stale when ENTITY_POSITION_FLAG_PRS_DIRTY is set. */
/** Cached local scale (XYZ). Stale when PRS_DIRTY is set. */
vec3 scale;
/*
* Cold fields only touched when actually rebuilding transforms.
* Cold fields - only touched when actually rebuilding transforms.
*/
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
@@ -338,7 +338,7 @@ entityposition_t *entityPositionGet(
* Signals that the PRS cache was modified externally. Sets both
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
* so all of localTransform is rebuilt lazily on the next read, clears
* ENTITY_POSITION_FLAG_PRS_DIRTY, and propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
* ENTITY_POSITION_FLAG_PRS_DIRTY, propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
* to self and all descendants.
*
* @param pos The position component whose PRS was modified.
@@ -36,7 +36,6 @@ void entityRenderableDispose(
const entityid_t entityId,
const componentid_t componentId
) {
}
void entityRenderableSetType(
@@ -80,6 +79,49 @@ void entityRenderableSetDraw(
r->data.custom.drawUser = user;
}
static errorret_t entityRenderableDrawSpritebatch(
const entityrenderablespritebatch_t *sb
) {
if(sb->spriteCount == 0) errorOk();
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
spriteBatchClear();
shadermaterial_t mat;
memoryZero(&mat, sizeof(shadermaterial_t));
mat.unlit.texture = sb->texture;
mat.unlit.color = COLOR_WHITE;
errorChain(spriteBatchBuffer(
sb->sprites, sb->spriteCount,
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
));
return spriteBatchFlush();
}
static errorret_t entityRenderableDrawMaterial(
const entityrenderablematerial_t *m
) {
errorChain(displaySetState(m->state));
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
assertNotNull(shader, "Shader cannot be null for material type");
errorChain(shaderBind(shader));
errorChain(shaderSetMaterial(shader, &m->material));
for(uint8_t i = 0; i < m->meshCount; i++) {
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
}
errorOk();
}
static errorret_t entityRenderableDrawCustom(
const entityid_t entityId,
const componentid_t componentId,
const entityrenderablecustom_t *custom
) {
return custom->draw(entityId, componentId, custom->drawUser);
}
errorret_t entityRenderableDraw(
const entityid_t entityId,
const componentid_t componentId
@@ -87,41 +129,13 @@ errorret_t entityRenderableDraw(
entityrenderable_t *r = componentGetData(
entityId, componentId, COMPONENT_TYPE_RENDERABLE
);
switch(r->type) {
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: {
const entityrenderablespritebatch_t *sb = &r->data.spritebatch;
errorChain(displaySetState((displaystate_t){
.flags = DISPLAY_STATE_FLAG_BLEND
}));
spriteBatchClear();
shadermaterial_t mat;
memoryZero(&mat, sizeof(shadermaterial_t));
mat.unlit.texture = sb->texture;
mat.unlit.color = COLOR_WHITE;
errorChain(spriteBatchBuffer(
sb->sprites, sb->spriteCount,
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
));
return spriteBatchFlush();
}
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: {
const entityrenderablematerial_t *m = &r->data.material;
errorChain(displaySetState(m->state));
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
assertNotNull(shader, "Shader cannot be null for material type");
errorChain(shaderBind(shader));
errorChain(shaderSetMaterial(shader, &m->material));
for(uint8_t i = 0; i < m->meshCount; i++) {
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
}
errorOk();
}
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
return entityRenderableDrawSpritebatch(&r->data.spritebatch);
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
return entityRenderableDrawMaterial(&r->data.material);
case ENTITY_RENDERABLE_TYPE_CUSTOM:
return r->data.custom.draw(entityId, componentId, r->data.custom.drawUser);
return entityRenderableDrawCustom(entityId, componentId, &r->data.custom);
default:
assertUnreachable("Invalid renderable type");
}
@@ -1,49 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityinteractable.h"
#include "entity/entitymanager.h"
void entityInteractableInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityinteractable_t *inter = entityInteractableGet(entityId, componentId);
inter->onInteract = NULL;
inter->user = NULL;
}
entityinteractable_t * entityInteractableGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(entityId, componentId, COMPONENT_TYPE_INTERACTABLE);
}
void entityInteractableSetCallback(
const entityid_t entityId,
const componentid_t componentId,
void (*onInteract)(
const entityid_t entityId,
const componentid_t componentId,
void *user
),
void *user
) {
entityinteractable_t *inter = entityInteractableGet(entityId, componentId);
inter->onInteract = onInteract;
inter->user = user;
}
void entityInteractableTrigger(
const entityid_t entityId,
const componentid_t componentId
) {
entityinteractable_t *inter = entityInteractableGet(entityId, componentId);
if(inter->onInteract == NULL) return;
inter->onInteract(entityId, componentId, inter->user);
}
@@ -1,71 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "entity/entitybase.h"
typedef struct {
void (*onInteract)(
const entityid_t entityId,
const componentid_t componentId,
void *user
);
void *user;
} entityinteractable_t;
/**
* Initializes the interactable component, clearing the callback and user pointer.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityInteractableInit(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Returns a pointer to the interactable component data.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Pointer to the entityinteractable_t data.
*/
entityinteractable_t * entityInteractableGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the callback invoked when this interactable is triggered.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @param onInteract Function called on interaction, or NULL to clear.
* @param user Arbitrary pointer forwarded to the callback.
*/
void entityInteractableSetCallback(
const entityid_t entityId,
const componentid_t componentId,
void (*onInteract)(
const entityid_t entityId,
const componentid_t componentId,
void *user
),
void *user
);
/**
* Fires the interactable's callback if one is set.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityInteractableTrigger(
const entityid_t entityId,
const componentid_t componentId
);
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "entityoverworld.h"
#include "entity/entitymanager.h"
#include "entity/component/display/entityrenderable.h"
#include "display/shader/shaderunlit.h"
#include "display/mesh/cube.h"
void entityOverworldInit(
const entityid_t entityId,
const componentid_t componentId
) {
entityoverworld_t *ow = entityOverworldGet(entityId, componentId);
ow->type = OVERWORLD_ENTITY_TYPE_NPC;
ow->facing = FACING_DIR_DOWN;
ow->renderCompId = entityGetComponent(entityId, COMPONENT_TYPE_RENDERABLE);
if(ow->renderCompId != COMPONENT_ID_INVALID) {
entityRenderableSetDraw(entityId, ow->renderCompId, entityOverworldDraw, NULL);
}
ow->physCompId = entityGetComponent(entityId, COMPONENT_TYPE_PHYSICS);
}
entityoverworld_t * entityOverworldGet(
const entityid_t entityId,
const componentid_t componentId
) {
return componentGetData(entityId, componentId, COMPONENT_TYPE_OVERWORLD);
}
void entityOverworldSetType(
const entityid_t entityId,
const componentid_t componentId,
const entityoverworldtype_t type
) {
entityOverworldGet(entityId, componentId)->type = type;
}
errorret_t entityOverworldDraw(
const entityid_t entityId,
const componentid_t componentId,
void *user
) {
componentid_t owCompId = entityGetComponent(entityId, COMPONENT_TYPE_OVERWORLD);
entityoverworld_t *ow = entityOverworldGet(entityId, owCompId);
color_t col = ow->type == OVERWORLD_ENTITY_TYPE_PLAYER ? COLOR_WHITE : COLOR_BLUE;
errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, col));
errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL));
return meshDraw(&CUBE_MESH_SIMPLE, 0, -1);
}
@@ -1,74 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "entity/entitybase.h"
#include "overworld/facingdir.h"
typedef enum {
OVERWORLD_ENTITY_TYPE_PLAYER = 0,
OVERWORLD_ENTITY_TYPE_NPC = 1,
} entityoverworldtype_t;
typedef struct {
entityoverworldtype_t type;
facingdir_t facing;
componentid_t renderCompId;
componentid_t physCompId;
} entityoverworld_t;
/**
* Initializes the overworld component, wiring up the draw callback if a
* renderable component is already present on the entity.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
*/
void entityOverworldInit(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Returns a pointer to the overworld component data.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @return Pointer to the entityoverworld_t data.
*/
entityoverworld_t * entityOverworldGet(
const entityid_t entityId,
const componentid_t componentId
);
/**
* Sets the overworld entity type.
*
* @param entityId The owning entity.
* @param componentId This component's ID.
* @param type The type to assign.
*/
void entityOverworldSetType(
const entityid_t entityId,
const componentid_t componentId,
const entityoverworldtype_t type
);
/**
* Draw callback registered on the renderable component.
*
* @param entityId The owning entity.
* @param componentId The renderable component's ID.
* @param user Unused.
* @return Error result.
*/
errorret_t entityOverworldDraw(
const entityid_t entityId,
const componentid_t componentId,
void *user
);

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