1 Commits

Author SHA1 Message Date
YourWishes 71c5756e71 Prog on knulli 2026-03-16 11:45:36 -05:00
610 changed files with 9114 additions and 28501 deletions
-74
View File
@@ -1,74 +0,0 @@
# Animation System
Source: `src/dusk/animation/`
Lightweight keyframe-based value interpolation using fixed-point math throughout.
---
## Easing (`animation/easing.h`)
`easingApply(type, t)` applies an easing function to a normalized time value `t ∈ [0, FIXED_ONE]` and returns the eased value in the same range.
All functions are also callable directly:
```c
fixed_t t = FIXED(0.5f);
fixed_t out = easingApply(EASING_IN_OUT_CUBIC, t);
```
Available easing types (all in `easingtype_t`):
| Enum value | Curve |
|---|---|
| `EASING_LINEAR` | straight line |
| `EASING_IN_SINE` / `OUT` / `IN_OUT` | sinusoidal |
| `EASING_IN_QUAD` / `OUT` / `IN_OUT` | quadratic |
| `EASING_IN_CUBIC` / `OUT` / `IN_OUT` | cubic |
| `EASING_IN_QUART` / `OUT` / `IN_OUT` | quartic |
| `EASING_IN_BACK` / `OUT` / `IN_OUT` | overshoots slightly |
`EASING_FUNCTIONS[EASING_COUNT]` is a table of `easingfn_t` function pointers for when you need to pick an easing at runtime without a switch.
---
## Keyframes (`animation/keyframe.h`)
```c
typedef struct {
fixed_t time; // time point this keyframe is at
fixed_t value; // output value at this time
easingtype_t easing; // easing to apply when interpolating toward the NEXT keyframe
} keyframe_t;
```
Keyframe arrays should be sorted ascending by `time`.
---
## Animation (`animation/animation.h`)
`animation_t` wraps a keyframe array and provides value lookup:
```c
keyframe_t frames[] = {
{ FIXED(0.0f), FIXED(0.0f), EASING_LINEAR },
{ FIXED(1.0f), FIXED(1.0f), EASING_IN_OUT_CUBIC },
{ FIXED(2.0f), FIXED(0.0f), EASING_LINEAR },
};
animation_t anim;
animationInit(&anim, frames, 3);
fixed_t value = animationGetValue(&anim, FIXED(0.75f)); // interpolated
```
`animationGetValue` finds the surrounding keyframes for the given `time`, computes the local `t` within that segment, applies the keyframe's easing, and linearly interpolates between the two keyframe values.
The animation does not own the keyframe array — it holds a pointer. Pass a static or long-lived array.
---
## Usage in the engine
Entity animations (`entityanim_t`) do NOT use this system — they use a simple countdown timer (`animTime`) and a state enum. The `animation_t` system is intended for property animation: UI transitions, camera easing, visual effects, anything that needs a time → value curve.
-81
View File
@@ -1,81 +0,0 @@
# Architecture
## Platform abstraction
Every subsystem that differs across platforms (display, input, asset loading, save, time, network, log) follows the same pattern:
1. `src/dusk/<subsystem>/<subsystem>platform.h` — included by the public header. Contains `#include "path/to/platform-specific-header.h"` resolved by the build system include path.
2. `src/dusk{platform}/<subsystem>/<subsystem>platform.h` — the actual platform-specific header included above (e.g. `src/duskgl/display/framebuffer/framebufferplatform.h`).
3. The shared header (`src/dusk/<subsystem>/<subsystem>.h`) `#error`s at compile time if the platform doesn't define the expected macros/types.
The active platform backends are selected by `DUSK_TARGET_SYSTEM` in CMake, which includes `cmake/targets/<system>.cmake`. That file sets compile definitions (`DUSK_LINUX`, `DUSK_SDL2`, `DUSK_OPENGL`, …) and links platform libraries.
Platform source directories:
- `src/duskgl/` — OpenGL rendering (used on Linux and as the GL layer for SDL2)
- `src/dusksdl2/` — SDL2 window/input/time (Linux desktop)
- `src/dusklinux/` — Linux filesystem/save/network
- `src/duskdolphin/` — GameCube & Wii (GX renderer, libogc)
- `src/duskpsp/` — PSP (GU renderer, PSPSDK)
- `src/duskvita/` — PS Vita
## Subsystem lifecycle
All subsystems follow `init → update (per frame) → dispose`. Engine initialization order matters and is centralized in `engine.c`:
```
systemInit → timeInit → consoleInit → inputInit → assetInit →
localeManagerInit → displayInit → uiInit → uiTextboxInit →
cutsceneInit → rpgInit → networkInit → sceneInit
```
Dispose runs in reverse. Each call uses `errorChain()` to propagate failures.
## Error handling
Functions that can fail return `errorret_t` (a code + pointer to thread-local error state). Three core macros:
```c
errorThrow("message %s", arg); // sets error, returns from current function
errorChain(someCall()); // if someCall() fails, propagates and returns
errorOk(); // returns success
```
Check with `errorIsOk(ret)` / `errorIsNotOk(ret)`. The error state carries file/function/line info for a stack-like trace.
## Fixed-point math
`fixed_t` is `int32_t` with Q24.8 format (8 fractional bits, ~0.004 resolution). Use it for all world/game values:
```c
fixed_t x = FIXED(1.5); // compile-time literal
fixed_t y = fixedFromI32(3); // runtime conversion
fixed_t z = fixedMul(x, y); // arithmetic
float_t f = fixedToFloat(z); // only where float is needed (e.g. GL uniforms)
```
## Code generation from CSV
Several subsystems define their data in CSV files and have corresponding Python tools that generate C headers at build time (via CMake `add_custom_command`):
| CSV | Tool | Output |
|-----|------|--------|
| `src/dusk/input/input.csv` | `tools/input/csv/` | input action enum + names |
| `src/dusk/display/color.csv` | `tools/color/csv/` | color constants |
| `src/dusk/rpg/item/item.csv` | `tools/item/csv/` | item enum + metadata |
| `src/dusk/rpg/story/storyflag.csv` | `tools/story/csv/` | story flag enum + initial values |
Generated headers are written to `build-<target>/generated/` and included via `target_include_directories`.
## Asset system
Assets are packed into `dusk.dsk` (a zip archive) at build time from the `assets/` directory. At runtime `asset.c` opens the archive and serves files from it.
Loading is asynchronous: `assetLock()` registers a load request; the background thread calls the appropriate loader; call `assetRequireLoaded()` to block until ready. `assetUnlock()` / `assetUnlockEntry()` releases the entry so it can be reclaimed.
Loaders are registered per type (`assetloadertype_t`) and live under `src/dusk/asset/loader/`. Platform-specific asset init (finding the .dsk file) is in `src/dusk{platform}/asset/`.
## Display subsystem
The display system is currently organized around immediate GPU-style rendering: `mesh_t` (vertex buffers), `shader_t` (GLSL on GL / TEV state on Dolphin), `texture_t`, and `framebuffer_t`. See [display-refactor.md](display-refactor.md) for the planned move to a render-queue model (needed for a future Saturn port).
The `spritebatch_t` (`display/spritebatch/`) accumulates 2D quads and flushes in batches — the primary 2D drawing primitive used by the RPG layer.
-115
View File
@@ -1,115 +0,0 @@
# Asset System
Source: `src/dusk/asset/`
All game assets are packed into `dusk.dsk` (a zip archive) at build time and served from it at runtime. The asset system manages async loading, reference counting, and platform-specific archive location.
See [architecture.md](architecture.md#asset-system) for the high-level overview.
---
## Asset archive
The archive is opened at `assetInit()`. `assetFileExists(filename)` checks for a file without loading it. The file path format inside the archive matches the layout of the `assets/` source directory.
On each platform, `assetInitPlatform()` locates the `.dsk` file (e.g. adjacent to the binary on Linux, on the SD card on PSP).
---
## Entry lifecycle
An `assetentry_t` represents one file being managed by the system. States:
```
NOT_STARTED → PENDING_ASYNC → LOADING_ASYNC → PENDING_SYNC → LOADING_SYNC → LOADED
└→ ERROR
```
- **PENDING_ASYNC / LOADING_ASYNC**: the background thread is handling I/O (file reads, decompression).
- **PENDING_SYNC / LOADING_SYNC**: the main thread needs to finish loading (e.g. uploading to GPU), triggered during `assetUpdate()`.
The async/sync split exists because GPU operations must happen on the main thread.
---
## Using assets
```c
// Acquire a loaded entry (blocks until loaded):
assetentry_t *entry = assetLock(filename, ASSET_LOADER_TYPE_TEXTURE, &input);
errorChain(assetRequireLoaded(entry));
// Use the loaded data:
texture_t *tex = &entry->data.texture.texture;
// Release when done:
assetUnlockEntry(entry);
```
`assetLock` finds-or-creates an entry and increments its reference count. `assetUnlock` / `assetUnlockEntry` decrements it; when it reaches zero the entry is reclaimed at the next `assetUpdate()`.
To subscribe to async completion instead of blocking:
```c
eventSubscribe(&entry->onLoaded, myCallback, myUser);
```
---
## Loader types
| Type constant | File | Output struct accessed via |
|---|---|---|
| `ASSET_LOADER_TYPE_TEXTURE` | `.png` etc. | `entry->data.texture.texture` |
| `ASSET_LOADER_TYPE_TILESET` | tileset descriptor | `entry->data.tileset.tileset` |
| `ASSET_LOADER_TYPE_MESH` | mesh data | `entry->data.mesh.mesh` |
| `ASSET_LOADER_TYPE_LOCALE` | `.po` file | internal to locale manager |
| `ASSET_LOADER_TYPE_JSON` | `.json` | `entry->data.json.*` |
Each loader type registers `loadAsync`, `loadSync`, and `dispose` callbacks in `ASSET_LOADER_CALLBACKS[]`.
The async callback runs on the loader thread; the sync callback runs on the main thread during `assetUpdate()`. Most loaders do file I/O async and GPU upload sync.
### Error handling inside loaders
Use these macros instead of `errorThrow` / `errorChain` inside loader callbacks — they also set the entry state to ERROR:
```c
assetLoaderErrorChain(loading, someCall());
assetLoaderErrorThrow(loading, "Descriptive message");
```
---
## Low-level file I/O (`asset/assetfile.h`)
`assetfile_t` wraps a `zip_file_t` handle and provides streaming reads:
```c
assetFileInit(&file, "textures/player.png", NULL, NULL);
assetFileOpen(&file);
assetFileRead(&file, buffer, size);
assetFileClose(&file);
assetFileDispose(&file);
// Read entire file into a malloc'd buffer:
uint8_t *buf; size_t size;
assetFileReadEntire(&file, &buf, &size); // caller frees buf
```
For line-by-line text parsing (`assetfilelinereader_t`):
```c
assetFileLineReaderInit(&reader, &file, readBuf, readBufSize, outBuf, outBufSize);
while(!reader.eof) {
errorChain(assetFileLineReaderNext(&reader));
// reader.outBuffer contains the line, reader.lineLength its length
}
```
---
## Background loader thread
`assetUpdateAsync(thread)` is the thread entry point. It calls `assetUpdate()` in a loop, sleeping briefly between iterations, until `threadShouldStop()` returns true. The main thread also calls `assetUpdate()` once per frame to process the sync phase.
Up to `ASSET_LOADING_COUNT_MAX` (4) entries can be loading concurrently.
Up to `ASSET_ENTRY_COUNT_MAX` (128) entries can exist at once.
-714
View File
@@ -1,714 +0,0 @@
# Display Layer Refactor
## Vision
The goal is to remove the implicit assumption that all platforms render
through a GL-like API, and replace it with a system where each platform
owns its rendering stack completely. The scene describes *what* to draw
in platform-neutral terms; the platform decides *how* to draw it.
This unlocks:
- Saturn (VDP1/VDP2 command-list, no Z-buffer, affine-only)
- PlayStation 1 (ordering table, affine textures, GTE fixed-point, CMake SDK)
- Nintendo 64 (RSP display list, hardware Z-buffer, perspective-correct,
real FPU -- closer to modern GL than to Saturn)
- SNES (PPU tile engine, Mode7 for overworld, no real 3D)
- Vulkan (explicit, modern, no legacy GL baggage)
- Native PSP GU (drop PSPGL which is just a compatibility shim)
- Legacy fixed-function GL as its own standalone target
- A real first-class 2D UI system not bolted onto 3D space
---
## Why
### The current abstraction assumes GPU-style rendering
The current display layer was designed around a GL-like mental model:
vertex buffers, shaders, Z-buffered triangle rasterization, and texture
objects. `duskgl` implements this with real OpenGL. `duskdolphin` does its
own GX thing but still matches the same interface (mesh, shader, texture,
framebuffer). PSP uses PSPGL -- a library that *emulates* GL on top of
the PSP's native GE/GU hardware, which is entirely different underneath.
Problems this creates:
**PSPGL is a lie.** The PSP has a native graphics engine (GE/GU) with its
own command list, its own vertex formats, and its own display list model.
PSPGL translates GL calls into GU calls, but imperfectly -- and we end up
paying the abstraction cost without getting GL correctness. Writing directly
to GU gives better performance, access to native formats, and correct
behavior on edge cases that PSPGL gets wrong.
**Legacy GL should not share code with modern GL.** The fixed-function
pipeline (no shaders, matrix stacks via glMatrixMode, glTexEnv) is
meaningfully different from modern GL (VAO/VBO, GLSL, explicit uniform
locations). Treating them as "the same thing with a flag" creates a tangle
of `#ifdef DUSK_OPENGL_LEGACY` guards throughout the rendering code.
They are separate targets and should be separate platform directories.
**Saturn cannot fit the model at all.** VDP1 is a command-list processor:
you write 32-byte command structs (sprites, quads, lines) into VRAM, then
poke a register to trigger execution. There are no vertex buffers, no
shaders, no Z-buffer. Depth is pure painter's algorithm -- command order
IS the depth. VDP2 composites up to 6 background planes at scanline time;
these are tile maps and rotation parameter tables, not meshes. Nothing
about the current API maps onto this hardware.
**SNES is even further removed.** The PPU renders tiles. VRAM holds 8x8
or 16x16 pixel tiles and tile maps; the PPU references these during
scanline rendering. There are no draw calls. Mode7 is an affine transform
applied to a single background layer (the basis for the overworld map and
road perspective effects). Sprites are entries in OAM (Object Attribute
Memory). The 65816 CPU writes to memory-mapped registers and VRAM; the
PPU does the rest. The concept of "mesh" or "shader" is meaningless here.
**Textures loaded as RGBA waste memory and exclude platforms.** Loading
every texture as 32-bit RGBA and converting at runtime is expensive on
memory-constrained platforms (Saturn has ~1 MB total RAM; SNES has 64 KB
VRAM) and simply wrong for platforms that have native formats incompatible
with RGBA (e.g., PSP's ABGR8888 / BGR5650, Saturn's RGB555 / CI4 / CI8,
SNES's 2bpp/4bpp/8bpp indexed). The asset pipeline must compile textures
to platform-native formats at build time.
**UI in 3D space is wasteful and limiting.** Currently UI elements are
rendered as geometry projected into screen space, going through the full
3D pipeline. On platforms with dedicated 2D hardware (Saturn VDP2,
SNES BG layers), this is actively wrong -- UI should map to a hardware
plane, not a 3D draw call. On modern platforms it should be a clean
screen-space pass that never touches the 3D depth buffer.
---
## Current Model (Summary)
```
Scene
-> shaderBind(shader)
-> textureBind(texture)
-> meshDraw(mesh) <-- immediate draw call per object
-> meshDraw(mesh)
-> ...
Platform receives each draw call immediately.
Depth is handled by Z-buffer hardware.
All textures live in GPU memory as RGBA (or Dolphin's tiled RGBA).
UI is rendered as 3D geometry with an orthographic projection.
```
Key current concepts:
- `mesh_t` -- vertex array (triangles/quads), in GPU VBO (GL) or CPU
memory (Dolphin)
- `shader_t` -- GLSL program (modern GL), GL fixed-function state
(legacy GL), or GX matrix + TEV config (Dolphin)
- `texture_t` -- GPU texture handle (GL) or tiled CPU buffer (Dolphin);
always RGBA at the engine level
- `framebuffer_t` -- FBO (GL) or fixed hardware XFB (Dolphin)
- `spritebatch_t` -- accumulates 2D quads and flushes in batches of 32;
the only existing deferred-submission system in the engine
The spritebatch hints at the right model. Everything needs to work this way.
---
## The Core Shift: Platform-Native Rendering
### Before
```
src/dusk/ Core engine + GL-like rendering API definition
src/duskgl/ OpenGL implementation
src/dusksdl2/ SDL2 window/input (shared)
src/duskpsp/ PSP via PSPGL (shim over GU)
src/duskvita/ Vita via GL ES (similar path to duskgl)
src/duskdolphin/ GameCube/Wii via GX (already custom)
src/dusklinux/ Linux (uses dusksdl2 + duskgl)
```
### After
```
src/dusk/ Core engine logic + render intent API ONLY
src/dusksdl2/ SDL2 window/input (unchanged)
src/duskgl/ Modern OpenGL (Linux, Vita modern path)
src/duskgllegacy/ Fixed-function OpenGL (older hardware, PSP with PSPGL
as a last resort)
src/duskvulkan/ Vulkan (Linux modern, future)
src/duskpsp/ PSP native GU (no PSPGL, direct command lists)
src/duskvita/ Vita native GXM (TBD)
src/duskdolphin/ GameCube/Wii GX (already custom, mostly kept)
src/dusksaturn/ Saturn VDP1/VDP2 (new)
src/duskps1/ PlayStation 1 ordering table + GTE (new)
src/duskn64/ Nintendo 64 RSP/RDP display list (new)
src/dusksnes/ SNES PPU/Mode7 (new, extremely constrained)
```
`src/dusk/` no longer knows about meshes, shaders, or framebuffers.
It defines the *render intent* system: what the scene wants to draw.
Each platform directory is entirely self-contained and responsible for
translating intents to its native API.
---
## Render Intent System (new)
Instead of the scene calling `meshDraw()` or `shaderBind()`, it submits
render intents into a `renderqueue_t`. An intent describes what should
appear on screen without prescribing how to draw it.
### Primitive intents (3D world)
```
RENDER_INTENT_QUAD -- textured quad, 4 vertices or transform + size
RENDER_INTENT_POLYGON -- filled polygon (convex, up to N vertices)
RENDER_INTENT_LINE -- line segment or polyline
RENDER_INTENT_SPRITE -- 2D billboard (always faces camera)
RENDER_INTENT_MESH -- arbitrary vertex array (GL/GX only; degraded
on command-list platforms)
```
Each intent carries: texture reference, color/tint, depth hint (for
painter's algorithm sorting), blend mode, and cull flags.
### Background plane intents (2D layers)
```
RENDER_INTENT_BGPLANE -- configure a background/tilemap layer
```
Carries: layer index, tile map data reference, scroll offset, palette,
and transform (for Mode7-style affine).
### UI intents (screen space)
```
RENDER_INTENT_UI_RECT -- solid colored rectangle
RENDER_INTENT_UI_SPRITE -- textured rectangle (UI image)
RENDER_INTENT_UI_TEXT -- text string at screen position
```
UI intents are always screen-space. They are never mixed into the 3D
world queue. See UI System section below.
### Platform translation
| Intent | Modern GL | PSP GU | Saturn VDP1 | PS1 OT | N64 RSP | SNES PPU |
|---|---|---|---|---|---|---|
| QUAD | VAO + glDraw | GU display list | distorted-sprite cmd | GPU quad packet | RSP display list | OAM + BG tile |
| POLYGON | VAO + glDraw | GU display list | polygon cmd | GPU poly packet | RSP display list | OAM |
| BGPLANE | fullscreen quad | fullscreen quad | VDP2 config | fullscreen quad | fullscreen quad | BG layer config |
| UI_SPRITE | 2D ortho quad | 2D GU quad | VDP2 BG plane | GPU rect packet | RDP rectangle | BG layer tile |
| MESH | VAO/VBO | GU buffers | (degrade: quads) | (degrade: tris/quads) | RSP display list | (not supported) |
Note: N64 supports both triangles and axis-aligned rectangles natively via
RDP. PS1 supports triangles and quads (4-vertex) natively, so neither needs
the dead-vertex trick that Saturn requires.
---
## Asset Pipeline: Platform-Native Formats
### The problem
All textures currently enter the engine as RGBA and are converted at
runtime by each platform (Dolphin retiles to 4x4 blocks; GL uploads as-is).
This wastes memory and CPU time, and is impossible for platforms where RGBA
is not a valid intermediate format at all.
### The solution
The asset compiler (offline, run at build time) produces platform-specific
binary bundles. A texture asset has one source (PNG or similar) but N
compiled outputs, one per target.
### Texture formats by platform
| Platform | Native Formats | Notes |
|---|---|---|
| Modern GL | RGBA8, RGB8, BC1-BC7 (compressed) | Upload directly, GPU handles |
| Legacy GL | RGBA8, RGB8, CI8 (palette via extension) | No compressed formats |
| Vulkan | VkFormat variants (RGBA8, BC, ASTC) | Chosen at compile time |
| PSP GU | ABGR8888, BGR5650, ABGR1555, ABGR4444, CI4, CI8 | Native swizzled format |
| Saturn VDP1/VDP2 | RGB555, CI4, CI8 (15-bit palette in CRAM) | Big-endian, packed |
| PlayStation 1 | RGB555 / CI4 / CI8 (CLUT in VRAM) | Little-endian; VRAM flat; CLUT at coord |
| Nintendo 64 | RGBA16, RGBA32, IA4-IA16, I4-I8, CI4, CI8 | 4 KB TMEM; tiles must fit in TMEM banks |
| GameCube/Wii GX | I4, I8, IA4, IA8, RGB565, RGB5A3, RGBA8, CMPR | 4x4 tiled, big-endian |
| SNES PPU | 2bpp, 4bpp, 8bpp indexed (CGRAM palette) | Tile-packed, no direct access |
### Asset bundle structure
The `.dsk` bundle gains a platform tag. The loader picks the right section
at runtime (or the build produces a single-platform bundle for constrained
targets like SNES/Saturn where there is no spare storage for unused data).
---
## UI System (first-class)
### Current problem
UI elements go through the 3D pipeline: they are meshes with an orthographic
shader, rendered in the same pass as the world. This means:
- UI competes for Z-buffer depth with world geometry
- On Saturn/SNES, UI cannot use dedicated hardware planes
- Text rendering is tied to the sprite batch which is tied to the 3D pass
- No separation between "draw the world" and "draw the HUD"
### New model
UI is a completely separate rendering context. The world renders first,
then the UI renders on top. They share no state.
UI coordinates are always in screen space (pixels or a logical resolution
that the platform scales to its native display size). No camera matrix,
no projection, no depth buffer involvement.
### Platform mapping
| Platform | UI implementation |
|---|---|
| Modern GL | Separate 2D ortho pass, screen-space quads, no depth test |
| Legacy GL | Same, using fixed-function |
| PSP GU | Separate GU display list, 2D mode |
| Saturn | VDP2 background plane(s) dedicated to UI |
| PlayStation 1 | Separate GPU packet chain, no Z; ordered after world OT |
| Nintendo 64 | RDP rectangle commands in a separate display list segment |
| GameCube/Wii | GX 2D mode or dedicated GX pass |
| SNES | Dedicated BG layer(s) for HUD tiles |
On Saturn, the UI occupying VDP2 planes is a genuine hardware win -- the
PPU composites it for free at scanline time, costing zero VDP1 commands.
On SNES, the HUD must live in a BG layer because there is no alternative.
### UI API (proposed)
```c
uiBegin();
uiDrawRect(x, y, w, h, color);
uiDrawSprite(x, y, w, h, texture, uvMin, uvMax);
uiDrawText(x, y, font, string);
uiEnd(); // platform flushes UI to hardware
```
The `uiBegin`/`uiEnd` block collects intents; the platform submits them
at frame end in whatever way is appropriate.
---
## SNES / Mode7
SNES is the most constrained platform the engine will ever support and
needs its own section because it breaks assumptions that even Saturn keeps.
### Hardware
- **CPU**: 65816 @ ~3.58 MHz (16-bit, no FPU, no cache)
- **PPU**: Tile-based scanline renderer. VRAM holds tile graphics and
tile maps. BG layers reference tiles by index.
- **Mode7**: A single BG layer with a 2D affine matrix applied per
scanline. Used for overworld maps, road perspective (F-Zero), rotation
effects. The matrix is set via HDMA (scanline DMA) for per-scanline
variation, enabling horizon-perspective effects.
- **Sprites/OAM**: Up to 128 sprites (8x8, 16x16, 32x32, 64x64 pixels),
4bpp indexed, up to 8 per scanline.
- **Palette**: CGRAM holds 256 entries of 15-bit RGB (512 bytes total).
BG layers use sub-palettes of 4/16/256 colors depending on bit depth.
- **VRAM**: 64 KB (tiles + tile maps)
- **WRAM**: 128 KB work RAM + usually 8 KB SRAM on cart for saves
- **No frame buffer.** The PPU renders scanlines directly. You cannot
read back what was drawn.
- **No general-purpose draw calls.** You configure registers and VRAM
before the frame and the PPU does the rest.
### What "3D" means on SNES
True 3D is not possible. What can be approximated:
- **Overworld map**: Mode7 with a flat texture and HDMA scroll gives a
top-down perspective with a horizon line (the classic JRPG overworld).
- **Depth illusion**: Mode7 matrix manipulation can simulate a moving
camera over flat terrain. Objects are sprites placed at screen positions
calculated by software perspective projection.
- **Sprite scaling**: Software-scaled sprites using pre-rendered frames
or the RSP-style tricks used in Super FX games (Star Fox). Super FX
is a co-processor on the cartridge -- base SNES cannot do this.
- **Basic 3D effects**: Some games use HDMA color gradient + Mode7 floor
with overlaid sprites to create a pseudo-3D look.
The engine plan for SNES: Mode7 overworld (confirmed), sprite-based world
objects, BG layer UI. "Basic 3D effects" (pseudo-perspective with sprites)
is aspirational -- implementation complexity TBD.
### SNES constraints on the engine
- **No dynamic allocation.** With 128 KB WRAM, a general-purpose allocator
is risky. The engine memory system may need a static pool mode for SNES.
- **No floating point.** `float_t` must resolve to integer or fixed-point.
- **No scripting (JerryScript).** The JS engine requires far more than
128 KB RAM. SNES scenes must be compiled C.
- **Asset data in ROM, not a .dsk bundle.** SNES loads from cartridge ROM
mapped into the address space. The asset system needs a ROM-mapped loader.
- **Tile pipeline.** Textures must be pre-converted to SNES tile format
(2bpp/4bpp/8bpp, 8x8 pixel tiles, CGRAM palette) at build time. This
is a completely different asset output from every other platform.
---
## Platform Inventory
A summary of what each platform's native rendering looks like after the
refactor, for reference when designing the intent API.
### Modern OpenGL (duskgl)
VAO + VBO mesh storage, GLSL shaders, FBO render targets, Z-buffer.
No fixed-function. Targets: Linux, possibly Vita (GXM is preferred).
### Legacy OpenGL (duskgllegacy)
Fixed-function pipeline: `glMatrixMode`, `glTexEnv`, client-side vertex
arrays. No VAO/VBO. Used for: very old desktop hardware, maybe PSP as
last resort (PSPGL is this). Targets: legacy desktop, embedded Linux.
### Vulkan (duskvulkan)
Explicit pipeline state objects, render passes, descriptor sets, command
buffers. Highest ceiling for performance and control. Targets: Linux
(modern), future platforms. Not immediate priority but the architecture
should not block it.
### PSP native GU (duskpsp)
The GE/GU is a display-list GPU. You build a command list in memory and
the GU DMA engine processes it asynchronously. Native vertex formats are
PSP-specific (ABGR byte order, swizzled textures for cache efficiency).
No PSPGL. Targets: PSP hardware and emulators.
### Vita (duskvita)
GXM is Sony's Vita GPU API -- closer to modern GL than GU, with explicit
shader binaries (.gxp), ring buffers, and GPU sync primitives.
### GameCube/Wii GX (duskdolphin)
Already a custom renderer. GX uses immediate-mode vertex submission
(`GX_Begin` / `GX_Position1x16` loops), TEV for texture compositing, and
hardware XFB double-buffering. Big-endian. Mostly kept as-is; may benefit
from being expressed in terms of render intents for consistency.
### Saturn VDP1/VDP2 (dusksaturn)
VDP1: command-list (32-byte structs), quad-based, affine texture mapping,
no Z-buffer (painter's algorithm). VDP2: up to 6 background planes
composited at scanline time. Big-endian dual SH-2, no FPU. Fixed-point
math required throughout.
### PlayStation 1 (duskps1)
MIPS R3000A @ 33.87 MHz, little-endian, no FPU. GTE (coprocessor 2)
handles fixed-point matrix math, perspective divide, and lighting.
GPU receives packets via DMA linked-list (the Ordering Table). Primitives:
triangles and quads natively (no dead-vertex needed). Texture mapping:
affine, same limitation as Saturn. No Z-buffer; depth is OT slot order.
VRAM is 1 MB flat (frame buffers + textures + CLUTs share it). SDK:
PSn00bSDK, which is CMake-native -- a direct fit for the dusk build system.
### Nintendo 64 (duskn64)
VR4300 @ 93.75 MHz, big-endian, real IEEE 754 FPU. Rendering is split
between the RSP (geometry: programmable MIPS SIMD, runs microcode up to
~1000 instructions in 4 KB IMEM) and the RDP (rasterization: fixed
hardware). RSP produces triangle commands from a CPU-built display list
in RDRAM. RDP features: perspective-correct texture mapping, bilinear
filtering, hardware Z-buffer. Primitives: triangles and axis-aligned rects.
TMEM is 4 KB on-chip texture cache; textures must be loaded into tiles
before drawing -- a significant memory management constraint.
SDK: libdragon (Unlicense, GCC 14, Makefile-based -- not CMake; this
requires a wrapper toolchain file for dusk's build system).
### SNES PPU/Mode7 (dusksnes)
Tile-based. VRAM holds tiles and tile maps. Mode7 provides affine transform
for one BG layer. Sprites via OAM. No frame buffer. All configuration is
memory-mapped registers. 65816 CPU, no FPU, extremely limited RAM.
---
## Threading Model
### Current model
The engine uses OS threads for async asset loading (`assetXxxLoaderAsync`).
Platforms that have pthreads or an equivalent RTOS (Linux, PSP, Vita) run
worker threads that load data in the background while the game loop runs.
The main thread polls or blocks on completion.
### The problem
Several target platforms have no OS threading whatsoever, and others have
hardware-specific async mechanisms that are nothing like pthreads.
### Per-platform reality
| Platform | Threading | Async mechanism |
|---|---|---|
| Linux | pthreads | Worker threads (current) |
| Vita | SceKernelThread | Per-SDK threads |
| PSP | SceKernelThread | Per-SDK threads |
| GameCube/Wii | libogc LWP | Lightweight processes |
| Saturn | None (OS) | Slave SH-2 for fixed jobs; CD-ROM via interrupt/callback |
| PlayStation 1 | None (OS) | V-blank ISR, 7 DMA channels, CD-ROM callbacks |
| Nintendo 64 | libdragon preview only | PI DMA for cartridge; RSP for parallel compute |
| SNES | None | DMA (GPDMA/HDMA); NMI V-blank; SPC700 audio is a separate CPU |
**Saturn slave SH-2**: The second SH-2 is not a general-purpose thread.
It runs a fixed subroutine you hand-load. The typical use is offloading
heavy per-frame computation (geometry transforms, depth sort) while the
master SH-2 handles game logic. Communication is via shared WRAM with
cache-through addresses to avoid coherency bugs. There is no scheduler
and no yield -- it runs to completion.
**SNES DMA**: GPDMA copies blocks of data (ROM to WRAM, WRAM to VRAM)
and halts the CPU for the duration -- it is synchronous from the game's
perspective. HDMA runs per-scanline during H-blank, writing to PPU
registers without CPU involvement; this is how Mode7 perspective is
achieved. Neither is "async" in the programming sense.
**SNES NMI**: The V-blank NMI fires at the start of every V-blank period.
This is the only safe window to write to VRAM and PPU registers. All
critical PPU updates must complete within ~1.2ms (the V-blank window).
### Proposed model
Introduce a compile-time threading capability flag:
```
DUSK_THREAD_PTHREAD -- Linux, maybe Vita
DUSK_THREAD_SCEKERNEL -- PSP, Vita SDK
DUSK_THREAD_LWP -- GameCube/Wii libogc
DUSK_THREAD_SLAVE_SH2 -- Saturn slave CPU (job dispatch only)
DUSK_THREAD_NONE -- SNES (and Saturn master thread view)
```
The asset loader's async path is gated on having a threading capability.
When `DUSK_THREAD_NONE` is defined, `assetXxxLoaderAsync` either does not
exist or is an alias for the synchronous version. On Saturn, the slave SH-2
is exposed as a distinct API (`sh2JobDispatch`, `sh2JobWait`) used only for
compute-heavy work, not for I/O.
### Asset loading without threads
**Saturn**: CD-ROM access is initiated via SBL/CDC routines and completes
via interrupt callback. The engine's asset loading loop can poll the
callback flag in the main loop rather than blocking a thread. This is
interrupt-driven cooperative async, not preemptive.
**SNES**: There is no loading. Assets live in ROM, mapped directly into the
65816 address space. "Loading a texture" means computing a pointer into ROM
and copying the tile data to VRAM during V-blank via GPDMA. The asset system
on SNES is essentially a VRAM/CGRAM allocator and a DMA scheduler, not a
file loader.
### Asset system changes
The asset pipeline needs to accommodate three loading models:
1. **File-based** (Linux, PSP, Vita, Saturn CD): open file, read bytes,
close. Can be sync or thread-async.
2. **DMA/interrupt** (Saturn CD-ROM, GC DVD): initiate transfer, poll or
callback on completion, no thread blocked.
3. **ROM-mapped** (SNES): data is already in the address space; "loading"
is a VRAM DMA copy scheduled for V-blank, not file I/O.
The `assetstream_t` abstraction that currently wraps file I/O needs a third
backend for ROM-mapped data, and the async path needs to support
callback-based completion as an alternative to thread-based blocking.
---
## What Needs to Change
### 1. Render intent API (new, in src/dusk/)
Replace `mesh_t` / `shader_t` / `meshDraw()` as scene-facing APIs with
`renderqueue_t` and intent submission functions. `src/dusk/` defines the
intent types and submission API; platforms implement the flush.
### 2. Platform renderer directories
Move rendering implementations out of `duskgl/` as a shared layer and
into fully self-contained platform directories. `duskgl/` becomes the
*modern GL* platform only. Add `duskgllegacy/`, `duskvulkan/` as peers.
### 3. Asset pipeline: platform-native texture formats
The offline asset compiler must produce per-platform texture bundles in
native formats. The runtime texture loader expects pre-converted data,
not RGBA. `textureformat_t` grows to cover all platform formats but each
platform only ever sees the formats it natively supports.
### 4. UI system (first-class, separate from 3D)
New `src/dusk/ui/` subsystem with `uiBegin` / `uiEnd` and intent types
for rects, sprites, and text. Platforms implement the flush independently.
The 3D spritebatch is retired or scoped to world-space billboards only.
### 5. Fixed-point / no-FPU math
`float_t` needs a fixed-point mode. Proposed: define `fixed_t` as a
16.16 signed integer; define `DUSK_MATH_FIXED` for platforms that require
it (Saturn, SNES). Engine math utilities (`mathSin`, `mathCos`, etc.)
have fixed-point implementations selected by this flag. `float_t` on
FPU-less platforms becomes a typedef for `fixed_t`.
### 6. Background plane abstraction (bgplane_t)
New concept in `src/dusk/display/bgplane/`. A BG plane has a tile map or
bitmap source, scroll offsets, a palette reference, and optional affine
parameters (for Mode7-style use). On GL platforms: rendered as a
fullscreen textured quad or shader pass. On Saturn: VDP2 config. On SNES:
PPU BG layer config.
### 7. Memory system: static pool mode
For SNES (and possibly Saturn), the general-purpose allocator may be
unviable. A compile-time static pool mode (`DUSK_MEMORY_STATIC`) that uses
a fixed-size arena instead of dynamic allocation. All `memoryAllocate`
calls hit the pool; `memoryFree` is a no-op or a stack pop.
### 8. Script runtime: optional
JerryScript requires too much RAM for SNES and is marginal on Saturn.
The scripting system should be compile-time optional (`DUSK_SCRIPTING`),
not assumed present. SNES/Saturn scenes would be compiled C.
---
## What to Keep
- Platform macro abstraction pattern (`displayplatform.h`, etc.) -- works,
no reason to change.
- Directory structure convention for platform directories.
- Entity-component system -- platform-agnostic, unaffected.
- Asset loading + `.dsk` bundle concept (extended for platform formats).
- The broad subsystem layout: asset, input, display, log, network, save,
system, time.
---
## Open Questions
1. **Render intent granularity**: How much does the intent API need to
express? A MESH intent works on GL/N64 but degrades poorly on Saturn
(must split into quads) and is impossible on SNES. Should MESH be a
valid intent with a "best effort" contract, or excluded from the portable
API entirely?
2. **Threading abstraction depth**: Should `DUSK_THREAD_SLAVE_SH2` be a
first-class concept in the engine's job system, or a Saturn-internal
implementation detail the core never sees? Same question applies to N64's
RSP as a compute co-processor.
3. **Asset loading async contract**: When a platform has no threads, should
`assetLoadAsync` be a no-op alias for `assetLoadSync`, or return
immediately with a completion flag to poll? The polling model is more
honest but requires all call sites to handle it.
4. **N64 build system**: libdragon uses GNU Make, not CMake. Options are:
(a) write a CMake toolchain file that wraps n64.mk, (b) maintain a
parallel Makefile just for N64, or (c) wait for upstream CMake support.
Which is acceptable long-term?
5. **N64 RSP microcode**: Standard libdragon microcodes (Fast3D/F3DEX2) or
Tiny3D (community microcode with full T&L + skinning)? Writing custom
microcode is powerful but limited to ~1000 MIPS SIMD instructions.
This decision gates what 3D features the N64 port can support.
6. **PSPGL fate**: Drop immediately in favor of native GU, or keep as a
fallback (`duskgllegacy`) while native GU is built? The two can coexist
during transition.
7. **Vulkan priority**: Design the intent API with Vulkan in mind from the
start, or add it later? Vulkan's explicit pipeline state model may
conflict with how stateful platforms (Saturn, SNES) expect things to work.
8. **Background planes on modern platforms**: Does `bgplane_t` degrade to a
fullscreen textured quad on GL/Vulkan/N64, or should modern platforms
support actual background scene rendering (3D world behind the foreground)?
9. **PS1 ordering table depth**: The OT is a fixed-size array (e.g. 4096
slots). Depth precision = number of slots. How deep should the engine's
default OT be, and should this be configurable per-scene?
10. **Fixed-point strategy**: Does `float_t` transparently become `fixed_t`
on FPU-less platforms (Saturn, PS1, SNES), or do we require explicit
`fixed_t` in math-heavy paths? Transparent is easiest to port; explicit
is faster.
11. **SNES V-blank budget**: All VRAM writes must finish within ~1.2ms.
Does the engine need a V-blank work queue with a budget checker, or is
this left to the game to manage manually?
12. **SNES scripting**: JerryScript is out. Pure compiled C, or a lighter
scripting layer (Lua is ~100 KB -- tight but possible)?
13. **Asset compiler**: New standalone tool, or an extension of the existing
asset pipeline? Part of the CMake build or a separate pre-build step?
---
## Proposed Sequence (Draft)
### Phase 1 -- Intent API (no behavior change)
1. Design and stabilize `renderqueue_t` and intent types
2. Refactor modern GL path to submit through render intents (same output,
new plumbing)
3. Refactor Dolphin path the same way
4. Validate no regressions on Linux + GameCube
### Phase 2 -- UI system
5. Extract UI rendering from the 3D path into `src/dusk/ui/`
6. Implement UI flush for GL and Dolphin
7. Wire existing UI elements through the new system
### Phase 3 -- Platform splits
8. Split `duskgl/` into `duskgl/` (modern) and `duskgllegacy/` (fixed-func)
9. Port PSP to native GU (`duskpsp/display/` rewrite, drop PSPGL dependency)
10. Stub `duskvulkan/` structure for future implementation
### Phase 4 -- Asset pipeline
11. Design platform-native texture format system
12. Extend asset compiler for per-platform output
13. Update texture loader to expect pre-converted data
### Phase 5 -- Saturn
14. CMake toolchain for SH-2 cross-compile (yaul / libyaul toolchain)
15. `src/dusksaturn/` -- input (SMPC), asset (CD-ROM), log, system
16. VDP1 backend for render queue (quads, polygons, painter's sort)
17. VDP2 backend for bgplane_t (tile maps, scroll, palette)
18. Fixed-point math mode (`DUSK_MATH_FIXED`)
19. UI backend (VDP2 plane(s))
### Phase 6 -- PlayStation 1
20. CMake toolchain wrapping PSn00bSDK (already CMake-native)
21. `src/duskps1/` -- input (BIOS pad), asset (CD-ROM libpsxcd), log, system
22. GTE integration for fixed-point math (reuse `DUSK_MATH_FIXED` path)
23. Ordering table builder for render queue (painter's sort, DMA linked-list)
24. GPU packet backend for intents (tris, quads, rects)
25. UI backend (separate GPU packet chain after world OT)
### Phase 7 -- Nintendo 64
26. CMake toolchain wrapping libdragon (n64.mk wrapper or toolchain file)
27. `src/duskn64/` -- input (N64 controller via PIF), asset (PI DMA /
DragonFS), log, system
28. RSP display list builder for render queue (Z-buffer path, no sorting)
29. TMEM tile management for textures
30. RDP rectangle backend for UI
31. Decide on RSP microcode (Tiny3D vs standard F3DEX2)
### Phase 8 -- SNES
32. SNES toolchain (cc65 or llvm-mos 65816 target)
33. Static memory pool mode (`DUSK_MEMORY_STATIC`)
34. PPU tile pipeline + VRAM management
35. Mode7 overworld implementation
36. OAM sprite system
37. BG layer UI
38. Scripting-optional build (`DUSK_SCRIPTING` off)
-215
View File
@@ -1,215 +0,0 @@
# Display System
Source: `src/dusk/display/`
The display system is the rendering pipeline. It is abstracted across platforms via `displayplatform.h` — see [architecture.md](architecture.md) for the abstraction pattern. The current concrete backends are OpenGL (`src/duskgl/`) and GX/Dolphin (`src/duskdolphin/`).
For the planned render-queue refactor (required for Saturn), see [display-refactor.md](display-refactor.md).
---
## Initialization order
Within the display system, init must follow this order (enforced in `engine.c`):
```
displayInit → uiInit → uiTextboxInit
```
Within `displayInit`, the platform typically initialises: framebuffer → screen → shader list → textures → spritebatch → text system.
---
## `display_t` / `displaystate_t`
`display_t DISPLAY` is the global display instance (type alias for `displayplatform_t`).
`displaystate_t` carries per-draw-call render state flags:
```c
DISPLAY_STATE_FLAG_CULL // face culling
DISPLAY_STATE_FLAG_DEPTH_TEST // depth testing
DISPLAY_STATE_FLAG_BLEND // alpha blending
```
Set state before drawing with `displaySetState(state)`.
---
## Screen (`display/screen/`)
`screen_t SCREEN` manages the logical viewport that game content renders into. On dynamic-size platforms (Linux/SDL2) the screen can differ from the native window/framebuffer resolution.
Screen modes:
```
SCREEN_MODE_BACKBUFFER — maps 1:1 to backbuffer
SCREEN_MODE_FIXED_SIZE — fixed pixel dimensions
SCREEN_MODE_ASPECT_RATIO — fixed aspect, scale to fit
SCREEN_MODE_FIXED_HEIGHT — fixed height, width scales
SCREEN_MODE_FIXED_WIDTH — fixed width, height scales
SCREEN_MODE_FIXED_VIEWPORT_HEIGHT — fixed viewport height
```
The linux target defines `DUSK_DISPLAY_SCREEN_HEIGHT=240`, producing a 240p fixed-height viewport.
Render loop usage:
```c
screenBind(); // set up viewport, projection
// ... draw game content ...
screenUnbind();
screenRender(); // blit to backbuffer / current framebuffer
```
`SCREEN.width` / `SCREEN.height` are the logical dimensions used for world-to-screen math — always prefer these over the framebuffer dimensions.
---
## Framebuffer (`display/framebuffer/`)
`framebuffer_t FRAMEBUFFER_BACKBUFFER` is the platform backbuffer. `FRAMEBUFFER_BOUND` points to the currently-bound framebuffer (or `NULL` for backbuffer).
```c
frameBufferInitBackBuffer(); // called once at startup
frameBufferBind(fb); // NULL → backbuffer
frameBufferClear(FRAMEBUFFER_CLEAR_COLOR | FRAMEBUFFER_CLEAR_DEPTH, COLOR_BLACK);
frameBufferGetWidth(fb) / frameBufferGetHeight(fb) / frameBufferGetAspect(fb);
```
On platforms with `DUSK_DISPLAY_SIZE_DYNAMIC`, off-screen framebuffers can be created with `frameBufferInit(fb, w, h)` and disposed with `frameBufferDispose(fb)`. Fixed-resolution platforms (PSP, GameCube) only ever use the backbuffer.
---
## Mesh (`display/mesh/`)
`mesh_t` is a vertex buffer. The type is `meshplatform_t` (e.g. a VAO+VBO on GL, a GX display list on Dolphin).
```c
meshInit(&mesh, MESH_PRIMITIVE_TYPE_TRIANGLES, vertexCount, verticesPtr);
meshFlush(&mesh, offset, count); // upload CPU vertices → GPU
meshDraw(&mesh, offset, count); // draw; pass -1 for count to draw all
meshDispose(&mesh);
```
**Key distinction**: `meshFlush` uploads data to GPU memory; `meshDraw` issues the draw call. For static geometry (chunk meshes) you call `meshFlush` once on load, then `meshDraw` every frame. For dynamic geometry (spritebatch) you `meshFlush` + `meshDraw` each frame.
`meshvertex_t` (`display/mesh/meshvertex.h`) contains:
- `float_t uv[2]` — texture coordinates
- `float_t pos[3]` — position
- Optionally `color_t color` if `MESH_ENABLE_COLOR` is defined (off by default)
Primitive mesh generators live alongside `mesh.h`: `quad.h`, `plane.h`, `cube.h`, `sphere.h`, `capsule.h`, `triprism.h`.
---
## Shader (`display/shader/`)
`shader_t` is `shaderplatform_t` (GLSL program on GL, TEV state block on Dolphin).
```c
shaderInit(&shader, &definition);
shaderBind(&shader);
shaderSetMatrix(&shader, "uModel", modelMat);
shaderSetTexture(&shader, "uTexture", &texture);
shaderSetColor(&shader, "uColor", COLOR_WHITE);
shaderSetMaterial(&shader, &material);
shaderDispose(&shader);
```
### Shader list (`display/shader/shaderlist.h`)
The engine maintains a small set of built-in shaders in `SHADER_LIST_DEFS[]`. Currently only one is defined:
- `SHADER_LIST_SHADER_UNLIT``SHADER_UNLIT` — unlit textured/colored rendering, used for all world and entity drawing.
`shaderListInit()` compiles/uploads all built-in shaders and sets shared projection/view matrices. Call once after display init.
### Materials (`display/shader/shadermaterial.h`)
`shadermaterial_t` is a union of all shader-specific material structs. Currently only `shaderunlitmaterial_t`:
```c
shadermaterial_t mat = {
.unlit = {
.color = COLOR_WHITE,
.texture = &myTexture, // NULL for solid color
}
};
shaderSetMaterial(&SHADER_UNLIT, &mat);
```
---
## Texture (`display/texture/`)
```c
textureInit(&texture, width, height, format, data);
textureDispose(&texture);
```
Width and height **must be powers of two** (asserted at init time).
`textureformat_t` is `textureformatplatform_t`. Supported formats vary by platform; the common ones are `TEXTURE_FORMAT_RGBA` and `TEXTURE_FORMAT_PALETTE`.
`texturedata_t` is a union:
```c
// RGBA:
data.rgbaColors = colorArray;
// Paletted:
data.paletted.indices = indexArray;
data.paletted.palette = &palette; // palette color count must be power of two
```
**Built-in textures** (defined in `texture.c`, no asset loading needed):
- `TEXTURE_WHITE` — 4×4 solid white
- `TEXTURE_TEST` — 4×4 black/magenta checkerboard
### Palette (`display/texture/palette.h`)
Up to `PALETTE_COUNT` (6) global palettes in `PALETTES[]`, each holding up to `PALETTE_COLOR_COUNT` (255) `color_t` entries.
### Tileset (`display/texture/tileset.h`)
A tileset slices a texture into a grid of equal-sized tiles. Used by fonts and UI frames. The tileset does not own the texture — it references a `texture_t *`.
---
## SpriteBatch (`display/spritebatch/`)
The primary 2D/billboard drawing primitive. Accumulates `spritebatchsprite_t` quads and flushes them in batches of `SPRITEBATCH_FLUSH_COUNT` (16) sprites at a time.
```c
// Per frame:
spriteBatchClear();
spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material); // auto-flushes when batch full
spriteBatchFlush(); // flush remaining
// Low-level: write directly to an external mesh (for baking static geometry):
spriteBatchBufferToMesh(sprites, count, vertices, verticesSize);
```
`spritebatchsprite_t`:
```c
typedef struct {
vec3 min, max; // 3D bounding box
vec2 uvMin, uvMax; // texture region
} spritebatchsprite_t;
```
The global `SPRITEBATCH` and its vertex backing array `SPRITEBATCH_VERTICES[]` are defined externally to the struct to satisfy alignment requirements on certain platforms.
---
## Text (`display/text/`)
Text rendering uses `FONT_DEFAULT` (loaded during `textInit()`), which references a texture and a tileset. Characters start at ASCII `!` (`TEXT_CHAR_START`).
```c
textDraw(x, y, "Hello", COLOR_WHITE, &FONT_DEFAULT);
textMeasure("Hello", &FONT_DEFAULT, &outWidth, &outHeight);
// Single-char sprite for manual layout:
spritebatchsprite_t s = textGetSprite(pos, 'A', &FONT_DEFAULT);
```
`font_t` holds a `texture_t *` and a `tileset_t *` — both are owned by the asset system, not the font struct.
-93
View File
@@ -1,93 +0,0 @@
# Input System
Source: `src/dusk/input/`
The input system decouples physical hardware buttons from logical game actions via a binding layer. Actions are defined in `src/dusk/input/input.csv` and code-generated into `inputaction_t` enum values — see [architecture.md](architecture.md#code-generation-from-csv).
---
## Concepts
**Button** (`inputbutton_t`) — a physical input source: a keyboard scancode, gamepad button, gamepad axis, or pointer axis. The available button types depend on which `DUSK_INPUT_*` defines are active for the target platform.
**Action** (`inputaction_t`) — a logical game input (e.g. `INPUT_ACTION_UP`, `INPUT_ACTION_CONFIRM`, `INPUT_ACTION_RAGEQUIT`). Each action accumulates a float value `[0.0, 1.0]` from all buttons bound to it.
**Binding** — a many-to-one mapping from buttons to actions. Bindings are registered at runtime with `inputBind(button, action)`.
---
## Querying actions
```c
// Boolean helpers (current frame):
inputIsDown(action) // value > 0 this frame
inputPressed(action) // down this frame but not last
inputReleased(action) // down last frame but not this
// Last frame state:
inputWasDown(action)
// Raw float value:
inputGetCurrentValue(action) // [0.0, 1.0]
inputGetLastValue(action)
// Axis helpers — combine two opposing actions into a signed float:
float_t h = inputAxis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT); // -1.0 to 1.0
inputAxis2D(negX, posX, negY, posY, result); // fills vec2
inputAngle2D(negX, posX, negY, posY, result); // atan2-based normalized direction
// Deadzone:
float_t clean = inputDeadzone(rawValue, 0.1f);
```
---
## Dynamic values (`DUSK_TIME_DYNAMIC`)
On platforms with variable frame rates, each action also tracks `dynamicDelta`-scaled values:
```c
inputGetCurrentValueDynamic(action)
inputGetLastValueDynamic(action)
```
These account for the actual time elapsed since the last frame, so movement calculated from them is frame-rate independent.
---
## Events on actions
Each `inputactiondata_t` exposes `onPressed` and `onReleased` events:
```c
eventSubscribe(&INPUT.actions[INPUT_ACTION_CONFIRM].onPressed, myCallback, myUser);
```
The callback signature is `void cb(void *params, void *user)`. `params` is always `NULL` for input events.
---
## Buttons and bindings
Physical buttons are typed via `inputbuttontype_t`:
| Constant | When available | Payload |
|---|---|---|
| `INPUT_BUTTON_TYPE_KEYBOARD` | `DUSK_INPUT_KEYBOARD` | `inputscancode_t` |
| `INPUT_BUTTON_TYPE_GAMEPAD` | `DUSK_INPUT_GAMEPAD` | `inputgamepadbutton_t` |
| `INPUT_BUTTON_TYPE_GAMEPAD_AXIS` | `DUSK_INPUT_GAMEPAD` | axis + positive direction flag |
| `INPUT_BUTTON_TYPE_POINTER` | `DUSK_INPUT_POINTER` | `inputpointeraxis_t` |
Button names and default bindings are defined in `input.csv`. Look up a button by name:
```c
inputbutton_t btn = inputButtonGetByName("keyboard_w");
inputBind(btn, INPUT_ACTION_UP);
```
`INPUT_BUTTON_DATA[]` holds runtime state (current/last raw values) for every physical button.
---
## Platform platform-specific reads
`inputButtonGetValuePlatform(button)` is the one required platform function — it returns the current raw `[0.0, 1.0]` value for a button. The platform implementations live in `src/dusk{platform}/input/`.
-149
View File
@@ -1,149 +0,0 @@
# Cutscenes
Two distinct layers: a low-level engine sequencer (`src/dusk/cutscene/`) and a higher-level RPG wrapper (`src/dusk/rpg/cutscene/`). Almost all game code works with the RPG layer.
---
## Engine sequencer (`src/dusk/cutscene/`)
`cutscene_t CUTSCENE` is a minimal event runner with up to `CUTSCENE_EVENT_COUNT_MAX` (16) `cutsceneevent_t` slots. Each event has three callbacks:
```c
typedef struct {
errorret_t (*onStart)(void);
errorret_t (*onUpdate)(void);
errorret_t (*onEnd)(void);
} cutsceneevent_t;
```
API:
```c
cutscenePlay(events, count); // copy events array and start from index 0
cutsceneAdvance(); // end current event, start next (deactivates after last)
cutsceneStop(); // abort immediately
cutsceneIsActive(); // bool
```
This layer is primarily used by the RPG cutscene system — game code doesn't normally touch it directly.
---
## RPG cutscene layer (`src/dusk/rpg/cutscene/`)
### Data structures
A `cutscene_t` is just a pointer to an item array and a count:
```c
typedef struct cutscene_s {
const cutsceneitem_t *items;
uint8_t itemCount;
} cutscene_t;
```
A `cutsceneitem_t` is a tagged union of all item types:
```c
typedef struct cutsceneitem_s {
cutsceneitemtype_t type;
union {
cutscenetext_t text; // display text in textbox
cutscenecallback_t callback; // call a void(*)(void) function
cutscenewait_t wait; // pause for a fixed_t duration (seconds)
const cutscene_t *cutscene; // nest another cutscene
};
} cutsceneitem_t;
```
### Item types
| Type constant | Payload | Behaviour |
|---|---|---|
| `CUTSCENE_ITEM_TYPE_TEXT` | `cutscenetext_t``text[256]` + `rpgtextboxpos_t position` | Shows textbox; advances on player confirm input |
| `CUTSCENE_ITEM_TYPE_CALLBACK` | `cutscenecallback_t` (function pointer) | Calls the function once, then immediately advances |
| `CUTSCENE_ITEM_TYPE_WAIT` | `cutscenewait_t` (a `fixed_t` in seconds) | Counts down `animTime` each frame, then advances |
| `CUTSCENE_ITEM_TYPE_CUTSCENE` | `const cutscene_t *` | Plays the nested cutscene before continuing |
### Runtime state
`cutscenesystem_t CUTSCENE_SYSTEM` tracks:
- `scene` — pointer to the active `cutscene_t`
- `currentItem` — index into `scene->items[]`
- `data` — per-item runtime data (`cutsceneitemdata_t`, currently just `cutscenewaitdata_t`)
- `mode` — the current `cutscenemode_t`
API:
```c
cutsceneSystemStartCutscene(cutscene); // begin playing a cutscene
cutsceneSystemNext(); // advance to next item
cutsceneSystemUpdate(); // called each frame from rpgUpdate
cutsceneSystemGetCurrentItem(); // inspect active item
```
### Cutscene mode (`cutscenemode.h`)
Each item can run in one of three modes:
```c
CUTSCENE_MODE_NONE // no cutscene active
CUTSCENE_MODE_FULL_FREEZE // pause everything (not yet used)
CUTSCENE_MODE_INPUT_FREEZE // player input blocked (default: CUTSCENE_MODE_INITIAL)
CUTSCENE_MODE_GAMEPLAY // player can still move during cutscene
```
`cutsceneModeIsInputAllowed()` is checked by `entityUpdate()` before invoking the movement callback — the player cannot walk when in INPUT_FREEZE mode.
### Defining a cutscene
Cutscenes are defined as `static const` arrays in header files under `rpg/cutscene/scene/`. Example (`testcutscene.h`):
```c
static const cutsceneitem_t MY_CUTSCENE_ITEMS[] = {
{
.type = CUTSCENE_ITEM_TYPE_TEXT,
.text = { .text = "Hello!", .position = RPG_TEXTBOX_POS_BOTTOM }
},
{
.type = CUTSCENE_ITEM_TYPE_WAIT,
.wait = FIXED(1.5f)
},
{
.type = CUTSCENE_ITEM_TYPE_CUTSCENE,
.cutscene = &ANOTHER_CUTSCENE
},
};
static const cutscene_t MY_CUTSCENE = {
.items = MY_CUTSCENE_ITEMS,
.itemCount = sizeof(MY_CUTSCENE_ITEMS) / sizeof(cutsceneitem_t)
};
```
Attach to an NPC via its interact component:
```c
entity->interact.type = ENTITY_INTERACT_CUTSCENE;
entity->interact.data.cutscene = &MY_CUTSCENE;
```
---
## Textbox (`src/dusk/rpg/rpgtextbox.h`)
`rpgtextbox_t RPG_TEXTBOX` is the global textbox state:
```c
typedef struct {
rpgtextboxpos_t position; // RPG_TEXTBOX_POS_TOP or RPG_TEXTBOX_POS_BOTTOM
bool_t visible;
char_t text[RPG_TEXTBOX_MAX_CHARS]; // 256 chars
} rpgtextbox_t;
```
API:
```c
rpgTextboxShow(position, text); // copies text, sets visible = true
rpgTextboxHide(); // sets visible = false
rpgTextboxIsVisible(); // bool
```
The textbox state is read by `ui/uitextbox.c` during the UI render pass to draw the dialogue box on screen. `rpgtextbox.c` itself does no rendering.
-140
View File
@@ -1,140 +0,0 @@
# Entities
Source: `src/dusk/rpg/entity/`
---
## Storage
Entities live in a single fixed global array:
```c
entity_t ENTITIES[ENTITY_COUNT]; // ENTITY_COUNT = 64
```
A slot is "empty" when `entity->type == ENTITY_TYPE_NULL`. Never allocate entity memory dynamically — always find a free slot with `entityGetAvailable()`, which returns its index (`0xFF` if none free).
---
## `entity_t` structure
```c
typedef struct entity_s {
uint8_t id; // index in ENTITIES[]
entitytype_t type; // ENTITY_TYPE_NULL / PLAYER / NPC
entitytypedata_t data; // union: player_t | npc_t
entitydir_t direction; // facing direction (N/S/E/W)
fixed_t position[3]; // current sub-tile position (x, y, z)
fixed_t lastPosition[3]; // position before last move (for interpolation)
entityanim_t animation; // IDLE / TURN / WALK
fixed_t animTime; // countdown timer for current animation
entityinteract_t interact; // optional interact component
} entity_t;
```
---
## Type system
Entity types are defined in `entitytype.h` using the enum+integer-typedef pattern:
```c
typedef enum { ENTITY_TYPE_NULL, ENTITY_TYPE_PLAYER, ENTITY_TYPE_NPC, ENTITY_TYPE_COUNT } entitytype_enum_t;
typedef uint8_t entitytype_t;
```
Each type has a `entitycallback_t` entry in the `ENTITY_CALLBACKS[ENTITY_TYPE_COUNT]` static table:
```c
typedef struct {
void (*init)(entity_t *entity);
void (*movement)(entity_t *entity);
bool_t (*interact)(entity_t *player, entity_t *entity);
} entitycallback_t;
```
Callbacks not applicable to a type are `NULL`; `entityUpdate()` guards against this before calling.
Type-specific data sits in `entitytypedata_t` (a union of `player_t` and `npc_t`). Currently both are stubs (`void *nothing`).
---
## Direction (`entitydir.h`)
```c
ENTITY_DIR_NORTH / EAST / SOUTH / WEST
```
Aliases: `UP = NORTH`, `DOWN = SOUTH`, `LEFT = WEST`, `RIGHT = EAST`.
Utilities:
- `entityDirGetOpposite(dir)` — returns the opposite direction.
- `entityDirGetRelative(dir, &relX, &relY)` — fills in the ±1 XY delta for that direction.
- `assertValidEntityDir(dir, msg)` — assertion macro.
---
## Animation (`entityanim.h`)
```c
ENTITY_ANIM_IDLE // standing still
ENTITY_ANIM_TURN // turning to a new direction (ENTITY_ANIM_TURN_DURATION = FIXED(0.06))
ENTITY_ANIM_WALK // mid-step (ENTITY_ANIM_WALK_DURATION = FIXED(0.1))
```
`entityAnimUpdate(entity)` decrements `animTime` each frame and transitions back to `IDLE` when it reaches zero.
`entityCanWalk(entity)` / `entityCanTurn(entity)` both return true only when `animation == ENTITY_ANIM_IDLE`.
The renderer interpolates between `lastPosition` and `position` using `animTime / WALK_DURATION` to produce smooth motion even at low frame rates.
---
## Movement
`entityWalk(entity, direction)`:
1. Converts `entity->position` to a `worldpos_t` (truncates fractional part).
2. Applies the directional delta to get `newPos`.
3. Checks the current and target tiles for ramp raise/fall logic (see [world.md](world.md)).
4. Checks `ENTITIES[]` for another entity occupying `newPos` — blocks if found.
5. On success: copies `position` to `lastPosition`, updates `position` to `newPos` (via `worldPosToFixed`), sets `animation = ENTITY_ANIM_WALK`.
`entityTurn(entity, direction)`: sets `direction` and starts a brief turn animation.
---
## Interaction (`entityinteract.h`)
The `entityinteract_t` component is embedded in every entity. It is optional — set `type = ENTITY_INTERACT_NULL` for non-interactable entities.
```c
typedef enum {
ENTITY_INTERACT_NULL,
ENTITY_INTERACT_CUTSCENE, // plays a cutscene_t *
ENTITY_INTERACT_PRINT, // prints a short message[32]
} entityinteracttype_t;
```
`entityInteractWith(player, target)` dispatches:
1. If the interact component's `type != NULL`, handles it (starts the cutscene or prints the message).
2. Otherwise falls back to `ENTITY_CALLBACKS[type].interact` if set.
---
## Player (`player.h` / `player.c`)
`playerInit()` is called via `ENTITY_CALLBACKS[ENTITY_TYPE_PLAYER].init`.
`playerInput(entity)` is the movement callback. It reads `PLAYER_INPUT_DIR_MAP[]` — a static table mapping input actions (`INPUT_ACTION_UP/DOWN/LEFT/RIGHT`) to entity directions — and calls `entityWalk` or `entityTurn` accordingly.
The player entity is normally `ENTITIES[0]` but there is no hardcoded assumption about its index beyond being initialized with `ENTITY_TYPE_PLAYER`.
---
## NPC (`npc.h` / `npc.c`)
`npcInit()`, `npcMovement()`, and `npcInteract()` provide the NPC type callbacks. Currently stubs; movement does nothing, interact returns false.
-18
View File
@@ -1,18 +0,0 @@
# RPG Layer
The RPG layer lives in `src/dusk/rpg/` and is the game-logic tier above the engine. It is initialized and ticked by `engine.c` via `rpgInit` / `rpgUpdate` / `rpgDispose`. The `rpg_t` struct is currently a stub; all meaningful state lives in the subsystems below.
## Contents
- [world.md](world.md) — scene manager, overworld map, chunks, tiles, coordinate system, camera
- [entity.md](entity.md) — entity pool, types, direction, animation, interaction, player, NPC
- [cutscene.md](cutscene.md) — cutscene system, item types, mode control, textbox
- [story.md](story.md) — story flags, items, inventory, backpack, save system
## Scene system
The scene manager (`src/dusk/scene/`) sits above the RPG layer and owns the single active scene. Scenes are identified by `scenetype_t` and registered in `SCENE_TYPES[]` (`scene/scenetype.c`) as `scenecallbacks_t` (init / update / render / dispose).
`scenedata_t` is a union so all scene structs share memory. `sceneSet(type)` defers the transition — the old scene disposes before the new one inits.
Currently the only scene is `SCENE_TYPE_OVERWORLD``src/dusk/scene/overworld/sceneoverworld.c`.
-124
View File
@@ -1,124 +0,0 @@
# Story, Items & Save
---
## Story flags (`src/dusk/rpg/story/`)
Story flags are the primary mechanism for tracking game-world state (quest progress, one-time events, unlocks). Each flag is a `uint8_t` value (`storyflagvalue_t`), so they can hold booleans or small counts.
### Defining flags
Flags are defined in `src/dusk/rpg/story/storyflag.csv`:
```
id,description,initial
test,"Test flag for debugging purposes",1
```
The build tool generates:
- A `storyflag_t` enum (e.g. `STORY_FLAG_TEST`) in the generated header.
- `STORY_FLAG_VALUES[]` — the runtime array, pre-populated with the `initial` column values.
To add a flag: add a row to the CSV. The build re-runs the Python tool automatically on the next CMake build.
### Access
```c
storyflagvalue_t v = storyFlagGet(STORY_FLAG_TEST); // macro: array read
storyFlagSet(STORY_FLAG_TEST, 1); // function: also marks save dirty
```
`storyFlagGet` is a macro that directly indexes `STORY_FLAG_VALUES[]` — no function call overhead.
---
## Items (`src/dusk/rpg/item/`)
### Item definitions
Items are defined in `src/dusk/rpg/item/item.csv`. The build tool generates `itemid_t` enum values and item metadata. `itemid_t` is a generated `uint8_t` typedef.
### Inventory (`inventory.h`)
`inventory_t` is a generic container backed by a caller-supplied `inventorystack_t` array:
```c
typedef struct {
itemid_t item;
uint8_t quantity; // max ITEM_STACK_QUANTITY_MAX (255)
} inventorystack_t;
typedef struct {
inventorystack_t *storage;
uint8_t storageSize;
} inventory_t;
```
Key operations:
```c
inventoryInit(&inv, storageArray, size);
inventoryAdd(&inv, ITEM_POTION, 3);
inventoryRemove(&inv, ITEM_POTION);
inventorySet(&inv, ITEM_POTION, 10);
inventoryGetCount(&inv, ITEM_POTION); // returns 0 if not present
inventoryItemExists(&inv, ITEM_POTION);
inventoryIsFull(&inv);
inventorySort(&inv, INVENTORY_SORT_BY_ID, false);
```
`inventory_t` itself holds no data — the backing array is always external. This avoids fixed-size struct limits and lets different inventories (backpack, shop, chest) share the same logic.
### Backpack (`backpack.h`)
The player's inventory is the global `BACKPACK` instance:
```c
extern inventorystack_t BACKPACK_STORAGE[BACKPACK_STORAGE_SIZE_MAX]; // 20 slots
extern inventory_t BACKPACK;
backpackInit(); // wires BACKPACK_STORAGE into BACKPACK
```
---
## Save system (`src/dusk/save/`)
The save system is stubbed out — it exists and compiles but is commented out of engine init (`engine.c`). What follows describes the design as implemented.
### Slots
`save_t SAVE` holds `SAVE_FILE_COUNT_MAX` slots:
```c
typedef struct {
savefile_t files[SAVE_FILE_COUNT_MAX];
saveplatform_t platform; // platform-specific state (paths, card handles)
} save_t;
```
### Streams
`savestream_t` (`save/savestream.h`) is a raw byte cursor used to serialize/deserialize `savefile_t`. Platform backends in `src/dusk{platform}/save/` implement the actual I/O:
- Linux: filesystem files in a save directory.
- GameCube/Wii: memory card via libogc.
### API
```c
saveInit();
saveLoad(slot); // reads platform storage → savefile_t
saveWrite(slot); // writes savefile_t → platform storage
saveDelete(slot);
saveExists(slot); // bool
saveGet(slot); // returns savefile_t *
saveDispose();
```
---
## Locale / i18n (`src/dusk/locale/`)
Translations are loaded from `.po` files in `assets/locale/` (e.g. `en_US.po`). `localemanager.c` manages the active locale and exposes a key→string lookup. `assetlocaleloader.c` parses the PO format via the asset system.
All player-visible strings must go through the locale system rather than being hardcoded. The locale is loaded asynchronously via the asset system so it is available before the first scene renders.
-114
View File
@@ -1,114 +0,0 @@
# World
Source: `src/dusk/rpg/overworld/`
---
## Coordinate system
Three nested coordinate spaces, each defined in `worldpos.h`:
| Type | Description | Unit |
|---|---|---|
| `worldpos_t` | Tile-level absolute position `{x, y, z}` | `worldunit_t` (int16) |
| `chunkpos_t` | Chunk-grid position `{x, y, z}` | `chunkunit_t` (int16) |
| `fixed_t[3]` | Smooth sub-tile position used by entities | Q24.8 fixed-point |
One chunk = `CHUNK_WIDTH × CHUNK_HEIGHT × CHUNK_DEPTH` tiles (16 × 16 × 8).
The loaded world window = `MAP_CHUNK_WIDTH × MAP_CHUNK_HEIGHT × MAP_CHUNK_DEPTH` chunks (5 × 5 × 3).
Conversion helpers (all in `worldpos.c`):
```c
worldPosToChunkPos(&worldPos, &chunkPos); // tile → chunk grid
chunkPosToWorldPos(&chunkPos, &worldPos); // chunk grid → tile origin
worldPosToChunkTileIndex(&worldPos); // tile → index within its chunk
chunkPosToIndex(&chunkPos); // chunk grid → linear index in MAP.chunks[]
worldPosToFixed(&worldPos, fixedOut); // tile → entity fixed position
fixedToWorldPos(fixedPos); // entity fixed → tile (truncates frac)
```
---
## Tiles
Defined in `tile.h` as a plain `tile_t` enum:
```
TILE_SHAPE_NULL — empty / unloaded
TILE_SHAPE_GROUND — solid flat tile
TILE_SHAPE_RAMP_* — directional ramps (N/S/E/W + diagonals NE/NW/SE/SW)
```
Key predicates:
- `tileIsWalkable(tile)` — true for GROUND and all ramp shapes.
- `tileIsRamp(tile)` — true only for ramp shapes.
Entity walk code (`entity.c`) checks both the current tile and the target tile to decide whether the entity steps forward flat, raises one Z level (walking up a ramp), or falls one Z level (stepping onto a downward ramp from above).
---
## Chunks
`chunk_t` (`chunk.h`) holds:
- `position` — its `chunkpos_t` in the world grid
- `tiles[CHUNK_TILE_COUNT]` — flat array of `tile_t`, indexed by `chunkGetTileIndex()`
- `vertices[CHUNK_VERTEX_COUNT]` / `mesh` — pre-baked mesh uploaded to GPU on load
- `entities[CHUNK_ENTITY_COUNT_MAX]` — indices into `ENTITIES[]` currently in this chunk (sentinel `0xFF`)
- `testColor` — temporary debug color (checkerboard), will be replaced by real tileset data
Tile layout within a chunk is `z * W*H + y * W + x` (Z-major, row-major in XY).
---
## Map
`map_t MAP` (`map.h`) is the single global map instance.
```c
chunk_t chunks[MAP_CHUNK_COUNT]; // flat storage — index is NOT world position
chunk_t *chunkOrder[MAP_CHUNK_COUNT]; // draw-order sorted pointers into chunks[]
chunkpos_t chunkPosition; // world-grid origin of the loaded window
bool_t loaded;
```
### Load / unload
`mapInit()` allocates chunk meshes and performs the initial load of all chunks in the starting window.
`mapPositionSet(newPos)` shifts the window:
1. Determines which of the `MAP_CHUNK_COUNT` slots remain within the new window vs. fall outside it.
2. Calls `mapChunkUnload()` on every chunk that falls outside (nulls its entity slots, zeroes `vertCount`).
3. Reuses freed slots for newly-in-range chunks; calls `mapChunkLoad()` on each.
4. Rebuilds `chunkOrder[]` in XYZ order for the new position.
### Chunk load (current stub)
`mapChunkLoad()` currently:
- Fills all tiles with `TILE_SHAPE_GROUND`
- Assigns a checkerboard debug color based on chunk XY parity
- Bakes a flat sprite-batch quad mesh for the z=0 layer and uploads it via `meshFlush()`
- Skips mesh generation for z > 0 chunks (they're empty)
### Tile lookup
```c
tile_t mapGetTile(const worldpos_t position);
```
Converts `position` to its chunk, looks up the chunk in `chunkOrder`, then indexes into `chunk->tiles[]`. Returns `TILE_SHAPE_NULL` for any out-of-bounds position or when the map is not loaded.
---
## Camera
`rpgcamera_t RPG_CAMERA` (`rpgcamera.h`) has two modes:
```c
RPG_CAMERA_MODE_FREE // free worldpos; camera.free holds the position
RPG_CAMERA_MODE_FOLLOW_ENTITY // tracks ENTITIES[followEntityId]
```
`rpgCameraGetPosition()` returns the active world tile position in either mode.
The scene renderer (`sceneoverworld.c`) uses `rpgCameraGetPosition()` to build the `glm_lookat` view matrix. When following an entity, it sub-tile interpolates between `entity->lastPosition` and `entity->position` using `entity->animTime / ENTITY_ANIM_WALK_DURATION` to smooth movement.
-96
View File
@@ -1,96 +0,0 @@
# Save & Locale
---
## Save system (`src/dusk/save/`)
Slot-based persistent storage. Currently disabled in engine init (commented out in `engine.c`) — the system is fully implemented but not yet wired up.
### Slots
Up to `SAVE_FILE_COUNT_MAX` (3) save slots. The global `SAVE` holds all slots:
```c
saveInit();
saveExists(slot); // bool_t — check before load
saveLoad(slot); // read from platform storage → SAVE.files[slot]
saveWrite(slot); // write SAVE.files[slot] → platform storage
saveDelete(slot);
savefile_t *f = saveGet(slot);
saveDispose();
```
### Save file format
`savefile_t` is the serialized struct stored per slot. Currently minimal:
```c
typedef struct {
char_t header[3]; // "DSK"
uint32_t version; // SAVE_FILE_VERSION = 1
bool_t exists;
} savefile_t;
```
Extend this struct to add game-specific save data (player position, story flags, etc.).
### Stream serialization (`save/savestream.h`)
`savestream_t` is a cursor used to read/write a save slot's bytes. It CRC32-checksums all data written through it and verifies the checksum on read.
Write a save:
```c
savestream_t stream;
// (platform opens stream for slot)
saveFileWriteHeader(&stream, SAVE_FILE_HEADER);
saveFileWriteVersion(&stream, SAVE_FILE_VERSION);
saveFileWriteBool(&stream, myFlag);
saveFileWriteInt32(&stream, myInt);
saveFileWriteString(&stream, myString, sizeof(myString));
saveStreamFinalizeWriteImpl(&stream); // writes CRC
```
Read a save:
```c
saveFileReadHeader(&stream, headerBuf);
saveFileReadVersion(&stream, &version);
saveFileReadBool(&stream, &myFlag);
saveFileReadInt32(&stream, &myInt);
saveFileReadString(&stream, myString, sizeof(myString));
saveStreamVerifyChecksumImpl(&stream, slot); // returns error if CRC mismatch
```
All multi-byte values are stored in little-endian byte order. The `saveFile*` macros are thin wrappers over the `*Impl` functions that integrate `errorChain` — always use the macros.
### Platform backends
Each `src/dusk{platform}/save/` provides `saveplatform_t` (e.g. a file path on Linux, a memory-card handle on GameCube). The stream implementations (`savestream{platform}.c`) do the actual I/O.
---
## Locale / i18n (`src/dusk/locale/`)
Translations are stored as GNU `.po` files in `assets/locale/`. Only `en_US.po` currently exists.
### Loading
`localemanager_t LOCALE` tracks the active locale and its in-progress asset entry:
```c
localeManagerInit(); // loads en_US by default
localeManagerSetLocale(&LOCALE_EN_US); // switch locale (async load)
localeManagerDispose();
```
`LOCALE_EN_US` is a predefined `localeinfo_t` constant (`name = "en-US"`, `file = "locale/en_US.po"`).
### Looking up strings
```c
char_t buf[128];
localeManagerGetText("my.key", buf, sizeof(buf), 1, /* format args */ );
```
The macro handles plural forms and `printf`-style format arguments. Pass plural `1` for singular, any other value for plural.
`assetlocaleloader.c` parses the `.po` format (msgid / msgstr pairs) into a key→string table during the async asset load phase.
-382
View File
@@ -1,382 +0,0 @@
# Coding Style
All source is C11. Everything below is derived from the existing codebase — match it exactly.
---
## File structure
### Headers (`.h`)
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "direct/dependency.h"
```
- `#pragma once` always, never `#ifndef` guards.
- No blank line between the license block and `#pragma once`.
- One blank line between `#pragma once` and the first `#include`.
### Sources (`.c`)
```c
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "thisfile.h"
#include "assert/assert.h"
#include "util/memory.h"
```
- First include is always the matching `.h` for this `.c` file.
- Remaining includes follow with no separator unless logically grouped (then one blank line between groups — see [Include order](#include-order)).
---
## Include order
In `.c` files, include in this order with a blank line between each group:
1. The matching header (e.g. `#include "entity.h"`)
2. Core utilities (`assert/assert.h`, `util/memory.h`, `util/math.h`, etc.)
3. Engine subsystems (`display/...`, `input/...`, etc.)
4. Domain subsystems (`rpg/...`, `scene/...`, etc.)
In `.h` files, only include what the header directly requires. Never include more than necessary to make the type definitions in that header compile.
All include paths are relative to `src/dusk/` (the root include directory). Use the full path:
```c
#include "rpg/overworld/map.h" // correct
#include "map.h" // wrong
```
---
## Line length
80-character limit. Break before it, not after.
Multi-parameter function signatures break one param per line, with 2-space indent, closing `)` on its own line before `{`:
```c
errorret_t textureInit(
texture_t *texture,
const int32_t width,
const int32_t height,
const textureformat_t format,
const texturedata_t data
) {
```
Same rule for calls that don't fit on one line:
```c
assertTrue(
data.paletted.palette->count ==
mathNextPowTwo(data.paletted.palette->count),
"Palette color count must be a power of 2"
);
```
---
## Indentation and spacing
- **2 spaces** per indent level. No tabs.
- No space between a control keyword and its `(`:
```c
if(x) { // correct
if (x) { // wrong
```
- No space between a function name and its `(` in either declarations or calls.
- Opening brace on the same line:
```c
void entityUpdate(entity_t *entity) {
if(x) {
for(int i = 0; i < n; i++) {
```
- Closing brace always on its own line, except `} else {` and `} while(...)`.
- One blank line between function definitions in `.c` files.
- No trailing whitespace.
---
## Naming
| Kind | Convention | Example |
|---|---|---|
| Types (struct/union/typedef) | `snake_case_t` | `entity_t`, `worldpos_t` |
| Struct tags | `struct name_s` | `struct entity_s` |
| Union tags | `union name_u` | `union texturedata_u` |
| Enum tags (when typedef'd separately) | `name_enum_t` | `entitytype_enum_t` |
| Functions | `subsystemVerb` (camelCase, noun-first) | `entityInit`, `mapGetTile` |
| Macro constants | `UPPER_SNAKE_CASE` | `CHUNK_WIDTH`, `FIXED_ONE` |
| Function-like macros | `camelCase` (same as functions) | `errorThrow`, `assertNotNull` |
| Global subsystem instances | `UPPER_SNAKE_CASE` | `ENGINE`, `MAP`, `ENTITIES` |
| Local variables | `camelCase` | `tileNew`, `spriteCount` |
| Parameters | `camelCase` | `texture`, `worldPos` |
Subsystem prefix always comes first in function names: `textureInit`, `shaderBind`, `spriteBatchFlush`. The verb describes the action: `Init`, `Update`, `Dispose`, `Get`, `Set`, `Is`, etc.
---
## Typedefs
### Structs
```c
typedef struct {
uint8_t id;
entitytype_t type;
} entity_t;
```
Use a named tag (`struct entity_s`) only when forward declaration is required:
```c
typedef struct entity_s {
// ...
} entity_t;
```
### Unions
```c
typedef union texturedata_u {
struct {
uint8_t *indices;
palette_t *palette;
} paletted;
color_t *rgbaColors;
} texturedata_t;
```
### Enums
When the enum values need to be a compact integer (common for arrays and flags), declare the enum separately and typedef an integer type:
```c
typedef enum {
ENTITY_TYPE_NULL,
ENTITY_TYPE_PLAYER,
ENTITY_TYPE_NPC,
ENTITY_TYPE_COUNT
} entitytype_enum_t;
typedef uint8_t entitytype_t; // actual type used everywhere
```
Always include `_NULL` as the first value (zero) and `_COUNT` as the last value.
---
## `#define` constants
All-caps, underscores. Wrap multi-token expressions in parentheses:
```c
#define CHUNK_WIDTH 16
#define CHUNK_HEIGHT CHUNK_WIDTH
#define CHUNK_TILE_COUNT (CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH)
```
Multi-line macros: backslash continuation, body indented 2 spaces, closing line has no backslash:
```c
#define errorThrow(message, ...) \
return errorThrowImpl(\
&ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, (message), \
##__VA_ARGS__ \
)
```
---
## `const` usage
Mark every pointer and value parameter `const` unless the function modifies it:
```c
void entityTurn(entity_t *entity, const entitydir_t direction);
errorret_t textureInit(texture_t *texture, const int32_t width, ...);
```
`entity_t *entity` is non-const because the function writes to it; `direction` is const because it is read-only.
---
## `void` in no-argument functions
Use `(void)` in definitions and declarations of zero-parameter functions:
```c
errorret_t engineUpdate(void);
errorret_t spriteBatchFlush(void);
```
---
## Global subsystem state
Each subsystem exposes a single global instance declared `extern` in the header and defined (once) in the `.c` file:
```c
// entity.h
extern entity_t ENTITIES[ENTITY_COUNT];
// entity.c
entity_t ENTITIES[ENTITY_COUNT];
```
Never define a subsystem global as `static` in a header.
---
## Assertions
Place assertions at the very top of a function, before any logic:
```c
void entityInit(entity_t *entity, const entitytype_t type) {
assertNotNull(entity, "Entity pointer cannot be NULL");
assertTrue(type < ENTITY_TYPE_COUNT, "Invalid entity type");
// ... actual logic
}
```
Available assertion macros (from `assert/assert.h`):
- `assertNotNull(ptr, msg)`
- `assertNull(ptr, msg)`
- `assertTrue(expr, msg)`
- `assertFalse(expr, msg)`
- `assertUnreachable(msg)`
- `assertStringEqual(a, b, msg)`
- `assertIsMainThread(msg)` / `assertNotMainThread(msg)`
---
## Error handling style
Functions that can fail return `errorret_t`. Three patterns:
```c
// Propagate a child call's failure and return from this function:
errorChain(someCall());
// Return success:
errorOk();
// Return failure:
errorThrow("Descriptive message %s", variable);
```
`errorChain` is used inline — do not capture the result first:
```c
errorChain(textureInitPlatform(texture, width, height, format, data)); // correct
errorret_t r = textureInitPlatform(...); errorChain(r); // wrong
```
---
## Struct initialization
Use C99 designated initializers for any struct literal with more than one field:
```c
static const entitycallback_t ENTITY_CALLBACKS[ENTITY_TYPE_COUNT] = {
[ENTITY_TYPE_NULL] = { NULL },
[ENTITY_TYPE_PLAYER] = {
.init = playerInit,
.movement = playerInput
},
};
```
```c
shadermaterial_t material = {
.unlit = {
.color = COLOR_WHITE,
.texture = NULL
}
};
```
---
## Fixed-size array iteration
Prefer pointer arithmetic with `do/while` over index loops for iterating through fixed global arrays:
```c
entity_t *ent = ENTITIES;
do {
if(ent->type == ENTITY_TYPE_NULL) continue;
// ...
} while(++ent, ent < &ENTITIES[ENTITY_COUNT]);
```
Use `for` loops when an index variable is actually needed.
---
## Comments
Comments explain *why*, not *what*. One short inline comment is fine; multi-line block comments for non-obvious invariants only.
```c
// Walking up a ramp — only the direction the ramp faces is valid.
if(tileIsRamp(tileCurrent) && ...) {
```
Section labels inside long functions are acceptable:
```c
// Chunks
{
...
}
// Entities
{
...
}
```
Doc comments on public functions use Javadoc style with `@param` / `@return`:
```c
/**
* Gets the tile at the given world position.
*
* @param position The world position.
* @return The tile at that position, or TILE_NULL if the chunk is unloaded.
*/
tile_t mapGetTile(const worldpos_t position);
```
---
## Platform-conditional code
Use the `DUSK_*` compile-definition macros set by `cmake/targets/<target>.cmake`:
```c
#ifdef DUSK_THREAD_PTHREAD
#include "thread/thread.h"
extern pthread_t ASSERT_MAIN_THREAD_ID;
#endif
```
Never use `#ifdef __linux__`, `#ifdef _WIN32`, etc. directly — go through the engine macros.
-177
View File
@@ -1,177 +0,0 @@
# Engine Systems
Smaller systems that support the engine but don't warrant their own file each.
---
## Time (`src/dusk/time/`)
`dusktime_t TIME` tracks fixed and dynamic delta time.
```c
TIME.delta // fixed_t: always DUSK_TIME_STEP (16ms default) on fixed-rate platforms
TIME.time // fixed_t: total elapsed time in seconds
```
On platforms with `DUSK_TIME_DYNAMIC` (Linux/SDL2):
```c
TIME.dynamicDelta // fixed_t: actual time since last frame
TIME.dynamicTime // fixed_t: total elapsed (dynamic)
TIME.dynamicUpdate // bool_t: true when a real tick occurred
```
Call `timeUpdate()` once per frame (before input/logic). `timeGetEpoch()` returns the current wall-clock time as a `dusktimeepoch_t`.
### Epoch time (`time/timeepoch.h`)
`dusktimeepoch_t` stores a Unix timestamp (double) with timezone offset. Utilities:
```c
dusktimeepoch_t e = timeGetEpoch();
timeEpochGetHours(e) // 023
timeEpochGetMinutes(e) // 059
timeEpochGetSeconds(e) // 059
timeEpochGetDayOfMonth(e) // 030
timeEpochGetMonth(e) // 011
timeEpochGetYear(e)
// Format: %Y year, %m month, %d day, %H hour, %M minute, %S second
timeEpochFormat(e, "%Y-%m-%d %H:%M:%S", buf, sizeof(buf));
// Compare: returns -1, 0, 1
timeEpochCompare(a, b);
// Timezone shift:
dusktimeepoch_t local = timeEpochSwitchTimeZone(e, offsetHours);
```
---
## Thread (`src/dusk/thread/`)
Currently only the pthread backend (`DUSK_THREAD_PTHREAD`) exists. Thread objects are `thread_t` — used primarily by the asset loader.
```c
threadInit(&thread, myCallback); // myCallback: void (*)(thread_t *)
threadStart(&thread); // blocks until thread is RUNNING
threadStop(&thread); // requests stop, blocks until STOPPED
// Inside the thread callback:
while(!threadShouldStop(&thread)) { /* work */ }
```
State flow: `STOPPED → STARTING → RUNNING → (STOP_REQUESTED) → STOPPED`.
### Mutex (`thread/threadmutex.h`)
```c
threadMutexInit(&lock);
threadMutexLock(&lock);
threadMutexUnlock(&lock);
threadMutexTryLock(&lock); // non-blocking; returns false if already held
threadMutexWaitLock(&lock); // release lock and sleep until signalled
threadMutexSignal(&lock); // wake a thread waiting on this mutex
threadMutexDispose(&lock);
```
### Thread-local storage
`THREAD_LOCAL` expands to `__thread` (GCC) on pthread platforms. Used for the per-thread error state (`ERROR_STATE` in `error/error.h`).
---
## Event (`src/dusk/event/`)
A fixed-capacity multicast callback list.
```c
// Declare backing arrays (choose a size):
eventcallback_t cbs[4];
void *users[4];
event_t myEvent;
eventInit(&myEvent, cbs, users, 4);
eventSubscribe(&myEvent, myCallback, myUser);
eventUnsubscribe(&myEvent, myCallback);
eventInvoke(&myEvent, params); // calls all subscribers; params passed as-is
```
Callback signature: `void cb(void *params, void *user)`.
`event_t` does not own its callback/user arrays. Always declare them alongside the event in the same struct or as static arrays.
---
## Console (`src/dusk/console/`)
`CONSOLE` is a scrolling in-game terminal for debug output. On POSIX platforms (`DUSK_CONSOLE_POSIX`) it also polls stdin in a thread so commands can be typed during a running session.
```c
consolePrint("Value is %d", x); // printf-style; thread-safe
consoleDraw(); // renders visible history lines to screen
```
Configuration constants (`consoledefs.h`):
- `CONSOLE_LINE_MAX` — 512 chars per line
- `CONSOLE_HISTORY_MAX` — 16 lines of scrollback
- `CONSOLE_EXEC_BUFFER_MAX` — 32 pending exec commands
`CONSOLE.visible` controls whether `consoleDraw()` actually renders anything.
---
## Log (`src/dusk/log/`)
Two simple output functions, implemented per-platform:
```c
logDebug("format %s", arg); // debug output (stdout on Linux, debug channel on consoles)
logError("format %s", arg); // error output; may pause execution on some platforms
```
These go directly to the platform's native output and are not buffered by the console history.
---
## System / Platform (`src/dusk/system/`)
`systemInit()` runs platform-specific startup (e.g. Wii PAD init, PSP kernel setup). Must be the first call in `engineInit()`.
```c
systemplatform_t p = systemGetPlatform(); // SYSTEM_PLATFORM_LINUX, _PSP, etc.
systemdialogtype_t d = systemGetActiveDialogType();
// SYSTEM_DIALOG_TYPE_NONE / RENDER_BLOCKING / TICK_BLOCKING
```
The full platform list is defined via X-macro in `system/systemplatformlist.h`:
| Constant | Value |
|---|---|
| `SYSTEM_PLATFORM_LINUX` | 0 |
| `SYSTEM_PLATFORM_KNULLI` | 1 |
| `SYSTEM_PLATFORM_PSP` | 2 |
| `SYSTEM_PLATFORM_GAMECUBE` | 3 |
| `SYSTEM_PLATFORM_WII` | 4 |
---
## Network (`src/dusk/network/`)
`network_t NETWORK` manages platform connection state. The engine calls `networkInit` / `networkUpdate` / `networkDispose`; game code uses the request API:
```c
networkRequestConnection(onConnected, onFailed, onDisconnect, user);
networkRequestDisconnection(onComplete, user);
networkIsConnected(); // bool_t
```
State machine: `DISCONNECTED → CONNECTING → CONNECTED → DISCONNECTING → DISCONNECTED`.
Network address info (after connection):
```c
networkinfo_t info = networkGetInfo();
// info.type = NETWORK_TYPE_IPV4 / IPV6
// info.ipv4.ip[4] or info.ipv6.ip[16]
```
Platform backends implement `networkPlatformInit/Update/Dispose/IsConnected`. Currently only Linux (socket-based) and PSP/Vita are implemented.
-123
View File
@@ -1,123 +0,0 @@
# UI System
Source: `src/dusk/ui/`
The UI system is an immediate-mode layer drawn on top of the game scene each frame. Elements register themselves; `uiUpdate()` ticks all elements and `uiRender()` draws them. All coordinates are in screen pixels.
---
## Lifecycle
```
uiInit() → uiTextboxInit() // init order matters — textbox depends on display being ready
uiUpdate() // each frame, before rendering
uiRender() // each frame, after scene render
uiDispose()
uiTextboxDispose()
```
---
## Element registration (`ui/uielement.h`)
Elements are stored in `UI_ELEMENTS[]`. Each has a type and a `draw` callback:
```c
typedef struct {
uielementtype_t type;
errorret_t (*draw)();
} uielement_t;
```
Currently `UI_ELEMENT_TYPE_NATIVE` elements call their `draw` function directly. New debug/HUD elements are registered by adding to this array.
---
## Textbox (`ui/uitextbox.h`)
`UI_TEXTBOX` is the global dialogue box. It word-wraps text, paginates it, and plays a typewriter scroll effect.
```c
uiTextboxSetText("Long dialogue string..."); // wraps to charsPerLine, paginates
uiTextboxUpdate(); // each frame: advance scroll, check input
uiTextboxDraw(); // draw box + text
// Pagination:
uiTextboxPageIsComplete() // true when all chars of current page are visible
uiTextboxHasNextPage()
uiTextboxNextPage()
// Subscibe to page events:
eventSubscribe(&UI_TEXTBOX.onPageComplete, cb, user);
eventSubscribe(&UI_TEXTBOX.onLastPage, cb, user);
```
`UI_TEXTBOX.advanceAction` defaults to the input action that advances dialogue — set it before calling `uiTextboxInit()` if the default doesn't suit.
Layout constants:
- `UI_TEXTBOX_TEXT_MAX` — 1024 chars
- `UI_TEXTBOX_LINES_MAX` — 64 lines
- `UI_TEXTBOX_LINES_PER_PAGE_MAX` — 3 lines visible at once
- `UI_TEXTBOX_SCROLL_CHARS_PER_TICK` — 1 char per tick (typewriter speed)
The textbox uses `UI_TEXTBOX.frame` (a `uiframe_t`) for its border rendering and `UI_TEXTBOX.font` for text.
---
## UI Frame (`ui/uiframe.h`)
9-slice bordered box rendered with a tileset:
```c
uiFrameInit(&frame);
uiFrameDraw(&frame, x, y, width, height);
uiFrameDispose(&frame);
```
The tileset is loaded from the asset system during `uiFrameInit()`. The 9 slices are arranged in the tileset grid as: top-left corner, top edge, top-right corner, left edge, fill, right edge, bottom-left, bottom edge, bottom-right.
---
## Loading overlay (`ui/uiloading.h`)
`UI_LOADING` is a full-screen loading indicator with fade-in/fade-out transitions:
```c
uiLoadingShow(onShownCallback, user); // fade in; calls callback when fully opaque
uiLoadingHide(onHiddenCallback, user); // fade out; calls callback when fully transparent
uiLoadingUpdate(delta); // each frame
uiLoadingDraw(); // each frame, over everything
```
`UI_LOADING_FADE_DURATION` is `FIXED(0.5f)` seconds. Subscribe to `UI_LOADING.onTransitionEnd` for completion events.
---
## Full-box overlay (`ui/uifullbox.h`)
Two global full-screen color overlays: `UI_FULLBOX_UNDER` (drawn before game content) and `UI_FULLBOX_OVER` (drawn after). Used for scene transitions (fade to black, etc.):
```c
uiFullboxTransition(
&UI_FULLBOX_OVER,
COLOR_TRANSPARENT, COLOR_BLACK,
FIXED(0.5f),
EASING_IN_OUT_CUBIC
);
eventSubscribe(&UI_FULLBOX_OVER.onTransitionEnd, myCallback, NULL);
uiFullboxUnderDraw(); // draw under layer
uiFullboxOverDraw(); // draw over layer
```
---
## FPS counter (`ui/uifps.h`)
`UIFPS` tracks a rolling average FPS. `uiFPSDraw()` renders it in the corner. Currently drawn as part of the debug HUD (not wired to an element slot).
---
## Player position HUD (`ui/uiplayerpos.h`)
`uiplayerpos.c` draws the player's current world tile coordinates. Debug overlay, currently drawn directly in the scene render.
-162
View File
@@ -1,162 +0,0 @@
# Utilities
Source: `src/dusk/util/`
---
## String (`util/string.h`)
**Always use these instead of stdlib equivalents** (`strcmp`, `strcpy`, `sprintf`, etc.).
```c
stringCopy(dest, src, destSize); // safe strncpy; always null-terminates
stringCompare(a, b); // -1 / 0 / 1
stringEquals(a, b); // bool_t
stringCompareInsensitive(a, b); // case-insensitive -1 / 0 / 1
stringTrim(str); // in-place strip leading/trailing whitespace
stringFindLastChar(str, c); // last occurrence pointer or NULL
stringFormat(dest, destSize, fmt, ...); // snprintf wrapper; pass NULL dest to get length
stringFormatVA(dest, destSize, fmt, args); // va_list version
stringIsWhitespace(c); // bool_t
// Parse:
stringToI32(str, &out) bool_t
stringToI64(str, &out) bool_t
stringToI16(str, &out) bool_t
stringToU16(str, &out) bool_t
stringToF32(str, &out) bool_t
// Suffix checks:
stringEndsWith(str, suffix) bool_t
stringEndsWithCaseInsensitive(str, suffix) bool_t
stringIncludesString(haystack, needle) bool_t
```
---
## Memory (`util/memory.h`)
**Always use these instead of `malloc`/`free`/`memcpy`/`memset` directly.**
```c
memoryAllocate(size) void * // malloc + tracks pointer count
memoryAlign(alignment, size) void * // aligned malloc
memoryFree(ptr) // free + decrements count
memoryReallocate(&ptr, size) // realloc
memoryResize(&ptr, oldSize, newSize) // realloc + copy (safe reshape)
memoryTrack(ptr) // track externally-malloc'd pointer
memoryCopy(dest, src, size) // memcpy
memoryMove(dest, src, size) // memmove
memorySet(dest, value, size) // memset
memoryZero(dest, size) // memset 0
memoryCompare(a, b, size) int_t // memcmp
// Useful for uploading vertex data with different source/dest layouts:
memoryCopyInterleaved(dest, destStride, src, srcStride, elementSize, count);
memoryCopyRangeSafe(dest, start, end, sizeMax); // copy with bounds check
memoryGetAllocatedCount() size_t // current malloc'd block count
```
---
## Math (`util/math.h`)
```c
mathNextPowTwo(value) uint32_t // next power of two >= value
mathMax(a, b) // macro
mathMin(a, b) // macro
mathClamp(x, lower, upper) // macro
mathAbs(amt) // macro
mathModFloat(x, y) float_t // always non-negative modulo
mathLerp(a, b, t) float_t // linear interpolation (floats)
```
For fixed-point lerp use `fixedLerp(a, b, t)` from `util/fixed.h`.
`MATH_PI` is defined as `M_PI`.
---
## Fixed-point (`util/fixed.h`)
Q24.8 format: 24-bit integer part, 8-bit fractional part (`int32_t`). See [architecture.md](architecture.md#fixed-point-math) for the full API.
Quick reference:
```c
FIXED(1.5f) // compile-time literal
fixedFromI32(3) // runtime int → fixed
fixedToFloat(f) // fixed → float (only for GL/platform APIs)
fixedMul(a, b) // multiplication (not just addition)
fixedDiv(a, b) // division
fixedLerp(a, b, t) // lerp where t ∈ [0, FIXED_ONE]
```
---
## Array (`util/array.h`)
```c
arrayReverse(array, count, size); // in-place reverse; size = sizeof(element)
```
---
## Sort (`util/sort.h`)
```c
sortQuick(array, count, size, compare); // quicksort
sortBubble(array, count, size, compare); // bubble sort (small arrays)
sort(array, count, size, compare); // macro alias for sortQuick
// Convenience uint8_t sorter:
sortArrayU8(array, count);
int sortArrayU8Compare(const void *a, const void *b); // comparator
```
`sortcompare_t` matches `qsort` comparator signature: `int (*)(const void *, const void *)`.
---
## Reference counting (`util/ref.h`)
```c
refInit(&ref, dataPtr, onLock, onUnlock, onAllUnlocked);
refLock(&ref); // increments count, calls onLock
bool_t hit_zero = refUnlock(&ref); // decrements; calls onAllUnlocked when 0
```
Used internally by the asset entry system to track how many callers hold a reference to a loaded asset entry.
---
## CRC32 (`util/crypt.h`)
```c
// One-shot:
uint32_t crc = cryptCRC32(data, size);
// Streaming:
uint32_t acc = cryptCRC32Begin();
cryptCRC32Update(&acc, chunk1, len1);
cryptCRC32Update(&acc, chunk2, len2);
uint32_t final = cryptCRC32End(acc);
```
Used by the save stream to checksum save files.
---
## Endian (`util/endian.h`)
All serialized data (save files, asset headers) is little-endian. Convert to/from host byte order:
```c
uint32_t val = endianLittleToHost32(rawU32);
uint16_t val = endianLittleToHost16(rawU16);
float_t val = endianLittleToHostFloat(rawFloat);
```
`isHostLittleEndian()` returns a bool at runtime. The compile-time defines `DUSK_PLATFORM_ENDIAN_LITTLE` / `DUSK_PLATFORM_ENDIAN_BIG` are set by `cmake/targets/<target>.cmake`.
+9 -86
View File
@@ -1,8 +1,13 @@
name: Build Dusk
on:
push:
tags:
- '*'
branches:
- main
pull_request:
branches:
- main
jobs:
run-tests:
runs-on: ubuntu-latest
@@ -50,42 +55,6 @@ jobs:
path: ./git-artifcats/Dusk
if-no-files-found: error
# build-vita:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v6
# - name: Set up Docker
# uses: docker/setup-docker-action@v5
# - name: Build Vita
# run: ./scripts/build-vita-docker.sh
# - name: Upload Vita binary
# uses: actions/upload-artifact@v6
# with:
# name: dusk-vita
# path: build-vita/Dusk.vpk
# if-no-files-found: error
build-knulli:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build knulli
run: ./scripts/build-knulli-docker.sh
- name: Move output to Dusk subfolder
run: |
mkdir -p ./git-artifcats/Dusk
cp -r build-knulli/dusk ./git-artifcats/Dusk
- name: Upload knulli binary
uses: actions/upload-artifact@v6
with:
name: dusk-knulli
path: ./git-artifcats/Dusk
if-no-files-found: error
build-gamecube:
runs-on: ubuntu-latest
steps:
@@ -105,29 +74,6 @@ jobs:
with:
name: dusk-gamecube
path: ./git-artifcats/Dusk
if-no-files-found: error
build-gamecube-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build GameCube ISO
run: ./scripts/build-gamecube-iso-docker.sh
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk
cp build-gamecube-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
cp build-gamecube-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
cp build-gamecube-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
- name: Upload GameCube ISO
uses: actions/upload-artifact@v6
with:
name: dusk-gamecube-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii:
runs-on: ubuntu-latest
@@ -141,34 +87,11 @@ jobs:
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/Dusk.dol
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
cp docker/dolphin/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
- name: Upload Wii binary
uses: actions/upload-artifact@v6
with:
name: dusk-wii
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build Wii ISO
run: ./scripts/build-wii-iso-docker.sh
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk
cp build-wii-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
cp build-wii-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
cp build-wii-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
- name: Upload Wii ISO
uses: actions/upload-artifact@v6
with:
name: dusk-wii-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
+1 -3
View File
@@ -83,6 +83,7 @@ assets/borrowed
.VSCode*
/vita
._*
*~
@@ -104,6 +105,3 @@ yarn.lock
/build2
/build*
/assets/test
/tools_old
/assets/test.png
-65
View File
@@ -1,65 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
Dusk is a C11 RPG game targeting resource-constrained hardware (PSP, GameCube, Wii, PS Vita, Knulli handhelds) and Linux/OpenGL. All game code lives in `src/dusk/`; platform-specific backends live in `src/dusk{platform}/` (e.g. `src/duskgl/`, `src/duskpsp/`, `src/duskdolphin/`).
Assets are zipped into `dusk.dsk` at build time and loaded at runtime via the asset system.
## Build
```bash
# Linux (host)
./scripts/build-linux.sh # outputs build-linux/Dusk
# Other targets (require Docker)
./scripts/build-psp-docker.sh
./scripts/build-gamecube-docker.sh
./scripts/build-wii-docker.sh
./scripts/build-knulli-docker.sh
```
Each script is a thin wrapper around:
```bash
cmake -S . -B build-<target> -DDUSK_TARGET_SYSTEM=<target>
cmake --build build-<target> -- -j$(nproc)
```
## Tests
```bash
./scripts/test-linux.sh # builds and runs all tests
# Manually run a single test binary after building:
./build-tests/test/<module>/test_<name>
```
Tests use [cmocka](https://cmocka.org/) and are only compiled when `DUSK_BUILD_TESTS=ON`. Test sources live under `test/`.
## Key conventions
- Use `stringCompare`, `stringCopy`, `stringEquals`, etc. from `util/string.h` — never `strcmp`, `strcpy`, and friends directly.
- All functions that can fail return `errorret_t`. Use `errorThrow(...)`, `errorChain(call())`, and `errorOk()` macros — see `error/error.h`.
- Positions and game-world values use `fixed_t` (Q24.8 fixed-point, `int32_t`) — not `float`. Use `FIXED(x)` for literals and the `fixedFrom*`/`fixedTo*` helpers in `util/fixed.h`.
## Coding style
See [`.claude/style.md`](.claude/style.md) for the full style guide: indentation, line length, naming, typedefs, defines, include order, `const` usage, assertion placement, error handling, struct initialization, and platform conditionals.
## Architecture & systems
| Doc | Covers |
|---|---|
| [`.claude/architecture.md`](.claude/architecture.md) | Platform abstraction pattern, subsystem lifecycle, error handling, code-generation pipeline |
| [`.claude/display.md`](.claude/display.md) | Rendering pipeline: display state, screen, framebuffer, mesh, shader, texture, spritebatch, text |
| [`.claude/input.md`](.claude/input.md) | Input actions, buttons, bindings, axis helpers, events |
| [`.claude/asset.md`](.claude/asset.md) | Asset archive, entry lifecycle, loader types, async/sync split, low-level file I/O |
| [`.claude/ui.md`](.claude/ui.md) | UI element system, textbox, frames, loading overlay, fullbox transitions, FPS counter |
| [`.claude/animation.md`](.claude/animation.md) | Keyframe animation, easing functions |
| [`.claude/systems.md`](.claude/systems.md) | Time, threading, mutex, events, console, logging, system/platform, network |
| [`.claude/util.md`](.claude/util.md) | String, memory, math, fixed-point, array, sort, ref counting, CRC32, endian |
| [`.claude/save.md`](.claude/save.md) | Save slots, stream serialization with CRC, locale/i18n |
| [`.claude/rpg/index.md`](.claude/rpg/index.md) | RPG layer overview → [world](.claude/rpg/world.md), [entities](.claude/rpg/entity.md), [cutscenes](.claude/rpg/cutscene.md), [story/items](.claude/rpg/story.md) |
| [`.claude/display-refactor.md`](.claude/display-refactor.md) | Planned render-queue refactor (Saturn port context) |
+2 -19
View File
@@ -4,22 +4,14 @@
# https://opensource.org/licenses/MIT
# Setup
cmake_minimum_required(VERSION 3.13)
cmake_minimum_required(VERSION 3.18)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF)
# Game identity — override these per-project
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YouWish" CACHE STRING "Game author / coder")
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
set(DUSK_GAME_LONG_DESCRIPTION "No description yet." CACHE STRING "Full description")
# Prep cache
set(DUSK_CACHE_TARGET "dusk-target")
@@ -50,7 +42,7 @@ file(MAKE_DIRECTORY ${DUSK_TEMP_DIR})
file(MAKE_DIRECTORY ${DUSK_BUILT_ASSETS_DIR})
# Required build packages
find_package(Python3 COMPONENTS Interpreter REQUIRED)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# Init Project.
project(${DUSK_LIBRARY_TARGET_NAME}
@@ -76,19 +68,10 @@ else()
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET})
endif()
if(NOT DEFINED DUSK_VERSION)
string(TIMESTAMP DUSK_VERSION "debug-%y%m%d%H%M%S")
endif()
# Definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}"
DUSK_GAME_NAME="${DUSK_GAME_NAME}"
DUSK_GAME_AUTHOR="${DUSK_GAME_AUTHOR}"
DUSK_GAME_SHORT_DESCRIPTION="${DUSK_GAME_SHORT_DESCRIPTION}"
DUSK_GAME_LONG_DESCRIPTION="${DUSK_GAME_LONG_DESCRIPTION}"
DUSK_VERSION="${DUSK_VERSION}"
)
# Toolchains
-107
View File
@@ -1,107 +0,0 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "camera.h"
#include "display/display.h"
#include "assert/assert.h"
#include "display/framebuffer/framebuffer.h"
#include "display/screen/screen.h"
void cameraInit(camera_t *camera) {
cameraInitPerspective(camera);
}
void cameraInitPerspective(camera_t *camera) {
assertNotNull(camera, "Not a camera component");
camera->projType = CAMERA_PROJECTION_TYPE_PERSPECTIVE;
camera->perspective.fov = glm_rad(45.0f);
camera->nearClip = 0.1f;
camera->farClip = 10000.0f;
camera->viewType = CAMERA_VIEW_TYPE_LOOKAT;
glm_vec3_copy((vec3){ 5.0f, 5.0f, 5.0f }, camera->lookat.position);
glm_vec3_copy((vec3){ 0.0f, 1.0f, 0.0f }, camera->lookat.up);
glm_vec3_copy((vec3){ 0.0f, 0.0f, 0.0f }, camera->lookat.target);
}
void cameraInitOrthographic(camera_t *camera) {
assertNotNull(camera, "Not a camera component");
camera->projType = CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC;
camera->orthographic.left = 0.0f;
camera->orthographic.right = SCREEN.width;
camera->orthographic.top = SCREEN.height;
camera->orthographic.bottom = 0.0f;
camera->nearClip = 0.1f;
camera->farClip = 1.0f;
camera->viewType = CAMERA_VIEW_TYPE_2D;
glm_vec2_copy((vec2){ 0.0f, 0.0f }, camera->_2d.position);
camera->_2d.zoom = 1.0f;
}
void cameraGetProjectionMatrix(camera_t *camera, mat4 dest) {
assertNotNull(camera, "Not a camera component");
assertNotNull(dest, "Destination matrix must not be null");
if(
camera->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
camera->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
) {
glm_mat4_identity(dest);
glm_perspective(
camera->perspective.fov,
SCREEN.aspect,
camera->nearClip,
camera->farClip,
dest
);
if(camera->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
dest[1][1] *= -1.0f;
}
} else if(camera->projType == CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
glm_mat4_identity(dest);
glm_ortho(
camera->orthographic.left,
camera->orthographic.right,
camera->orthographic.top,
camera->orthographic.bottom,
camera->nearClip,
camera->farClip,
dest
);
}
}
void cameraGetViewMatrix(camera_t *camera, mat4 dest) {
assertNotNull(camera, "Not a camera component");
assertNotNull(dest, "Destination matrix must not be null");
if(camera->viewType == CAMERA_VIEW_TYPE_MATRIX) {
glm_mat4_ucopy(camera->view, dest);
} else if(camera->viewType == CAMERA_VIEW_TYPE_LOOKAT) {
glm_mat4_identity(dest);
glm_lookat(
camera->lookat.position,
camera->lookat.target,
camera->lookat.up,
dest
);
} else if(camera->viewType == CAMERA_VIEW_TYPE_2D) {
glm_mat4_identity(dest);
glm_lookat(
(vec3){ camera->_2d.position[0], camera->_2d.position[1], 0.5f },
(vec3){ camera->_2d.position[0], camera->_2d.position[1], 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
dest
);
} else if(camera->viewType == CAMERA_VIEW_TYPE_LOOKAT_PIXEL_PERFECT) {
assertUnreachable("LOOKAT_PIXEL_PERFECT view type is not implemented yet");
}
}
+2 -2
View File
@@ -156,7 +156,7 @@ class Map:
newTopLeftChunkY = y // CHUNK_HEIGHT - (MAP_HEIGHT // 2)
newTopLeftChunkZ = z // CHUNK_DEPTH - (MAP_DEPTH // 2)
if(newTopLeftChunkX != self.topLeftX or
if (newTopLeftChunkX != self.topLeftX or
newTopLeftChunkY != self.topLeftY or
newTopLeftChunkZ != self.topLeftZ):
@@ -166,7 +166,7 @@ class Map:
chunkWorldX = chunk.x
chunkWorldY = chunk.y
chunkWorldZ = chunk.z
if(chunkWorldX < newTopLeftChunkX or
if (chunkWorldX < newTopLeftChunkX or
chunkWorldX >= newTopLeftChunkX + MAP_WIDTH or
chunkWorldY < newTopLeftChunkY or
chunkWorldY >= newTopLeftChunkY + MAP_HEIGHT or
@@ -8,4 +8,6 @@
#pragma once
#include "dusk.h"
typedef void (*cutscenecallback_t)(void);
typedef struct {
void *nothing;
} inventory_t;
+1
View File
@@ -16,6 +16,7 @@ typedef enum {
typedef struct {
rpgcameramode_t mode;
union {
worldpos_t free;
struct {
-21
View File
@@ -1,21 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli',
[System.PLATFORM_PSP]: 'PSP',
[System.PLATFORM_GAMECUBE]: 'GameCube',
[System.PLATFORM_WII]: 'Wii',
};
Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
UIFullboxOver.setColor(Color.BLACK);
requireAsync('testscene.js').then(Scene.set).catch(err => {
Console.print('Error loading scene: ' + err);
Engine.exit();
});
+77
View File
@@ -0,0 +1,77 @@
module('input')
module('platform')
module('scene')
module('locale')
-- Default Input bindings.
if PSP then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("circle", INPUT_ACTION_CANCEL)
inputBind("cross", INPUT_ACTION_ACCEPT)
inputBind("select", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif DOLPHIN then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("b", INPUT_ACTION_CANCEL)
inputBind("a", INPUT_ACTION_ACCEPT)
inputBind("z", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif LINUX then
if INPUT_KEYBOARD then
inputBind("w", INPUT_ACTION_UP)
inputBind("s", INPUT_ACTION_DOWN)
inputBind("a", INPUT_ACTION_LEFT)
inputBind("d", INPUT_ACTION_RIGHT)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("enter", INPUT_ACTION_ACCEPT)
inputBind("e", INPUT_ACTION_ACCEPT)
inputBind("q", INPUT_ACTION_CANCEL)
inputBind("escape", INPUT_ACTION_RAGEQUIT)
end
if INPUT_GAMEPAD then
inputBind("gamepad_up", INPUT_ACTION_UP)
inputBind("gamepad_down", INPUT_ACTION_DOWN)
inputBind("gamepad_left", INPUT_ACTION_LEFT)
inputBind("gamepad_right", INPUT_ACTION_RIGHT)
inputBind("gamepad_a", INPUT_ACTION_ACCEPT)
inputBind("gamepad_b", INPUT_ACTION_CANCEL)
inputBind("gamepad_back", INPUT_ACTION_RAGEQUIT)
inputBind("gamepad_lstick_up", INPUT_ACTION_UP)
inputBind("gamepad_lstick_down", INPUT_ACTION_DOWN)
inputBind("gamepad_lstick_left", INPUT_ACTION_LEFT)
inputBind("gamepad_lstick_right", INPUT_ACTION_RIGHT)
end
if INPUT_POINTER then
inputBind("mouse_x", INPUT_ACTION_POINTERX)
inputBind("mouse_y", INPUT_ACTION_POINTERY)
end
else
print("Unknown platform, no default input bindings set.")
end
sceneSet('scene/minesweeper.lua')
+5 -56
View File
@@ -1,60 +1,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: ExampleApp 1.0\n"
"Language: en\n"
"Language: en_US\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ui/menu.c:10
msgid "ui.title"
msgstr ""
"Welcome"
#: ui/user.c:22
msgid "ui.greeting"
msgstr "Hello, %s!"
#: ui/files.c:40
msgid "ui.file_status"
msgstr "%s has %d files."
#: ui/cart.c:55
msgid "cart.item_count"
msgid_plural "cart.item_count"
msgstr[0] "%d item"
msgstr[1] "%d items (dual)"
msgstr[2] "%d items (few)"
msgstr[3] "%d items (many)"
#: ui/notifications.c:71
msgid ""
"ui.multiline_help"
msgstr ""
"Line one of the help text.\n"
"Line two continues here.\n"
"Line three ends here."
#: ui/errors.c:90
msgid ""
"error.upload_failed.long"
msgstr ""
"Upload failed for file \"%s\".\n"
"Please try again later or contact support."
#: ui/messages.c:110
msgid ""
"user.invite_status"
msgid_plural ""
"user.invite_status"
msgstr[0] ""
"%s invited %d user.\n"
"Please review the request."
msgstr[1] ""
"%s invited %d users (dual).\n"
"Please review the requests."
msgstr[2] ""
"%s invited %d users (few).\n"
"Please review the requests."
msgstr[3] ""
"%s invited %d users (many).\n"
"Please review the requests."
msgid "ui.test"
msgstr "Hello this is a test."
Binary file not shown.
-63
View File
@@ -1,63 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
const PLAYER_SPEED = 5.0;
// 1 world unit = 16 pixels.
const PIXEL_SCALE = 1.0 / 16.0;
// Player sprite is 32x32 px (test.png dimensions).
const PLAYER_W = 32 * PIXEL_SCALE;
const PLAYER_H = 32 * PIXEL_SCALE;
var player = {};
player.getAssets = () => {
return [
{ path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA }
];
}
player.init = function(scene) {
var texture = scene.assets.getAssetByPath('test.png');
Console.print('Player init: got texture ' + texture);
_entity = Entity.create();
_position = _entity.add(Component.POSITION);
_physics = _entity.add(Component.PHYSICS);
_physics.bodyType = Physics.DYNAMIC;
_physics.shape = Physics.SHAPE_CUBE;
_physics.gravityScale = 1.0;
var r = _entity.add(Component.RENDERABLE);
r.texture = texture.texture;
r.type = Renderable.SPRITEBATCH;
r.color = new Color(220, 80, 80);
// Upright quad centered on X, bottom-aligned on Y.
r.sprites = [[-PLAYER_W/2, 0, 0, PLAYER_W/2, PLAYER_H, 0, 0, 1, 1, 0]];
_position.localPosition = new Vec3(0, PLAYER_H, 0);
};
player.getPosition = function() {
return _position;
};
player.update = function() {
if(!_physics) return;
var vx = Input.axis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT) * PLAYER_SPEED;
var vz = Input.axis(INPUT_ACTION_DOWN, INPUT_ACTION_UP) * PLAYER_SPEED;
// Preserve vertical velocity so gravity and landing work correctly.
var vy = _physics.velocity.y;
_physics.velocity = new Vec3(vx, vy, vz);
};
player.dispose = function() {
Entity.dispose(_entity);
_entity = null;
_position = null;
_physics = null;
};
module.exports = player;
+256
View File
@@ -0,0 +1,256 @@
module('spritebatch')
module('camera')
module('color')
module('ui')
module('screen')
module('time')
module('glm')
module('text')
module('tileset')
module('texture')
module('input')
CELL_STATE_DEFAULT = 0
CELL_STATE_HOVER = 1
CELL_STATE_DOWN = 2
CELL_STATE_DISABLED = 3
screenSetBackground(colorCornflowerBlue())
camera = cameraCreate(CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC)
-- tilesetUi = tilesetGetByName("ui")
-- textureUi = textureLoad(tilesetUi.texture)
-- tilesetBorder = tilesetGetByName("border")
-- textureBorder = textureLoad(tilesetBorder.texture)
-- textureGrid = textureLoad("minesweeper/grid_bg.dpi")
-- tilesetCell = tilesetGetByName("cell")
-- textureCell = textureLoad(tilesetCell.texture)
-- cellSliceDefault = tilesetPositionGetUV(tilesetCell, 3, 5)
-- cellSliceHover = tilesetPositionGetUV(tilesetCell, 3, 4)
-- cellSliceDown = tilesetPositionGetUV(tilesetCell, 3, 6)
-- cellSliceDisabled = tilesetPositionGetUV(tilesetCell, 3, 7)
-- sweepwerCols = 10
-- sweeperRows = 14
-- mouseX = -1
-- mouseY = -1
-- centerX = 0
-- centerY = 0
-- boardWidth = sweepwerCols * tilesetCell.tileWidth
-- boardHeight = sweeperRows * tilesetCell.tileHeight
-- i = 0
-- cells = {}
-- for y = 1, sweeperRows do
-- for x = 1, sweepwerCols do
-- cells[i] = CELL_STATE_DEFAULT
-- i = i + 1
-- end
-- end
function cellDraw(x, y, type)
local slice = cellSliceDefault
if type == CELL_STATE_HOVER then
slice = cellSliceHover
elseif type == CELL_STATE_DOWN then
slice = cellSliceDown
elseif type == CELL_STATE_DISABLED then
slice = cellSliceDisabled
end
spriteBatchPush(textureCell,
x, y,
x + tilesetCell.tileWidth, y + tilesetCell.tileHeight,
colorWhite(),
slice.u0, slice.v0,
slice.u1, slice.v1
)
end
function backgroundDraw()
local t = (TIME.time / 40) % 1
local scaleX = screenGetWidth() / textureGrid.width
local scaleY = screenGetHeight() / textureGrid.height
local u0 = t * scaleX
local v0 = t * scaleY
local u1 = scaleX + u0
local v1 = scaleY + v0
spriteBatchPush(textureGrid,
0, 0,
screenGetWidth(), screenGetHeight(),
colorWhite(),
u0, v0,
u1, v1
)
end
function borderDraw(x, y, innerWidth, innerHeight)
-- Top Left
local uv = tilesetPositionGetUV(tilesetBorder, 0, 0)
spriteBatchPush(textureBorder,
x - tilesetBorder.tileWidth, y - tilesetBorder.tileWidth,
x, y,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Top Right
uv = tilesetPositionGetUV(tilesetBorder, 10, 0)
spriteBatchPush(textureBorder,
x + innerWidth, y - tilesetBorder.tileHeight,
x + innerWidth + tilesetBorder.tileWidth, y,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Bottom Left
uv = tilesetPositionGetUV(tilesetBorder, 0, 10)
spriteBatchPush(textureBorder,
x - tilesetBorder.tileWidth, y + innerHeight,
x, y + innerHeight + tilesetBorder.tileHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Bottom Right
uv = tilesetPositionGetUV(tilesetBorder, 10, 10)
spriteBatchPush(textureBorder,
x + innerWidth, y + innerHeight,
x + innerWidth + tilesetBorder.tileWidth, y + innerHeight + tilesetBorder.tileHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Top
uv = tilesetPositionGetUV(tilesetBorder, 1, 0)
spriteBatchPush(textureBorder,
x, y - tilesetBorder.tileHeight,
x + innerWidth, y,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Bottom
uv = tilesetPositionGetUV(tilesetBorder, 1, 10)
spriteBatchPush(textureBorder,
x, y + innerHeight,
x + innerWidth, y + innerHeight + tilesetBorder.tileHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Left
uv = tilesetPositionGetUV(tilesetBorder, 0, 1)
spriteBatchPush(textureBorder,
x - tilesetBorder.tileWidth, y,
x, y + innerHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
-- Right
uv = tilesetPositionGetUV(tilesetBorder, 10, 1)
spriteBatchPush(textureBorder,
x + innerWidth, y,
x + innerWidth + tilesetBorder.tileWidth, y + innerHeight,
colorWhite(),
uv.u0, uv.v0,
uv.u1, uv.v1
)
end
x = 0
y = 0
function sceneDispose()
end
function sceneUpdate()
x = x + inputAxis(INPUT_ACTION_LEFT, INPUT_ACTION_RIGHT)
y = y + inputAxis(INPUT_ACTION_UP, INPUT_ACTION_DOWN)
end
function sceneRender()
-- Update camera
cameraPushMatrix(camera)
camera.bottom = screenGetHeight()
camera.right = screenGetWidth()
spriteBatchPush(
nil,
x, y, x + 32, y + 32,
colorBlue()
)
-- Update mouse position
-- if INPUT_POINTER then
-- mouseX = inputGetValue(INPUT_ACTION_POINTERX) * screenGetWidth()
-- mouseY = inputGetValue(INPUT_ACTION_POINTERY) * screenGetHeight()
-- -- Draw cursor
-- spriteBatchPush(
-- nil,
-- mouseX - 2, mouseY - 2,
-- mouseX + 2, mouseY + 2,
-- colorRed(),
-- 0, 0,
-- 1, 1
-- )
-- end
-- textDraw(10, 10, "Hello World")
-- centerX = math.floor(screenGetWidth() / 2)
-- centerY = math.floor(screenGetHeight() / 2)
-- Draw elements
-- backgroundDraw()
-- borderDraw(
-- centerX - (boardWidth / 2), centerY - (boardHeight / 2),
-- boardWidth, boardHeight
-- )
-- i = 0
-- -- Foreach cell
-- local offX = centerX - (boardWidth / 2)
-- local offY = centerY - (boardHeight / 2)
-- for y = 0, sweeperRows - 1 do
-- for x = 0, sweepwerCols - 1 do
-- i = y * sweepwerCols + x
-- -- Hovered
-- if
-- cells[i] == CELL_STATE_DEFAULT and
-- mouseX >= x * tilesetCell.tileWidth + offX and mouseX < (x + 1) * tilesetCell.tileWidth + offX and
-- mouseY >= y * tilesetCell.tileHeight + offY and mouseY < (y + 1) * tilesetCell.tileHeight + offY
-- then
-- cells[i] = CELL_STATE_HOVER
-- else
-- cells[i] = CELL_STATE_DEFAULT
-- end
-- cellDraw(
-- x * tilesetCell.tileWidth + offX,
-- y * tilesetCell.tileHeight + offY,
-- cells[i]
-- )
-- end
-- end
spriteBatchFlush()
cameraPopMatrix()
end
-42
View File
@@ -1,42 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
var scene = {};
// Pokemon DS-style camera: ~34 degrees elevation (atan(6/9)).
// CAM_HEIGHT / CAM_DIST ratio controls the tilt - keep it under 0.7 for
// the characteristically shallow DS angle.
const CAM_HEIGHT = 6;
const CAM_DIST = 9;
scene.init = async function() {
// Camera
scene.cam = Entity.create();
var camPos = scene.cam.add(Component.POSITION);
var cam = scene.cam.add(Component.CAMERA);
camPos.localPosition = new Vec3(3, 3, 3);
camPos.lookAt(new Vec3(0, 0, 0));
// Floor - large flat slab, no texture needed.
scene.floor = Entity.create();
var floorPos = scene.floor.add(Component.POSITION);
var floorR = scene.floor.add(Component.RENDERABLE);
floorR.type = Renderable.SHADER_MATERIAL;
floorR.color = Color.BLUE;
// floorPos.localScale = new Vec3(16, 0.2, 16);
// floorPos.localPosition = new Vec3(0, -0.1, 0);
await UIFullboxOver.transition(Color.BLACK, Color.TRANSPARENT, 1.0);
};
scene.update = function() {
};
scene.dispose = function() {
Entity.dispose(scene.floor);
Entity.dispose(scene.cam);
};
module.exports = scene;
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

-6
View File
@@ -1,6 +0,0 @@
module = {
render() {
Text.draw(0, 0, "Hello World");
SpriteBatch.flush();
}
};
-31
View File
@@ -1,31 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(FetchContent)
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
)
# Fetch stb if not already done
FetchContent_MakeAvailable(stb)
# Find the stb_image.h header
set(STB_INCLUDE_DIR "${stb_SOURCE_DIR}")
set(STB_IMAGE_HEADER "${stb_SOURCE_DIR}/stb_image.h")
if(EXISTS "${STB_IMAGE_HEADER}")
add_library(stb_image INTERFACE)
target_include_directories(stb_image INTERFACE "${STB_INCLUDE_DIR}")
set(STB_IMAGE_FOUND TRUE)
else()
set(STB_IMAGE_FOUND FALSE)
endif()
# Standard CMake variables
set(STB_IMAGE_INCLUDE_DIRS "${STB_INCLUDE_DIR}")
set(STB_IMAGE_LIBRARIES stb_image)
mark_as_advanced(STB_IMAGE_INCLUDE_DIRS STB_IMAGE_LIBRARIES STB_IMAGE_FOUND)
-18
View File
@@ -1,18 +0,0 @@
include(FetchContent)
if(NOT TARGET yyjson)
FetchContent_Declare(
yyjson
GIT_REPOSITORY https://github.com/ibireme/yyjson.git
GIT_TAG 0.12.0
)
FetchContent_MakeAvailable(yyjson)
endif()
# Provide an imported target if not already available
if(NOT TARGET yyjson::yyjson)
add_library(yyjson::yyjson ALIAS yyjson)
endif()
# Mark yyjson as found for find_package compatibility
set(yyjson_FOUND TRUE)
+33 -45
View File
@@ -1,69 +1,57 @@
# Build type: DOL (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "DOL" CACHE STRING "Dolphin asset source: DOL (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "DOL" "ISO")
# Numeric tokens so #if DUSK_DOLPHIN_BUILD_TYPE == DOL works in C.
# DUSK_DOLPHIN_BUILD_TYPE is passed without quotes so it expands to the identifier.
# Target definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
)
# Custom compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
# Need PkgConfig
find_package(PkgConfig REQUIRED)
pkg_check_modules(zip IMPORTED_TARGET libzip)
# Disable all warnings
target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w)
# cglm: fetched at source level via Findcglm.cmake (FetchContent, headers only)
# Custom flags for cglm
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Pre-create ZLIB::ZLIB so any downstream cmake module that resolves it
# (FindZLIB, pkg-config IMPORTED_TARGET Requires processing) gets plain -lz
# rather than an unresolvable IMPORTED target that the PPC linker rejects.
if(NOT TARGET ZLIB::ZLIB)
add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL)
set_target_properties(ZLIB::ZLIB PROPERTIES INTERFACE_LINK_LIBRARIES "z")
endif()
# Compile lua
include(FetchContent)
FetchContent_Declare(
liblua
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz
)
FetchContent_MakeAvailable(liblua)
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
# Mark libzip as found so src/dusk/CMakeLists.txt skips Findlibzip.cmake.
# Findlibzip.cmake calls find_package(ZLIB) which can recreate a broken
# ZLIB::ZLIB IMPORTED target, bypassing the shim above.
set(libzip_FOUND TRUE CACHE BOOL "libzip found (devkitpro portlibs)" FORCE)
# Locate zip.h in the devkitpro sysroot (respects CMAKE_FIND_ROOT_PATH).
find_path(_dusk_zip_inc NAMES zip.h)
if(_dusk_zip_inc)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE "${_dusk_zip_inc}")
endif()
# Link libraries.
# zip/z/lzma use target_link_options (raw flags) to bypass cmake target
# resolution — pkg-config-generated targets for these carry ZLIB::ZLIB in
# INTERFACE_LINK_LIBRARIES which breaks the PPC link step.
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
cglm
liblua
m
zip
bz2
zstd
z
lzma
fat
PkgConfig::zip
)
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN_BUILD_ISO)
else()
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE fat)
endif()
# Postbuild: ELF -> DOL
# Postbuild
set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND elf2dol
-19
View File
@@ -3,22 +3,3 @@ include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_GAMECUBE
)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
# bba
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"GCN"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building GameCube ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
-46
View File
@@ -1,46 +0,0 @@
# Find link platform-specific libraries
set(OpenGL_GL_PREFERENCE LEGACY)
find_package(SDL2 REQUIRED)
find_library(EGL_LIB EGL REQUIRED)
find_library(GL_LIB GL REQUIRED)
find_package(OpenGL REQUIRED)
# Setup endianess at compile time to optimize.
include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_BIG
)
else()
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_LITTLE
)
endif()
# Link required libraries.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
SDL2
pthread
OpenGL::GLES2
${GL_LIB}
${EGL_LIB}
m
)
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_OPENGL_ES
DUSK_LINUX
DUSK_KNULLI
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
)
-8
View File
@@ -1,7 +1,6 @@
# Find link platform-specific libraries
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
# find_package(CURL REQUIRED)
# Setup endianess at compile time to optimize.
include(TestBigEndian)
@@ -23,17 +22,12 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
OpenGL::GL
GL
m
# CURL::libcurl
)
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_CONSOLE_POSIX
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
@@ -43,6 +37,4 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
DUSK_NETWORK_IPV6
DUSK_THREAD_PTHREAD
)
+3 -20
View File
@@ -1,16 +1,6 @@
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_ARCHIVE_CREATE "$ENV{PSPDEV}/bin/psp-ar qc <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_APPEND "$ENV{PSPDEV}/bin/psp-ar q <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_FINISH "$ENV{PSPDEV}/bin/psp-ranlib <TARGET>")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE)
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
SDL2
pthread
@@ -34,27 +24,20 @@ target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
pspvfpu
pspvram
psphprm
pspnet
pspnet_inet
pspnet_apctl
psphttp
pspssl
)
target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_PSP
DUSK_INPUT_GAMEPAD
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=480
DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD
)
# Postbuild, create .pbp file for PSP.
-88
View File
@@ -1,88 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
if(NOT DEFINED ENV{VITASDK})
message(FATAL_ERROR "VITASDK environment variable is not set.")
endif()
include("$ENV{VITASDK}/share/vita.cmake" REQUIRED)
set(VITA_APP_NAME "Dusk")
set(VITA_TITLEID "DUSK00001")
set(VITA_VERSION "01.00")
find_package(SDL2 REQUIRED)
# Custom flags for cglm
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
cglm
SDL2
SDL2main
zip
bz2
z
zstd
crypto
lzma
m
pthread
stdc++
vitaGL
mathneon
vitashark
kubridge_stub
SceAppMgr_stub
SceAudio_stub
SceCtrl_stub
SceCommonDialog_stub
SceDisplay_stub
SceKernelDmacMgr_stub
SceGxm_stub
SceShaccCg_stub
SceSysmodule_stub
ScePower_stub
SceTouch_stub
SceVshBridge_stub
SceIofilemgr_stub
SceShaccCgExt
libtaihen_stub.a
# SceKernel_stub
SceAppUtil_stub
SceHid_stub
SceRtc_stub
)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_VITA
DUSK_INPUT_GAMEPAD
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=960
DUSK_DISPLAY_HEIGHT=544
)
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE)
# Post-build: package SELF + assets into a .vpk installable on the Vita
vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self
VERSION ${VITA_VERSION}
NAME ${VITA_APP_NAME}
FILE ${DUSK_ASSETS_ZIP} dusk.dsk
)
-22
View File
@@ -3,25 +3,3 @@ include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_WII
)
# Generate Homebrew Channel meta.xml from project identity variables
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
configure_file(
"${CMAKE_SOURCE_DIR}/docker/dolphin/meta.xml.in"
"${DUSK_BUILD_DIR}/meta.xml"
@ONLY
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"WII"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building Wii ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
-29
View File
@@ -1,29 +0,0 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_ASM_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_SYSROOT /)
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH
/usr/aarch64-linux-gnu
/usr/lib/aarch64-linux-gnu
/usr/include/aarch64-linux-gnu
)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_LIBDIR} "/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "/usr/lib/aarch64-linux-gnu/pkgconfig")
set(CMAKE_PREFIX_PATH "/usr/aarch64-linux-gnu;/usr/lib/aarch64-linux-gnu")
# Optional: helps some Find modules
set(SDL2_DIR "/usr/lib/aarch64-linux-gnu/cmake/SDL2" CACHE PATH "")
@@ -0,0 +1,16 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-buildroot-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-buildroot-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/aarch64-buildroot-linux-gnu_sdk-buildroot/aarch64-buildroot-linux-gnu/sysroot)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT})
set(ENV{PKG_CONFIG_LIBDIR} "${CMAKE_SYSROOT}/usr/lib/pkgconfig:${CMAKE_SYSROOT}/usr/share/pkgconfig")
+3 -4
View File
@@ -1,7 +1,6 @@
FROM ghcr.io/extremscorner/libogc2
FROM devkitpro/devkitppc
WORKDIR /workdir
RUN apt update && \
dkp-pacman -Syu --noconfirm && \
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip
VOLUME ["/workdir"]
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>Dusk</name>
<version>1.00</version>
<release_date></release_date>
<coder>YouWish</coder>
<short_description>Dusk game</short_description>
<long_description>No description yet.</long_description>
<ahb_access/>
</app>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>@DUSK_GAME_NAME@</name>
<version>@PROJECT_VERSION@</version>
<release_date>@DUSK_BUILD_DATE@</release_date>
<coder>@DUSK_GAME_AUTHOR@</coder>
<short_description>@DUSK_GAME_SHORT_DESCRIPTION@</short_description>
<long_description>@DUSK_GAME_LONG_DESCRIPTION@</long_description>
<ahb_access/>
</app>
+38 -33
View File
@@ -1,35 +1,40 @@
FROM debian:trixie
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /workdir
RUN dpkg --add-architecture arm64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
crossbuild-essential-arm64 \
ca-certificates \
pkg-config \
cmake \
make \
ninja-build \
git \
file \
python3 \
python3-pip \
python3-polib \
python3-pil \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.4-dev:arm64 \
xz-utils:arm64 \
libbz2-dev:arm64 \
zlib1g-dev:arm64 \
libzip-dev:arm64 \
libssl-dev:arm64 \
libsdl2-dev:arm64 \
liblzma-dev:arm64 \
libopengl0:arm64 \
libgl1:arm64 \
libegl1:arm64 \
libgles2:arm64 \
libgl1-mesa-dev:arm64 && \
rm -rf /var/lib/apt/lists/*
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get -y update && apt-get -y install \
python3-dotenv \
bc \
bison \
build-essential \
bzip2 \
bzr \
cmake \
cmake-curses-gui \
cpio \
device-tree-compiler \
flex \
git \
imagemagick \
libncurses5-dev \
locales \
make \
nano \
p7zip-full \
rsync \
sharutils \
scons \
tree \
unzip \
vim \
wget \
zip \
python3 \
python3-pip \
python3-polib \
python3-pil \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
&& apt clean
VOLUME ["/workdir"]
CMD ["/bin/bash"]
+1 -1
View File
@@ -14,8 +14,8 @@ RUN apt-get install -y \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.3-dev \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
libzip-dev \
-13
View File
@@ -1,13 +0,0 @@
FROM vitasdk/vitasdk:latest
WORKDIR /workdir
# Install vitaGL and its dependencies (vitashark, SceShaccCg) via vdpm
RUN which vdpm
# Install Python (needed for Dusk code generation tools)
RUN apk add --no-cache \
python3 \
py3-pip \
py3-dotenv
VOLUME ["/workdir"]
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
if [ -z "$DEVKITPRO" ]; then
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
exit 1
fi
mkdir -p build-gamecube-iso
cmake -S. -Bbuild-gamecube-iso \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake"
cd build-gamecube-iso
make -j$(nproc) VERBOSE=1
+1 -5
View File
@@ -5,10 +5,6 @@ if [ -z "$DEVKITPRO" ]; then
fi
mkdir -p build-gamecube
cmake -S. -Bbuild-gamecube \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \
-DDKP_OGC_PLATFORM_LIBRARY=libogc2
cmake -S. -Bbuild-gamecube -DDUSK_TARGET_SYSTEM=gamecube -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake"
cd build-gamecube
make -j$(nproc) VERBOSE=1
+2 -2
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
docker build --no-cache -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+13 -22
View File
@@ -1,24 +1,15 @@
#!/bin/bash
cmake -S . -B build-knulli -G Ninja \
-DDUSK_BUILD_TESTS=ON \
-DDUSK_TARGET_SYSTEM=knulli \
-DCMAKE_TOOLCHAIN_FILE=./cmake/toolchains/aarch64-linux-gnu.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-knulli -- -j$(nproc)
git clone --depth 1 --branch SDL2 https://github.com/libsdl-org/SDL.git /tmp/SDL2 && \
cmake -S /tmp/SDL2 -B /tmp/SDL2/build \
-DCMAKE_TOOLCHAIN_FILE=/workdir/toolchain-aarch64-buildroot.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$SYSROOT/usr" && \
cmake --build /tmp/SDL2/build -j"$(nproc)" && \
cmake --install /tmp/SDL2/build
# Copy necessary libs out
mkdir -p ./build-knulli/dusk
cp ./build-knulli/Dusk ./build-knulli/dusk/Dusk
cp ./build-knulli/dusk.dsk ./build-knulli/dusk/dusk.dsk
echo '#!/bin/bash' > build-knulli/dusk/Dusk.sh
echo 'cd "$(dirname "$(readlink -f "$0")")"' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(dirname "$(readlink -f "$0")")' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/gl4es' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib' >> build-knulli/dusk/Dusk.sh
echo '$(dirname "$(readlink -f "$0")")/Dusk' >> build-knulli/dusk/Dusk.sh
chmod +x build-knulli/dusk/Dusk.sh
cp /usr/lib/aarch64-linux-gnu/liblua5.4.so.0 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libGL.so.1 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libEGL.so.1 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libGLESv2.so.2 build-knulli/dusk/
cmake -S . \
-B build-knulli \
-DDUSK_BUILD_TESTS=ON \
-DDUSK_TARGET_SYSTEM=linux \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/toolchain-aarch64-buildroot.cmake
cmake --build build-knulli -- -j$(nproc)
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/build-linux.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-psp -f docker/psp/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
docker run --rm -v $(pwd):/workdir dusk-psp /bin/bash -c "./scripts/build-psp.sh"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
if [ -z "$VITASDK" ]; then
echo "VITASDK environment variable is not set. Please set it to the path of your VitaSDK installation."
exit 1
fi
mkdir -p build-vita
cd build-vita
cmake \
-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake \
-DDUSK_TARGET_SYSTEM=vita \
..
make -j$(nproc)
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
if [ -z "$DEVKITPRO" ]; then
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
exit 1
fi
mkdir -p build-wii-iso
cmake -S. -Bbuild-wii-iso \
-DDUSK_TARGET_SYSTEM=wii \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cd build-wii-iso
make -j$(nproc) VERBOSE=1
+1 -5
View File
@@ -5,10 +5,6 @@ if [ -z "$DEVKITPRO" ]; then
fi
mkdir -p build-wii
cmake -S. -Bbuild-wii \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL
cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cd build-wii
make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
+1 -5
View File
@@ -1,7 +1,3 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run \
--rm \
-v "${GITHUB_WORKSPACE}:/workdir" \
dusk-linux \
/bin/bash -c "./scripts/test-linux.sh"
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/test-linux.sh"
-2
View File
@@ -1,6 +1,4 @@
#!/bin/bash
set -e
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j$(nproc)
ctest --output-on-failure --test-dir build-tests
+2 -7
View File
@@ -5,7 +5,7 @@
add_subdirectory(dusk)
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
if(DUSK_TARGET_SYSTEM STREQUAL "linux")
add_subdirectory(dusklinux)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
@@ -15,12 +15,7 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
add_subdirectory(duskvita)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii")
add_subdirectory(duskdolphin)
endif()
+24 -32
View File
@@ -14,29 +14,18 @@ if(NOT libzip_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip)
endif()
if(NOT stb_image_FOUND)
find_package(stb REQUIRED)
if(STB_IMAGE_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image)
else()
message(FATAL_ERROR "stb_image not found. Please ensure stb is correctly fetched.")
if(NOT Lua_FOUND)
find_package(Lua REQUIRED)
if(Lua_FOUND AND NOT TARGET Lua::Lua)
add_library(Lua::Lua INTERFACE IMPORTED)
set_target_properties(
Lua::Lua
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}"
INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
)
endif()
endif()
if(NOT yyjson_FOUND)
find_package(yyjson REQUIRED)
if(yyjson_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson)
else()
message(FATAL_ERROR "yyjson not found. Please ensure yyjson is correctly fetched.")
endif()
endif()
if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_BACKTRACE
)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Lua::Lua)
endif()
# Includes
@@ -51,25 +40,28 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
main.c
)
# Defs
dusk_env_to_h(duskdefs.env duskdefs.h)
# Subdirs
add_subdirectory(animation)
add_subdirectory(event)
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(cutscene)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(log)
add_subdirectory(display)
add_subdirectory(engine)
add_subdirectory(error)
add_subdirectory(event)
add_subdirectory(input)
add_subdirectory(item)
add_subdirectory(locale)
add_subdirectory(rpg)
add_subdirectory(map)
add_subdirectory(scene)
add_subdirectory(system)
add_subdirectory(script)
add_subdirectory(story)
add_subdirectory(time)
add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(save)
add_subdirectory(util)
add_subdirectory(thread)
# if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "psp")
# add_subdirectory(thread)
# endif()
-10
View File
@@ -1,10 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
)
-59
View File
@@ -1,59 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "animation.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/fixed.h"
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
}
fixed_t animationGetValue(animation_t *anim, const fixed_t time) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
if(current > last) {
end = start;
break;
}
} while(true);
fixed_t span = fixedSub(end->time, start->time);
fixed_t progress = span != 0
? fixedDiv(fixedSub(time, start->time), span)
: FIXED_ONE;
return fixedLerp(
start->value,
end->value,
easingApply(start->easing, progress)
);
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "keyframe.h"
typedef struct {
keyframe_t *keyframes;
uint16_t keyframeCount;
} animation_t;
/**
* Initializes an animation.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
*/
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
);
/**
* Gets the value of the animation at a given time.
*
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
*/
fixed_t animationGetValue(animation_t *anim, const fixed_t time);
-131
View File
@@ -1,131 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "easing.h"
#include "assert/assert.h"
#include "util/math.h"
#include "util/fixed.h"
#define EASING_C1 1.70158f
#define EASING_C2 (EASING_C1 * 1.525f)
#define EASING_C3 (EASING_C1 + 1.0f)
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear,
easingInSine,
easingOutSine,
easingInOutSine,
easingInQuad,
easingOutQuad,
easingInOutQuad,
easingInCubic,
easingOutCubic,
easingInOutCubic,
easingInQuart,
easingOutQuart,
easingInOutQuart,
easingInBack,
easingOutBack,
easingInOutBack,
};
fixed_t easingApply(const easingtype_t type, const fixed_t t) {
assertTrue(type < EASING_COUNT, "Invalid easing type");
return EASING_FUNCTIONS[type](t);
}
fixed_t easingLinear(const fixed_t t) {
return t;
}
fixed_t easingInSine(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(1.0f - cosf(f * MATH_PI * 0.5f));
}
fixed_t easingOutSine(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(sinf(f * MATH_PI * 0.5f));
}
fixed_t easingInOutSine(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(-(cosf(MATH_PI * f) - 1.0f) * 0.5f);
}
fixed_t easingInQuad(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(f * f);
}
fixed_t easingOutQuad(const fixed_t t) {
float_t f = fixedToFloat(t);
float_t u = 1.0f - f;
return fixedFromFloat(1.0f - u * u);
}
fixed_t easingInOutQuad(const fixed_t t) {
float_t f = fixedToFloat(t);
if(f < 0.5f) return fixedFromFloat(2.0f * f * f);
float_t u = -2.0f * f + 2.0f;
return fixedFromFloat(1.0f - u * u * 0.5f);
}
fixed_t easingInCubic(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(f * f * f);
}
fixed_t easingOutCubic(const fixed_t t) {
float_t f = fixedToFloat(t);
float_t u = 1.0f - f;
return fixedFromFloat(1.0f - u * u * u);
}
fixed_t easingInOutCubic(const fixed_t t) {
float_t f = fixedToFloat(t);
if(f < 0.5f) return fixedFromFloat(4.0f * f * f * f);
float_t u = -2.0f * f + 2.0f;
return fixedFromFloat(1.0f - u * u * u * 0.5f);
}
fixed_t easingInQuart(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(f * f * f * f);
}
fixed_t easingOutQuart(const fixed_t t) {
float_t f = fixedToFloat(t);
float_t u = 1.0f - f;
return fixedFromFloat(1.0f - u * u * u * u);
}
fixed_t easingInOutQuart(const fixed_t t) {
float_t f = fixedToFloat(t);
if(f < 0.5f) return fixedFromFloat(8.0f * f * f * f * f);
float_t u = -2.0f * f + 2.0f;
return fixedFromFloat(1.0f - u * u * u * u * 0.5f);
}
fixed_t easingInBack(const fixed_t t) {
float_t f = fixedToFloat(t);
return fixedFromFloat(EASING_C3 * f * f * f - EASING_C1 * f * f);
}
fixed_t easingOutBack(const fixed_t t) {
float_t f = fixedToFloat(t);
float_t u = f - 1.0f;
return fixedFromFloat(1.0f + EASING_C3 * u * u * u + EASING_C1 * u * u);
}
fixed_t easingInOutBack(const fixed_t t) {
float_t f = fixedToFloat(t);
if(f < 0.5f) {
float_t u = 2.0f * f;
return fixedFromFloat(u * u * ((EASING_C2 + 1.0f) * u - EASING_C2) * 0.5f);
}
float_t u = 2.0f * f - 2.0f;
return fixedFromFloat((u * u * ((EASING_C2 + 1.0f) * u + EASING_C2) + 2.0f) * 0.5f);
}
-59
View File
@@ -1,59 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "dusk.h"
#include "util/fixed.h"
typedef enum {
EASING_LINEAR,
EASING_IN_SINE,
EASING_OUT_SINE,
EASING_IN_OUT_SINE,
EASING_IN_QUAD,
EASING_OUT_QUAD,
EASING_IN_OUT_QUAD,
EASING_IN_CUBIC,
EASING_OUT_CUBIC,
EASING_IN_OUT_CUBIC,
EASING_IN_QUART,
EASING_OUT_QUART,
EASING_IN_OUT_QUART,
EASING_IN_BACK,
EASING_OUT_BACK,
EASING_IN_OUT_BACK,
EASING_COUNT
} easingtype_t;
typedef fixed_t (*easingfn_t)(const fixed_t t);
extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT];
/**
* Applies the specified easing function to t.
*
* @param type The easing type to apply.
* @param t The input progress in [0, FIXED_ONE].
* @return The eased value in [0, FIXED_ONE].
*/
fixed_t easingApply(const easingtype_t type, const fixed_t t);
fixed_t easingLinear(const fixed_t t);
fixed_t easingInSine(const fixed_t t);
fixed_t easingOutSine(const fixed_t t);
fixed_t easingInOutSine(const fixed_t t);
fixed_t easingInQuad(const fixed_t t);
fixed_t easingOutQuad(const fixed_t t);
fixed_t easingInOutQuad(const fixed_t t);
fixed_t easingInCubic(const fixed_t t);
fixed_t easingOutCubic(const fixed_t t);
fixed_t easingInOutCubic(const fixed_t t);
fixed_t easingInQuart(const fixed_t t);
fixed_t easingOutQuart(const fixed_t t);
fixed_t easingInOutQuart(const fixed_t t);
fixed_t easingInBack(const fixed_t t);
fixed_t easingOutBack(const fixed_t t);
fixed_t easingInOutBack(const fixed_t t);
-14
View File
@@ -1,14 +0,0 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "easing.h"
#include "util/fixed.h"
typedef struct {
fixed_t time;
fixed_t value;
easingtype_t easing;
} keyframe_t;
+2 -71
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026 Dominic Masters
* Copyright (c) 2023 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -7,20 +7,8 @@
#include "assert.h"
#include "log/log.h"
#include "util/string.h"
#include "util/memory.h"
#ifdef DUSK_THREAD_PTHREAD
pthread_t ASSERT_MAIN_THREAD_ID = 0;
#endif
#ifndef DUSK_ASSERTIONS_FAKED
void assertInit(void) {
#ifdef DUSK_THREAD_PTHREAD
ASSERT_MAIN_THREAD_ID = pthread_self();
#endif
}
#ifdef DUSK_TEST_ASSERT
void assertTrueImpl(
const char *file,
@@ -36,25 +24,6 @@
);
}
#else
#ifdef DUSK_BACKTRACE
#include <execinfo.h>
#include <stdlib.h>
static void assertLogBacktrace(void) {
void *frames[64];
int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count);
memoryTrack(symbols);
logError("Stack trace:\n");
if(symbols) {
for(int i = 0; i < count; i++) {
logError(" %s\n", symbols[i]);
}
memoryFree(symbols);
}
}
#endif
void assertTrueImpl(
const char *file,
const int32_t line,
@@ -63,14 +32,11 @@
) {
if(x != true) {
logError(
"Assertion Failed in %s:%i\n\n%s\n\n",
"Assertion Failed in %s:%i\n\n%s\n",
file,
line,
message
);
#ifdef DUSK_BACKTRACE
assertLogBacktrace();
#endif
abort();
}
}
@@ -132,39 +98,4 @@
) {
assertUnreachableImpl(file, line, message);
}
void assertStringEqualImpl(
const char *file,
const int32_t line,
const char *a,
const char *b,
const char *message
) {
assertTrueImpl(file, line, stringCompare(a, b) == 0, message);
}
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() == ASSERT_MAIN_THREAD_ID, message
);
#endif
}
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
) {
#ifdef DUSK_THREAD_PTHREAD
assertTrueImpl(
file, line, pthread_self() != ASSERT_MAIN_THREAD_ID, message
);
#endif
}
#endif
+1 -90
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026 Dominic Masters
* Copyright (c) 2023 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -16,18 +16,7 @@
#endif
#endif
#ifdef DUSK_THREAD_PTHREAD
#include "thread/thread.h"
extern pthread_t ASSERT_MAIN_THREAD_ID;
#endif
#ifndef DUSK_ASSERTIONS_FAKED
/**
* Initializes the assert system. Must be the very first call in engine
* startup.
*/
void assertInit(void);
/**
* Assert a given value to be true.
*
@@ -115,45 +104,6 @@
const char *message
);
/**
* Asserts two strings to be equal.
*
* @param file File that the assertion is being made from.
* @param line Line that the assertion is being made from.
* @param a First string to compare.
* @param b Second string to compare.
* @param message Message to throw against assertion failure.
*/
void assertStringEqualImpl(
const char *file,
const int32_t line,
const char *a,
const char *b,
const char *message
);
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertIsMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
void assertNotMainThreadImpl(
const char *file,
const int32_t line,
const char *message
);
/**
* Asserts a given value to be true.
*
@@ -228,38 +178,8 @@
#define assertStrLenMin(str, len, message) \
assertTrue(strlen(str) >= len, message)
/**
* Asserts two strings to be equal.
*
* @param a First string to compare.
* @param b Second string to compare.
* @param message Message to throw against assertion failure.
*/
#define assertStringEqual(a, b, message) \
assertStringEqualImpl(__FILE__, __LINE__, a, b, message)
/**
* Asserts that the current thread is the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertIsMainThread(message) \
assertIsMainThreadImpl(__FILE__, __LINE__, message)
/**
* Asserts that the current thread is NOT the main thread.
*
* @param message Message to throw against assertion failure.
*/
#define assertNotMainThread(message) \
assertNotMainThreadImpl(__FILE__, __LINE__, message)
#else
// If assertions are faked, we define the macros to do nothing.
#define assertInit() ((void)0)
#define assertMainThreadInit() ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#define assertTrue(x, message) ((void)0)
#define assertFalse(x, message) ((void)0)
#define assertUnreachable(message) ((void)0)
@@ -268,14 +188,5 @@
#define assertDeprecated(message) ((void)0)
#define assertStrLenMax(str, len, message) ((void)0)
#define assertStrLenMin(str, len, message) ((void)0)
#define assertStringEqual(a, b, message) ((void)0)
#define assertIsMainThread(message) ((void)0)
#define assertNotMainThread(message) ((void)0)
#endif
// Static Assertions
#define assertStructSize(struct, size) \
_Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
// EOF
+1 -3
View File
@@ -7,9 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
asset.c
assetbatch.c
assetfile.c
)
# Subdirs
add_subdirectory(loader)
add_subdirectory(type)
+83 -368
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026 Dominic Masters
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -9,420 +9,135 @@
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include "asset/assettype.h"
#include "engine/engine.h"
#include "util/string.h"
#include "console/console.h"
#include <unistd.h>
asset_t ASSET;
errorret_t assetInit(void) {
memoryZero(&ASSET, sizeof(asset_t));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
// assetInitPlatform must either define ASSET.zip or throw an error.
errorChain(assetInitPlatform());
assertNotNull(ASSET.zip, "Asset zip null without error.");
threadInit(&ASSET.loadThread, assetUpdateAsync);
threadStart(&ASSET.loadThread);
errorOk();
}
bool_t assetFileExists(const char_t *filename) {
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
if(idx < 0) return false;
return true;
}
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
// Is there an existing asset?
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(stringEquals(entry->name, name)) {
assertTrue(entry->type == type, "Asset entry type mismatch.");
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorret_t assetLoad(const char_t *filename, void *output) {
assertStrLenMax(filename, FILENAME_MAX, "Filename too long.");
assertNotNull(output, "Output pointer cannot be NULL.");
// We did not find one existing, Find first available slot.
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("No available asset entry slots.");
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.");
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
// Determine the asset type by reading the extension
const assettypedef_t *def = NULL;
for(uint_fast8_t i = 0; i < ASSET_TYPE_COUNT; i++) {
const assettypedef_t *cmp = &ASSET_TYPE_DEFINITIONS[i];
assertNotNull(cmp, "Asset type definition cannot be NULL.");
if(cmp->extension == NULL) continue;
if(!stringEndsWithCaseInsensitive(filename, cmp->extension)) continue;
def = cmp;
break;
}
if(def == NULL) {
errorThrow("Unknown asset type for file: %s", filename);
}
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
// Get file size of the asset.
zip_stat_t st;
zip_stat_init(&st);
if(!zip_stat(ASSET.zip, filename, 0, &st) == 0) {
errorThrow("Failed to stat asset file: %s", filename);
}
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();
// Minimum file size.
zip_int64_t fileSize = (zip_int64_t)st.size;
if(fileSize <= 0) {
errorThrow("Asset file is empty: %s", filename);
}
if(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
entry->state == ASSET_ENTRY_STATE_ERROR
) {
errorOk();
// Try to open the file
zip_file_t *file = zip_fopen(ASSET.zip, filename, 0);
if(file == NULL) {
errorThrow("Failed to open asset file: %s", filename);
}
assertTrue(
entry->refs.count == 0,
"Cannot require disposal of an entry with active references."
);
// Load the asset data
switch(def->loadStrategy) {
case ASSET_LOAD_STRAT_ENTIRE:
assertNotNull(def->entire, "Asset load function cannot be NULL.");
// 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();
}
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assetentry_t *entry = assetGetEntry(name, type, input);
assetEntryLock(entry);
return entry;
}
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)
) {
assetEntryUnlock(entry);
return;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("Asset entry not found for unlock.");
}
void assetUnlockEntry(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assetEntryUnlock(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;
assetloading_t *loading = ASSET.loading;
assetentry_t *entry;
do {
// We only care about NULL entry references. Nothing async touches this so
// it's fine to use raw here.
if(loading->entry != NULL) {
loading++;
continue;
}
availableLoading[availableLoadingCount++] = loading;
loading++;
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Now we can check for pending asset entries, we can't do anything if there
// is no available slots though.
if(availableLoadingCount > 0) {
entry = ASSET.entries;
do {
// Is this asset "ready to start loading" ?
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
// Must have more to read
if(fileSize <= 0) {
zip_fclose(file);
errorThrow("No data remaining to read for asset: %s", filename);
}
// We only care about assets not started.
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
entry++;
continue;
}
// Pop a loading slot for this asset entry.
loading = availableLoading[--availableLoadingCount];
// Start loading this asset.
assetEntryStartLoading(entry, loading);
entry++;
// Did we run out of loading slots?
if(availableLoadingCount == 0) {
break;
}
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
}
// Now walk over all the loading slots and see what needs to be done.
loading = ASSET.loading;
do {
// Is the loading slot in use? Entry can only be modified synchronously.
if(loading->entry == NULL) {
loading++;
continue;
}
// Lock the loading slot. This will prevent any async modifications.
threadMutexLock(&loading->mutex);
// Check the state of the entry.
switch(loading->entry->state) {
// This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC:
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)
if(fileSize > def->dataSize) {
zip_fclose(file);
errorThrow(
"Asset file has too much data remaining after header: %s",
filename
);
// After a sync load, these are the only valid states.
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_LOADED ||
loading->entry->state == ASSET_ENTRY_STATE_ERROR ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC,
"Loader did not set entry state to loaded or error on finished load."
);
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);
}
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
// 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:
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
loading++;
break;
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);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Create space to read the entire asset data
void *data = memoryAllocate(fileSize);
if(!data) {
zip_fclose(file);
errorThrow(
"Failed to allocate memory for asset data of file: %s", filename
);
}
// Read in the asset data.
zip_int64_t bytesRead = zip_fread(file, data, fileSize);
if(bytesRead == 0 || bytesRead > fileSize) {
memoryFree(data);
zip_fclose(file);
errorThrow("Failed to read asset data for file: %s", filename);
}
fileSize -= bytesRead;
// Reap unused entries.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
// Close the file now we have the data
zip_fclose(file);
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
// Pass to the asset type loader
assetentire_t entire = {
.data = data,
.output = output
};
errorret_t ret = def->entire(entire);
memoryFree(data);
if(entry->refs.count > 0) {
entry++;
continue;
}
errorChain(ret);
break;
consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
case ASSET_LOAD_STRAT_CUSTOM:
assertNotNull(def->custom, "Asset load function cannot be NULL.");
assetcustom_t customData = {
.zipFile = file,
.output = output
};
errorChain(def->custom(customData));
break;
default:
assertUnreachable("Unknown asset load strategy.");
}
errorOk();
}
void assetUpdateAsync(thread_t *thread) {
assertNotMainThread("assetUpdateAsync must not run on the main thread.");
while(!threadShouldStop(thread)) {
// Walk over each asset
assetloading_t *loading;
loading = ASSET.loading;
do {
threadMutexLock(&loading->mutex);
if(loading->entry == NULL) {
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
switch(loading->entry->state) {
case ASSET_ENTRY_STATE_PENDING_ASYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC;
assertNotNull(
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
"Loader does not support async loading."
);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
);
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."
);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_ASYNC:
assertUnreachable(
"Entry is in a pending async state still?"
);
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
if(threadShouldStop(thread)) break;
usleep(1000);
}
}
errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
// 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) {
errorThrow("Failed to close asset zip archive.");
+8 -104
View File
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2026 Dominic Masters
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
@@ -7,11 +7,8 @@
#pragma once
#include "error/error.h"
#include "assettype.h"
#include "asset/assetplatform.h"
#include "assetfile.h"
#include "thread/thread.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloading.h"
#ifndef assetInitPlatform
#error "Platform must define assetInitPlatform function."
@@ -23,27 +20,15 @@
#define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 4
#define ASSET_ENTRY_COUNT_MAX 128
typedef struct asset_s {
typedef struct {
zip_t *zip;
assetplatform_t platform;
// Background loading thread.
thread_t loadThread;
// Assets that are mid loading.
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
} asset_t;
extern asset_t ASSET;
/**
* Initializes the asset system.
*
* @return An error code if the asset system could not be initialized properly.
*/
errorret_t assetInit(void);
@@ -56,94 +41,13 @@ errorret_t assetInit(void);
bool_t assetFileExists(const char_t *filename);
/**
* Gets, or creates, a new asset entry. Internal - prefer assetLock.
* Loads an asset by its filename, the output type depends on the asset type.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
* @param filename The filename of the asset to retrieve.
* @param output The output pointer to store the loaded asset data.
* @return An error code if the asset could not be loaded.
*/
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
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
* reclaimed.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
* @return The locked asset entry.
*/
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Releases a lock on an asset entry by name. When all locks are released the
* entry will be reclaimed at the start of the next assetUpdate.
*
* @param name Filename of the asset to unlock.
*/
void assetUnlock(const char_t *name);
/**
* Releases a lock on an asset entry by pointer. When all locks are released
* the entry will be reclaimed at the start of the next assetUpdate.
*
* @param entry The asset entry to unlock.
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
*
* @param entry The asset entry to require.
* @return An error code if the asset entry could not be loaded properly.
*/
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.
*
* @return An error code if the asset system could not be updated properly.
*/
errorret_t assetUpdate(void);
/**
* Starts the background asset loading thread. The thread runs assetUpdate
* in a loop with a short sleep until stopped.
*
* @param thread The thread runner.
*/
void assetUpdateAsync(thread_t *thread);
errorret_t assetLoad(const char_t *filename, void *output);
/**
* Disposes/cleans up the asset system.
-165
View File
@@ -1,165 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetbatch.h"
#include "asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
const uint16_t count,
const assetbatchdesc_t *descs
) {
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."
);
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++) {
batch->inputs[i] = descs[i].input;
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
);
}
}
}
void assetBatchLock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryLock(batch->entries[i]);
}
}
void assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
}
return true;
}
bool_t assetBatchHasError(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
}
return false;
}
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
bool_t allDone;
do {
allDone = true;
for(uint16_t i = 0; i < batch->count; i++) {
const assetentrystate_t state = batch->entries[i]->state;
if(state == ASSET_ENTRY_STATE_ERROR) {
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
}
if(state != ASSET_ENTRY_STATE_LOADED) {
allDone = false;
}
}
if(!allDone) {
usleep(1000);
errorChain(assetUpdate());
}
} while(!allDone);
errorOk();
}
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; 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);
}
}
-124
View File
@@ -1,124 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#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_EVENT_MAX 4
typedef struct {
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
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;
/**
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @param batch Batch to initialise.
* @param descs Array of entry descriptors (need not outlive this call).
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
*/
void assetBatchInit(
assetbatch_t *batch,
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Acquires one additional lock on every entry in the batch.
*
* @param batch Batch to lock.
*/
void assetBatchLock(assetbatch_t *batch);
/**
* Releases one lock from every entry in the batch. When an entry's lock
* count reaches zero it will be reaped on the next assetUpdate.
*
* @param batch Batch to unlock.
*/
void assetBatchUnlock(assetbatch_t *batch);
/**
* Returns true if every entry in the batch has finished loading.
*
* @param batch Batch to query.
*/
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
/**
* Returns true if any entry in the batch is in an error state.
*
* @param batch Batch to query.
*/
bool_t assetBatchHasError(const assetbatch_t *batch);
/**
* Blocks until every entry is loaded. Returns an error if any entry fails.
*
* @param batch Batch to wait on.
*/
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
/**
* Releases the batch's lock on every entry and clears the batch. After this
* call the batch struct may be reused with assetBatchInit.
*
* @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);
-380
View File
@@ -1,380 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "asset/asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
errorret_t assetFileInit(
assetfile_t *file,
const char_t *filename,
void *params,
void *output
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
memoryZero(file, sizeof(assetfile_t));
memoryCopy(file->filename, filename, ASSET_FILE_NAME_MAX);
file->params = params;
file->output = output;
// Stat the file
zip_stat_init(&file->stat);
if(!zip_stat(ASSET.zip, filename, 0, &file->stat) == 0) {
errorThrow("Failed to stat asset file: %s", filename);
}
// Minimum file size.
file->size = (zip_int64_t)file->stat.size;
if(file->size <= 0) {
errorThrow("Invalid asset file size: %s", filename);
}
errorOk();
}
errorret_t assetFileRewind(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->zipFile, "Asset file must be opened before rewinding.");
if(file->position == 0) {
errorOk();
}
errorChain(assetFileClose(file));
errorChain(assetFileOpen(file));
errorOk();
}
errorret_t assetFileOpen(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->filename, "Asset file filename cannot be NULL.");
assertNotNull(ASSET.zip, "Asset zip cannot be NULL.");
assertNull(file->zipFile, "Asset file already open.");
file->zipFile = zip_fopen(ASSET.zip, file->filename, 0);
if(file->zipFile == NULL) {
errorThrow("Failed to open asset file: %s", file->filename);
}
file->position = 0;
errorOk();
}
errorret_t assetFileRead(
assetfile_t *file,
void *buffer,
const size_t bufferSize
) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->zipFile, "Asset file must be opened before reading.");
if(buffer == NULL) {
size_t bytesRemaining = bufferSize;
uint8_t tempBuffer[256];
while(bytesRemaining > 0) {
size_t chunkSize = mathMin(bytesRemaining, sizeof(tempBuffer));
errorChain(assetFileRead(file, tempBuffer, chunkSize));
file->position += chunkSize;
bytesRemaining -= chunkSize;
}
file->lastRead = bufferSize;
errorOk();
}
// I assume zip_fread takes buffer NULL for skipping?
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
if(bytesRead < 0) {
errorThrow("Failed to read from asset file: %s", file->filename);
}
file->position += bytesRead;
file->lastRead = bytesRead;
errorOk();
}
errorret_t assetFileClose(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->zipFile, "Asset file must be opened before closing.");
if(zip_fclose(file->zipFile) != 0) {
errorThrow("Failed to close asset file: %s", file->filename);
}
file->zipFile = NULL;
file->position = 0;
errorOk();
}
errorret_t assetFileDispose(assetfile_t *file) {
if(file->zipFile != NULL) {
errorChain(assetFileClose(file));
}
memoryZero(file, sizeof(assetfile_t));
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,
assetfile_t *file,
uint8_t *readBuffer,
const size_t readBufferSize,
uint8_t *outBuffer,
const size_t outBufferSize
) {
assertNotNull(reader, "Line reader cannot be NULL.");
assertNotNull(file, " File cannot be NULL.");
assertNotNull(readBuffer, "Read buffer cannot be NULL.");
assertNotNull(outBuffer, "Output buffer cannot be NULL.");
assertTrue(readBufferSize > 0, "Read buffer size must be greater than 0.");
assertTrue(outBufferSize > 0, "Output buffer size must be greater than 0.");
memoryZero(reader, sizeof(assetfilelinereader_t));
reader->file = file;
reader->readBuffer = readBuffer;
reader->readBufferSize = readBufferSize;
reader->outBuffer = outBuffer;
reader->outBufferSize = outBufferSize;
}
size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertTrue(reader->bufferEnd >= reader->bufferStart, "Invalid buffer state.");
return reader->bufferEnd - reader->bufferStart;
}
const uint8_t *assetFileLineReaderUnreadPtr(
const assetfilelinereader_t *reader
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
return reader->readBuffer + reader->bufferStart;
}
static errorret_t assetFileLineReaderAppend(
assetfilelinereader_t *reader,
const uint8_t *src,
size_t srcLength
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
if(srcLength == 0) {
errorOk();
}
/* reserve room for optional NUL terminator */
if(reader->lineLength + srcLength >= reader->outBufferSize) {
errorThrow("Line length exceeds output buffer size.");
}
memoryCopy(reader->outBuffer + reader->lineLength, src, srcLength);
reader->lineLength += srcLength;
errorOk();
}
static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
assertTrue(
reader->lineLength < reader->outBufferSize,
"Line length exceeds out buffer."
);
reader->outBuffer[reader->lineLength] = '\0';
}
static ssize_t assetFileLineReaderFindNewline(
const assetfilelinereader_t *reader
) {
size_t i;
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
for(i = reader->bufferStart; i < reader->bufferEnd; ++i) {
if(reader->readBuffer[i] == '\n') {
return (ssize_t)i;
}
}
return -1;
}
errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->file, "File cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
if(reader->eof) errorOk();
errorret_t ret;
size_t unreadBytes = assetFileLineReaderUnreadBytes(reader);
/* If buffer is fully consumed, refill from start. */
if(unreadBytes == 0) {
reader->bufferStart = 0;
reader->bufferEnd = 0;
errorChain(assetFileRead(
reader->file,
reader->readBuffer,
reader->readBufferSize
));
if(reader->file->lastRead == 0) {
reader->eof = true;
errorOk();
}
reader->bufferStart = 0;
reader->bufferEnd = reader->file->lastRead;
errorOk();
}
/*
* There are unread bytes left but no newline in them.
* If bufferStart > 0, slide unread bytes to front so we can read more.
* This only happens when necessary to make space.
*/
if(reader->bufferEnd == reader->readBufferSize) {
if(reader->bufferStart == 0) {
/*
* Entire read buffer is unread and contains no newline.
* Caller must have a large enough outBuffer to accumulate across fills,
* so we consume these bytes into outBuffer before refilling.
*/
errorOk();
}
memoryMove(
reader->readBuffer,
reader->readBuffer + reader->bufferStart,
unreadBytes
);
reader->bufferStart = 0;
reader->bufferEnd = unreadBytes;
}
errorChain(assetFileRead(
reader->file,
reader->readBuffer + reader->bufferEnd,
reader->readBufferSize - reader->bufferEnd
));
if(reader->file->lastRead == 0) {
reader->eof = true;
errorOk();
}
reader->bufferEnd += reader->file->lastRead;
errorOk();
}
errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->file, "File cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
reader->lineLength = 0;
for(;;) {
ssize_t newlineIndex = assetFileLineReaderFindNewline(reader);
if(newlineIndex >= 0) {
size_t chunkLength = (size_t)newlineIndex - reader->bufferStart;
errorret_t ret;
/* strip CR in CRLF */
if(
chunkLength > 0 &&
reader->readBuffer[(size_t)newlineIndex - 1] == '\r'
) {
chunkLength--;
}
errorChain(assetFileLineReaderAppend(
reader,
reader->readBuffer + reader->bufferStart,
chunkLength
));
reader->bufferStart = (size_t)newlineIndex + 1;
assetFileLineReaderTerminate(reader);
errorOk();
}
if(assetFileLineReaderUnreadBytes(reader) > 0) {
errorChain(assetFileLineReaderAppend(
reader,
assetFileLineReaderUnreadPtr(reader),
assetFileLineReaderUnreadBytes(reader)
));
reader->bufferStart = reader->bufferEnd;
}
if(reader->eof) {
if(reader->lineLength > 0) {
assetFileLineReaderTerminate(reader);
errorOk();
}
errorThrow("End of file reached.");
}
errorChain(assetFileLineReaderFill(reader));
}
}
-156
View File
@@ -1,156 +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 <zip.h>
#define ASSET_FILE_NAME_MAX 48
typedef struct assetfile_s assetfile_t;
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
// Describes a file not yet loaded.
typedef struct assetfile_s {
char_t filename[ASSET_FILE_NAME_MAX];
void *params;
void *output;
zip_stat_t stat;
zip_int64_t size;
zip_int64_t position;
zip_int64_t lastRead;
zip_file_t *zipFile;
} assetfile_t;
/**
* Initializes the asset file structure in preparation for loading. This will
* stat the file but not open the handle.
*
* @param file The asset file structure to initialize.
* @param filename The name of the asset file to load.
* @param params Optional loader params.
* @param output Output pointer for the loader.
* @return Error indicating success or failure.
*/
errorret_t assetFileInit(
assetfile_t *file,
const char_t *filename,
void *params,
void *output
);
/**
* Opens the asset file for reading. After opening the loader is responsible
* for closing the file.
*
* @param file The asset file to open.
* @return An error code if the file could not be opened.
*/
errorret_t assetFileOpen(assetfile_t *file);
/**
* Rewind the file to the initial position.
*
* @param file The asset file to rewind.
*/
errorret_t assetFileRewind(assetfile_t *file);
/**
* Read bytes from the asset file. Assumes the file has already been opened
* prior to trying to read anything.
*
* @param file File to read from.
* @param buffer Buffer to read the file data into., or NULL to skip bytes.
* @param size Size of the buffer to read into.
*/
errorret_t assetFileRead(
assetfile_t *file,
void *buffer,
const size_t bufferSize
);
/**
* Closes the asset file and releases any resources associated with it.
*
* @param file The asset file to close.
* @return An error code if the file could not be closed properly.
*/
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;
size_t readBufferSize;
uint8_t *outBuffer;
size_t outBufferSize;
// A
size_t bufferStart;
size_t bufferEnd;
bool_t eof;//?
// Updated each reach:
size_t lineLength;
} assetfilelinereader_t;
/**
* Initializes a line reader for the given asset file. The line reader will read
* lines from the file into the provided line buffer, using the provided buffer
* for reading chunks of the file.
*
* @param file The asset file to read from. Must already be opened.
* @param readBuffer Buffer to use for reading chunks of the file.
* @param readBufferSize Size of the read buffer.
* @param outBuffer Buffer to read lines into. Lines will be null-terminated.
* @param outBufferSize Size of the output buffer.
* @return An initialized line reader structure.
*/
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
assetfile_t *file,
uint8_t *readBuffer,
const size_t readBufferSize,
uint8_t *outBuffer,
const size_t outBufferSize
);
/**
* Reads the next line from the asset file into the line buffer. The line
* buffer is null-terminated and does not include the newline character.
*
* @param reader The line reader to read from.
* @return An error code if a failure occurs, or errorOk() if a line was read
* successfully. If the end of the file is reached, errorEndOfFile() is
* returned.
*/
errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader);
+106
View File
@@ -0,0 +1,106 @@
/**
* Copyright (c) 2025 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "type/assettexture.h"
// #include "type/assetpalette.h"
#include "type/assettileset.h"
#include "type/assetlanguage.h"
#include "type/assetscript.h"
#include "type/assetmap.h"
#include "type/assetmapchunk.h"
#include <zip.h>
typedef enum {
ASSET_TYPE_NULL,
ASSET_TYPE_TEXTURE,
// ASSET_TYPE_PALETTE,
ASSET_TYPE_TILESET,
ASSET_TYPE_LANGUAGE,
ASSET_TYPE_SCRIPT,
ASSET_TYPE_MAP,
ASSET_TYPE_MAP_CHUNK,
ASSET_TYPE_COUNT,
} assettype_t;
typedef enum {
ASSET_LOAD_STRAT_ENTIRE,
ASSET_LOAD_STRAT_CUSTOM
} assetloadstrat_t;
typedef struct assetentire_s {
void *data;
void *output;
} assetentire_t;
typedef struct assetcustom_s {
zip_file_t *zipFile;
void *output;
} assetcustom_t;
typedef struct {
const char_t *extension;
const size_t dataSize;
const assetloadstrat_t loadStrategy;
union {
errorret_t (*entire)(assetentire_t entire);
errorret_t (*custom)(assetcustom_t custom);
};
} assettypedef_t;
static const assettypedef_t ASSET_TYPE_DEFINITIONS[ASSET_TYPE_COUNT] = {
[ASSET_TYPE_NULL] = {
0
},
[ASSET_TYPE_TEXTURE] = {
.extension = "dpt",
.loadStrategy = ASSET_LOAD_STRAT_ENTIRE,
.dataSize = sizeof(assettexture_t),
.entire = assetTextureLoad
},
// [ASSET_TYPE_PALETTE] = {
// .extension = "dpf",
// .loadStrategy = ASSET_LOAD_STRAT_ENTIRE,
// .dataSize = sizeof(palette_t),
// .entire = assetPaletteLoad
// },
[ASSET_TYPE_TILESET] = {
.extension = "dtf",
.loadStrategy = ASSET_LOAD_STRAT_ENTIRE,
.dataSize = sizeof(assettileset_t),
.entire = assetTilesetLoad
},
[ASSET_TYPE_LANGUAGE] = {
.extension = "DLF",
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
.custom = assetLanguageHandler
},
[ASSET_TYPE_SCRIPT] = {
.extension = "lua",
.loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
.custom = assetScriptHandler
},
// [ASSET_TYPE_MAP] = {
// .extension = "DMF",
// .loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
// .custom = assetMapHandler
// },
// [ASSET_TYPE_MAP_CHUNK] = {
// .extension = "DMC",
// .loadStrategy = ASSET_LOAD_STRAT_CUSTOM,
// .custom = assetMapChunkHandler
// },
};
-17
View File
@@ -1,17 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetentry.c
assetloader.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
-104
View File
@@ -1,104 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetentry.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertNotNull(entry, "Entry cannot be NULL");
assertStrLenMin(name, 1, "Name cannot be empty");
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->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);
}
void assetEntryStartLoading(
assetentry_t *entry,
assetloading_t *loading
) {
assertNotNull(entry, "Entry cannot be NULL");
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(
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));
loading->type = entry->type;
loading->entry = entry;
// At this point the asset manager will manage this thing's loading
}
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();
}
-112
View File
@@ -1,112 +0,0 @@
/**
* 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 {
ASSET_ENTRY_STATE_NOT_STARTED,
ASSET_ENTRY_STATE_PENDING_ASYNC,
ASSET_ENTRY_STATE_LOADING_ASYNC,
ASSET_ENTRY_STATE_PENDING_SYNC,
ASSET_ENTRY_STATE_LOADING_SYNC,
ASSET_ENTRY_STATE_LOADED,
ASSET_ENTRY_STATE_ERROR
} assetentrystate_t;
/** 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];
assetloadertype_t type;
assetloaderoutput_t data;
assetentrystate_t state;
ref_t refs;
assetloaderinput_t *input;
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.
* @param input Data that will be passed to the loader about how it should load.
*/
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* 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);
/**
* 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.
*/
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);
-42
View File
@@ -1,42 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetloader.h"
assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
[ASSET_LOADER_TYPE_NULL] = { 0 },
[ASSET_LOADER_TYPE_MESH] = {
.loadSync = assetMeshLoaderSync,
.loadAsync = assetMeshLoaderAsync,
.dispose = assetMeshDispose
},
[ASSET_LOADER_TYPE_TEXTURE] = {
.loadSync = assetTextureLoaderSync,
.loadAsync = assetTextureLoaderAsync,
.dispose = assetTextureDispose
},
[ASSET_LOADER_TYPE_TILESET] = {
.loadSync = assetTilesetLoaderSync,
.loadAsync = assetTilesetLoaderAsync,
.dispose = assetTilesetDispose
},
[ASSET_LOADER_TYPE_LOCALE] = {
.loadSync = assetLocaleLoaderSync,
.loadAsync = assetLocaleLoaderAsync,
.dispose = assetLocaleDispose
},
[ASSET_LOADER_TYPE_JSON] = {
.loadSync = assetJsonLoaderSync,
.loadAsync = assetJsonLoaderAsync,
.dispose = assetJsonDispose
},
};
-91
View File
@@ -1,91 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/display/assetmeshloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
typedef union {
assetmeshloaderinput_t mesh;
assettextureloaderinput_t texture;
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
} assetloaderinput_t;
typedef union {
assetmeshloaderloading_t mesh;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
} assetloaderloading_t;
typedef union {
assetmeshoutput_t mesh;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
} assetloaderoutput_t;
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef errorret_t (assetloadersynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderasynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderdisposecallback_t)(assetentry_t *entry);
typedef struct {
assetloadersynccallback_t *loadSync;
assetloaderasynccallback_t *loadAsync;
assetloaderdisposecallback_t *dispose;
} assetloadercallbacks_t;
extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
/**
* Shorthand method to both chain an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ret The error return value to check and chain if it's an error.
*/
#define assetLoaderErrorChain(loading, _expr) {\
errorret_t _alec = (_expr); \
if(errorIsNotOk(_alec)) { \
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorChain(_alec); \
} \
}
/**
* Shorthand method to both throw an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ... Format string and arguments for the error message.
*/
#define assetLoaderErrorThrow(loading, ...) {\
loading->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorThrow(__VA_ARGS__); \
}
-26
View File
@@ -1,26 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "assetloader.h"
#include "asset/assetfile.h"
#include "thread/threadmutex.h"
typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s {
threadmutex_t mutex;
assetloadertype_t type;
assetentry_t *entry;
assetloaderloading_t loading;
} assetloading_t;
typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading);
typedef struct {
assetloadingcallback_t *loadSync;
} assetloadingcallbacks_t;
@@ -1,12 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetmeshloader.c
assettextureloader.c
assettilesetloader.c
)
@@ -1,180 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetmeshloader.h"
#include "assert/assert.h"
#include "util/endian.h"
#include "util/memory.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
errorOk();
}
assetmeshoutput_t *out = &loading->entry->data.mesh;
assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->inputData.mesh;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
assetLoaderErrorChain(loading, assetFileOpen(file));
// Skip the 80-byte STL header.
assetLoaderErrorChain(loading, assetFileRead(file, NULL, 80));
if(file->lastRead != 80) {
assetLoaderErrorThrow(loading, "Failed to skip STL header.");
}
uint32_t triangleCount;
assetLoaderErrorChain(loading,
assetFileRead(file, &triangleCount, sizeof(uint32_t))
);
if(file->lastRead != sizeof(uint32_t)) {
assetLoaderErrorThrow(loading, "Failed to read tri count");
}
triangleCount = endianLittleToHost32(triangleCount);
out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3);
meshvertex_t *verts = out->vertices;
errorret_t ret;
for(uint32_t i = 0; i < triangleCount; i++) {
assetmeshstltriangle_t triData;
ret = assetFileRead(file, &triData, sizeof(triData));
if(errorIsNotOk(ret)) {
memoryFree(verts);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
if(file->lastRead != sizeof(triData)) {
memoryFree(verts);
out->vertices = NULL;
assetLoaderErrorThrow(loading, "Failed to read triangle data");
}
for(uint8_t j = 0; j < 3; j++) {
#if MESH_ENABLE_COLOR
verts[i * 3 + j].color.r = (
(uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f)
);
verts[i * 3 + j].color.g = (
(uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f)
);
verts[i * 3 + j].color.b = (
(uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 255.0f)
);
verts[i * 3 + j].color.a = 0xFF;
#endif
verts[i * 3 + j].uv[0] = 0.0f;
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]
);
}
switch(axis) {
case MESH_INPUT_AXIS_Z_UP: {
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
break;
}
case MESH_INPUT_AXIS_X_UP: {
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = temp;
break;
}
case MESH_INPUT_AXIS_Y_DOWN:
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[1];
break;
case MESH_INPUT_AXIS_Z_DOWN: {
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
break;
}
case MESH_INPUT_AXIS_X_DOWN: {
float_t temp = verts[i * 3 + j].pos[0];
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -temp;
break;
}
case MESH_INPUT_AXIS_Y_UP:
default:
break;
}
}
}
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(verts);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
assetFileDispose(file);
loading->loading.mesh.triangleCount = triangleCount;
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_CREATE_MESH;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetMeshLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
switch(loading->loading.mesh.state) {
case ASSET_MESH_LOADING_STATE_INITIAL:
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_MESH_LOADING_STATE_CREATE_MESH:
break;
default:
errorOk();
}
assetmeshoutput_t *out = &loading->entry->data.mesh;
assertNotNull(out->vertices, "Mesh vertices should have been loaded by now.");
errorret_t ret = meshInit(
&out->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
loading->loading.mesh.triangleCount * 3,
out->vertices
);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
memoryFree(out->vertices);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetMeshDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
errorChain(meshDispose(&entry->data.mesh.mesh));
memoryFree(entry->data.mesh.vertices);
errorOk();
}
@@ -1,58 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/mesh/mesh.h"
#include "assert/assert.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef enum {
MESH_INPUT_AXIS_Y_UP,
MESH_INPUT_AXIS_Z_UP,
MESH_INPUT_AXIS_X_UP,
MESH_INPUT_AXIS_Y_DOWN,
MESH_INPUT_AXIS_Z_DOWN,
MESH_INPUT_AXIS_X_DOWN,
} assetmeshinputaxis_t;
typedef assetmeshinputaxis_t assetmeshloaderinput_t;
typedef enum {
ASSET_MESH_LOADING_STATE_INITIAL,
ASSET_MESH_LOADING_STATE_READ_FILE,
ASSET_MESH_LOADING_STATE_CREATE_MESH,
ASSET_MESH_LOADING_STATE_DONE
} assetmeshloadingstate_t;
typedef struct {
assetfile_t file;
assetmeshloadingstate_t state;
uint32_t triangleCount;
} assetmeshloaderloading_t;
typedef struct {
mesh_t mesh;
meshvertex_t *vertices;
} assetmeshoutput_t;
#pragma pack(push, 1)
typedef struct {
vec3 normal;
float_t positions[3][3];
uint16_t attributeByteCount;
} assetmeshstltriangle_t;
#pragma pack(pop)
assertStructSize(assetmeshstltriangle_t, 50);
errorret_t assetMeshLoaderAsync(assetloading_t *loading);
errorret_t assetMeshLoaderSync(assetloading_t *loading);
errorret_t assetMeshDispose(assetentry_t *entry);
@@ -1,171 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettextureloader.h"
#include "assert/assert.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "log/log.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = {
.read = assetTextureReader,
.skip = assetTextureSkipper,
.eof = assetTextureEOF
};
int assetTextureReader(void *user, char *data, int size) {
assertNotNull(data, "Data buffer for stb_image callbacks cannot be NULL.");
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, data, (size_t)size);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return -1;
}
return file->lastRead;
}
void assetTextureSkipper(void *user, int n) {
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, NULL, (size_t)n);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
}
}
int assetTextureEOF(void *user) {
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
return file->position >= file->size;
}
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){
errorOk();
}
// Init the file
assertNull(
loading->loading.texture.data, "Pixels already defined?"
);
assetfile_t *file = &loading->loading.texture.file;
assetLoaderErrorChain(loading, assetFileInit(
file,
loading->entry->name,
NULL,
&loading->entry->data.texture
));
assetLoaderErrorChain(loading, assetFileOpen(file));
// Determine channels
int channelsDesired;
switch(loading->entry->inputData.texture) {
case TEXTURE_FORMAT_RGBA:
channelsDesired = 4;
break;
default:
assetLoaderErrorThrow(loading, "Bad texture format.");
}
// Load image pixels.
loading->loading.texture.data = stbi_load_from_callbacks(
&ASSET_TEXTURE_STB_CALLBACKS,
file,
&loading->loading.texture.width,
&loading->loading.texture.height,
&loading->loading.texture.channels,
channelsDesired
);
// Close out the file.
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
// 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
);
}
// Fixes a specific bug probably with Dolphin but for now just assuming endian
if(!isHostLittleEndian()) {
stbi__vertical_flip(
loading->loading.texture.data,
loading->loading.texture.width,
loading->loading.texture.height,
loading->loading.texture.channels
);
}
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
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:
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE:
break;
default:
errorOk();
}
// Create the texture.
assertNotNull(
loading->loading.texture.data, "Pixels should have been loaded by now."
);
assetLoaderErrorChain(loading, textureInit(
(texture_t*)&loading->entry->data.texture,
loading->loading.texture.width,
loading->loading.texture.height,
loading->entry->inputData.texture,
(texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data
}
));
// Free the pixels.
stbi_image_free(loading->loading.texture.data);
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
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);
}
@@ -1,81 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/texture/texture.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef textureformat_t assettextureloaderinput_t;
typedef enum {
ASSET_TEXTURE_LOADING_STATE_INITIAL,
ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS,
ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE,
ASSET_TEXTURE_LOADING_STATE_DONE
} assettextureloadingstate_t;
typedef struct {
assetfile_t file;
assettextureloadingstate_t state;
int channels, width, height;
uint8_t *data;
} assettextureloaderloading_t;
typedef texture_t assettextureoutput_t;
/**
* STB image read callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param data Buffer to read the file data into.
* @param size Size of the buffer to read into.
* @return Number of bytes read, or -1 on error.
*/
int assetTextureReader(void *user, char *data, int size);
/**
* STB image skip callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param n Number of bytes to skip in the file.
*/
void assetTextureSkipper(void *user, int n);
/**
* STB image EOF callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @return Non-zero if end of file has been reached, zero otherwise.
*/
int assetTextureEOF(void *user);
/**
* Synchronous loader for texture assets.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for texture assets.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoaderSync(assetloading_t *loading);
/**
* Disposer for texture assets.
*
* @param entry Asset entry containing the texture to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTextureDispose(assetentry_t *entry);
@@ -1,124 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettilesetloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
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();
}
assertNull(loading->loading.tileset.data, "Data already defined?");
assetfile_t *file = &loading->loading.tileset.file;
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."
);
loading->loading.tileset.data = data;
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
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:
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TILESET_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.tileset.data;
assertNotNull(data, "Tileset data should have been loaded by now.");
tileset_t *out = &loading->entry->data.tileset;
if(data[0] != 'D' || data[1] != 'T' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid tileset header");
}
if(data[3] != 0x00) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Unsupported tileset version");
}
out->tileWidth = endianLittleToHost16(*(uint16_t *)(data + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(data + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(data + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(data + 10));
if(out->tileWidth == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile width cannot be 0");
}
if(out->tileHeight == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile height cannot be 0");
}
if(out->columns == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Column count cannot be 0");
}
if(out->rows == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
}
out->tileCount = out->columns * out->rows;
memoryFree(data);
loading->loading.tileset.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
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();
}
@@ -1,60 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/texture/tileset.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assettilesetloaderinput_t;
typedef enum {
ASSET_TILESET_LOADING_STATE_INITIAL,
ASSET_TILESET_LOADING_STATE_READ_FILE,
ASSET_TILESET_LOADING_STATE_PARSE,
ASSET_TILESET_LOADING_STATE_DONE
} assettilesetloadingstate_t;
typedef struct {
assetfile_t file;
assettilesetloadingstate_t state;
uint8_t *data;
} assettilesetloaderloading_t;
typedef tileset_t assettilesetoutput_t;
/**
* Asynchronous loader for tileset assets. Reads the raw DTF file bytes into
* the loading buffer so the sync phase can parse without blocking the main
* thread on I/O.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTilesetLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for tileset assets. Parses the DTF binary previously
* read by the async phase and populates the output tileset_t.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTilesetLoaderSync(assetloading_t *loading);
/**
* Disposer for tileset assets.
*
* @param entry Asset entry containing the tileset to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTilesetDispose(assetentry_t *entry);
@@ -1,95 +0,0 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetjsonloader.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
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();
}
assertNull(loading->loading.json.buffer, "Buffer already defined?");
assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
}
size_t fileSize = (size_t)file->size;
uint8_t *buffer = memoryAllocate(fileSize);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
loading->loading.json.buffer = buffer;
loading->loading.json.size = fileSize;
loading->loading.json.state = ASSET_JSON_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
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:
loading->loading.json.state = ASSET_JSON_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_JSON_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *buffer = loading->loading.json.buffer;
assertNotNull(buffer, "JSON buffer should have been loaded by now.");
loading->entry->data.json = yyjson_read(
(char *)buffer,
loading->loading.json.size,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
loading->loading.json.buffer = NULL;
if(!loading->entry->data.json) {
assetLoaderErrorThrow(loading, "Failed to parse JSON");
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
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();
}

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