Compare commits
69 Commits
alpha-0.0.3
...
we-ball
| Author | SHA1 | Date | |
|---|---|---|---|
| 5184064a26 | |||
| 6a43363539 | |||
| b9195fbbad | |||
| f715ad2176 | |||
| ca02ee0352 | |||
| fbaa54145e | |||
| 28754ffbf2 | |||
| 470c0eba7a | |||
| 7098dcec43 | |||
| 07137f57af | |||
| 8b7491a3d3 | |||
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 | |||
| a73f55beb0 | |||
| 0bd2491ab7 | |||
| 860c797c9c | |||
| 38c5080f9f | |||
| fae191d8fe | |||
| d83a953e2d | |||
| 2dcf0d0f0d | |||
| 6dee37d8c1 | |||
| 3bad03afb3 | |||
| 6a6d8448f7 | |||
| 189babd2cf | |||
| 988a0f2294 | |||
| 85bf455731 | |||
| 589e4224f3 | |||
| bd7fa154b4 | |||
| a292f1992b | |||
| 88ddf429b7 | |||
| 7a1f6662df | |||
| afc68bccc6 | |||
| b4cdc4a64f | |||
| 3ad5afb81c | |||
| bed3f20118 | |||
| de67315178 | |||
| 8d5c0c7cad | |||
| a8271e01bd | |||
| 900b3f8558 | |||
| fdc4e056f9 | |||
| 7b98e40ccf | |||
| 01d89cf22c | |||
| 503a3c799a | |||
| 0b21388844 | |||
| 117bdf0c00 | |||
| 172dc5d37b | |||
| 874c6258ab | |||
| a2d0a12c1a | |||
| 38b24e1c3d | |||
| 5ecbbe296b | |||
| 9d7a769d8f | |||
| 9b75e5ed83 | |||
| a9ab8dc1d8 | |||
| a34831aa02 | |||
| 84ebaa0751 | |||
| 55352805ee | |||
| c0a3f2e16a | |||
| 28ff331a28 | |||
| f9a53ec719 | |||
| 6a403e6caf | |||
| a6f449bb93 | |||
| 0614bfc446 | |||
| bb020c36c1 | |||
| f17b0bfcfb | |||
| 2a85c9503f |
@@ -4,36 +4,6 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-polib \
|
||||
python3-pil \
|
||||
libsdl2-dev \
|
||||
libgl1-mesa-dev \
|
||||
libzip-dev \
|
||||
python3-dotenv \
|
||||
python3-pyqt5 \
|
||||
python3-opengl \
|
||||
xz-utils \
|
||||
liblzma-dev \
|
||||
libbz2-dev \
|
||||
zlib1g-dev \
|
||||
git \
|
||||
libssl-dev
|
||||
- name: Run tests
|
||||
run: ./scripts/test-linux.sh
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Test Dusk
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-polib \
|
||||
python3-pil \
|
||||
libsdl2-dev \
|
||||
libgl1-mesa-dev \
|
||||
libzip-dev \
|
||||
python3-dotenv \
|
||||
python3-pyqt5 \
|
||||
python3-opengl \
|
||||
xz-utils \
|
||||
liblzma-dev \
|
||||
libbz2-dev \
|
||||
zlib1g-dev \
|
||||
git \
|
||||
libssl-dev
|
||||
- name: Run tests
|
||||
run: ./scripts/test-linux.sh
|
||||
@@ -1,455 +0,0 @@
|
||||
# Dusk — Claude Code rules
|
||||
|
||||
## File headers
|
||||
Every C, H, and JS file starts with:
|
||||
|
||||
```c
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
```
|
||||
|
||||
JS files use `//` comment style instead.
|
||||
|
||||
---
|
||||
|
||||
## C conventions
|
||||
|
||||
### Types
|
||||
Always use the project-defined aliases instead of bare C primitives:
|
||||
|
||||
| Use | Not |
|
||||
|-----------|--------------|
|
||||
| `bool_t` | `bool` |
|
||||
| `int_t` | `int` |
|
||||
| `float_t` | `float` |
|
||||
| `char_t` | `char` |
|
||||
|
||||
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
|
||||
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
|
||||
|
||||
### Naming
|
||||
- **Functions** — snake_case, prefixed with their module:
|
||||
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
|
||||
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
|
||||
- **Macros / constants** — UPPER_SNAKE_CASE:
|
||||
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
|
||||
- **Files** — snake_case matching the primary type: `entityposition.c`,
|
||||
`moduleassetbatch.c`
|
||||
|
||||
### Header files (`.h`)
|
||||
- Use `#pragma once` — no include guards.
|
||||
- Declare every public function, `#define`, and `extern` global.
|
||||
- Write a JSDoc block (`/** … */`) above every declaration explaining
|
||||
purpose, `@param`s, and `@returns`.
|
||||
- Only include headers that the `.h` file itself strictly requires for
|
||||
the types it exposes. Move everything else to the `.c` file.
|
||||
Do not use forward declarations as a workaround — use the real
|
||||
include in the `.c` file instead.
|
||||
|
||||
### Implementation files (`.c`)
|
||||
- Contain function bodies only; no declarations.
|
||||
- Pull in whatever additional includes the implementation needs.
|
||||
- Do not use `static` or `inline` on **functions**. Every function,
|
||||
including internal helpers, must be declared in the matching `.h` and
|
||||
defined in the `.c` file. Internal helpers belong near the bottom of
|
||||
the `.c` file, not at the top with a `static` qualifier.
|
||||
`static` and `inline` on functions are only appropriate when the
|
||||
function body is written directly inside a `.h` file.
|
||||
`static` on **variables** (file-scope state) is fine and expected.
|
||||
|
||||
### Formatting
|
||||
- Hard-wrap all lines at **80 characters**.
|
||||
|
||||
### Error handling
|
||||
Return `errorret_t` from fallible functions. Use these macros:
|
||||
|
||||
```c
|
||||
errorOk(); // return success
|
||||
errorThrow("msg %d", val); // return failure with message
|
||||
errorChain(someCall()); // propagate failure, continue on success
|
||||
errorIsOk(ret) / errorIsNotOk(ret) // test a result
|
||||
errorCatch(ret); // handle + free an error
|
||||
```
|
||||
|
||||
Never return raw error codes or use `errno` for in-engine errors.
|
||||
|
||||
### Memory
|
||||
Use the project allocator — never raw `malloc`/`free`:
|
||||
|
||||
```c
|
||||
memoryAllocate(size) // allocate
|
||||
memoryFree(ptr) // free
|
||||
memoryZero(dest, size) // zero a block
|
||||
memoryCopy(dest, src, size) // copy
|
||||
```
|
||||
|
||||
### Asserts
|
||||
Prefer specific assert macros over bare `assert()`:
|
||||
|
||||
```c
|
||||
assertNotNull(ptr, "msg");
|
||||
assertTrue(cond, "msg");
|
||||
assertFalse(cond, "msg");
|
||||
assertUnreachable("msg");
|
||||
assertIsMainThread("msg");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build system
|
||||
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
|
||||
|
||||
```cmake
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
myfile.c
|
||||
)
|
||||
```
|
||||
|
||||
Never add source files to the root `CMakeLists.txt` directly.
|
||||
|
||||
---
|
||||
|
||||
## Platform support
|
||||
|
||||
### Targets
|
||||
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
||||
|
||||
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|
||||
|----------------------|-------------------|------------------|
|
||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
||||
|
||||
### Layer structure
|
||||
```
|
||||
src/dusk/ core, platform-agnostic game logic
|
||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
||||
src/dusklinux/ Linux + Knulli platform impl
|
||||
src/duskpsp/ PSP platform impl
|
||||
src/duskvita/ Vita platform impl
|
||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
||||
```
|
||||
|
||||
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
||||
it uses native GameCube/Wii rendering and input APIs.
|
||||
|
||||
### Platform guards
|
||||
Use the compile-time macros for platform-specific code:
|
||||
|
||||
```c
|
||||
#ifdef DUSK_PSP
|
||||
// PSP-only path
|
||||
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
||||
// GameCube / Wii path
|
||||
#else
|
||||
// Generic / Linux fallback
|
||||
#endif
|
||||
```
|
||||
|
||||
Additional capability macros set per-target:
|
||||
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
||||
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
||||
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
||||
|
||||
### Abstraction pattern
|
||||
Platform-specific implementations are wired in via `#define` macros in
|
||||
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
||||
the core calls through. Functions that a platform does not support are
|
||||
simply left undefined — the core guards calls with `#ifdef`.
|
||||
|
||||
### Adding platform-specific code
|
||||
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
||||
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
||||
or capability macro.
|
||||
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
||||
the platform header macros instead.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new asset loader type
|
||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
||||
`src/dusk/asset/loader/assetloader.h`.
|
||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
||||
`src/dusk/asset/loader/assetloader.c`.
|
||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new entity component
|
||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
||||
`entityMyCompDispose()`.
|
||||
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
||||
3. Add a row to `src/dusk/entity/componentlist.h`:
|
||||
```c
|
||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
||||
```
|
||||
This auto-generates the enum, union field, and definition entry.
|
||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
||||
|
||||
---
|
||||
|
||||
## Adding a new script (JS) module
|
||||
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
||||
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
||||
- Register props/funcs in `moduleMyModInit()` with
|
||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
||||
`scriptProtoDefineStaticFunc`.
|
||||
2. `#include` the header in
|
||||
`src/dusk/script/module/modulelist.c` and call
|
||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
||||
`moduleListDispose()`).
|
||||
3. For component modules also register in
|
||||
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
||||
so `entity.add()` returns the typed wrapper.
|
||||
4. Create `types/<category>/mymod.d.ts` and add a
|
||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Script module type declarations
|
||||
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
||||
check whether the corresponding `types/**/*.d.ts` needs updating and
|
||||
apply any changes before finishing the task.
|
||||
|
||||
---
|
||||
|
||||
## JavaScript (asset scripts)
|
||||
- Use `var` for module-level state; `const` for values that never
|
||||
change.
|
||||
- Always use semicolons.
|
||||
- Scene objects are plain objects (`var scene = {}`) with assigned
|
||||
methods.
|
||||
- Export via `module.exports = scene`.
|
||||
- Async scene init should use `async function` and `await`.
|
||||
|
||||
---
|
||||
|
||||
## Coding style
|
||||
|
||||
### ASCII only
|
||||
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
||||
Non-ASCII characters are banned even in comments and string literals.
|
||||
Use ASCII-only substitutes instead:
|
||||
- `--` or `-` instead of `—` (em dash)
|
||||
- `->` instead of `→` (arrow)
|
||||
- `x` or `*` instead of `×` (multiplication)
|
||||
|
||||
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
||||
|
||||
### Indentation
|
||||
2 spaces. No tabs.
|
||||
|
||||
### Keyword and operator spacing
|
||||
No space between a keyword or function name and its opening parenthesis:
|
||||
|
||||
```c
|
||||
if(!ptr) return;
|
||||
for(uint8_t i = 0; i < count; i++) {
|
||||
while(entry->state != DONE) {
|
||||
switch(type) {
|
||||
sizeof(assetbatch_t)
|
||||
memoryZero(ptr, size)
|
||||
```
|
||||
|
||||
Spaces around all binary operators and after every comma:
|
||||
|
||||
```c
|
||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
||||
(size_t)end - (size_t)start
|
||||
foo(a, b, c)
|
||||
```
|
||||
|
||||
### Braces
|
||||
Opening brace on the **same line** as the statement (K&R style) for all
|
||||
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
||||
|
||||
```c
|
||||
void assetEntryLock(assetentry_t *entry) {
|
||||
...
|
||||
}
|
||||
|
||||
if(dirty) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Guard returns
|
||||
Short guards go on one line with no braces:
|
||||
|
||||
```c
|
||||
if(!ptr) return;
|
||||
if(!b || !b->batch) return jerry_undefined();
|
||||
if(!(flags & DIRTY)) return;
|
||||
```
|
||||
|
||||
### Blank lines
|
||||
- One blank line between functions; no blank line at the start or end of
|
||||
a function body.
|
||||
- One blank line between logical blocks inside a function body.
|
||||
- No trailing blank lines at the end of a file.
|
||||
|
||||
### Pointer placement
|
||||
`*` is attached to the variable name, not the type:
|
||||
|
||||
```c
|
||||
assetentry_t *entry
|
||||
const char_t *name
|
||||
void *ptr
|
||||
uint8_t *d = (uint8_t *)dest;
|
||||
```
|
||||
|
||||
### Casts
|
||||
Space between cast and operand:
|
||||
|
||||
```c
|
||||
(assetbatch_t *)user
|
||||
(uint8_t *)dest
|
||||
(textureformat_t)v
|
||||
```
|
||||
|
||||
### Return
|
||||
No parentheses around the return value:
|
||||
|
||||
```c
|
||||
return ptr;
|
||||
return MEMORY_POINTERS_IN_USE;
|
||||
```
|
||||
|
||||
### switch / case
|
||||
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
||||
|
||||
```c
|
||||
switch(type) {
|
||||
case ASSET_LOADER_TYPE_TEXTURE:
|
||||
descs[i].input.texture = (textureformat_t)v;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-line function signatures
|
||||
When parameters don't fit on one line, put each on its own line indented
|
||||
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
||||
its own line at column 0:
|
||||
|
||||
```c
|
||||
void assetEntryInit(
|
||||
assetentry_t *entry,
|
||||
const char_t *name,
|
||||
const assetloadertype_t type,
|
||||
assetloaderinput_t *input
|
||||
) {
|
||||
|
||||
errorret_t memoryCompare(
|
||||
const void *a,
|
||||
const void *b,
|
||||
const size_t size
|
||||
);
|
||||
```
|
||||
|
||||
### Structs and enums
|
||||
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
||||
brace and name on the same line:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
errorcode_t code;
|
||||
char_t *message;
|
||||
} errorstate_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
```
|
||||
|
||||
### Designated initialisers
|
||||
Spaces inside braces; `.field = value`:
|
||||
|
||||
```c
|
||||
jsassetentry_t e = { .entry = entry };
|
||||
assetbatchloadedpend_t init = { .batch = batch };
|
||||
```
|
||||
|
||||
### Ternary operator
|
||||
Spaces around `?` and `:`:
|
||||
|
||||
```c
|
||||
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
||||
```
|
||||
|
||||
### const placement
|
||||
`const` before the type, `*` attached to the variable:
|
||||
|
||||
```c
|
||||
const char_t *name
|
||||
const void *src
|
||||
const size_t size
|
||||
```
|
||||
|
||||
### Comments in `.c` files
|
||||
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
||||
functions follow one another with a single blank line between them.
|
||||
- Multi-line explanatory comments inside function bodies use `//` lines:
|
||||
```c
|
||||
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
||||
// so their finalizers fire before assetDispose() checks ref counts.
|
||||
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
||||
```
|
||||
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
||||
function bodies.
|
||||
|
||||
### Comments in `.h` files
|
||||
Every public declaration gets a Javadoc block (`/** … */`) with
|
||||
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
||||
above the declaration with no blank line in between.
|
||||
|
||||
---
|
||||
|
||||
## Color system
|
||||
|
||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
||||
|
||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
||||
The script emits four `#define` variants per color plus a bare alias:
|
||||
|
||||
```
|
||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
||||
COLOR_<NAME> COLOR_<NAME>_4B
|
||||
```
|
||||
|
||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
||||
|
||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
||||
hand-edit the generated header.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
- Tests live in `test/` mirroring `src/dusk/` structure.
|
||||
- Use cmocka; include `dusktest.h`.
|
||||
- Test functions: `static void test_something(void **state)`.
|
||||
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
||||
leaks.
|
||||
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
||||
@@ -1,5 +1,6 @@
|
||||
# Dusk
|
||||
RPG Game Project, small and able to run on a PSP.
|
||||
# Documentation
|
||||
- [Scripting](docs/SCRIPTING.md) — writing gameplay logic in JavaScript.
|
||||
- [UI](docs/UI.md) — building buttons/menus/etc from engine/game C code.
|
||||
|
||||
# Building
|
||||
Each build target has different requirements. You can take a look at the git
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
var Actions;
|
||||
var camera, cameraPosition;
|
||||
var cube, cubePosition, cubeRenderable, cubeMesh;
|
||||
var ground, groundPosition, groundRenderable;
|
||||
|
||||
// init() is called via scriptManagerCallGlobal(), which pumps the asset
|
||||
// system + job queue until any promise it returns settles - so it's safe
|
||||
// to await include() here even though this runs before the main loop.
|
||||
async function init() {
|
||||
Actions = await include("input.js");
|
||||
|
||||
camera = new Entity();
|
||||
cameraPosition = camera.add(POSITION);
|
||||
camera.add(CAMERA);
|
||||
cameraPosition.position = new Vec3(3, 3, -6);
|
||||
cameraPosition.lookAt(new Vec3(0, 0, 0));
|
||||
|
||||
cube = new Entity();
|
||||
cubePosition = cube.add(POSITION);
|
||||
cubeRenderable = cube.add(RENDERABLE);
|
||||
cubeMesh = Mesh.createCube();
|
||||
cubeRenderable.mesh = cubeMesh;
|
||||
cubeRenderable.color = Color.red();
|
||||
|
||||
ground = new Entity();
|
||||
groundPosition = ground.add(POSITION);
|
||||
groundRenderable = ground.add(RENDERABLE);
|
||||
groundRenderable.mesh = Mesh.createCube();
|
||||
groundRenderable.color = Color.dark_gray();
|
||||
}
|
||||
|
||||
// Runs every frame (including dynamic/interpolation frames) - use for
|
||||
// smooth, purely presentational animation.
|
||||
function update() {
|
||||
cubePosition.rotation.y += TIME.delta * 1.5;
|
||||
cubePosition.rotation.x += TIME.delta * 0.7;
|
||||
}
|
||||
|
||||
// Runs once per fixed timestep only - use for gameplay logic that should
|
||||
// be deterministic and independent of display refresh rate.
|
||||
function fixedUpdate() {
|
||||
}
|
||||
|
||||
function deinit() {
|
||||
cube.dispose();
|
||||
camera.dispose();
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Binds physical buttons to abstract actions, then exports the action
|
||||
// constants so other scripts don't need to know raw INPUT_ACTION_* names.
|
||||
Input.bind("w", INPUT_ACTION_UP);
|
||||
Input.bind("s", INPUT_ACTION_DOWN);
|
||||
Input.bind("a", INPUT_ACTION_LEFT);
|
||||
Input.bind("d", INPUT_ACTION_RIGHT);
|
||||
Input.bind("space", INPUT_ACTION_ACCEPT);
|
||||
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
|
||||
|
||||
if(typeof INPUT_GAMEPAD !== "undefined") {
|
||||
Input.bind("gamepad_up", INPUT_ACTION_UP);
|
||||
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
|
||||
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
|
||||
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
|
||||
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
|
||||
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
|
||||
}
|
||||
|
||||
module = {
|
||||
UP: INPUT_ACTION_UP,
|
||||
DOWN: INPUT_ACTION_DOWN,
|
||||
LEFT: INPUT_ACTION_LEFT,
|
||||
RIGHT: INPUT_ACTION_RIGHT,
|
||||
ACCEPT: INPUT_ACTION_ACCEPT,
|
||||
CANCEL: INPUT_ACTION_CANCEL,
|
||||
RAGEQUIT: INPUT_ACTION_RAGEQUIT
|
||||
};
|
||||
+2
-60
@@ -1,60 +1,2 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: en\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"
|
||||
|
||||
#: 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 "test.string"
|
||||
msgstr "This is a test string"
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,6 +0,0 @@
|
||||
module = {
|
||||
render() {
|
||||
Text.draw(0, 0, "Hello World");
|
||||
SpriteBatch.flush();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Turn things off we don't need
|
||||
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
|
||||
set(JERRY_EXT ON CACHE BOOL "" FORCE)
|
||||
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
|
||||
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Fetch Jerry
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
jerryscript
|
||||
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
|
||||
GIT_TAG float32-fix
|
||||
)
|
||||
FetchContent_MakeAvailable(jerryscript)
|
||||
|
||||
# Mark found
|
||||
set(jerryscript_FOUND ON)
|
||||
|
||||
# Define targets
|
||||
if(TARGET jerryscript-core)
|
||||
set(JERRY_CORE_TARGET jerryscript-core)
|
||||
elseif(TARGET jerry-core)
|
||||
set(JERRY_CORE_TARGET jerry-core)
|
||||
endif()
|
||||
|
||||
if(TARGET jerryscript-ext)
|
||||
set(JERRY_EXT_TARGET jerryscript-ext)
|
||||
elseif(TARGET jerry-ext)
|
||||
set(JERRY_EXT_TARGET jerry-ext)
|
||||
endif()
|
||||
|
||||
if(TARGET jerryscript-port-default)
|
||||
set(JERRY_PORT_TARGET jerryscript-port-default)
|
||||
elseif(TARGET jerry-port-default)
|
||||
set(JERRY_PORT_TARGET jerry-port-default)
|
||||
elseif(TARGET jerryscript-port)
|
||||
set(JERRY_PORT_TARGET jerryscript-port)
|
||||
elseif(TARGET jerry-port)
|
||||
set(JERRY_PORT_TARGET jerry-port)
|
||||
endif()
|
||||
|
||||
if(NOT JERRY_CORE_TARGET)
|
||||
message(FATAL_ERROR "JerryScript core target not found")
|
||||
endif()
|
||||
|
||||
if(NOT JERRY_EXT_TARGET)
|
||||
message(FATAL_ERROR "JerryScript ext target not found")
|
||||
endif()
|
||||
|
||||
if(NOT JERRY_PORT_TARGET)
|
||||
message(FATAL_ERROR "JerryScript port target not found")
|
||||
endif()
|
||||
|
||||
foreach(tgt IN ITEMS
|
||||
${JERRY_CORE_TARGET}
|
||||
${JERRY_EXT_TARGET}
|
||||
${JERRY_PORT_TARGET}
|
||||
)
|
||||
if(TARGET ${tgt})
|
||||
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
|
||||
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
|
||||
JERRY_NUMBER_TYPE_FLOAT64=0
|
||||
JERRY_BUILTIN_DATE=0
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Export include dirs through the targets
|
||||
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
|
||||
${jerryscript_SOURCE_DIR}/jerry-core/include
|
||||
)
|
||||
|
||||
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
|
||||
${jerryscript_SOURCE_DIR}/jerry-ext/include
|
||||
)
|
||||
|
||||
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
|
||||
${jerryscript_SOURCE_DIR}/jerry-port/default/include
|
||||
)
|
||||
|
||||
# Suppress JerryScript-only warning
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
|
||||
-Wno-error
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
|
||||
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
|
||||
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
|
||||
@@ -16,6 +16,9 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DOL=1
|
||||
ISO=2
|
||||
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
|
||||
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
|
||||
# like every other target instead of relying on endian.h's runtime probe.
|
||||
DUSK_PLATFORM_ENDIAN_BIG
|
||||
)
|
||||
|
||||
# Custom compiler flags
|
||||
|
||||
@@ -43,4 +43,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_INPUT_POINTER
|
||||
DUSK_INPUT_GAMEPAD
|
||||
DUSK_TIME_DYNAMIC
|
||||
DUSK_THREAD_PTHREAD
|
||||
)
|
||||
@@ -36,7 +36,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
# DUSK_OPENGL_LEGACY
|
||||
DUSK_LINUX
|
||||
DUSK_DISPLAY_SIZE_DYNAMIC
|
||||
DUSK_DISPLAY_WIDTH_DEFAULT=640
|
||||
DUSK_DISPLAY_WIDTH_DEFAULT=854
|
||||
DUSK_DISPLAY_HEIGHT_DEFAULT=480
|
||||
DUSK_DISPLAY_SCREEN_HEIGHT=240
|
||||
DUSK_INPUT_KEYBOARD
|
||||
|
||||
@@ -55,8 +55,16 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_DISPLAY_WIDTH=480
|
||||
DUSK_DISPLAY_HEIGHT=272
|
||||
DUSK_THREAD_PTHREAD
|
||||
DUSK_TIME_DYNAMIC
|
||||
DUSK_DISPLAY_OVERSCAN=6
|
||||
)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_ASSERTIONS_FAKED
|
||||
)
|
||||
endif()
|
||||
|
||||
# Postbuild, create .pbp file for PSP.
|
||||
create_pbp_file(
|
||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||
|
||||
@@ -75,6 +75,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_OPENGL_LEGACY
|
||||
DUSK_DISPLAY_WIDTH=960
|
||||
DUSK_DISPLAY_HEIGHT=544
|
||||
DUSK_THREAD_PTHREAD
|
||||
)
|
||||
|
||||
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
# Scripting
|
||||
|
||||
Dusk embeds [JerryScript](https://github.com/jerryscript-project/jerryscript) to
|
||||
drive gameplay logic from JavaScript. The engine itself (rendering, physics,
|
||||
asset loading, entity storage) is all C; scripts sit on top and manipulate that
|
||||
state through a small set of bound objects.
|
||||
|
||||
This document covers the JS-facing scripting API. **UI (buttons, sliders,
|
||||
menus, etc.) is not exposed to scripts** — it's a separate, C-only API. See
|
||||
[UI.md](UI.md) if you're building screens/menus from engine/game C code.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
On startup, `engineInit()` loads and evaluates `assets/engine.js` as the main
|
||||
script, then calls the global `init()` function if one is defined. From then
|
||||
on, every engine tick calls (in order):
|
||||
|
||||
1. `fixedUpdate()` — once per fixed timestep. Use this for gameplay logic that
|
||||
must be deterministic and independent of display refresh rate (movement,
|
||||
physics-adjacent input handling, etc). Skipped on interpolation/dynamic
|
||||
frames when the build has variable-timestep rendering enabled.
|
||||
2. `update()` — once per rendered frame, including interpolation frames. Use
|
||||
this for smooth, purely presentational animation (nothing that needs to be
|
||||
deterministic).
|
||||
|
||||
On shutdown, `deinit()` is called once.
|
||||
|
||||
All four hooks (`init`, `update`, `fixedUpdate`, `deinit`) are **optional** —
|
||||
if a script doesn't define one, the engine simply skips it, no error.
|
||||
|
||||
```js
|
||||
function update() {
|
||||
cubePosition.rotation.y += TIME.delta * 1.5;
|
||||
}
|
||||
```
|
||||
|
||||
### `async init()` and `include()`
|
||||
|
||||
`init` (or any of the other hooks) can be declared `async` and use `await`
|
||||
freely, including awaiting `include()` (see below), even though the engine
|
||||
calls these functions synchronously from C with no external JS event loop.
|
||||
When a hook returns a pending `Promise`, the engine keeps driving the asset
|
||||
system and JerryScript's job queue until that promise settles before
|
||||
continuing — so by the time e.g. `init()` "returns" from the engine's point of
|
||||
view, everything it awaited has actually finished.
|
||||
|
||||
```js
|
||||
async function init() {
|
||||
Actions = await include("input.js");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
If the awaited work throws/rejects, it surfaces as a C-level error.
|
||||
|
||||
## Loading other scripts: `include(path)`
|
||||
|
||||
```js
|
||||
var Actions = await include("input.js");
|
||||
```
|
||||
|
||||
`include(path)` always returns a `Promise`. The named file is loaded and
|
||||
evaluated once no matter how many times (or from how many different scripts)
|
||||
you `include()` it — later calls for the same path are handed the same
|
||||
in-flight/resolved promise rather than re-running the file.
|
||||
|
||||
The included script communicates its result back by assigning to the bare
|
||||
global `module`:
|
||||
|
||||
```js
|
||||
// input.js
|
||||
Input.bind("w", INPUT_ACTION_UP);
|
||||
// ...
|
||||
|
||||
module = {
|
||||
UP: INPUT_ACTION_UP,
|
||||
DOWN: INPUT_ACTION_DOWN,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Whatever `input.js` assigns to `module` becomes the resolved value of the
|
||||
promise `include("input.js")` returned — that's what `Actions` ends up being
|
||||
in the example above. If the included script throws, the promise rejects
|
||||
instead.
|
||||
|
||||
## Full example
|
||||
|
||||
This is the actual shipped example content (`assets/engine.js` +
|
||||
`assets/input.js`):
|
||||
|
||||
```js
|
||||
// input.js — binds physical buttons to abstract actions, then exports the
|
||||
// action constants so other scripts don't need to know raw INPUT_ACTION_* names.
|
||||
Input.bind("w", INPUT_ACTION_UP);
|
||||
Input.bind("s", INPUT_ACTION_DOWN);
|
||||
Input.bind("a", INPUT_ACTION_LEFT);
|
||||
Input.bind("d", INPUT_ACTION_RIGHT);
|
||||
Input.bind("space", INPUT_ACTION_ACCEPT);
|
||||
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
|
||||
|
||||
if(typeof INPUT_GAMEPAD !== "undefined") {
|
||||
Input.bind("gamepad_up", INPUT_ACTION_UP);
|
||||
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
|
||||
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
|
||||
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
|
||||
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
|
||||
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
|
||||
}
|
||||
|
||||
module = {
|
||||
UP: INPUT_ACTION_UP,
|
||||
DOWN: INPUT_ACTION_DOWN,
|
||||
LEFT: INPUT_ACTION_LEFT,
|
||||
RIGHT: INPUT_ACTION_RIGHT,
|
||||
ACCEPT: INPUT_ACTION_ACCEPT,
|
||||
CANCEL: INPUT_ACTION_CANCEL,
|
||||
RAGEQUIT: INPUT_ACTION_RAGEQUIT
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
// engine.js
|
||||
var Actions;
|
||||
var camera, cameraPosition;
|
||||
var cube, cubePosition, cubeRenderable, cubeMesh;
|
||||
|
||||
async function init() {
|
||||
Actions = await include("input.js");
|
||||
|
||||
camera = new Entity();
|
||||
cameraPosition = camera.add(POSITION);
|
||||
camera.add(CAMERA);
|
||||
cameraPosition.position = new Vec3(3, 3, -6);
|
||||
cameraPosition.lookAt(new Vec3(0, 0, 0));
|
||||
|
||||
cube = new Entity();
|
||||
cubePosition = cube.add(POSITION);
|
||||
cubeRenderable = cube.add(RENDERABLE);
|
||||
cubeMesh = Mesh.createCube();
|
||||
cubeRenderable.mesh = cubeMesh;
|
||||
cubeRenderable.color = Color.red();
|
||||
}
|
||||
|
||||
function update() {
|
||||
cubePosition.rotation.y += TIME.delta * 1.5;
|
||||
cubePosition.rotation.x += TIME.delta * 0.7;
|
||||
}
|
||||
|
||||
function fixedUpdate() {
|
||||
var move = 3.0 * TIME.delta;
|
||||
if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move;
|
||||
if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move;
|
||||
if(Input.isDown(Actions.UP)) cubePosition.position.z += move;
|
||||
if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move;
|
||||
if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0);
|
||||
}
|
||||
|
||||
function deinit() {
|
||||
cube.dispose();
|
||||
camera.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
## API reference
|
||||
|
||||
### `TIME`
|
||||
|
||||
Plain global object, live getters (read fresh engine state every access, not
|
||||
snapshotted):
|
||||
|
||||
| Property | Type | Description |
|
||||
|---|---|---|
|
||||
| `TIME.delta` | number | Seconds since the last frame. |
|
||||
| `TIME.time` | number | Total elapsed engine time, in seconds. |
|
||||
|
||||
### `PLATFORM`
|
||||
|
||||
A single global string constant — the compile-time target name, e.g.
|
||||
`"linux"`, `"psp"`, `"vita"`, `"dolphin"`. Individual platform builds may
|
||||
inject additional platform-specific globals via their own
|
||||
`modulePlatformPlatform()` hook; those aren't documented here since they vary
|
||||
per target.
|
||||
|
||||
### `Input`
|
||||
|
||||
Static namespace (not constructible — there's no `new Input()`).
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `Input.bind(buttonName, action)` | Binds a physical button/key (string, e.g. `"w"`, `"space"`, `"gamepad_up"`) to an abstract `INPUT_ACTION_*` constant. Many buttons can bind to the same action. Throws on an empty/unrecognized button name or invalid action. |
|
||||
| `Input.isDown(action)` → boolean | Is the action currently held. |
|
||||
| `Input.pressed(action)` → boolean | Action transitioned to down this frame. |
|
||||
| `Input.released(action)` → boolean | Action transitioned to up this frame. |
|
||||
| `Input.getValue(action)` → number | Current analog value for the action. |
|
||||
| `Input.axis(negAction, posAction)` → number | Combined axis value from two opposing actions. |
|
||||
| `Input.axis2D(negX, posX, negY, posY)` → `Vec2` | Combined 2D axis from four actions. |
|
||||
|
||||
Global `INPUT_ACTION_*` constants (names are stable API; treat the numeric
|
||||
values as opaque/build-specific): `INPUT_ACTION_UP`, `INPUT_ACTION_DOWN`,
|
||||
`INPUT_ACTION_LEFT`, `INPUT_ACTION_RIGHT`, `INPUT_ACTION_ACCEPT`,
|
||||
`INPUT_ACTION_CANCEL`, `INPUT_ACTION_PAUSE`, `INPUT_ACTION_RAGEQUIT`,
|
||||
`INPUT_ACTION_CONSOLE`, `INPUT_ACTION_POINTERX`, `INPUT_ACTION_POINTERY`.
|
||||
|
||||
Conditionally-defined boolean globals reflecting build capability — only
|
||||
present at all if the corresponding input method is compiled in, so
|
||||
feature-test with `typeof`, don't assume they exist:
|
||||
`INPUT_KEYBOARD`, `INPUT_GAMEPAD`, `INPUT_POINTER`, `INPUT_TOUCH`.
|
||||
|
||||
### `Vec2` / `Vec3` / `Vec4`
|
||||
|
||||
`new Vec2(x?, y?)`, `new Vec3(x?, y?, z?)`, `new Vec4(x?, y?, z?, w?)` — all
|
||||
components optional, default `0`.
|
||||
|
||||
Common instance surface across all three: `.dot(other)`, `.length()`,
|
||||
`.lengthSq()`, `.normalize()`, `.negate()`, `.add(other)`, `.sub(other)`,
|
||||
`.scale(n)`, `.lerp(other, t)` — each of `add`/`sub`/`scale`/`negate`/
|
||||
`normalize`/`lerp` returns a **new** vector (non-mutating). `Vec3` additionally
|
||||
has `.cross(other)` and `.distance(other)`; `Vec2` has `.distance(other)` too;
|
||||
`Vec4` has neither `.cross()` nor `.distance()`.
|
||||
|
||||
`Vec4` also has UV aliases over the same four floats: `.u0` (= `.x`), `.v0`
|
||||
(= `.y`), `.u1` (= `.z`), `.v1` (= `.w`) — handy for texture-rect style code.
|
||||
|
||||
All three have `.x`/`.y`(/`.z`/`.w`) get/set properties and a `.toString()`
|
||||
like `"Vec3(1, 2, 3)"`.
|
||||
|
||||
**"Vec3Ref" — live references.** Several engine properties (entity
|
||||
`position`/`rotation`/`scale`, physics `velocity`, a mesh vertex's `position`)
|
||||
return a vector-*like* object instead of a plain `Vec3`. It has the identical
|
||||
`.x`/`.y`/`.z` surface, but reads/writes go straight into the underlying
|
||||
native buffer — writing `.x` on `entity.position.position` immediately moves
|
||||
the entity, no separate assignment needed. Anywhere the API expects a `Vec3`
|
||||
argument, a Vec3Ref works too. You never construct one directly; you only
|
||||
ever receive them from properties like the ones above.
|
||||
|
||||
### `Mat4`
|
||||
|
||||
`new Mat4()` — always constructs identity; no other constructor form.
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.mul(other)` → `Mat4` | `this * other`. |
|
||||
| `.transpose()` → `Mat4` | |
|
||||
| `.inverse()` → `Mat4` | |
|
||||
| `.determinant()` → number | |
|
||||
| `.mulVec3(vec3, w?)` → `Vec3` | `w` defaults to `1.0` (point); pass `0` for a direction. |
|
||||
| `.mulVec4(vec4)` → `Vec4` | |
|
||||
| `.translate(vec3)` → `Mat4` | Non-mutating — returns a translated copy. |
|
||||
| `.scale(vec3)` → `Mat4` | Non-mutating — returns a scaled copy. |
|
||||
| `Mat4.identity()` → `Mat4` | Static. |
|
||||
| `Mat4.perspective(fov, aspect, near, far)` → `Mat4` | Static, all 4 args required. |
|
||||
| `Mat4.lookAt(eye, center, up)` → `Mat4` | Static, all 3 args required `Vec3`s. |
|
||||
|
||||
### `Color`
|
||||
|
||||
`new Color(r?, g?, b?, a?)` — each an int `0..255`, default `255` (so
|
||||
`new Color()` is opaque white). Properties `.r`/`.g`/`.b`/`.a` get/set.
|
||||
|
||||
Named factories, each a zero-arg static returning a new opaque `Color`
|
||||
(alpha `255` unless noted): `Color.black()`, `Color.white()`, `Color.red()`,
|
||||
`Color.green()`, `Color.blue()`, `Color.yellow()`, `Color.cyan()`,
|
||||
`Color.magenta()`, `Color.transparent()` (alpha 0), `Color.transparent_white()`
|
||||
(alpha 0), `Color.transparent_black()` (alpha 0), `Color.gray()`,
|
||||
`Color.light_gray()`, `Color.dark_gray()`, `Color.orange()`, `Color.purple()`,
|
||||
`Color.brown()`, `Color.pink()`, `Color.lime()`, `Color.navy()`,
|
||||
`Color.teal()`, `Color.cornflower_blue()`.
|
||||
|
||||
`Color.rainbow(t?, speed?)` → `Color` — `t` defaults to `TIME.time * 4.0`;
|
||||
produces a shifting rainbow color, useful for debug visuals.
|
||||
|
||||
### `Mesh`
|
||||
|
||||
`new Mesh(vertexCount)` — allocates an uninitialized CPU-side vertex buffer
|
||||
(not yet uploaded to the GPU).
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.vertices` | Array of vertex wrappers, each with a `.position` (Vec3Ref, writes straight into that vertex). |
|
||||
| `.vertexCount` | Read-only. |
|
||||
| `.flush()` | Uploads to the GPU. First call initializes the GPU mesh; later calls re-upload the current vertex data — call this after editing `.vertices[i].position`. |
|
||||
| `.dispose()` | Frees GPU + CPU resources. |
|
||||
|
||||
Static engine-owned singletons (read-only, not something you dispose):
|
||||
`Mesh.DEFAULT_CUBE`, `Mesh.DEFAULT_QUAD`, `Mesh.DEFAULT_SPHERE`,
|
||||
`Mesh.DEFAULT_PLANE`, `Mesh.DEFAULT_CAPSULE`, `Mesh.DEFAULT_TRIPRISM`.
|
||||
|
||||
Static factories (each builds and uploads a brand-new `Mesh`):
|
||||
|
||||
| Factory | Notes |
|
||||
|---|---|
|
||||
| `Mesh.createCube(min?, max?)` | Both `Vec3`, default `(-0.5,-0.5,-0.5)`..`(0.5,0.5,0.5)`. |
|
||||
| `Mesh.createQuad(minX?, minY?, maxX?, maxY?)` | Default `-0.5..0.5` both axes; UV fixed `0,0`–`1,1`. |
|
||||
| `Mesh.createSphere(radius?, stacks?, sectors?)` | `radius` default `0.5`. |
|
||||
| `Mesh.createPlane(width?, height?)` | Defaults `1.0`/`1.0`; XZ-aligned, centered at origin. |
|
||||
| `Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)` | Defaults `0.5`, `0.5`. |
|
||||
| `Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)` | All 8 args required — a triangular cross-section extruded along Z. |
|
||||
|
||||
### `Entity` and components
|
||||
|
||||
```js
|
||||
var e = new Entity();
|
||||
var pos = e.add(POSITION);
|
||||
```
|
||||
|
||||
`new Entity()` allocates an entity. `.id` is the read-only numeric engine ID.
|
||||
`.add(TYPE)` adds a component and returns its wrapper (`TYPE` is one of the
|
||||
constants below). `.dispose()` removes the entity and all its components.
|
||||
|
||||
Component-type constants: `POSITION`, `CAMERA`, `RENDERABLE`, `PHYSICS`,
|
||||
`TRIGGER`. Each entity also exposes a lowercase getter that returns the
|
||||
existing wrapper if the component is present, or `undefined` if not (it does
|
||||
**not** add the component — use `.add()` for that): `entity.position`,
|
||||
`entity.camera`, `entity.renderable`, `entity.physics`, `entity.trigger`.
|
||||
|
||||
#### `entity.add(POSITION)` → position component
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.position` | Vec3Ref. Writing rebuilds the transform automatically. |
|
||||
| `.rotation` | Vec3Ref, Euler angles. Same rebuild-on-write behavior. |
|
||||
| `.scale` | Vec3Ref. Same rebuild-on-write behavior. |
|
||||
| `.parent` | Get/set another position-component wrapper, or `null` to clear parenting. |
|
||||
| `.lookAt(target, up?)` | `target` a `Vec3`; `up` defaults to `(0,1,0)`. |
|
||||
|
||||
#### `entity.add(CAMERA)` → camera component
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.zNear` / `.zFar` | Numbers. |
|
||||
| `.fov` | Only meaningful when `projectionType` is `CAMERA_TYPE_PERSPECTIVE`; otherwise get returns `undefined` and set is a no-op. |
|
||||
| `.projectionType` | `CAMERA_TYPE_PERSPECTIVE` or `CAMERA_TYPE_ORTHOGRAPHIC`. |
|
||||
| `.orthoTop` / `.orthoBottom` / `.orthoLeft` / `.orthoRight` | Only meaningful in orthographic mode, same undefined/no-op rule otherwise. |
|
||||
|
||||
#### `entity.add(RENDERABLE)` → renderable component
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.type` | `ENTITY_RENDERABLE_TYPE_MATERIAL`, `_SPRITEBATCH`, or `_CALLBACK`. |
|
||||
| `.mesh` | Get/set a `Mesh` instance or a `Mesh.DEFAULT_*` singleton. |
|
||||
| `.color` | Get/set a `Color` instance (throws if given something else). |
|
||||
| `.addSprite({ min?, max?, uvMin?, uvMax? })` | Adds a sprite to this renderable's sprite batch; all fields optional, default zero. |
|
||||
| `.clearSprites()` | Clears the sprite batch. |
|
||||
| `.setCallback(fn?)` | Switches to `ENTITY_RENDERABLE_TYPE_CALLBACK` and calls `fn()` on every render of this entity. Omit/pass non-function to clear. Exceptions inside `fn` surface as a C error. |
|
||||
|
||||
#### `entity.add(PHYSICS)` → physics component
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.velocity` | Vec3Ref, plain (no rebuild-on-write). |
|
||||
| `.onGround` | Read-only boolean. |
|
||||
| `.bodyType` | `PHYSICS_BODY_STATIC`, `PHYSICS_BODY_DYNAMIC`, `PHYSICS_BODY_KINEMATIC`. |
|
||||
| `.applyImpulse(vec3)` | Adds to velocity. No-op on static bodies. |
|
||||
| `.setShapeCube(halfExtents)` | `halfExtents` a `Vec3`. |
|
||||
| `.setShapeSphere(radius)` | Number. |
|
||||
| `.setShapeCapsule(radius, halfHeight)` | Two numbers. |
|
||||
| `.setShapePlane(normal, distance)` | `Vec3` + number. |
|
||||
|
||||
Shape-type constants (for reading `.type` on the underlying shape, not for
|
||||
`.bodyType`): `PHYSICS_SHAPE_CUBE`, `PHYSICS_SHAPE_SPHERE`,
|
||||
`PHYSICS_SHAPE_CAPSULE`, `PHYSICS_SHAPE_PLANE`.
|
||||
|
||||
#### `entity.add(TRIGGER)` → trigger component
|
||||
|
||||
| Member | Description |
|
||||
|---|---|
|
||||
| `.min` / `.max` | Plain `Vec3` values (copies, not live refs). |
|
||||
| `.setBounds(min, max)` | Sets both at once. |
|
||||
| `.contains(point)` → boolean | `point` a `Vec3`. |
|
||||
|
||||
## Not yet available to scripts
|
||||
|
||||
The following C modules exist and are fully implemented, but aren't currently
|
||||
wired into script registration (`moduleRegister()` in
|
||||
`src/dusk/script/module/module.h`), so none of these globals exist in a
|
||||
script today: `Screen`, `SpriteBatch`, `Text`, `Scene`, `Easing`, `Console`,
|
||||
`Engine`. If you need one of these from a script, it needs to be registered
|
||||
in `moduleRegister()` first — see the existing entries there and the modules
|
||||
under `src/dusk/script/module/` for the pattern to follow.
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
# UI
|
||||
|
||||
Dusk's UI system (buttons, checkboxes, sliders, dropdowns, tabs, menus, focus
|
||||
navigation) is a **C-only API**. It is not exposed to JerryScript — see
|
||||
[SCRIPTING.md](SCRIPTING.md) for what scripts *can* touch. If you need a
|
||||
script to open/react to a menu, wire it through a C callback or a game-side
|
||||
flag scripts can poll; there's no bridge for this today.
|
||||
|
||||
## Mental model
|
||||
|
||||
There is no retained-mode UI tree, no automatic dispatch, no scissor/clip-rect
|
||||
API. Every widget is a plain struct you own (usually as a global or
|
||||
scene-owned variable). You call its `xxxInit(...)` once, then call its
|
||||
`xxxDraw(widget, x, y)` yourself, every frame you want it visible, at whatever
|
||||
screen position you choose. Nothing draws itself automatically except three
|
||||
fixed system overlays (overscan bars, debug console, FPS counter) — see
|
||||
[System overlays](#system-overlays-automatic) below.
|
||||
|
||||
### Where UI rendering happens in the frame
|
||||
|
||||
- `uiInit()` / `uiDispose()` run once, at engine startup/shutdown.
|
||||
- `uiUpdate()` runs once per tick (drives focus-navigation input handling).
|
||||
- `uiRender()` runs once per frame, called from inside `sceneRender()` — i.e.
|
||||
**after** the active scene's own 3D/game-world rendering, using an
|
||||
orthographic screen-space projection. Your own widget `xxxDraw()` calls
|
||||
should happen around the same point — typically from your scene's render
|
||||
callback, after world content, so UI draws on top.
|
||||
|
||||
## Widgets
|
||||
|
||||
Every widget follows the same shape: `xxxInit(widget, ...)` zeroes the struct
|
||||
and sets its fields; `xxxDraw(const widget*, x, y) -> errorret_t` draws it at
|
||||
that screen position.
|
||||
|
||||
> **Init before Draw.** `uislider_t`, `uidropdown_t`, and `uitab_t` cache
|
||||
> their label's measured width/height at `Init` time (an optimization —
|
||||
> label text doesn't change after that point). Calling `Draw` before `Init`,
|
||||
> or mutating `->label` directly instead of re-initializing, leaves stale
|
||||
> layout. `uibutton_t`/`uicheckbox_t` don't have this restriction.
|
||||
|
||||
### Button
|
||||
|
||||
```c
|
||||
void uiButtonInit(uibutton_t *button, const char_t *label);
|
||||
bool_t uiButtonIsHighlighted(const uibutton_t *button);
|
||||
void uiButtonSetHighlighted(uibutton_t *button, bool_t highlighted);
|
||||
errorret_t uiButtonDraw(const uibutton_t *button, float_t x, float_t y);
|
||||
```
|
||||
|
||||
Draws `label` in red when highlighted, white otherwise.
|
||||
|
||||
### Checkbox
|
||||
|
||||
```c
|
||||
void uiCheckboxInit(uicheckbox_t *checkbox, const char_t *label);
|
||||
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox);
|
||||
void uiCheckboxSetChecked(uicheckbox_t *checkbox, bool_t checked);
|
||||
void uiCheckboxToggle(uicheckbox_t *checkbox);
|
||||
bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox);
|
||||
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, bool_t highlighted);
|
||||
errorret_t uiCheckboxDraw(const uicheckbox_t *checkbox, float_t x, float_t y);
|
||||
```
|
||||
|
||||
Draws `"Y "`/`"N "` then the label.
|
||||
|
||||
### Slider
|
||||
|
||||
```c
|
||||
typedef union { float_t f; int32_t i; } uislidervalue_t;
|
||||
|
||||
void uiSliderInitFloat(uislider_t*, const char_t *label,
|
||||
float_t value, float_t min, float_t max, float_t step);
|
||||
void uiSliderInitInt(uislider_t*, const char_t *label,
|
||||
int32_t value, int32_t min, int32_t max, int32_t step);
|
||||
float_t uiSliderGetFloat(const uislider_t*); // works for either type
|
||||
int32_t uiSliderGetInt(const uislider_t*); // asserts type == INT
|
||||
void uiSliderSetFloat(uislider_t*, float_t value); // asserts type == FLOAT, clamps
|
||||
void uiSliderSetInt(uislider_t*, int32_t value); // asserts type == INT, clamps
|
||||
void uiSliderStepUp(uislider_t*); // wraps to min past max
|
||||
void uiSliderStepDown(uislider_t*); // wraps to max past min
|
||||
float_t uiSliderGetRatio(const uislider_t*); // normalized 0..1
|
||||
int32_t uiSliderGetStepCount(const uislider_t*); // 0 for float sliders
|
||||
bool_t uiSliderIsHighlighted(const uislider_t*);
|
||||
void uiSliderSetHighlighted(uislider_t*, bool_t highlighted);
|
||||
errorret_t uiSliderDraw(const uislider_t*, float_t x, float_t y);
|
||||
```
|
||||
|
||||
Draws label, a track, a fill proportional to the current ratio, discrete step
|
||||
markers if it's an int slider with fewer than 10 steps, then the value as
|
||||
text.
|
||||
|
||||
### Dropdown
|
||||
|
||||
```c
|
||||
void uiDropdownInit(uidropdown_t *dropdown, const char_t *label,
|
||||
const char_t *const *options, uint8_t optionCount,
|
||||
uint8_t selectedIndex);
|
||||
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
|
||||
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
|
||||
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, uint8_t index);
|
||||
void uiDropdownStepNext(uidropdown_t *dropdown); // wraps
|
||||
void uiDropdownStepPrev(uidropdown_t *dropdown); // wraps
|
||||
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
|
||||
void uiDropdownSetHighlighted(uidropdown_t *dropdown, bool_t highlighted);
|
||||
errorret_t uiDropdownDraw(const uidropdown_t *dropdown, float_t x, float_t y);
|
||||
```
|
||||
|
||||
`options` is a caller-owned array of strings that must outlive the dropdown
|
||||
(it isn't copied). Draws `label` then `"< Option >"`.
|
||||
|
||||
### Tab
|
||||
|
||||
```c
|
||||
void uiTabInit(uitab_t *tab, const char_t *label);
|
||||
bool_t uiTabIsActive(const uitab_t *tab);
|
||||
void uiTabSetActive(uitab_t *tab, bool_t active);
|
||||
errorret_t uiTabDraw(const uitab_t *tab, float_t x, float_t y);
|
||||
```
|
||||
|
||||
Draws a background box sized to the label (green if active, red if inactive)
|
||||
with the label on top.
|
||||
|
||||
## Menus: assembling widgets into a navigable list
|
||||
|
||||
`uimenu_t` is the one aggregate widget — it owns an array of items (labels,
|
||||
spacers, and any of the widgets above), lays them out in a grid, and wires
|
||||
keyboard/gamepad navigation via the focus system for you.
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
UI_MENU_WIDGET_TYPE_NONE, UI_MENU_WIDGET_TYPE_LABEL,
|
||||
UI_MENU_WIDGET_TYPE_SPACER, UI_MENU_WIDGET_TYPE_CHECKBOX,
|
||||
UI_MENU_WIDGET_TYPE_BUTTON, UI_MENU_WIDGET_TYPE_TAB,
|
||||
UI_MENU_WIDGET_TYPE_SLIDER, UI_MENU_WIDGET_TYPE_DROPDOWN,
|
||||
} uimenuwidgettype_t;
|
||||
|
||||
void uiMenuInit(uimenu_t *menu, uimenuselectedcallback_t selected,
|
||||
uimenuclosedcallback_t closed, uimenuchangedcallback_t changed);
|
||||
void uiMenuSetItems(uimenu_t *menu, const uimenuitem_t *items,
|
||||
uint8_t itemCount, uint8_t columns);
|
||||
void uiMenuSetPosition(uimenu_t *menu, uint8_t x, uint8_t y); // focus cursor cell, not pixels
|
||||
void uiMenuOpen(uimenu_t *menu); // pushes onto the focus stack
|
||||
void uiMenuClose(uimenu_t *menu); // pops it
|
||||
bool_t uiMenuIsActive(const uimenu_t *menu);
|
||||
errorret_t uiMenuDraw(const uimenu_t *menu, float_t x, float_t y,
|
||||
float_t width, float_t height);
|
||||
```
|
||||
|
||||
- `selected(menu, index, item)` fires when the player presses accept on an item.
|
||||
- `changed(menu, index, item)` fires when the highlighted item changes.
|
||||
- `closed(menu)` fires when the menu is popped off the focus stack.
|
||||
- LEFT/RIGHT on a highlighted slider/checkbox/dropdown adjusts its value in
|
||||
place instead of moving focus off it (handled internally).
|
||||
|
||||
### Building a menu with the `MENU_*` macros
|
||||
|
||||
`uimenu.h` provides macros that cut the boilerplate of filling in a
|
||||
`uimenuitem_t` array. They expand into statements using local variables named
|
||||
`menu`, `menuIndex`, and `menuCapacity`, so use them together, inside one
|
||||
function, starting with `MENU_BEGIN` and ending with `MENU_END`:
|
||||
|
||||
```c
|
||||
static uimenuitem_t optionsItems[8];
|
||||
static uimenu_t optionsMenu;
|
||||
static const char_t *qualityOptions[] = { "Low", "Medium", "High" };
|
||||
|
||||
static void onOptionsSelected(
|
||||
const uimenu_t *menu, const uint8_t index, const uimenuitem_t *item
|
||||
) {
|
||||
if(index == 4) uiMenuClose(&optionsMenu); // "Back" button
|
||||
}
|
||||
|
||||
static void onOptionsClosed(const uimenu_t *menu) {
|
||||
// e.g. return to the previous screen
|
||||
}
|
||||
|
||||
void optionsMenuBuild(void) {
|
||||
MENU_BEGIN(&optionsMenu, optionsItems, onOptionsSelected, onOptionsClosed, NULL);
|
||||
MENU_LABEL("Options");
|
||||
MENU_CHECKBOX("Fullscreen");
|
||||
MENU_SLIDER_FLOAT("Volume", 0.8f, 0.0f, 1.0f, 0.05f);
|
||||
MENU_DROPDOWN("Quality", qualityOptions, 3, 1);
|
||||
MENU_BUTTON("Back");
|
||||
MENU_END(optionsItems, 1);
|
||||
}
|
||||
|
||||
// Once, when the menu screen becomes active:
|
||||
uiMenuOpen(&optionsMenu);
|
||||
|
||||
// Every frame the menu should be visible:
|
||||
uiMenuDraw(&optionsMenu, 20.0f, 20.0f, 200.0f, 100.0f);
|
||||
|
||||
// When leaving the menu screen:
|
||||
uiMenuClose(&optionsMenu);
|
||||
```
|
||||
|
||||
`MENU_LABEL`/`MENU_SPACER` force a row break and aren't focusable/selectable.
|
||||
Every other `MENU_*` macro calls the matching widget's own `Init` for you.
|
||||
|
||||
> This example is constructed directly from the widget/menu API surface (all
|
||||
> function and macro signatures above are verified against the source), but
|
||||
> there's currently no real menu-building call site anywhere else in the
|
||||
> engine to cross-check the *pattern* against — treat it as a starting point,
|
||||
> not a copy of shipped code.
|
||||
|
||||
## Focus system: navigation underneath `uimenu`
|
||||
|
||||
If you're building a custom widget that needs keyboard/gamepad navigation
|
||||
without going through `uimenu`, use `ui/focus/uifocus.h` directly. `uimenu`
|
||||
is implemented entirely in terms of this API, so it's a reasonable reference.
|
||||
|
||||
```c
|
||||
uifocusitem_t * uiFocusPush(
|
||||
uint8_t cols, uint8_t rows,
|
||||
uifocusitemcallback_t selected, // fires on accept
|
||||
uifocusitemcallback_t changed, // fires on cursor move (and once immediately)
|
||||
uifocusitemcallback_t closed, // fires on pop
|
||||
uifocusitemdirectioncallback_t direction, // optional pre-empt of a direction press; NULL for default grid movement
|
||||
void *user
|
||||
);
|
||||
void uiFocusPop(void);
|
||||
void uiFocusPopItem(uifocusitem_t *item);
|
||||
void uiFocusSetPosition(uifocusitem_t *item, uint8_t x, uint8_t y); // wraps
|
||||
void uiFocusMoveDirection(uifocusitem_t *item, uifocusdirection_t dir);
|
||||
```
|
||||
|
||||
`uiFocusUpdate()` runs automatically from `uiUpdate()` every tick — you don't
|
||||
call it yourself. It reads `INPUT_ACTION_ACCEPT` (fires `selected`),
|
||||
`INPUT_ACTION_CANCEL` (pops the stack), and the four directional actions
|
||||
(with hold-to-repeat timing) to move the cursor within the topmost pushed
|
||||
item. Only the topmost stack entry (max depth 8) receives input at a time —
|
||||
opening a submenu means pushing a new focus item on top; closing it pops back
|
||||
to the parent.
|
||||
|
||||
There's no separate "is this widget focused" query — "focused" is expressed
|
||||
as the pushed item's current `(x, y)` cursor cell matching a given slot, which
|
||||
is exactly how `uimenu`'s `changed` callback decides which item to highlight.
|
||||
|
||||
## System overlays (automatic)
|
||||
|
||||
Three small overlays are wired into a fixed internal list and draw themselves
|
||||
every frame with no call needed from game code:
|
||||
|
||||
- **Overscan bars** (`ui/overlay/uicrop.h`) — draws opaque bars over the
|
||||
screen area outside `SCREEN.scanX/scanY/scanWidth/scanHeight` (the
|
||||
overscan-safe viewport). A no-op on platforms/configs where the scan area
|
||||
already equals the full viewport. `UI_CROP.color` (default black) is the
|
||||
only thing you'd normally touch here.
|
||||
- **Debug console** (`ui/debug/uiconsole.h`) — draws console history when
|
||||
visible.
|
||||
- **FPS counter** (`ui/debug/uifps.h`) — draws a live FPS/frame-time readout.
|
||||
|
||||
None of these have a scissor/clip-rect equivalent for your own widgets —
|
||||
there is no clipping API in this UI system; everything draws unclipped at
|
||||
whatever position you give it.
|
||||
@@ -6,7 +6,7 @@ fi
|
||||
|
||||
mkdir -p build-psp
|
||||
cd build-psp
|
||||
psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 ..
|
||||
psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 .. CMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
# psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 -DCMAKE_BUILD_TYPE=Debug ..
|
||||
# make
|
||||
+12
-3
@@ -32,6 +32,15 @@ if(NOT yyjson_FOUND)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT jerryscript_FOUND)
|
||||
find_package(jerryscript REQUIRED)
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
jerryscript::core
|
||||
jerryscript::ext
|
||||
jerryscript::port
|
||||
)
|
||||
endif()
|
||||
|
||||
if(DUSK_BACKTRACE)
|
||||
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
|
||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
@@ -53,22 +62,22 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(event)
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(asset)
|
||||
add_subdirectory(console)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(entity)
|
||||
add_subdirectory(log)
|
||||
add_subdirectory(engine)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(rpg)
|
||||
add_subdirectory(physics)
|
||||
add_subdirectory(scene)
|
||||
add_subdirectory(script)
|
||||
add_subdirectory(system)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(network)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(util)
|
||||
add_subdirectory(thread)
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include "easing.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
|
||||
easingLinear,
|
||||
@@ -36,15 +35,15 @@ float_t easingLinear(const float_t t) {
|
||||
}
|
||||
|
||||
float_t easingInSine(const float_t t) {
|
||||
return 1.0f - cosf(t * MATH_PI * 0.5f);
|
||||
return 1.0f - cosf(t * EASING_PI * 0.5f);
|
||||
}
|
||||
|
||||
float_t easingOutSine(const float_t t) {
|
||||
return sinf(t * MATH_PI * 0.5f);
|
||||
return sinf(t * EASING_PI * 0.5f);
|
||||
}
|
||||
|
||||
float_t easingInOutSine(const float_t t) {
|
||||
return -(cosf(MATH_PI * t) - 1.0f) * 0.5f;
|
||||
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f;
|
||||
}
|
||||
|
||||
float_t easingInQuad(const float_t t) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
asset.c
|
||||
assetbatch.c
|
||||
assetfile.c
|
||||
)
|
||||
|
||||
|
||||
+49
-50
@@ -59,20 +59,28 @@ assetentry_t * assetGetEntry(
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
|
||||
// We did not find one existing, Find first available slot.
|
||||
entry = ASSET.entries;
|
||||
do {
|
||||
if(entry->type != ASSET_LOADER_TYPE_NULL) {
|
||||
entry++;
|
||||
continue;
|
||||
}
|
||||
// We did not find one existing. Find first available slot, reaping
|
||||
// zero-ref entries to make room if none are immediately available.
|
||||
bool_t reaped = false;
|
||||
for(;;) {
|
||||
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);
|
||||
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
|
||||
assetEntryInit(entry, name, type, input);
|
||||
return entry;
|
||||
}
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
|
||||
if(reaped) break;
|
||||
reaped = true;
|
||||
errorCatch(assetReapUnused());
|
||||
}
|
||||
|
||||
assertUnreachable("No available asset entry slots.");
|
||||
return NULL;
|
||||
@@ -191,6 +199,32 @@ void assetUnlockEntry(assetentry_t *entry) {
|
||||
assetEntryUnlock(entry);
|
||||
}
|
||||
|
||||
errorret_t assetReapUnused(void) {
|
||||
assertIsMainThread("assetReapUnused must be called from the main thread.");
|
||||
|
||||
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
|
||||
// This handles dependency chains where an entry (e.g. a model) holds refs
|
||||
// on child entries (mesh, texture): dispose parents first so child ref
|
||||
// counts drop to zero, then pick up the children on the next pass. Without
|
||||
// this, a forward-only scan fails when a shared child entry appears before
|
||||
// a parent that still holds a ref to it.
|
||||
bool_t any;
|
||||
do {
|
||||
any = false;
|
||||
assetentry_t *entry = ASSET.entries;
|
||||
do {
|
||||
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
|
||||
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
|
||||
if(entry->refs.count > 0) { entry++; continue; }
|
||||
errorChain(assetEntryDispose(entry));
|
||||
any = true;
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
} while(any);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetUpdate(void) {
|
||||
assertIsMainThread("assetUpdate must be called from the main thread.");
|
||||
|
||||
@@ -286,7 +320,7 @@ errorret_t assetUpdate(void) {
|
||||
"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->entry = NULL;
|
||||
}
|
||||
|
||||
loading++;
|
||||
@@ -307,10 +341,8 @@ errorret_t assetUpdate(void) {
|
||||
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;
|
||||
}
|
||||
@@ -322,30 +354,6 @@ errorret_t assetUpdate(void) {
|
||||
}
|
||||
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
|
||||
|
||||
|
||||
// Reap unused entries.
|
||||
entry = ASSET.entries;
|
||||
do {
|
||||
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
|
||||
entry++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(entry->type == ASSET_LOADER_TYPE_NULL) {
|
||||
entry++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(entry->refs.count > 0) {
|
||||
entry++;
|
||||
continue;
|
||||
}
|
||||
|
||||
consolePrint("Reaping asset %s", entry->name);
|
||||
errorChain(assetEntryDispose(entry));
|
||||
entry++;
|
||||
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -411,16 +419,7 @@ 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);
|
||||
errorChain(assetReapUnused());
|
||||
|
||||
// Cleanup zip file.
|
||||
if(ASSET.zip != NULL) {
|
||||
|
||||
+12
-2
@@ -23,8 +23,8 @@
|
||||
#define ASSET_FILE_NAME "dusk.dsk"
|
||||
#define ASSET_HEADER_SIZE 3
|
||||
|
||||
#define ASSET_LOADING_COUNT_MAX 4
|
||||
#define ASSET_ENTRY_COUNT_MAX 128
|
||||
#define ASSET_LOADING_COUNT_MAX 16
|
||||
#define ASSET_ENTRY_COUNT_MAX 64
|
||||
|
||||
typedef struct asset_s {
|
||||
zip_t *zip;
|
||||
@@ -112,6 +112,16 @@ void assetUnlock(const char_t *name);
|
||||
*/
|
||||
void assetUnlockEntry(assetentry_t *entry);
|
||||
|
||||
/**
|
||||
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
|
||||
* until a full pass frees nothing further, since disposing a parent entry
|
||||
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
|
||||
* making it eligible for reaping too.
|
||||
*
|
||||
* @return An error code if any entry could not be disposed properly.
|
||||
*/
|
||||
errorret_t assetReapUnused(void);
|
||||
|
||||
/**
|
||||
* Requires an asset entry to be loaded. This will block until the asset entry
|
||||
* is fully loaded.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(script)
|
||||
@@ -35,22 +35,6 @@ void assetEntryInit(
|
||||
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) {
|
||||
@@ -97,7 +81,6 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
|
||||
"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();
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "event/event.h"
|
||||
#include "util/ref.h"
|
||||
|
||||
typedef enum {
|
||||
@@ -20,9 +19,6 @@ typedef enum {
|
||||
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 {
|
||||
@@ -33,30 +29,6 @@ struct assetentry_s {
|
||||
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];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -104,7 +76,6 @@ 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.
|
||||
|
||||
@@ -16,6 +16,12 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.dispose = assetMeshDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_MODEL] = {
|
||||
.loadSync = assetModelLoaderSync,
|
||||
.loadAsync = assetModelLoaderAsync,
|
||||
.dispose = assetModelDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_TEXTURE] = {
|
||||
.loadSync = assetTextureLoaderSync,
|
||||
.loadAsync = assetTextureLoaderAsync,
|
||||
@@ -40,9 +46,8 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.dispose = assetJsonDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_CHUNK] = {
|
||||
.loadSync = assetChunkLoaderSync,
|
||||
.loadAsync = assetChunkLoaderAsync,
|
||||
.dispose = assetChunkDispose
|
||||
[ASSET_LOADER_TYPE_SCRIPT] = {
|
||||
.loadSync = assetScriptLoaderSync,
|
||||
.dispose = assetScriptDispose
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,53 +6,54 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/loader/display/assetmeshloader.h"
|
||||
#include "asset/loader/dmf/assetmeshloader.h"
|
||||
#include "asset/loader/dmf/assetmodelloader.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"
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/script/assetscriptloader.h"
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
|
||||
ASSET_LOADER_TYPE_MESH,
|
||||
ASSET_LOADER_TYPE_MODEL,
|
||||
ASSET_LOADER_TYPE_TEXTURE,
|
||||
ASSET_LOADER_TYPE_TILESET,
|
||||
ASSET_LOADER_TYPE_LOCALE,
|
||||
ASSET_LOADER_TYPE_JSON,
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_SCRIPT,
|
||||
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
|
||||
typedef union {
|
||||
assetmeshloaderinput_t mesh;
|
||||
assettextureloaderinput_t texture;
|
||||
assettilesetloaderinput_t tileset;
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
assetchunkloaderinput_t chunk;
|
||||
} assetloaderinput_t;
|
||||
|
||||
typedef union {
|
||||
assetmeshloaderloading_t mesh;
|
||||
assetmodelloaderloading_t model;
|
||||
assettextureloaderloading_t texture;
|
||||
assettilesetloaderloading_t tileset;
|
||||
assetlocaleloaderloading_t locale;
|
||||
assetjsonloaderloading_t json;
|
||||
assetchunkloaderloading_t chunk;
|
||||
} assetloaderloading_t;
|
||||
|
||||
typedef union {
|
||||
assetmeshoutput_t mesh;
|
||||
assetmodeloutput_t model;
|
||||
assettextureoutput_t texture;
|
||||
assettilesetoutput_t tileset;
|
||||
assetlocaleoutput_t locale;
|
||||
assetjsonoutput_t json;
|
||||
assetchunkoutput_t chunk;
|
||||
assetscriptoutput_t script;
|
||||
} assetloaderoutput_t;
|
||||
|
||||
typedef union {
|
||||
assettextureloaderinput_t texture;
|
||||
assettilesetloaderinput_t tileset;
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
} assetloaderinput_t;
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
@@ -83,6 +84,25 @@ extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Like @ref assetLoaderErrorChain, but also frees `_ptr` (via memoryFree)
|
||||
* before chaining the error if `_expr` failed. Use this for any loader step
|
||||
* that runs after a buffer has already been allocated, so a later I/O
|
||||
* failure doesn't leak it.
|
||||
*
|
||||
* @param loading The asset loading slot.
|
||||
* @param _ptr A heap pointer to free if `_expr` fails.
|
||||
* @param _expr The error return value to check and chain if it's an error.
|
||||
*/
|
||||
#define assetLoaderErrorChainFree(loading, _ptr, _expr) {\
|
||||
errorret_t _alecf = (_expr); \
|
||||
if(errorIsNotOk(_alecf)) { \
|
||||
memoryFree(_ptr); \
|
||||
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
|
||||
errorChain(_alecf); \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand method to both throw an error (against the loader state) and to
|
||||
* set the asset entry state to error.
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetchunkloader.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 assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.chunk.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 chunk file."
|
||||
);
|
||||
|
||||
loading->loading.chunk.data = data;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
switch(loading->loading.chunk.state) {
|
||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
break;
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
||||
break;
|
||||
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.chunk.data;
|
||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported chunk file version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
assetchunkoutput_t *out = &loading->entry->data.chunk;
|
||||
size_t offset = 8;
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
memoryCopy(out->tiles, data + offset, tileSize);
|
||||
offset += tileSize;
|
||||
|
||||
out->vertCount = endianLittleToHost32(*(uint32_t *)(data + offset));
|
||||
offset += sizeof(uint32_t);
|
||||
|
||||
assertTrue(
|
||||
out->vertCount <= CHUNK_VERTEX_COUNT,
|
||||
"Chunk vertex count exceeds maximum."
|
||||
);
|
||||
|
||||
memoryCopy(
|
||||
out->vertices,
|
||||
data + offset,
|
||||
out->vertCount * sizeof(meshvertex_t)
|
||||
);
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,65 +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 "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 1
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetchunkloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
||||
ASSET_CHUNK_LOADING_STATE_DONE
|
||||
} assetchunkloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetchunkloadingstate_t state;
|
||||
uint8_t *data;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
tile_t tiles[CHUNK_TILE_COUNT];
|
||||
uint32_t vertCount;
|
||||
meshvertex_t vertices[CHUNK_VERTEX_COUNT];
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for chunk assets. Reads the raw DCF 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 assetChunkLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||
* read by the async phase and populates the output assetchunkoutput_t.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetChunkLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for chunk assets.
|
||||
*
|
||||
* @param entry Asset entry containing the chunk data to dispose.
|
||||
* @return Error code indicating success or failure of the dispose operation.
|
||||
*/
|
||||
errorret_t assetChunkDispose(assetentry_t *entry);
|
||||
@@ -6,7 +6,6 @@
|
||||
# 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);
|
||||
@@ -27,11 +27,12 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *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));
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChainFree(loading, data, assetFileRead(file, data, file->size));
|
||||
assetLoaderErrorChainFree(loading, data, assetFileClose(file));
|
||||
assetLoaderErrorChainFree(loading, data, assetFileDispose(file));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire tileset file."
|
||||
@@ -102,6 +103,11 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
|
||||
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
|
||||
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
|
||||
|
||||
if(out->uv[0] < 0.0f || out->uv[0] > 1.0f) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid u0 value in tileset");
|
||||
}
|
||||
|
||||
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
save.c
|
||||
savestream.c
|
||||
assetmeshloader.c
|
||||
assetmodelloader.c
|
||||
)
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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/memory.h"
|
||||
#include "util/endian.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
|
||||
errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.mesh.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.mesh.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
|
||||
uint8_t *raw = memoryAllocate(file->size);
|
||||
assetLoaderErrorChainFree(loading, raw, assetFileRead(file, raw, file->size));
|
||||
assetLoaderErrorChainFree(loading, raw, assetFileClose(file));
|
||||
assetLoaderErrorChainFree(loading, raw, assetFileDispose(file));
|
||||
assertTrue(file->lastRead == file->size, "Failed to read entire DMF file.");
|
||||
|
||||
if(raw[0] != 'D' || raw[1] != 'M' || raw[2] != 'F') {
|
||||
memoryFree(raw);
|
||||
assetLoaderErrorThrow(loading, "Invalid DMF file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(raw + 4));
|
||||
if(version != ASSET_MESH_FILE_VERSION) {
|
||||
memoryFree(raw);
|
||||
assetLoaderErrorThrow(loading, "Unsupported DMF version %u", version);
|
||||
}
|
||||
|
||||
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
|
||||
meshvertex_t *vertices = NULL;
|
||||
if(vertCount > 0) {
|
||||
// 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
|
||||
// require this for the DMA'd vertex data to actually reach the GPU
|
||||
// coherently. Static compiled-in vertex arrays happen to get this from
|
||||
// the linker; a plain memoryAllocate here would not.
|
||||
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
|
||||
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
|
||||
|
||||
for(uint32_t v = 0; v < vertCount; v++) {
|
||||
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
|
||||
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
|
||||
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
|
||||
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
|
||||
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
|
||||
}
|
||||
}
|
||||
memoryFree(raw);
|
||||
|
||||
loading->loading.mesh.vertCount = vertCount;
|
||||
loading->loading.mesh.data = (uint8_t *)vertices;
|
||||
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.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
uint32_t vertCount = loading->loading.mesh.vertCount;
|
||||
meshvertex_t *vertices = (meshvertex_t *)loading->loading.mesh.data;
|
||||
loading->loading.mesh.data = NULL;
|
||||
|
||||
assetmeshoutput_t *out = &loading->entry->data.mesh;
|
||||
out->vertices = vertices;
|
||||
|
||||
if(vertCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t ret = meshInit(
|
||||
&out->mesh,
|
||||
MESH_PRIMITIVE_TYPE_TRIANGLES,
|
||||
(int32_t)vertCount,
|
||||
out->vertices
|
||||
);
|
||||
if(errorIsNotOk(ret)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
out->meshInitialized = true;
|
||||
|
||||
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
|
||||
if(errorIsNotOk(ret)) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
meshDispose(&out->mesh);
|
||||
out->meshInitialized = false;
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
||||
// VBO owns the data now; CPU copy is no longer needed. The platform
|
||||
// mesh object itself still needs meshDispose later - tracked via
|
||||
// out->meshInitialized, independent of the CPU buffer's lifetime.
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
#endif
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetMeshDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetmeshoutput_t *out = &entry->data.mesh;
|
||||
if(out->meshInitialized) {
|
||||
errorChain(meshDispose(&out->mesh));
|
||||
out->meshInitialized = false;
|
||||
}
|
||||
if(out->vertices != NULL) {
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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"
|
||||
|
||||
#define ASSET_MESH_FILE_VERSION 1
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_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 vertCount;
|
||||
uint8_t *data;
|
||||
} assetmeshloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
mesh_t mesh;
|
||||
bool_t meshInitialized;
|
||||
meshvertex_t *vertices;
|
||||
} assetmeshoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for DMF mesh assets. Reads the file, validates the
|
||||
* header, and prepares a ready-to-use vertex buffer so the sync phase only
|
||||
* needs to upload to the GPU.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetMeshLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for DMF mesh assets. Initializes the mesh from the
|
||||
* vertex buffer prepared by the async phase and flushes it to the GPU.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetMeshLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for DMF mesh assets. Disposes the mesh and frees the vertex
|
||||
* buffer.
|
||||
*
|
||||
* @param entry Asset entry containing the mesh data to dispose.
|
||||
* @return Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetMeshDispose(assetentry_t *entry);
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetmodelloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
errorret_t assetModelLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetModelLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_MODEL, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
switch(loading->loading.model.state) {
|
||||
case ASSET_MODEL_LOADING_STATE_INITIAL:
|
||||
loading->loading.model.state = ASSET_MODEL_LOADING_STATE_LOCK_ASSETS;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
break;
|
||||
|
||||
case ASSET_MODEL_LOADING_STATE_LOCK_ASSETS: {
|
||||
// Lock the model's JSON descriptor as a JSON sub-asset. The entry
|
||||
// key is prefixed with "json:" to avoid a type-collision with the
|
||||
// model entry itself (both share the same filename). The JSON loader
|
||||
// reads the real file path supplied in the input.
|
||||
char_t jsonKey[ASSET_FILE_NAME_MAX];
|
||||
stringFormat(
|
||||
jsonKey, sizeof(jsonKey), "json:%s", loading->entry->name
|
||||
);
|
||||
assetloaderinput_t jsonInput;
|
||||
memoryZero(&jsonInput, sizeof(jsonInput));
|
||||
stringCopy(
|
||||
jsonInput.json.path, loading->entry->name, ASSET_FILE_NAME_MAX
|
||||
);
|
||||
assetentry_t *jsonEntry = assetLock(
|
||||
jsonKey, ASSET_LOADER_TYPE_JSON, &jsonInput
|
||||
);
|
||||
errorret_t ret = assetRequireLoaded(jsonEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
yyjson_val *root = yyjson_doc_get_root(jsonEntry->data.json);
|
||||
|
||||
// Parse required mesh path.
|
||||
yyjson_val *meshVal = yyjson_obj_get(root, "mesh");
|
||||
if(!meshVal || !yyjson_is_str(meshVal)) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Model JSON missing 'mesh' string");
|
||||
}
|
||||
const char *meshStr = yyjson_get_str(meshVal);
|
||||
size_t meshLen = yyjson_get_len(meshVal);
|
||||
if(meshLen >= ASSET_FILE_NAME_MAX) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Model mesh path exceeds max length");
|
||||
}
|
||||
char_t meshName[ASSET_FILE_NAME_MAX];
|
||||
memoryCopy(meshName, meshStr, meshLen + 1);
|
||||
|
||||
// Parse optional texture path (absent = color only, no texture).
|
||||
char_t textureName[ASSET_FILE_NAME_MAX];
|
||||
textureName[0] = '\0';
|
||||
yyjson_val *texVal = yyjson_obj_get(root, "texture");
|
||||
if(texVal && yyjson_is_str(texVal)) {
|
||||
const char *texStr = yyjson_get_str(texVal);
|
||||
size_t texLen = yyjson_get_len(texVal);
|
||||
if(texLen >= ASSET_FILE_NAME_MAX) {
|
||||
assetUnlockEntry(jsonEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Model texture path exceeds max length");
|
||||
}
|
||||
memoryCopy(textureName, texStr, texLen + 1);
|
||||
}
|
||||
|
||||
// Parse optional color (RGBA array 0-255, default white).
|
||||
color_t color = COLOR_WHITE;
|
||||
yyjson_val *colorVal = yyjson_obj_get(root, "color");
|
||||
if(colorVal && yyjson_is_arr(colorVal)) {
|
||||
uint8_t ch[4] = {255, 255, 255, 255};
|
||||
size_t colorIdx, colorLen;
|
||||
yyjson_val *colorElem;
|
||||
yyjson_arr_foreach(colorVal, colorIdx, colorLen, colorElem) {
|
||||
if(colorIdx >= 4) break;
|
||||
if(yyjson_is_int(colorElem)) {
|
||||
ch[colorIdx] = (uint8_t)yyjson_get_int(colorElem);
|
||||
}
|
||||
}
|
||||
color.r = ch[0];
|
||||
color.g = ch[1];
|
||||
color.b = ch[2];
|
||||
color.a = ch[3];
|
||||
}
|
||||
|
||||
// Release JSON entry; all needed fields are now in local variables.
|
||||
assetUnlockEntry(jsonEntry);
|
||||
|
||||
// Lock and load the mesh sub-asset.
|
||||
assetentry_t *meshEntry = assetLock(meshName, ASSET_LOADER_TYPE_MESH, NULL);
|
||||
ret = assetRequireLoaded(meshEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(meshEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
// Lock and load the optional texture sub-asset.
|
||||
assetentry_t *texEntry = NULL;
|
||||
if(textureName[0] != '\0') {
|
||||
assetloaderinput_t texInput = { .texture = TEXTURE_FORMAT_RGBA };
|
||||
texEntry = assetLock(textureName, ASSET_LOADER_TYPE_TEXTURE, &texInput);
|
||||
ret = assetRequireLoaded(texEntry);
|
||||
if(errorIsNotOk(ret)) {
|
||||
assetUnlockEntry(texEntry);
|
||||
assetUnlockEntry(meshEntry);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(ret);
|
||||
}
|
||||
}
|
||||
|
||||
assetmodeloutput_t *out = &loading->entry->data.model;
|
||||
out->meshEntry = meshEntry;
|
||||
out->texEntry = texEntry;
|
||||
out->color = color;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetModelDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_MODEL, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetmodeloutput_t *out = &entry->data.model;
|
||||
if(out->meshEntry != NULL) {
|
||||
assetUnlockEntry(out->meshEntry);
|
||||
out->meshEntry = NULL;
|
||||
}
|
||||
if(out->texEntry != NULL) {
|
||||
assetUnlockEntry(out->texEntry);
|
||||
out->texEntry = NULL;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 "display/color.h"
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_MODEL_LOADING_STATE_INITIAL,
|
||||
ASSET_MODEL_LOADING_STATE_LOCK_ASSETS
|
||||
} assetmodelloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetmodelloadingstate_t state;
|
||||
} assetmodelloaderloading_t;
|
||||
|
||||
typedef struct {
|
||||
assetentry_t *meshEntry;
|
||||
assetentry_t *texEntry;
|
||||
color_t color;
|
||||
} assetmodeloutput_t;
|
||||
|
||||
/**
|
||||
* Async loader stub for model assets. Model loading is performed entirely
|
||||
* on the main thread; this callback is never reached.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @returns Always success.
|
||||
*/
|
||||
errorret_t assetModelLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for model assets. Locks the JSON descriptor file as
|
||||
* a sub-asset, waits for it to load, parses the mesh path, optional
|
||||
* texture path, and color tint, then releases the JSON entry. Locks and
|
||||
* waits for mesh and optional texture sub-assets sequentially to stay
|
||||
* within ASSET_LOADING_COUNT_MAX slot limits.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @returns Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetModelLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for model assets. Releases the locks on the mesh and texture
|
||||
* sub-asset entries.
|
||||
*
|
||||
* @param entry Asset entry containing the model to dispose.
|
||||
* @returns Error code indicating success or failure.
|
||||
*/
|
||||
errorret_t assetModelDispose(assetentry_t *entry);
|
||||
@@ -22,21 +22,24 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
|
||||
assertNull(loading->loading.json.buffer, "Buffer already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.json.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
const char_t *filePath = (
|
||||
loading->entry->input != NULL &&
|
||||
loading->entry->input->json.path[0] != '\0'
|
||||
) ? loading->entry->input->json.path : loading->entry->name;
|
||||
assetLoaderErrorChain(loading, assetFileInit(file, filePath, NULL, NULL));
|
||||
|
||||
if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
|
||||
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
|
||||
}
|
||||
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
|
||||
size_t fileSize = (size_t)file->size;
|
||||
uint8_t *buffer = memoryAllocate(fileSize);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
|
||||
assetLoaderErrorChainFree(loading, buffer, assetFileRead(file, buffer, fileSize));
|
||||
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
assetLoaderErrorChainFree(loading, buffer, assetFileClose(file));
|
||||
assetLoaderErrorChainFree(loading, buffer, assetFileDispose(file));
|
||||
|
||||
loading->loading.json.buffer = buffer;
|
||||
loading->loading.json.size = fileSize;
|
||||
|
||||
@@ -14,7 +14,16 @@
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct { void *nothing; } assetjsonloaderinput_t;
|
||||
/**
|
||||
* Optional file path override. When path[0] is non-zero the JSON loader
|
||||
* reads this path from the archive instead of loading->entry->name. Use
|
||||
* this to decouple the asset pool key from the actual file location (e.g.
|
||||
* when a parent loader locks a JSON entry under a synthetic key to avoid
|
||||
* a type-collision with an entry of the same name but different type).
|
||||
*/
|
||||
typedef struct {
|
||||
char_t path[ASSET_FILE_NAME_MAX];
|
||||
} assetjsonloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_JSON_LOADING_STATE_INITIAL,
|
||||
|
||||
@@ -482,6 +482,24 @@ errorret_t assetLocaleGetString(
|
||||
assertTrue(pluralCount >= 0, "Plural index cannot be negative.");
|
||||
assertNotNull(stringBuffer, "String buffer cannot be NULL.");
|
||||
assertTrue(stringBufferSize > 0, "String buffer size must be > 0");
|
||||
|
||||
// Check the cache before rewinding/rescanning the whole file.
|
||||
for(uint8_t i = 0; i < ASSET_LOCALE_STRING_CACHE_SIZE; i++) {
|
||||
assetlocalecacheentry_t *entry = &file->stringCache[i];
|
||||
if(
|
||||
!entry->valid ||
|
||||
entry->pluralCount != pluralCount ||
|
||||
stringCompare(messageId, entry->messageId) != 0
|
||||
) continue;
|
||||
|
||||
size_t valueLen = strlen(entry->value);
|
||||
if(valueLen >= stringBufferSize) {
|
||||
errorThrow("String buffer overflow");
|
||||
}
|
||||
memoryCopy(stringBuffer, entry->value, valueLen + 1);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assetfilelinereader_t reader;
|
||||
|
||||
bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false;
|
||||
@@ -506,12 +524,17 @@ errorret_t assetLocaleGetString(
|
||||
sizeof(lineBuffer)
|
||||
);
|
||||
|
||||
// Prime the reader with the first line before scanning; outBuffer holds
|
||||
// uninitialized memory until the first Next() call fills it.
|
||||
errorChain(assetFileLineReaderNext(&reader));
|
||||
|
||||
// Skip blanks, comments, etc and start looking for msgid's
|
||||
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
||||
|
||||
while(!reader.eof) {
|
||||
while(true) {
|
||||
// Is this msgid?
|
||||
if(memoryCompare(lineBuffer, "msgid", 5) != 0) {
|
||||
if(reader.eof) break;
|
||||
errorChain(assetFileLineReaderNext(&reader));
|
||||
msgidBuffer[0] = '\0';
|
||||
continue;
|
||||
@@ -535,7 +558,7 @@ errorret_t assetLocaleGetString(
|
||||
}
|
||||
|
||||
// We are either going to see a msgstr or a msgid_plural
|
||||
while(!reader.eof) {
|
||||
while(true) {
|
||||
errorChain(assetLocaleLineSkipBlanks(&reader, lineBuffer));
|
||||
|
||||
// Is msgid_plural?
|
||||
@@ -620,6 +643,20 @@ errorret_t assetLocaleGetString(
|
||||
errorThrow("Failed to find msgstr for message ID: %s", messageId);
|
||||
}
|
||||
|
||||
// Cache the resolved string for future lookups, if it fits.
|
||||
if(
|
||||
strlen(messageId) < ASSET_LOCALE_CACHE_MESSAGE_ID_MAX &&
|
||||
strlen(stringBuffer) < ASSET_LOCALE_CACHE_VALUE_MAX
|
||||
) {
|
||||
assetlocalecacheentry_t *entry = &file->stringCache[file->stringCacheNext];
|
||||
stringCopy(entry->messageId, messageId, sizeof(entry->messageId) - 1);
|
||||
stringCopy(entry->value, stringBuffer, sizeof(entry->value) - 1);
|
||||
entry->pluralCount = pluralCount;
|
||||
entry->valid = true;
|
||||
file->stringCacheNext =
|
||||
(file->stringCacheNext + 1) % ASSET_LOCALE_STRING_CACHE_SIZE;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,27 @@ typedef struct {
|
||||
/** Maximum number of distinct plural forms a locale file may declare. */
|
||||
#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6
|
||||
|
||||
/** Max distinct resolved strings cached per open locale file. */
|
||||
#define ASSET_LOCALE_STRING_CACHE_SIZE 16
|
||||
|
||||
/** Max message ID length (incl. null terminator) storable in the cache. */
|
||||
#define ASSET_LOCALE_CACHE_MESSAGE_ID_MAX 128
|
||||
|
||||
/** Max resolved string length (incl. null terminator) storable in the cache. */
|
||||
#define ASSET_LOCALE_CACHE_VALUE_MAX 256
|
||||
|
||||
/**
|
||||
* A single cached (messageId, pluralCount) -> resolved string lookup, used by
|
||||
* @ref assetLocaleGetString to avoid rescanning the whole PO file for
|
||||
* repeated lookups of the same message.
|
||||
*/
|
||||
typedef struct {
|
||||
bool_t valid;
|
||||
char_t messageId[ASSET_LOCALE_CACHE_MESSAGE_ID_MAX];
|
||||
int32_t pluralCount;
|
||||
char_t value[ASSET_LOCALE_CACHE_VALUE_MAX];
|
||||
} assetlocalecacheentry_t;
|
||||
|
||||
/**
|
||||
* Comparison operator used in a plural-form expression.
|
||||
*
|
||||
@@ -98,6 +119,12 @@ typedef struct {
|
||||
|
||||
/** Form index used when no conditional clause matches. */
|
||||
uint8_t pluralDefaultIndex;
|
||||
|
||||
/** Ring buffer of recently resolved (messageId, pluralCount) lookups. */
|
||||
assetlocalecacheentry_t stringCache[ASSET_LOCALE_STRING_CACHE_SIZE];
|
||||
|
||||
/** Next slot in @ref stringCache to overwrite. */
|
||||
uint8_t stringCacheNext;
|
||||
} assetlocalefile_t;
|
||||
|
||||
/** Convenience alias - the loaded output type of a locale asset entry. */
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
entityinteract.c
|
||||
assetscriptloader.c
|
||||
)
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetscriptloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
|
||||
static void assetScriptSettleWithError(
|
||||
assetscriptoutput_t *output,
|
||||
const char_t *message
|
||||
) {
|
||||
jerry_value_t errVal = jerry_string_sz(message);
|
||||
jerry_value_t rejectResult = jerry_promise_reject(output->promise, errVal);
|
||||
jerry_value_free(rejectResult);
|
||||
jerry_value_free(errVal);
|
||||
}
|
||||
|
||||
errorret_t assetScriptLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetentry_t *entry = loading->entry;
|
||||
assetscriptoutput_t *output = &entry->data.script;
|
||||
assertTrue(
|
||||
output->promise != 0,
|
||||
"Script entry has no promise - was it requested via include()?"
|
||||
);
|
||||
|
||||
assetfile_t file;
|
||||
uint8_t *buffer = NULL;
|
||||
size_t size = 0;
|
||||
|
||||
errorret_t err = assetFileInit(&file, entry->name, NULL, NULL);
|
||||
if(errorIsOk(err)) err = assetFileReadEntire(&file, &buffer, &size);
|
||||
if(errorIsOk(err)) err = assetFileDispose(&file);
|
||||
|
||||
if(errorIsNotOk(err)) {
|
||||
assetScriptSettleWithError(output, err.state->message);
|
||||
entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorChain(err);
|
||||
}
|
||||
|
||||
char_t *src = (char_t *)memoryAllocate(size + 1);
|
||||
memoryCopy(src, buffer, size);
|
||||
src[size] = '\0';
|
||||
memoryFree(buffer);
|
||||
|
||||
// Scripts export their public API by assigning to the global `module`.
|
||||
// Swap it out around the eval so this doesn't clobber a caller's own
|
||||
// in-flight include() of a different file.
|
||||
jerry_value_t global = jerry_current_realm();
|
||||
jerry_value_t moduleKey = jerry_string_sz("module");
|
||||
jerry_value_t prevModule = jerry_object_get(global, moduleKey);
|
||||
jerry_value_t undef = jerry_undefined();
|
||||
jerry_object_set(global, moduleKey, undef);
|
||||
jerry_value_free(undef);
|
||||
|
||||
jerry_value_t evalResult = jerry_eval(
|
||||
(const jerry_char_t *)src, size, JERRY_PARSE_NO_OPTS
|
||||
);
|
||||
memoryFree(src);
|
||||
|
||||
if(jerry_value_is_exception(evalResult)) {
|
||||
jerry_value_t errVal = jerry_exception_value(evalResult, false);
|
||||
jerry_value_t rejectResult = jerry_promise_reject(output->promise, errVal);
|
||||
jerry_value_free(rejectResult);
|
||||
jerry_value_free(errVal);
|
||||
jerry_value_free(evalResult);
|
||||
|
||||
jerry_value_t moduleVal = jerry_object_get(global, moduleKey);
|
||||
jerry_value_free(moduleVal);
|
||||
jerry_object_set(global, moduleKey, prevModule);
|
||||
jerry_value_free(prevModule);
|
||||
jerry_value_free(moduleKey);
|
||||
jerry_value_free(global);
|
||||
|
||||
entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||
errorThrow("Script error in '%s'", entry->name);
|
||||
}
|
||||
jerry_value_free(evalResult);
|
||||
|
||||
jerry_value_t moduleVal = jerry_object_get(global, moduleKey);
|
||||
jerry_object_set(global, moduleKey, prevModule);
|
||||
jerry_value_free(prevModule);
|
||||
jerry_value_free(moduleKey);
|
||||
jerry_value_free(global);
|
||||
|
||||
jerry_value_t resolveResult = jerry_promise_resolve(output->promise, moduleVal);
|
||||
jerry_value_free(resolveResult);
|
||||
jerry_value_free(moduleVal);
|
||||
|
||||
entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetScriptDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Asset entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetscriptoutput_t *output = &entry->data.script;
|
||||
if(output->promise != 0) {
|
||||
jerry_value_free(output->promise);
|
||||
output->promise = 0;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 <jerryscript.h>
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
/**
|
||||
* Output data for a script asset entry. The promise is created once, the
|
||||
* first time a script requests this file (see moduleIncludeInclude), and is
|
||||
* owned by the entry for its lifetime - every subsequent request for the
|
||||
* same file is handed a copy of this same promise instead of starting a
|
||||
* second load. It is resolved (with the script's exported `module` value)
|
||||
* or rejected (with the JS exception) exactly once, the moment the entry
|
||||
* transitions to ASSET_ENTRY_STATE_LOADED or ASSET_ENTRY_STATE_ERROR.
|
||||
*/
|
||||
typedef struct {
|
||||
jerry_value_t promise;
|
||||
} assetscriptoutput_t;
|
||||
|
||||
/**
|
||||
* Loads and evaluates a script asset synchronously (reads the whole file
|
||||
* from the archive, then runs it) and resolves/rejects the entry's promise
|
||||
* with the outcome. Scripts are small and read instantly, so there is no
|
||||
* async (background-thread) phase - this is the only loader function.
|
||||
*
|
||||
* @param loading The asset loading slot.
|
||||
* @return An error if reading failed, or errorOk() (a script exception is
|
||||
* reported via promise rejection, not a returned error).
|
||||
*/
|
||||
errorret_t assetScriptLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Releases the promise reference held by a script asset entry.
|
||||
*
|
||||
* @param entry The asset entry to dispose.
|
||||
*/
|
||||
errorret_t assetScriptDispose(assetentry_t *entry);
|
||||
@@ -17,7 +17,7 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = true;
|
||||
CONSOLE.visible = false;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
|
||||
@@ -21,7 +21,7 @@ add_subdirectory(texture)
|
||||
# Color definitions
|
||||
dusk_run_python(
|
||||
dusk_color_defs
|
||||
tools.color.csv
|
||||
tools.color
|
||||
--csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv
|
||||
--output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "display/display.h"
|
||||
#include "display/framebuffer/framebuffer.h"
|
||||
#include "scene/scene.h"
|
||||
#include "entity/entityrender.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/mesh/quad.h"
|
||||
#include "display/mesh/cube.h"
|
||||
@@ -80,6 +81,7 @@ errorret_t displayUpdate(void) {
|
||||
);
|
||||
|
||||
errorChain(sceneRender());
|
||||
errorChain(entityRenderAll());
|
||||
|
||||
// Finish up
|
||||
screenUnbind();
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_DISPLAY_SCALE_UI
|
||||
#define DUSK_DISPLAY_SCALE_UI 1
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_DISPLAY_SCALE_3D
|
||||
#define DUSK_DISPLAY_SCALE_3D 1
|
||||
#endif
|
||||
|
||||
// Main Display Struct, platform-speicifc
|
||||
typedef displayplatform_t display_t;
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ errorret_t capsuleInit() {
|
||||
0.5f,
|
||||
CAPSULE_CAP_RINGS,
|
||||
CAPSULE_SECTORS
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE_4B
|
||||
#endif
|
||||
);
|
||||
errorChain(meshInit(
|
||||
&CAPSULE_MESH_SIMPLE,
|
||||
@@ -40,9 +37,6 @@ void capsuleBuffer(
|
||||
const float_t halfHeight,
|
||||
const int32_t capRings,
|
||||
const int32_t sectors
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
) {
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
assertNotNull(center, "Center vector cannot be NULL");
|
||||
@@ -53,25 +47,13 @@ void capsuleBuffer(
|
||||
const float_t sectorStep = 2.0f * (float_t)GLM_PI / (float_t)sectors;
|
||||
int32_t vi = 0;
|
||||
|
||||
/* Helper macro: write one vertex. */
|
||||
#if MESH_ENABLE_COLOR
|
||||
#define CAP_VERT(px, py, pz, u, v) \
|
||||
vertices[vi].color = color; \
|
||||
vertices[vi].pos[0] = (px); \
|
||||
vertices[vi].pos[1] = (py); \
|
||||
vertices[vi].pos[2] = (pz); \
|
||||
vertices[vi].uv[0] = (u); \
|
||||
vertices[vi].uv[1] = (v); \
|
||||
vi++;
|
||||
#else
|
||||
#define CAP_VERT(px, py, pz, u, v) \
|
||||
vertices[vi].pos[0] = (px); \
|
||||
vertices[vi].pos[1] = (py); \
|
||||
vertices[vi].pos[2] = (pz); \
|
||||
vertices[vi].uv[0] = (u); \
|
||||
vertices[vi].uv[1] = (v); \
|
||||
vi++;
|
||||
#endif
|
||||
#define CAP_VERT(px, py, pz, u, v) \
|
||||
vertices[vi].pos[0] = (px); \
|
||||
vertices[vi].pos[1] = (py); \
|
||||
vertices[vi].pos[2] = (pz); \
|
||||
vertices[vi].uv[0] = (u); \
|
||||
vertices[vi].uv[1] = (v); \
|
||||
vi++;
|
||||
|
||||
/* ---- Top hemisphere ---- */
|
||||
/* phi ranges from PI/2 (top pole) down to 0 (equator). */
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define CAPSULE_CAP_RINGS 4
|
||||
#define CAPSULE_SECTORS 16
|
||||
@@ -46,7 +45,4 @@ void capsuleBuffer(
|
||||
const float_t halfHeight,
|
||||
const int32_t capRings,
|
||||
const int32_t sectors
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
);
|
||||
|
||||
@@ -14,12 +14,7 @@ meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||
errorret_t cubeInit() {
|
||||
vec3 min = { 0.0f, 0.0f, 0.0f };
|
||||
vec3 max = { 1.0f, 1.0f, 1.0f };
|
||||
cubeBuffer(
|
||||
CUBE_MESH_SIMPLE_VERTICES, min, max
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE_4B
|
||||
#endif
|
||||
);
|
||||
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max);
|
||||
errorChain(meshInit(
|
||||
&CUBE_MESH_SIMPLE,
|
||||
CUBE_PRIMITIVE_TYPE,
|
||||
@@ -29,31 +24,17 @@ errorret_t cubeInit() {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Helper macro: set one vertex position, UV and color.
|
||||
#if MESH_ENABLE_COLOR
|
||||
#define CUBE_VERT(i, px, py, pz, u, v) \
|
||||
vertices[i].color = color; \
|
||||
vertices[i].pos[0] = (px); \
|
||||
vertices[i].pos[1] = (py); \
|
||||
vertices[i].pos[2] = (pz); \
|
||||
vertices[i].uv[0] = (u); \
|
||||
vertices[i].uv[1] = (v);
|
||||
#else
|
||||
#define CUBE_VERT(i, px, py, pz, u, v) \
|
||||
vertices[i].pos[0] = (px); \
|
||||
vertices[i].pos[1] = (py); \
|
||||
vertices[i].pos[2] = (pz); \
|
||||
vertices[i].uv[0] = (u); \
|
||||
vertices[i].uv[1] = (v);
|
||||
#endif
|
||||
#define CUBE_VERT(i, px, py, pz, u, v) \
|
||||
vertices[i].pos[0] = (px); \
|
||||
vertices[i].pos[1] = (py); \
|
||||
vertices[i].pos[2] = (pz); \
|
||||
vertices[i].uv[0] = (u); \
|
||||
vertices[i].uv[1] = (v);
|
||||
|
||||
void cubeBuffer(
|
||||
meshvertex_t *vertices,
|
||||
const vec3 min,
|
||||
const vec3 max
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
) {
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
assertNotNull(min, "Min vector cannot be NULL");
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define CUBE_FACE_COUNT 6
|
||||
#define CUBE_VERTICES_PER_FACE 6
|
||||
@@ -37,7 +36,4 @@ void cubeBuffer(
|
||||
meshvertex_t *vertices,
|
||||
const vec3 min,
|
||||
const vec3 max
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
);
|
||||
|
||||
@@ -7,20 +7,11 @@
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#ifndef MESH_ENABLE_COLOR
|
||||
#define MESH_ENABLE_COLOR 0
|
||||
#endif
|
||||
|
||||
#define MESH_VERTEX_UV_SIZE 2
|
||||
#define MESH_VERTEX_POS_SIZE 3
|
||||
|
||||
typedef struct {
|
||||
#if MESH_ENABLE_COLOR
|
||||
color_t color;
|
||||
#endif
|
||||
|
||||
float_t uv[MESH_VERTEX_UV_SIZE];
|
||||
float_t pos[MESH_VERTEX_POS_SIZE];
|
||||
} meshvertex_t;
|
||||
@@ -20,11 +20,8 @@ errorret_t planeInit() {
|
||||
PLANE_MESH_SIMPLE_VERTICES,
|
||||
PLANE_AXIS_XZ,
|
||||
min,
|
||||
max
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE_4B
|
||||
#endif
|
||||
, uvMin,
|
||||
max,
|
||||
uvMin,
|
||||
uvMax
|
||||
);
|
||||
errorChain(meshInit(
|
||||
@@ -36,33 +33,19 @@ errorret_t planeInit() {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
/* Helper macro to write one vertex. */
|
||||
#if MESH_ENABLE_COLOR
|
||||
#define PLANE_VERT(i, px, py, pz, u, v) \
|
||||
vertices[i].color = color; \
|
||||
vertices[i].pos[0] = (px); \
|
||||
vertices[i].pos[1] = (py); \
|
||||
vertices[i].pos[2] = (pz); \
|
||||
vertices[i].uv[0] = (u); \
|
||||
vertices[i].uv[1] = (v);
|
||||
#else
|
||||
#define PLANE_VERT(i, px, py, pz, u, v) \
|
||||
vertices[i].pos[0] = (px); \
|
||||
vertices[i].pos[1] = (py); \
|
||||
vertices[i].pos[2] = (pz); \
|
||||
vertices[i].uv[0] = (u); \
|
||||
vertices[i].uv[1] = (v);
|
||||
#endif
|
||||
#define PLANE_VERT(i, px, py, pz, u, v) \
|
||||
vertices[i].pos[0] = (px); \
|
||||
vertices[i].pos[1] = (py); \
|
||||
vertices[i].pos[2] = (pz); \
|
||||
vertices[i].uv[0] = (u); \
|
||||
vertices[i].uv[1] = (v);
|
||||
|
||||
void planeBuffer(
|
||||
meshvertex_t *vertices,
|
||||
const planeaxis_t axis,
|
||||
const vec3 min,
|
||||
const vec3 max
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
, const vec2 uvMin,
|
||||
const vec3 max,
|
||||
const vec2 uvMin,
|
||||
const vec2 uvMax
|
||||
) {
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define PLANE_VERTEX_COUNT 6
|
||||
#define PLANE_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
|
||||
@@ -52,10 +51,7 @@ void planeBuffer(
|
||||
meshvertex_t *vertices,
|
||||
const planeaxis_t axis,
|
||||
const vec3 min,
|
||||
const vec3 max
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
, const vec2 uvMin,
|
||||
const vec3 max,
|
||||
const vec2 uvMin,
|
||||
const vec2 uvMax
|
||||
);
|
||||
|
||||
+31
-150
@@ -10,55 +10,12 @@
|
||||
|
||||
mesh_t QUAD_MESH_SIMPLE;
|
||||
meshvertex_t QUAD_MESH_SIMPLE_VERTICES[QUAD_VERTEX_COUNT] = {
|
||||
{
|
||||
#if MESH_ENABLE_COLOR
|
||||
.color = COLOR_WHITE_4B,
|
||||
#endif
|
||||
|
||||
.uv = { 0.0f, 0.0f },
|
||||
.pos = { 0.0f, 0.0f, 0.0f }
|
||||
},
|
||||
|
||||
{
|
||||
#if MESH_ENABLE_COLOR
|
||||
.color = COLOR_WHITE_4B,
|
||||
#endif
|
||||
.uv = { 1.0f, 0.0f },
|
||||
.pos = { 1.0f, 0.0f, 0.0f }
|
||||
},
|
||||
|
||||
{
|
||||
#if MESH_ENABLE_COLOR
|
||||
.color = COLOR_WHITE_4B,
|
||||
#endif
|
||||
.uv = { 1.0f, 1.0f },
|
||||
.pos = { 1.0f, 1.0f, 0.0f }
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
#if MESH_ENABLE_COLOR
|
||||
.color = COLOR_WHITE_4B,
|
||||
#endif
|
||||
.uv = { 0.0f, 0.0f },
|
||||
.pos = { 0.0f, 0.0f, 0.0f }
|
||||
},
|
||||
|
||||
{
|
||||
#if MESH_ENABLE_COLOR
|
||||
.color = COLOR_WHITE_4B,
|
||||
#endif
|
||||
.uv = { 1.0f, 1.0f },
|
||||
.pos = { 1.0f, 1.0f, 0.0f }
|
||||
},
|
||||
|
||||
{
|
||||
#if MESH_ENABLE_COLOR
|
||||
.color = COLOR_WHITE_4B,
|
||||
#endif
|
||||
.uv = { 0.0f, 1.0f },
|
||||
.pos = { 0.0f, 1.0f, 0.0f }
|
||||
}
|
||||
{ .uv = { 0.0f, 0.0f }, .pos = { 0.0f, 0.0f, 0.0f } },
|
||||
{ .uv = { 1.0f, 0.0f }, .pos = { 1.0f, 0.0f, 0.0f } },
|
||||
{ .uv = { 1.0f, 1.0f }, .pos = { 1.0f, 1.0f, 0.0f } },
|
||||
{ .uv = { 0.0f, 0.0f }, .pos = { 0.0f, 0.0f, 0.0f } },
|
||||
{ .uv = { 1.0f, 1.0f }, .pos = { 1.0f, 1.0f, 0.0f } },
|
||||
{ .uv = { 0.0f, 1.0f }, .pos = { 0.0f, 1.0f, 0.0f } }
|
||||
};
|
||||
|
||||
errorret_t quadInit() {
|
||||
@@ -81,68 +38,27 @@ void quadBuffer(
|
||||
const float_t v0,
|
||||
const float_t u1,
|
||||
const float_t v1
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
) {
|
||||
const float_t z = 0.0f; // Z coordinate for 2D rendering
|
||||
const float_t z = 0.0f;
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
|
||||
// First triangle
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[0].color = color;
|
||||
#endif
|
||||
vertices[0].uv[0] = u0;
|
||||
vertices[0].uv[1] = v1;
|
||||
vertices[0].pos[0] = minX;
|
||||
vertices[0].pos[1] = maxY;
|
||||
vertices[0].pos[2] = z;
|
||||
vertices[0].uv[0] = u0; vertices[0].uv[1] = v1;
|
||||
vertices[0].pos[0] = minX; vertices[0].pos[1] = maxY; vertices[0].pos[2] = z;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[1].color = color;
|
||||
#endif
|
||||
vertices[1].uv[0] = u1;
|
||||
vertices[1].uv[1] = v0;
|
||||
vertices[1].pos[0] = maxX;
|
||||
vertices[1].pos[1] = minY;
|
||||
vertices[1].pos[2] = z;
|
||||
vertices[1].uv[0] = u1; vertices[1].uv[1] = v0;
|
||||
vertices[1].pos[0] = maxX; vertices[1].pos[1] = minY; vertices[1].pos[2] = z;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[2].color = color;
|
||||
#endif
|
||||
vertices[2].uv[0] = u0;
|
||||
vertices[2].uv[1] = v0;
|
||||
vertices[2].pos[0] = minX;
|
||||
vertices[2].pos[1] = minY;
|
||||
vertices[2].pos[2] = z;
|
||||
vertices[2].uv[0] = u0; vertices[2].uv[1] = v0;
|
||||
vertices[2].pos[0] = minX; vertices[2].pos[1] = minY; vertices[2].pos[2] = z;
|
||||
|
||||
// Second triangle
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[3].color = color;
|
||||
#endif
|
||||
vertices[3].uv[0] = u0;
|
||||
vertices[3].uv[1] = v1;
|
||||
vertices[3].pos[0] = minX;
|
||||
vertices[3].pos[1] = maxY;
|
||||
vertices[3].pos[2] = z;
|
||||
vertices[3].uv[0] = u0; vertices[3].uv[1] = v1;
|
||||
vertices[3].pos[0] = minX; vertices[3].pos[1] = maxY; vertices[3].pos[2] = z;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[4].color = color;
|
||||
#endif
|
||||
vertices[4].uv[0] = u1;
|
||||
vertices[4].uv[1] = v1;
|
||||
vertices[4].pos[0] = maxX;
|
||||
vertices[4].pos[1] = maxY;
|
||||
vertices[4].pos[2] = z;
|
||||
vertices[4].uv[0] = u1; vertices[4].uv[1] = v1;
|
||||
vertices[4].pos[0] = maxX; vertices[4].pos[1] = maxY; vertices[4].pos[2] = z;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[5].color = color;
|
||||
#endif
|
||||
vertices[5].uv[0] = u1;
|
||||
vertices[5].uv[1] = v0;
|
||||
vertices[5].pos[0] = maxX;
|
||||
vertices[5].pos[1] = minY;
|
||||
vertices[5].pos[2] = z;
|
||||
vertices[5].uv[0] = u1; vertices[5].uv[1] = v0;
|
||||
vertices[5].pos[0] = maxX; vertices[5].pos[1] = minY; vertices[5].pos[2] = z;
|
||||
}
|
||||
|
||||
void quadBuffer3D(
|
||||
@@ -151,9 +67,6 @@ void quadBuffer3D(
|
||||
const vec3 max,
|
||||
const vec2 uvMin,
|
||||
const vec2 uvMax
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
) {
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
assertNotNull(min, "Min vector cannot be NULL");
|
||||
@@ -161,59 +74,27 @@ void quadBuffer3D(
|
||||
assertNotNull(uvMin, "UV Min vector cannot be NULL");
|
||||
assertNotNull(uvMax, "UV Max vector cannot be NULL");
|
||||
|
||||
// First triangle
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[0].color = color;
|
||||
#endif
|
||||
vertices[0].uv[0] = uvMin[0];
|
||||
vertices[0].uv[1] = uvMin[1];
|
||||
vertices[0].pos[0] = min[0];
|
||||
vertices[0].pos[1] = min[1];
|
||||
vertices[0].uv[0] = uvMin[0]; vertices[0].uv[1] = uvMin[1];
|
||||
vertices[0].pos[0] = min[0]; vertices[0].pos[1] = min[1];
|
||||
vertices[0].pos[2] = min[2];
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[1].color = color;
|
||||
#endif
|
||||
vertices[1].uv[0] = uvMax[0];
|
||||
vertices[1].uv[1] = uvMin[1];
|
||||
vertices[1].pos[0] = max[0];
|
||||
vertices[1].pos[1] = min[1];
|
||||
vertices[1].uv[0] = uvMax[0]; vertices[1].uv[1] = uvMin[1];
|
||||
vertices[1].pos[0] = max[0]; vertices[1].pos[1] = min[1];
|
||||
vertices[1].pos[2] = min[2];
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[2].color = color;
|
||||
#endif
|
||||
vertices[2].uv[0] = uvMax[0];
|
||||
vertices[2].uv[1] = uvMax[1];
|
||||
vertices[2].pos[0] = max[0];
|
||||
vertices[2].pos[1] = max[1];
|
||||
vertices[2].uv[0] = uvMax[0]; vertices[2].uv[1] = uvMax[1];
|
||||
vertices[2].pos[0] = max[0]; vertices[2].pos[1] = max[1];
|
||||
vertices[2].pos[2] = min[2];
|
||||
|
||||
// Second triangle
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[3].color = color;
|
||||
#endif
|
||||
vertices[3].uv[0] = uvMin[0];
|
||||
vertices[3].uv[1] = uvMin[1];
|
||||
vertices[3].pos[0] = min[0];
|
||||
vertices[3].pos[1] = min[1];
|
||||
vertices[3].uv[0] = uvMin[0]; vertices[3].uv[1] = uvMin[1];
|
||||
vertices[3].pos[0] = min[0]; vertices[3].pos[1] = min[1];
|
||||
vertices[3].pos[2] = min[2];
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[4].color = color;
|
||||
#endif
|
||||
vertices[4].uv[0] = uvMax[0];
|
||||
vertices[4].uv[1] = uvMax[1];
|
||||
vertices[4].pos[0] = max[0];
|
||||
vertices[4].pos[1] = max[1];
|
||||
vertices[4].uv[0] = uvMax[0]; vertices[4].uv[1] = uvMax[1];
|
||||
vertices[4].pos[0] = max[0]; vertices[4].pos[1] = max[1];
|
||||
vertices[4].pos[2] = min[2];
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[5].color = color;
|
||||
#endif
|
||||
vertices[5].uv[0] = uvMin[0];
|
||||
vertices[5].uv[1] = uvMax[1];
|
||||
vertices[5].pos[0] = min[0];
|
||||
vertices[5].pos[1] = max[1];
|
||||
vertices[5].uv[0] = uvMin[0]; vertices[5].uv[1] = uvMax[1];
|
||||
vertices[5].pos[0] = min[0]; vertices[5].pos[1] = max[1];
|
||||
vertices[5].pos[2] = min[2];
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "mesh.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define QUAD_VERTEX_COUNT 6
|
||||
#define QUAD_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
|
||||
@@ -46,9 +45,6 @@ void quadBuffer(
|
||||
const float_t v0,
|
||||
const float_t u1,
|
||||
const float_t v1
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -57,7 +53,6 @@ void quadBuffer(
|
||||
* @param vertices The vertex array to buffer into.
|
||||
* @param min The minimum XYZ coordinates of the quad.
|
||||
* @param max The maximum XYZ coordinates of the quad.
|
||||
* @param color The color of the quad.
|
||||
* @param uvMin The minimum UV coordinates of the quad.
|
||||
* @param uvMax The maximum UV coordinates of the quad.
|
||||
*/
|
||||
@@ -67,7 +62,4 @@ void quadBuffer3D(
|
||||
const vec3 max,
|
||||
const vec2 uvMin,
|
||||
const vec2 uvMax
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
);
|
||||
@@ -19,9 +19,6 @@ errorret_t sphereInit() {
|
||||
0.5f,
|
||||
SPHERE_STACKS,
|
||||
SPHERE_SECTORS
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE_4B
|
||||
#endif
|
||||
);
|
||||
errorChain(meshInit(
|
||||
&SPHERE_MESH_SIMPLE,
|
||||
@@ -38,9 +35,6 @@ void sphereBuffer(
|
||||
const float_t radius,
|
||||
const int32_t stacks,
|
||||
const int32_t sectors
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
) {
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
assertNotNull(center, "Center vector cannot be NULL");
|
||||
@@ -79,67 +73,29 @@ void sphereBuffer(
|
||||
const float_t u1 = (float_t)j / (float_t)sectors;
|
||||
const float_t u2 = (float_t)(j + 1) / (float_t)sectors;
|
||||
|
||||
/* Triangle 1: top-left, bottom-left, top-right */
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[vi].color = color;
|
||||
#endif
|
||||
vertices[vi].pos[0] = center[0] + x11;
|
||||
vertices[vi].pos[1] = center[1] + y1;
|
||||
vertices[vi].pos[0] = center[0] + x11; vertices[vi].pos[1] = center[1] + y1;
|
||||
vertices[vi].pos[2] = center[2] + z11;
|
||||
vertices[vi].uv[0] = u1;
|
||||
vertices[vi].uv[1] = v1;
|
||||
vi++;
|
||||
vertices[vi].uv[0] = u1; vertices[vi].uv[1] = v1; vi++;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[vi].color = color;
|
||||
#endif
|
||||
vertices[vi].pos[0] = center[0] + x21;
|
||||
vertices[vi].pos[1] = center[1] + y2;
|
||||
vertices[vi].pos[0] = center[0] + x21; vertices[vi].pos[1] = center[1] + y2;
|
||||
vertices[vi].pos[2] = center[2] + z21;
|
||||
vertices[vi].uv[0] = u1;
|
||||
vertices[vi].uv[1] = v2;
|
||||
vi++;
|
||||
vertices[vi].uv[0] = u1; vertices[vi].uv[1] = v2; vi++;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[vi].color = color;
|
||||
#endif
|
||||
vertices[vi].pos[0] = center[0] + x12;
|
||||
vertices[vi].pos[1] = center[1] + y1;
|
||||
vertices[vi].pos[0] = center[0] + x12; vertices[vi].pos[1] = center[1] + y1;
|
||||
vertices[vi].pos[2] = center[2] + z12;
|
||||
vertices[vi].uv[0] = u2;
|
||||
vertices[vi].uv[1] = v1;
|
||||
vi++;
|
||||
vertices[vi].uv[0] = u2; vertices[vi].uv[1] = v1; vi++;
|
||||
|
||||
/* Triangle 2: top-right, bottom-left, bottom-right */
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[vi].color = color;
|
||||
#endif
|
||||
vertices[vi].pos[0] = center[0] + x12;
|
||||
vertices[vi].pos[1] = center[1] + y1;
|
||||
vertices[vi].pos[0] = center[0] + x12; vertices[vi].pos[1] = center[1] + y1;
|
||||
vertices[vi].pos[2] = center[2] + z12;
|
||||
vertices[vi].uv[0] = u2;
|
||||
vertices[vi].uv[1] = v1;
|
||||
vi++;
|
||||
vertices[vi].uv[0] = u2; vertices[vi].uv[1] = v1; vi++;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[vi].color = color;
|
||||
#endif
|
||||
vertices[vi].pos[0] = center[0] + x21;
|
||||
vertices[vi].pos[1] = center[1] + y2;
|
||||
vertices[vi].pos[0] = center[0] + x21; vertices[vi].pos[1] = center[1] + y2;
|
||||
vertices[vi].pos[2] = center[2] + z21;
|
||||
vertices[vi].uv[0] = u1;
|
||||
vertices[vi].uv[1] = v2;
|
||||
vi++;
|
||||
vertices[vi].uv[0] = u1; vertices[vi].uv[1] = v2; vi++;
|
||||
|
||||
#if MESH_ENABLE_COLOR
|
||||
vertices[vi].color = color;
|
||||
#endif
|
||||
vertices[vi].pos[0] = center[0] + x22;
|
||||
vertices[vi].pos[1] = center[1] + y2;
|
||||
vertices[vi].pos[0] = center[0] + x22; vertices[vi].pos[1] = center[1] + y2;
|
||||
vertices[vi].pos[2] = center[2] + z22;
|
||||
vertices[vi].uv[0] = u2;
|
||||
vertices[vi].uv[1] = v2;
|
||||
vi++;
|
||||
vertices[vi].uv[0] = u2; vertices[vi].uv[1] = v2; vi++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define SPHERE_STACKS 8
|
||||
#define SPHERE_SECTORS 16
|
||||
@@ -41,7 +40,4 @@ void sphereBuffer(
|
||||
const float_t radius,
|
||||
const int32_t stacks,
|
||||
const int32_t sectors
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
);
|
||||
|
||||
@@ -14,13 +14,10 @@ meshvertex_t TRIPRISM_MESH_SIMPLE_VERTICES[TRIPRISM_VERTEX_COUNT];
|
||||
errorret_t triPrismInit() {
|
||||
triPrismBuffer(
|
||||
TRIPRISM_MESH_SIMPLE_VERTICES,
|
||||
0.0f, 0.0f, /* p0: bottom-left */
|
||||
1.0f, 0.0f, /* p1: bottom-right */
|
||||
0.5f, 1.0f, /* p2: apex */
|
||||
0.0f, 1.0f /* minZ, maxZ */
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE_4B
|
||||
#endif
|
||||
0.0f, 0.0f,
|
||||
1.0f, 0.0f,
|
||||
0.5f, 1.0f,
|
||||
0.0f, 1.0f
|
||||
);
|
||||
errorChain(meshInit(
|
||||
&TRIPRISM_MESH_SIMPLE,
|
||||
@@ -38,32 +35,17 @@ void triPrismBuffer(
|
||||
const float_t x2, const float_t y2,
|
||||
const float_t minZ,
|
||||
const float_t maxZ
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
) {
|
||||
assertNotNull(vertices, "Vertices cannot be NULL");
|
||||
|
||||
/* Helper macro: write one vertex then advance index. */
|
||||
int32_t vi = 0;
|
||||
#if MESH_ENABLE_COLOR
|
||||
#define PRISM_VERT(px, py, pz, u, v) \
|
||||
vertices[vi].color = color; \
|
||||
vertices[vi].pos[0] = (px); \
|
||||
vertices[vi].pos[1] = (py); \
|
||||
vertices[vi].pos[2] = (pz); \
|
||||
vertices[vi].uv[0] = (u); \
|
||||
vertices[vi].uv[1] = (v); \
|
||||
vi++;
|
||||
#else
|
||||
#define PRISM_VERT(px, py, pz, u, v) \
|
||||
vertices[vi].pos[0] = (px); \
|
||||
vertices[vi].pos[1] = (py); \
|
||||
vertices[vi].pos[2] = (pz); \
|
||||
vertices[vi].uv[0] = (u); \
|
||||
vertices[vi].uv[1] = (v); \
|
||||
vi++;
|
||||
#endif
|
||||
#define PRISM_VERT(px, py, pz, u, v) \
|
||||
vertices[vi].pos[0] = (px); \
|
||||
vertices[vi].pos[1] = (py); \
|
||||
vertices[vi].pos[2] = (pz); \
|
||||
vertices[vi].uv[0] = (u); \
|
||||
vertices[vi].uv[1] = (v); \
|
||||
vi++;
|
||||
|
||||
/* --- Front face (z = maxZ), CCW from +Z --- */
|
||||
PRISM_VERT(x0, y0, maxZ, 0.0f, 0.0f)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/color.h"
|
||||
|
||||
#define TRIPRISM_VERTEX_COUNT 24
|
||||
#define TRIPRISM_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
|
||||
@@ -44,7 +43,4 @@ void triPrismBuffer(
|
||||
const float_t x2, const float_t y2,
|
||||
const float_t minZ,
|
||||
const float_t maxZ
|
||||
#if MESH_ENABLE_COLOR
|
||||
, const color_t color
|
||||
#endif
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ const screencropaspectinfo_t SCREEN_CROP_ASPECTS[SCREEN_CROP_ASPECT_COUNT] = {
|
||||
errorret_t screenInit() {
|
||||
memoryZero(&SCREEN, sizeof(screen_t));
|
||||
|
||||
SCREEN.scaleUi = DUSK_DISPLAY_SCALE_UI;
|
||||
SCREEN.scale3d = DUSK_DISPLAY_SCALE_3D;
|
||||
SCREEN.background = COLOR_CORNFLOWER_BLUE;
|
||||
|
||||
#ifdef DUSK_DISPLAY_SIZE_DYNAMIC
|
||||
@@ -35,9 +37,6 @@ errorret_t screenInit() {
|
||||
1.0f, 1.0f,
|
||||
0.0f, 0.0f,
|
||||
1.0f, 1.0f
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE
|
||||
#endif
|
||||
);
|
||||
errorChain(meshInit(
|
||||
&SCREEN.frameBufferMesh,
|
||||
@@ -381,13 +380,10 @@ errorret_t screenRender() {
|
||||
|
||||
quadBuffer(
|
||||
SCREEN.frameBufferMeshVertices,
|
||||
centerX - fbWidth * 0.5f, centerY + fbHeight * 0.5f, // top-left
|
||||
centerX + fbWidth * 0.5f, centerY - fbHeight * 0.5f, // bottom-right
|
||||
centerX - fbWidth * 0.5f, centerY + fbHeight * 0.5f,
|
||||
centerX + fbWidth * 0.5f, centerY - fbHeight * 0.5f,
|
||||
0.0f, 0.0f,
|
||||
1.0f, 1.0f
|
||||
#if MESH_ENABLE_COLOR
|
||||
, COLOR_WHITE
|
||||
#endif
|
||||
);
|
||||
|
||||
frameBufferClear(
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "display/display.h"
|
||||
#include "display/framebuffer/framebuffer.h"
|
||||
#include "display/mesh/quad.h"
|
||||
#include "display/color.h"
|
||||
@@ -58,6 +59,8 @@ typedef enum {
|
||||
// } screenscalemode_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t scaleUi;
|
||||
int32_t scale3d;
|
||||
screenmode_t mode;
|
||||
// screenscalemode_t scaleMode;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ spritebatchsprite_t textGetSprite(
|
||||
tileIndex = ((int32_t)'@') - TEXT_CHAR_START;
|
||||
}
|
||||
assertTrue(
|
||||
tileIndex >= 0 && tileIndex <= font->tileset->tileCount,
|
||||
tileIndex >= 0 && tileIndex < font->tileset->tileCount,
|
||||
"Character is out of bounds for font tiles"
|
||||
);
|
||||
|
||||
|
||||
@@ -10,16 +10,27 @@
|
||||
#include "time/time.h"
|
||||
#include "input/input.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "rpg/rpg.h"
|
||||
#include "display/display.h"
|
||||
#include "scene/scene.h"
|
||||
#include "asset/asset.h"
|
||||
#include "ui/ui.h"
|
||||
#include "assert/assert.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "physics/physicsmanager.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "network/network.h"
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
#include "save/save.h"
|
||||
|
||||
double jerry_port_current_time(void) {
|
||||
dusktimeepoch_t epoch = timeGetEpoch();
|
||||
return epoch.time * 1000.0;
|
||||
}
|
||||
|
||||
int32_t jerry_port_local_tza(double unix_ms) {
|
||||
(void) unix_ms;
|
||||
return 0;
|
||||
}
|
||||
|
||||
engine_t ENGINE;
|
||||
|
||||
@@ -37,17 +48,25 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
// errorChain(saveInit());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(scriptManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
entityManagerInit();
|
||||
physicsManagerInit();
|
||||
errorChain(networkInit());
|
||||
errorChain(sceneInit());
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
errorChain(scriptManagerExecFile("engine.js", NULL));
|
||||
errorChain(scriptManagerCallGlobal("init"));
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
|
||||
#ifdef DUSK_ASSERTIONS_FAKED
|
||||
consolePrint("Assertions faked");
|
||||
#else
|
||||
consolePrint("Assertions real");
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -58,8 +77,22 @@ errorret_t engineUpdate(void) {
|
||||
timeUpdate();
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
errorChain(rpgUpdate());
|
||||
physicsManagerUpdate();
|
||||
|
||||
// Fixed-step logic: runs once per DUSK_TIME_STEP tick. On platforms
|
||||
// without dynamic timing every frame is a fixed step; on platforms with
|
||||
// it, dynamic (interpolation) frames are skipped.
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
if(!TIME.dynamicUpdate) {
|
||||
#endif
|
||||
entityManagerFixedUpdate();
|
||||
errorChain(scriptManagerCallGlobal("fixedUpdate"));
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
}
|
||||
#endif
|
||||
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(scriptManagerCallGlobal("update"));
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
|
||||
@@ -74,15 +107,21 @@ void engineExit(void) {
|
||||
}
|
||||
|
||||
errorret_t engineDispose(void) {
|
||||
errorChain(scriptManagerCallGlobal("deinit"));
|
||||
errorChain(sceneDispose());
|
||||
errorChain(networkDispose());
|
||||
errorChain(rpgDispose());
|
||||
entityManagerDispose();
|
||||
localeManagerDispose();
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
// errorChain(saveDispose());
|
||||
|
||||
// Must run before scriptManagerDispose(): asset entries (e.g. loaded
|
||||
// scripts) hold jerry_value_t references (promises) that need to be
|
||||
// released via their loader's dispose callback before jerry_cleanup()
|
||||
// runs, which fatally asserts if anything is still held.
|
||||
errorChain(assetDispose());
|
||||
errorChain(scriptManagerDispose());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
entity.c
|
||||
entitymanager.c
|
||||
component.c
|
||||
entityrender.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(component)
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitymanager.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
||||
[COMPONENT_TYPE_NULL] = { 0 },
|
||||
|
||||
#define X(enm, type, field, iMethod, dMethod, fMethod) \
|
||||
[COMPONENT_TYPE_##enm] = { \
|
||||
.enumName = #enm, \
|
||||
.name = #field, \
|
||||
.init = iMethod, \
|
||||
.dispose = dMethod, \
|
||||
.fixedUpdate = fMethod \
|
||||
},
|
||||
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
[COMPONENT_TYPE_COUNT] = { 0 }
|
||||
};
|
||||
|
||||
void componentInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
) {
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
||||
|
||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||
memoryZero(cmp, sizeof(component_t));
|
||||
|
||||
cmp->type = type;
|
||||
if(COMPONENT_DEFINITIONS[type].init) {
|
||||
COMPONENT_DEFINITIONS[type].init(entityId, componentId);
|
||||
}
|
||||
}
|
||||
|
||||
void * componentGetData(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
) {
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
||||
|
||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||
assertTrue(cmp->type == type, "Component type mismatch");
|
||||
|
||||
return &cmp->data;
|
||||
}
|
||||
|
||||
componentindex_t componentGetIndex(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId;
|
||||
}
|
||||
|
||||
entityid_t componentGetEntitiesWithComponent(
|
||||
const componenttype_t type,
|
||||
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||
) {
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
|
||||
assertNotNull(outEntities, "Output entities array cannot be null");
|
||||
assertNotNull(outComponents, "Output components array cannot be null");
|
||||
|
||||
entityid_t written = 0;
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
componentid_t used = ENTITY_MANAGER.entitiesWithComponent[
|
||||
type * ENTITY_COUNT_MAX + i
|
||||
];
|
||||
if(used == COMPONENT_ID_INVALID) continue;
|
||||
assertTrue(
|
||||
ENTITY_MANAGER.components[componentGetIndex(i, used)].type == type,
|
||||
"Component type mismatch in entitiesWithComponent lookup"
|
||||
);
|
||||
assertTrue(
|
||||
(ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) != 0,
|
||||
"Inactive entity in entitiesWithComponent lookup"
|
||||
);
|
||||
assertTrue(
|
||||
used < ENTITY_COMPONENT_COUNT_MAX,
|
||||
"Component ID OOB in entitiesWithComponent lookup"
|
||||
);
|
||||
assertTrue(
|
||||
componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
|
||||
"Component index OOB in entitiesWithComponent lookup"
|
||||
);
|
||||
outComponents[written] = used;
|
||||
outEntities[written++] = i;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
void componentDispose(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
|
||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
||||
|
||||
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
||||
COMPONENT_DEFINITIONS[cmp->type].dispose(entityId, componentId);
|
||||
}
|
||||
|
||||
cmp->type = COMPONENT_TYPE_NULL;
|
||||
}
|
||||
|
||||
void componentFixedUpdateAll(void) {
|
||||
for(entityid_t e = 0; e < ENTITY_COUNT_MAX; e++) {
|
||||
if((ENTITY_MANAGER.entities[e].state & ENTITY_STATE_ACTIVE) == 0) continue;
|
||||
|
||||
for(componentid_t c = 0; c < ENTITY_COMPONENT_COUNT_MAX; c++) {
|
||||
componentindex_t index = componentGetIndex(e, c);
|
||||
componenttype_t type = ENTITY_MANAGER.components[index].type;
|
||||
if(type == COMPONENT_TYPE_NULL) continue;
|
||||
if(COMPONENT_DEFINITIONS[type].fixedUpdate) {
|
||||
COMPONENT_DEFINITIONS[type].fixedUpdate(e, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entitybase.h"
|
||||
|
||||
#define X(enumName, type, field, init, dispose, fixedUpdate) \
|
||||
// do nothing
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
typedef union {
|
||||
#define X(enumName, type, field, init, dispose, fixedUpdate) type field;
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
} componentdata_t;
|
||||
|
||||
typedef struct {
|
||||
const char_t *enumName;
|
||||
const char_t *name;
|
||||
void (*init)(const entityid_t, const componentid_t);
|
||||
void (*dispose)(const entityid_t, const componentid_t);
|
||||
void (*fixedUpdate)(const entityid_t, const componentid_t);
|
||||
} componentdefinition_t;
|
||||
|
||||
typedef enum {
|
||||
COMPONENT_TYPE_NULL,
|
||||
|
||||
#define X(enumName, type, field, init, dispose, fixedUpdate) \
|
||||
COMPONENT_TYPE_##enumName,
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
COMPONENT_TYPE_COUNT
|
||||
} componenttype_t;
|
||||
|
||||
typedef struct {
|
||||
componenttype_t type;
|
||||
componentdata_t data;
|
||||
} component_t;
|
||||
|
||||
extern componentdefinition_t COMPONENT_DEFINITIONS[];
|
||||
|
||||
/**
|
||||
* Initializes a component of the given type for the entity with component ID.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The type of the component to initialize.
|
||||
*/
|
||||
void componentInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the pointer to the data of a component for the entity with component ID.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The type of the component to get, only used for assertion.
|
||||
* @return A pointer to the component data.
|
||||
*/
|
||||
void * componentGetData(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the index of a component for the entity with component ID.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The index of the component in the component array.
|
||||
*/
|
||||
componentindex_t componentGetIndex(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the entity IDs of all entities with a component of the given type.
|
||||
*
|
||||
* @param type The type of the component to get entities for.
|
||||
* @param outEntities An array to write the entity IDs to, must be at least
|
||||
* ENTITY_COUNT_MAX in size.
|
||||
* @param outComponents An array to write the component IDs to.
|
||||
* @return The number of entity IDs written to outEntities.
|
||||
*/
|
||||
entityid_t componentGetEntitiesWithComponent(
|
||||
const componenttype_t type,
|
||||
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of a component for the entity with component ID.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
*/
|
||||
void componentDispose(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls fixedUpdate on every active component that defines one. Intended to
|
||||
* be called once per fixed timestep - see entityManagerFixedUpdate().
|
||||
*/
|
||||
void componentFixedUpdateAll(void);
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(physics)
|
||||
add_subdirectory(script)
|
||||
add_subdirectory(trigger)
|
||||
+3
-4
@@ -6,8 +6,7 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
npc.c
|
||||
npcturn.c
|
||||
npcwalk.c
|
||||
npcpath.c
|
||||
entityposition.c
|
||||
entitycamera.c
|
||||
entityrenderable.c
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entity/entitymanager.h"
|
||||
#include "entity/entity.h"
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "display/framebuffer/framebuffer.h"
|
||||
#include "display/screen/screen.h"
|
||||
|
||||
void entityCameraInit(const entityid_t ent, const componentid_t comp) {
|
||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||
ent, comp, COMPONENT_TYPE_CAMERA
|
||||
);
|
||||
cam->nearClip = 0.1f;
|
||||
cam->farClip = 100.0f;
|
||||
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||
cam->perspective.fov = glm_rad(45.0f);
|
||||
}
|
||||
|
||||
void entityCameraGetProjection(
|
||||
const entityid_t ent,
|
||||
const componentid_t comp,
|
||||
mat4 out
|
||||
) {
|
||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||
ent, comp, COMPONENT_TYPE_CAMERA
|
||||
);
|
||||
|
||||
if(
|
||||
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
|
||||
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
|
||||
) {
|
||||
glm_mat4_identity(out);
|
||||
glm_perspective(
|
||||
cam->perspective.fov,
|
||||
SCREEN.aspect,
|
||||
cam->nearClip,
|
||||
cam->farClip,
|
||||
out
|
||||
);
|
||||
|
||||
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
|
||||
out[1][1] *= -1.0f;
|
||||
}
|
||||
} else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||
glm_mat4_identity(out);
|
||||
glm_ortho(
|
||||
cam->orthographic.left,
|
||||
cam->orthographic.right,
|
||||
cam->orthographic.top,
|
||||
cam->orthographic.bottom,
|
||||
cam->nearClip,
|
||||
cam->farClip,
|
||||
out
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
entityid_t entityCameraGetCurrent(void) {
|
||||
entityid_t camEnts[ENTITY_COUNT_MAX];
|
||||
componentid_t camComps[ENTITY_COUNT_MAX];
|
||||
entityid_t count = componentGetEntitiesWithComponent(
|
||||
COMPONENT_TYPE_CAMERA, camEnts, camComps
|
||||
);
|
||||
if(count == 0) return ENTITY_COUNT_MAX;
|
||||
return camEnts[0];
|
||||
}
|
||||
|
||||
void entityCameraGetForward(const entityid_t entityId, vec2 out) {
|
||||
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
|
||||
entityposition_t *pos = entityPositionGet(entityId, posComp);
|
||||
// View matrix column layout: M[col][row],
|
||||
// forward = {-M[0][2], -M[1][2], -M[2][2]}
|
||||
float_t fx = -pos->worldTransform[0][2];
|
||||
float_t fz = -pos->worldTransform[2][2];
|
||||
float_t len = sqrtf(fx * fx + fz * fz);
|
||||
if(len > 1e-6f) { fx /= len; fz /= len; }
|
||||
out[0] = fx;
|
||||
out[1] = fz;
|
||||
}
|
||||
|
||||
void entityCameraGetRight(const entityid_t entityId, vec2 out) {
|
||||
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
|
||||
entityposition_t *pos = entityPositionGet(entityId, posComp);
|
||||
// View matrix column layout: right = {M[0][0], M[1][0], M[2][0]}
|
||||
float_t rx = pos->worldTransform[0][0];
|
||||
float_t rz = pos->worldTransform[2][0];
|
||||
float_t len = sqrtf(rx * rx + rz * rz);
|
||||
if(len > 1e-6f) { rx /= len; rz /= len; }
|
||||
out[0] = rx;
|
||||
out[1] = rz;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
|
||||
typedef enum {
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED,
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC
|
||||
} entitycameraprojectiontype_t;
|
||||
|
||||
typedef struct {
|
||||
union {
|
||||
struct {
|
||||
float_t fov;
|
||||
} perspective;
|
||||
|
||||
struct {
|
||||
float_t left;
|
||||
float_t right;
|
||||
float_t top;
|
||||
float_t bottom;
|
||||
} orthographic;
|
||||
};
|
||||
|
||||
float_t nearClip;
|
||||
float_t farClip;
|
||||
entitycameraprojectiontype_t projType;
|
||||
} entitycamera_t;
|
||||
|
||||
/**
|
||||
* Initializes an entity camera component.
|
||||
*
|
||||
* @param ent The entity ID.
|
||||
* @param comp The component ID.
|
||||
*/
|
||||
void entityCameraInit(const entityid_t ent, const componentid_t comp);
|
||||
|
||||
/**
|
||||
* Renders out the projection matrix for the given camera.
|
||||
*
|
||||
* @param ent The entity ID.
|
||||
* @param comp The component ID.
|
||||
* @param out The output projection matrix.
|
||||
*/
|
||||
void entityCameraGetProjection(
|
||||
const entityid_t ent,
|
||||
const componentid_t comp,
|
||||
mat4 out
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the entity ID of the first active camera, or ENTITY_COUNT_MAX if
|
||||
* none are active.
|
||||
*/
|
||||
entityid_t entityCameraGetCurrent(void);
|
||||
|
||||
/**
|
||||
* Gets the camera's horizontal forward direction (XZ plane) from its position
|
||||
* component. Automatically finds the position component on the entity.
|
||||
*
|
||||
* @param entityId The camera entity ID.
|
||||
* @param out Output vec2: {forwardX, forwardZ} normalized.
|
||||
*/
|
||||
void entityCameraGetForward(const entityid_t entityId, vec2 out);
|
||||
|
||||
/**
|
||||
* Gets the camera's horizontal right direction (XZ plane) from its position
|
||||
* component. Automatically finds the position component on the entity.
|
||||
*
|
||||
* @param entityId The camera entity ID.
|
||||
* @param out Output vec2: {rightX, rightZ} normalized.
|
||||
*/
|
||||
void entityCameraGetRight(const entityid_t entityId, vec2 out);
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entity/entitymanager.h"
|
||||
|
||||
// Lazily recompute worldTransform from the parent chain.
|
||||
static void entityPositionUpdateWorld(entityposition_t *pos) {
|
||||
if(!pos->dirty) return;
|
||||
|
||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||
glm_mat4_copy(pos->localTransform, pos->worldTransform);
|
||||
} else {
|
||||
entityposition_t *parent = componentGetData(
|
||||
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionUpdateWorld(parent);
|
||||
glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform);
|
||||
}
|
||||
|
||||
pos->dirty = false;
|
||||
}
|
||||
|
||||
void entityPositionMarkDirty(entityposition_t *pos) {
|
||||
pos->dirty = true;
|
||||
for(uint8_t i = 0; i < pos->childCount; i++) {
|
||||
entityposition_t *child = componentGetData(
|
||||
pos->childEntityIds[i], pos->childComponentIds[i], COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionMarkDirty(child);
|
||||
}
|
||||
}
|
||||
|
||||
void entityPositionInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
|
||||
glm_vec3_zero(pos->position);
|
||||
glm_vec3_zero(pos->rotation);
|
||||
glm_vec3_one(pos->scale);
|
||||
glm_mat4_identity(pos->localTransform);
|
||||
glm_mat4_identity(pos->worldTransform);
|
||||
pos->dirty = false;
|
||||
pos->parentEntityId = ENTITY_ID_INVALID;
|
||||
pos->parentComponentId = COMPONENT_ID_INVALID;
|
||||
pos->childCount = 0;
|
||||
}
|
||||
|
||||
void entityPositionLookAt(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 target,
|
||||
vec3 up,
|
||||
vec3 eye
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_lookat(eye, target, up, pos->localTransform);
|
||||
entityPositionDecompose(pos);
|
||||
entityPositionMarkDirty(pos);
|
||||
}
|
||||
|
||||
void entityPositionGetTransform(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionUpdateWorld(pos);
|
||||
glm_mat4_copy(pos->worldTransform, dest);
|
||||
}
|
||||
|
||||
void entityPositionGetLocalTransform(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_mat4_copy(pos->localTransform, dest);
|
||||
}
|
||||
|
||||
void entityPositionGetPosition(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(pos->position, dest);
|
||||
}
|
||||
|
||||
void entityPositionSetPosition(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 position
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(position, pos->position);
|
||||
entityPositionRebuild(pos);
|
||||
}
|
||||
|
||||
void entityPositionGetRotation(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(pos->rotation, dest);
|
||||
}
|
||||
|
||||
void entityPositionSetRotation(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 rotation
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(rotation, pos->rotation);
|
||||
entityPositionRebuild(pos);
|
||||
}
|
||||
|
||||
void entityPositionGetScale(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(pos->scale, dest);
|
||||
}
|
||||
|
||||
void entityPositionSetScale(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 scale
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(scale, pos->scale);
|
||||
entityPositionRebuild(pos);
|
||||
}
|
||||
|
||||
void entityPositionSetParent(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityid_t parentEntityId,
|
||||
const componentid_t parentComponentId
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
|
||||
// Remove from old parent's child list.
|
||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||
entityposition_t *oldParent = componentGetData(
|
||||
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
for(uint8_t i = 0; i < oldParent->childCount; i++) {
|
||||
if(
|
||||
oldParent->childEntityIds[i] == entityId &&
|
||||
oldParent->childComponentIds[i] == componentId
|
||||
) {
|
||||
oldParent->childCount--;
|
||||
for(uint8_t j = i; j < oldParent->childCount; j++) {
|
||||
oldParent->childEntityIds[j] = oldParent->childEntityIds[j + 1];
|
||||
oldParent->childComponentIds[j] = oldParent->childComponentIds[j + 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos->parentEntityId = parentEntityId;
|
||||
pos->parentComponentId = parentComponentId;
|
||||
|
||||
// Register with new parent.
|
||||
if(parentEntityId != ENTITY_ID_INVALID) {
|
||||
entityposition_t *parent = componentGetData(
|
||||
parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
|
||||
parent->childEntityIds[parent->childCount] = entityId;
|
||||
parent->childComponentIds[parent->childCount] = componentId;
|
||||
parent->childCount++;
|
||||
}
|
||||
}
|
||||
|
||||
entityPositionMarkDirty(pos);
|
||||
}
|
||||
|
||||
entityposition_t *entityPositionGet(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
return componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
}
|
||||
|
||||
void entityPositionRebuild(entityposition_t *pos) {
|
||||
glm_mat4_identity(pos->localTransform);
|
||||
glm_translate(pos->localTransform, pos->position);
|
||||
if(pos->rotation[0] != 0.0f) {
|
||||
glm_rotate_x(pos->localTransform, pos->rotation[0], pos->localTransform);
|
||||
}
|
||||
if(pos->rotation[1] != 0.0f) {
|
||||
glm_rotate_y(pos->localTransform, pos->rotation[1], pos->localTransform);
|
||||
}
|
||||
if(pos->rotation[2] != 0.0f) {
|
||||
glm_rotate_z(pos->localTransform, pos->rotation[2], pos->localTransform);
|
||||
}
|
||||
glm_scale(pos->localTransform, pos->scale);
|
||||
entityPositionMarkDirty(pos);
|
||||
}
|
||||
|
||||
void entityPositionDisposeDeep(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityposition_t *pos = entityPositionGet(entityId, componentId);
|
||||
|
||||
// Detach from parent so the parent's child list stays consistent.
|
||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||
entityPositionSetParent(entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID);
|
||||
}
|
||||
|
||||
// Copy the child list before disposing self (entityDispose invalidates pos).
|
||||
uint8_t childCount = pos->childCount;
|
||||
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||
for(uint8_t i = 0; i < childCount; i++) {
|
||||
childEntityIds[i] = pos->childEntityIds[i];
|
||||
childComponentIds[i] = pos->childComponentIds[i];
|
||||
// Sever the child's parent link so it won't try to modify our disposed data.
|
||||
entityposition_t *child = entityPositionGet(childEntityIds[i], childComponentIds[i]);
|
||||
child->parentEntityId = ENTITY_ID_INVALID;
|
||||
child->parentComponentId = COMPONENT_ID_INVALID;
|
||||
}
|
||||
|
||||
entityDispose(entityId);
|
||||
|
||||
for(uint8_t i = 0; i < childCount; i++) {
|
||||
entityPositionDisposeDeep(childEntityIds[i], childComponentIds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void entityPositionDecompose(entityposition_t *pos) {
|
||||
// Translation: column 3
|
||||
pos->position[0] = pos->localTransform[3][0];
|
||||
pos->position[1] = pos->localTransform[3][1];
|
||||
pos->position[2] = pos->localTransform[3][2];
|
||||
|
||||
// Scale: length of each basis column (xyz only)
|
||||
pos->scale[0] = sqrtf(
|
||||
pos->localTransform[0][0] * pos->localTransform[0][0] +
|
||||
pos->localTransform[0][1] * pos->localTransform[0][1] +
|
||||
pos->localTransform[0][2] * pos->localTransform[0][2]
|
||||
);
|
||||
pos->scale[1] = sqrtf(
|
||||
pos->localTransform[1][0] * pos->localTransform[1][0] +
|
||||
pos->localTransform[1][1] * pos->localTransform[1][1] +
|
||||
pos->localTransform[1][2] * pos->localTransform[1][2]
|
||||
);
|
||||
pos->scale[2] = sqrtf(
|
||||
pos->localTransform[2][0] * pos->localTransform[2][0] +
|
||||
pos->localTransform[2][1] * pos->localTransform[2][1] +
|
||||
pos->localTransform[2][2] * pos->localTransform[2][2]
|
||||
);
|
||||
|
||||
// Normalize columns to isolate the rotation matrix.
|
||||
float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
|
||||
float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
|
||||
float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
|
||||
|
||||
mat4 r;
|
||||
glm_mat4_identity(r);
|
||||
r[0][0] = pos->localTransform[0][0] * invS0;
|
||||
r[0][1] = pos->localTransform[0][1] * invS0;
|
||||
r[0][2] = pos->localTransform[0][2] * invS0;
|
||||
r[1][0] = pos->localTransform[1][0] * invS1;
|
||||
r[1][1] = pos->localTransform[1][1] * invS1;
|
||||
r[1][2] = pos->localTransform[1][2] * invS1;
|
||||
r[2][0] = pos->localTransform[2][0] * invS2;
|
||||
r[2][1] = pos->localTransform[2][1] * invS2;
|
||||
r[2][2] = pos->localTransform[2][2] * invS2;
|
||||
|
||||
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
|
||||
float sinBeta = glm_clamp(r[2][0], -1.0f, 1.0f);
|
||||
pos->rotation[1] = asinf(sinBeta);
|
||||
float cosBeta = cosf(pos->rotation[1]);
|
||||
|
||||
if(fabsf(cosBeta) > 1e-6f) {
|
||||
pos->rotation[0] = atan2f(-r[2][1], r[2][2]);
|
||||
pos->rotation[2] = atan2f(-r[1][0], r[0][0]);
|
||||
} else {
|
||||
// Gimbal lock: pin Z to 0, recover X.
|
||||
pos->rotation[2] = 0.0f;
|
||||
pos->rotation[0] = (sinBeta > 0.0f)
|
||||
? atan2f(r[0][1], r[1][1])
|
||||
: -atan2f(r[0][1], r[1][1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
|
||||
#define ENTITY_POSITION_CHILDREN_MAX 8
|
||||
|
||||
typedef struct {
|
||||
mat4 localTransform;
|
||||
mat4 worldTransform;
|
||||
vec3 position;
|
||||
vec3 rotation;
|
||||
vec3 scale;
|
||||
bool dirty;
|
||||
entityid_t parentEntityId;
|
||||
componentid_t parentComponentId;
|
||||
uint8_t childCount;
|
||||
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||
} entityposition_t;
|
||||
|
||||
/**
|
||||
* Initialize the entity position component.
|
||||
*/
|
||||
void entityPositionInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Transforms the entity's local transform to look at a target point.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param target The target point to look at.
|
||||
* @param up The up vector.
|
||||
* @param eye The eye/camera position.
|
||||
*/
|
||||
void entityPositionLookAt(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 target,
|
||||
vec3 up,
|
||||
vec3 eye
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the world-space transform matrix, recomputing it lazily if dirty.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param dest Destination matrix.
|
||||
*/
|
||||
void entityPositionGetTransform(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the local transform matrix (does not include parent transforms).
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param dest Destination matrix.
|
||||
*/
|
||||
void entityPositionGetLocalTransform(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the cached local position.
|
||||
*/
|
||||
void entityPositionGetPosition(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the local position and marks the world transform dirty.
|
||||
*/
|
||||
void entityPositionSetPosition(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 position
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the cached local euler rotation (XYZ, radians).
|
||||
*/
|
||||
void entityPositionGetRotation(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the local euler rotation (XYZ, radians) and marks the world transform dirty.
|
||||
*/
|
||||
void entityPositionSetRotation(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 rotation
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the cached local scale.
|
||||
*/
|
||||
void entityPositionGetScale(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the local scale and marks the world transform dirty.
|
||||
*/
|
||||
void entityPositionSetScale(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 scale
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the parent of this entity's position component.
|
||||
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
|
||||
*
|
||||
* @param entityId The child entity ID.
|
||||
* @param componentId The child component ID.
|
||||
* @param parentEntityId The parent entity ID.
|
||||
* @param parentComponentId The parent component ID.
|
||||
*/
|
||||
void entityPositionSetParent(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityid_t parentEntityId,
|
||||
const componentid_t parentComponentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a direct pointer to the entity position component data.
|
||||
* After modifying localTransform directly, call entityPositionMarkDirty().
|
||||
*/
|
||||
entityposition_t *entityPositionGet(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Rebuilds the local transform matrix from the cached position/rotation/scale,
|
||||
* then marks this node and all descendants dirty.
|
||||
*/
|
||||
void entityPositionRebuild(entityposition_t *pos);
|
||||
|
||||
/**
|
||||
* Marks this node and all descendants as having a stale world transform.
|
||||
*/
|
||||
void entityPositionMarkDirty(entityposition_t *pos);
|
||||
|
||||
/**
|
||||
* Disposes this entity and all of its position-component descendants
|
||||
* recursively. Detaches from any parent before destroying.
|
||||
*
|
||||
* @param entityId The root entity ID.
|
||||
* @param componentId The root position component ID.
|
||||
*/
|
||||
void entityPositionDisposeDeep(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Decomposes the local transform matrix back into the position, rotation
|
||||
* (XYZ euler, radians), and scale cache fields.
|
||||
*/
|
||||
void entityPositionDecompose(entityposition_t *pos);
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entityrenderable.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "display/mesh/cube.h"
|
||||
|
||||
void entityRenderableInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->type = ENTITY_RENDERABLE_TYPE_MATERIAL;
|
||||
r->mesh = &CUBE_MESH_SIMPLE;
|
||||
r->shader = &SHADER_UNLIT;
|
||||
r->material.unlit.color = COLOR_WHITE;
|
||||
}
|
||||
|
||||
entityrenderabletype_t entityRenderableGetType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
return r->type;
|
||||
}
|
||||
|
||||
void entityRenderableSetType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityrenderabletype_t type
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->type = type;
|
||||
}
|
||||
|
||||
mesh_t * entityRenderableGetMesh(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
return r->mesh;
|
||||
}
|
||||
|
||||
void entityRenderableSetMesh(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mesh_t *mesh
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->mesh = mesh;
|
||||
}
|
||||
|
||||
shader_t * entityRenderableGetShader(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
return r->shader;
|
||||
}
|
||||
|
||||
void entityRenderableSetShader(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
shader_t *shader
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->shader = shader;
|
||||
}
|
||||
|
||||
shadermaterial_t * entityRenderableGetShaderMaterial(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
return &r->material;
|
||||
}
|
||||
|
||||
void entityRenderableSetColor(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const color_t color
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->material.unlit.color = color;
|
||||
}
|
||||
|
||||
void entityRenderableSpriteBatchAdd(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const spritebatchsprite_t *sprite
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
if(r->spritebatch.spriteCount >= ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX) return;
|
||||
r->spritebatch.sprites[r->spritebatch.spriteCount++] = *sprite;
|
||||
}
|
||||
|
||||
void entityRenderableSpriteBatchClear(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->spritebatch.spriteCount = 0;
|
||||
}
|
||||
|
||||
void entityRenderableDispose(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
if(
|
||||
r->type == ENTITY_RENDERABLE_TYPE_CALLBACK &&
|
||||
r->userFree &&
|
||||
r->user
|
||||
) {
|
||||
r->userFree(r->user);
|
||||
r->user = NULL;
|
||||
}
|
||||
r->mesh = NULL;
|
||||
r->shader = NULL;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
#include "display/mesh/mesh.h"
|
||||
#include "display/shader/shadermaterial.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
|
||||
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
||||
|
||||
typedef enum {
|
||||
ENTITY_RENDERABLE_TYPE_MATERIAL = 0,
|
||||
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
|
||||
ENTITY_RENDERABLE_TYPE_CALLBACK,
|
||||
} entityrenderabletype_t;
|
||||
|
||||
typedef errorret_t (*entityrenderablecallback_t)(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const mat4 view,
|
||||
const mat4 proj,
|
||||
const mat4 model,
|
||||
void *user
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
|
||||
uint16_t spriteCount;
|
||||
} entityrenderablespritebatch_t;
|
||||
|
||||
typedef struct {
|
||||
entityrenderabletype_t type;
|
||||
shader_t *shader;
|
||||
union {
|
||||
struct {
|
||||
mesh_t *mesh;
|
||||
shadermaterial_t material;
|
||||
};
|
||||
entityrenderablespritebatch_t spritebatch;
|
||||
struct {
|
||||
entityrenderablecallback_t callback;
|
||||
void (*userFree)(void *user);
|
||||
void *user;
|
||||
};
|
||||
};
|
||||
} entityrenderable_t;
|
||||
|
||||
/**
|
||||
* Initializes the entity renderable component. Defaults to
|
||||
* ENTITY_RENDERABLE_TYPE_MATERIAL, the unlit shader, white color, no mesh.
|
||||
*/
|
||||
void entityRenderableInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes the entity renderable component, freeing any callback user data.
|
||||
*/
|
||||
void entityRenderableDispose(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the renderable type.
|
||||
*/
|
||||
entityrenderabletype_t entityRenderableGetType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the renderable type.
|
||||
*/
|
||||
void entityRenderableSetType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityrenderabletype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the mesh pointer (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||
*/
|
||||
mesh_t * entityRenderableGetMesh(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the mesh pointer (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||
*/
|
||||
void entityRenderableSetMesh(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mesh_t *mesh
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the shader pointer.
|
||||
*/
|
||||
shader_t * entityRenderableGetShader(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the shader pointer.
|
||||
*/
|
||||
void entityRenderableSetShader(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
shader_t *shader
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets a pointer to the shader material union
|
||||
* (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||
*/
|
||||
shadermaterial_t * entityRenderableGetShaderMaterial(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the unlit color (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||
*/
|
||||
void entityRenderableSetColor(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const color_t color
|
||||
);
|
||||
|
||||
/**
|
||||
* Appends a sprite to the spritebatch renderable
|
||||
* (ENTITY_RENDERABLE_TYPE_SPRITEBATCH only).
|
||||
* Does nothing if the sprite buffer is full.
|
||||
*/
|
||||
void entityRenderableSpriteBatchAdd(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const spritebatchsprite_t *sprite
|
||||
);
|
||||
|
||||
/**
|
||||
* Clears all buffered sprites from the spritebatch renderable
|
||||
* (ENTITY_RENDERABLE_TYPE_SPRITEBATCH only).
|
||||
*/
|
||||
void entityRenderableSpriteBatchClear(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
assetchunkloader.c
|
||||
entityphysics.c
|
||||
)
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entityphysics.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "physics/physicsmanager.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void entityPhysicsInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
|
||||
memoryZero(phys, sizeof(entityphysics_t));
|
||||
|
||||
// Default to cube
|
||||
phys->type = PHYSICS_BODY_DYNAMIC;
|
||||
phys->shape.type = PHYSICS_SHAPE_CUBE;
|
||||
phys->shape.data.cube.halfExtents[0] = 0.5f;
|
||||
phys->shape.data.cube.halfExtents[1] = 0.5f;
|
||||
phys->shape.data.cube.halfExtents[2] = 0.5f;
|
||||
phys->gravityScale = 1.0f;
|
||||
phys->onGround = false;
|
||||
}
|
||||
|
||||
entityphysics_t *entityPhysicsGet(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
return componentGetData(entityId, componentId, COMPONENT_TYPE_PHYSICS);
|
||||
}
|
||||
|
||||
void entityPhysicsSetShape(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsshape_t shape
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
phys->shape = shape;
|
||||
// TODO: Do I need to reset the state for ground/active?
|
||||
}
|
||||
|
||||
physicsshape_t entityPhysicsGetShape(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
return phys->shape;
|
||||
}
|
||||
|
||||
void entityPhysicsGetVelocity(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
|
||||
glm_vec3_copy(phys->velocity, dest);
|
||||
}
|
||||
|
||||
void entityPhysicsSetVelocity(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 velocity
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
glm_vec3_copy(velocity, phys->velocity);
|
||||
}
|
||||
|
||||
void entityPhysicsApplyImpulse(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 impulse
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
|
||||
if(phys->type == PHYSICS_BODY_STATIC) return;
|
||||
glm_vec3_add(phys->velocity, impulse, phys->velocity);
|
||||
}
|
||||
|
||||
bool_t entityPhysicsIsOnGround(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
return phys->onGround;
|
||||
}
|
||||
|
||||
void entityPhysicsSetBodyType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsbodytype_t type
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
phys->type = type;
|
||||
}
|
||||
|
||||
physicsbodytype_t entityPhysicsGetBodyType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(entityId, componentId);
|
||||
assertNotNull(phys, "Failed to get physics component data");
|
||||
return phys->type;
|
||||
}
|
||||
|
||||
void entityPhysicsDispose(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
#include "physics/physicsshape.h"
|
||||
#include "physics/physicsbodytype.h"
|
||||
|
||||
typedef struct {
|
||||
physicsbodytype_t type;
|
||||
physicsshape_t shape;
|
||||
vec3 velocity;
|
||||
float_t gravityScale;
|
||||
bool_t onGround;
|
||||
} entityphysics_t;
|
||||
|
||||
/**
|
||||
* Initializes the physics component: allocates a body in PHYSICS_WORLD.
|
||||
* Asserts if the world body limit is reached.
|
||||
*/
|
||||
void entityPhysicsInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the underlying physics structure (temporarily) for the given entity.
|
||||
* This is really just intended for doing operations faster than using the
|
||||
* getters and setters, but it is preferred that you use those.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The physics component data for the given entity and component ID.
|
||||
*/
|
||||
entityphysics_t *entityPhysicsGet(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the shape of the entity's physics body. This will not reset the body
|
||||
* state, so if you change from a cube to a sphere, it will keep the same
|
||||
* velocity and onGround state.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param shape The new shape to set on the physics body.
|
||||
*/
|
||||
void entityPhysicsSetShape(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsshape_t shape
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the shape of the entity's physics body.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The shape of the physics body.
|
||||
*/
|
||||
physicsshape_t entityPhysicsGetShape(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the velocity of the entity's physics body.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param dest The destination vec3 to write the velocity to.
|
||||
*/
|
||||
void entityPhysicsGetVelocity(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the velocity of the entity's physics body. This is not an impulse, so
|
||||
* it will be affected by mass and drag.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param velocity The new velocity to set on the physics body.
|
||||
*/
|
||||
void entityPhysicsSetVelocity(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 velocity
|
||||
);
|
||||
|
||||
/**
|
||||
* Applies an impulse to the entity's physics body. This is an immediate
|
||||
* velocity change that is not affected by mass or drag. No-op on STATIC bodies.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param impulse The impulse to apply to the physics body.
|
||||
*/
|
||||
void entityPhysicsApplyImpulse(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 impulse
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the entity's physics body rested on a surface during the last
|
||||
* step or move.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return True if the body is on the ground, false otherwise.
|
||||
*/
|
||||
bool_t entityPhysicsIsOnGround(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the body type of the entity's physics body.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The body type to set.
|
||||
*/
|
||||
void entityPhysicsSetBodyType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsbodytype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the body type of the entity's physics body.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The body type of the physics body.
|
||||
*/
|
||||
physicsbodytype_t entityPhysicsGetBodyType(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Releases the body slot back to PHYSICS_WORLD. Called automatically when
|
||||
* the component is disposed via the component system.
|
||||
*/
|
||||
void entityPhysicsDispose(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
+2
-4
@@ -3,7 +3,5 @@
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
sceneoverworld.c
|
||||
)
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
event.c
|
||||
entitytrigger.c
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entity/entitymanager.h"
|
||||
|
||||
void entityTriggerInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entitytrigger_t *t = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
glm_vec3_zero(t->min);
|
||||
glm_vec3_zero(t->max);
|
||||
}
|
||||
|
||||
entitytrigger_t * entityTriggerGet(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
return componentGetData(entityId, componentId, COMPONENT_TYPE_TRIGGER);
|
||||
}
|
||||
|
||||
bool_t entityTriggerContains(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const vec3 point
|
||||
) {
|
||||
entitytrigger_t *t = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
return (
|
||||
point[0] >= t->min[0] && point[0] <= t->max[0] &&
|
||||
point[1] >= t->min[1] && point[1] <= t->max[1] &&
|
||||
point[2] >= t->min[2] && point[2] <= t->max[2]
|
||||
);
|
||||
}
|
||||
|
||||
void entityTriggerSetBounds(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const vec3 min,
|
||||
const vec3 max
|
||||
) {
|
||||
entitytrigger_t *t = componentGetData(
|
||||
entityId, componentId, COMPONENT_TYPE_TRIGGER
|
||||
);
|
||||
glm_vec3_copy((float_t*)min, t->min);
|
||||
glm_vec3_copy((float_t*)max, t->max);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entity/entitybase.h"
|
||||
|
||||
typedef struct {
|
||||
vec3 min;
|
||||
vec3 max;
|
||||
} entitytrigger_t;
|
||||
|
||||
/**
|
||||
* Initializes the trigger component with zeroed bounds.
|
||||
*/
|
||||
void entityTriggerInit(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a pointer to the trigger component data.
|
||||
*/
|
||||
entitytrigger_t * entityTriggerGet(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the given world-space point lies within [min, max].
|
||||
*/
|
||||
bool_t entityTriggerContains(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const vec3 point
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets both bounds at once.
|
||||
*/
|
||||
void entityTriggerSetBounds(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const vec3 min,
|
||||
const vec3 max
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "entity/component/display/entitycamera.h"
|
||||
#include "entity/component/display/entityrenderable.h"
|
||||
#include "entity/component/physics/entityphysics.h"
|
||||
#include "entity/component/trigger/entitytrigger.h"
|
||||
|
||||
// Name (Uppercase)
|
||||
// Structure
|
||||
// Field name (lowercase)
|
||||
// Init function (optional)
|
||||
// Dispose function (optional)
|
||||
// FixedUpdate function (optional) - called once per fixed timestep
|
||||
|
||||
X(POSITION, entityposition_t, position, entityPositionInit, NULL, NULL)
|
||||
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL, NULL)
|
||||
X(RENDERABLE, entityrenderable_t, renderable, entityRenderableInit, entityRenderableDispose, NULL)
|
||||
X(PHYSICS, entityphysics_t, physics, entityPhysicsInit, entityPhysicsDispose, NULL)
|
||||
X(TRIGGER, entitytrigger_t, trigger, entityTriggerInit, NULL, NULL)
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitymanager.h"
|
||||
#include "component/display/entityposition.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityInit(const entityid_t entityId) {
|
||||
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
||||
|
||||
memoryZero(ent, sizeof(entity_t));
|
||||
|
||||
// Mark all component types not using this entity.
|
||||
for(
|
||||
componenttype_t compType = 0;
|
||||
compType < COMPONENT_TYPE_COUNT;
|
||||
compType++
|
||||
) {
|
||||
ENTITY_MANAGER.entitiesWithComponent[
|
||||
compType * ENTITY_COUNT_MAX + entityId
|
||||
] = COMPONENT_ID_INVALID;
|
||||
}
|
||||
|
||||
ent->state |= ENTITY_STATE_ACTIVE;
|
||||
}
|
||||
|
||||
componentid_t entityAddComponent(
|
||||
const entityid_t entityId,
|
||||
const componenttype_t type
|
||||
) {
|
||||
componentindex_t compInd;
|
||||
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
||||
|
||||
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||
compInd = componentGetIndex(entityId, i);
|
||||
if(ENTITY_MANAGER.components[compInd].type != COMPONENT_TYPE_NULL) {
|
||||
assertTrue(
|
||||
ENTITY_MANAGER.components[compInd].type != type,
|
||||
"Entity already has component of this type"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
componentInit(entityId, i, type);
|
||||
ENTITY_MANAGER.entitiesWithComponent[
|
||||
type * ENTITY_COUNT_MAX + entityId
|
||||
] = i;
|
||||
return i;
|
||||
}
|
||||
|
||||
assertUnreachable("Entity has no more component slots available");
|
||||
return COMPONENT_ID_INVALID;
|
||||
}
|
||||
|
||||
componentid_t entityGetComponent(
|
||||
const entityid_t entityId,
|
||||
const componenttype_t type
|
||||
) {
|
||||
componentid_t compId = ENTITY_MANAGER.entitiesWithComponent[
|
||||
type * ENTITY_COUNT_MAX + entityId
|
||||
];
|
||||
if(compId == COMPONENT_ID_INVALID) return compId;
|
||||
assertTrue(
|
||||
ENTITY_MANAGER.components[componentGetIndex(entityId, compId)].type == type,
|
||||
"Component type mismatch"
|
||||
);
|
||||
return compId;
|
||||
}
|
||||
|
||||
void entityDisposeDeep(const entityid_t entityId) {
|
||||
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
|
||||
if(posComp != COMPONENT_ID_INVALID) {
|
||||
entityPositionDisposeDeep(entityId, posComp);
|
||||
} else {
|
||||
entityDispose(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
void entityDispose(const entityid_t entityId) {
|
||||
componentindex_t compInd;
|
||||
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
||||
|
||||
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||
compInd = componentGetIndex(entityId, i);
|
||||
componenttype_t type = ENTITY_MANAGER.components[compInd].type;
|
||||
if(type == COMPONENT_TYPE_NULL) continue;
|
||||
ENTITY_MANAGER.entitiesWithComponent[
|
||||
type * ENTITY_COUNT_MAX + entityId
|
||||
] = COMPONENT_ID_INVALID;
|
||||
componentDispose(entityId, i);
|
||||
}
|
||||
|
||||
ent->state = 0;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "component.h"
|
||||
|
||||
#define ENTITY_STATE_ACTIVE (1 << 0)
|
||||
|
||||
typedef struct {
|
||||
uint8_t state;
|
||||
} entity_t;
|
||||
|
||||
/**
|
||||
* Initializes an entity with the given ID.
|
||||
*
|
||||
* @param entityId The ID of the entity to initialize.
|
||||
*/
|
||||
void entityInit(const entityid_t entityId);
|
||||
|
||||
/**
|
||||
* Adds a component of the given type to the entity with the given ID.
|
||||
*
|
||||
* @param entityId The ID of the entity to add the component to.
|
||||
* @param type The type of the component to add.
|
||||
* @return The ID of the entity with component.
|
||||
*/
|
||||
componentid_t entityAddComponent(
|
||||
const entityid_t entityId,
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the ID of the component of the given type on the entity with the given
|
||||
* ID, or COMPONENT_ID_INVALID if the entity lacks the component.
|
||||
*
|
||||
* @param entityId The ID of the entity to get the component from.
|
||||
* @param type The type of the component to get.
|
||||
* @return The ID of the component.
|
||||
*/
|
||||
componentid_t entityGetComponent(
|
||||
const entityid_t entityId,
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of an entity with the given ID.
|
||||
*
|
||||
* @param entityId The ID of the entity to dispose of.
|
||||
*/
|
||||
void entityDispose(const entityid_t entityId);
|
||||
|
||||
/**
|
||||
* Disposes of an entity and all of its position-component descendants
|
||||
* recursively. If the entity has no position component, behaves like
|
||||
* entityDispose.
|
||||
*
|
||||
* @param entityId The root entity ID.
|
||||
*/
|
||||
void entityDisposeDeep(const entityid_t entityId);
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
#define ENTITY_COUNT_MAX 20
|
||||
#define ENTITY_COMPONENT_COUNT_MAX 8
|
||||
|
||||
#define ENTITY_ID_INVALID 0xFF
|
||||
#define COMPONENT_ID_INVALID 0xFF
|
||||
|
||||
typedef uint8_t entityid_t;
|
||||
typedef uint8_t componentid_t;
|
||||
typedef uint16_t componentindex_t;
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitymanager.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "console/console.h"
|
||||
|
||||
entitymanager_t ENTITY_MANAGER;
|
||||
|
||||
void entityManagerInit(void) {
|
||||
memoryZero(&ENTITY_MANAGER, sizeof(entitymanager_t));
|
||||
memorySet(
|
||||
ENTITY_MANAGER.entitiesWithComponent, COMPONENT_ID_INVALID,
|
||||
sizeof(entityid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX
|
||||
);
|
||||
|
||||
consolePrint(
|
||||
"Entity Manager size: %zu bytes (%.2f KB)",
|
||||
sizeof(entitymanager_t),
|
||||
sizeof(entitymanager_t) / 1024.0f
|
||||
);
|
||||
}
|
||||
|
||||
entityid_t entityManagerAdd() {
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) != 0) continue;
|
||||
entityInit(i);
|
||||
return i;
|
||||
}
|
||||
assertUnreachable("No more entity IDs available");
|
||||
return ENTITY_ID_INVALID;
|
||||
}
|
||||
|
||||
void entityManagerFixedUpdate(void) {
|
||||
componentFixedUpdateAll();
|
||||
}
|
||||
|
||||
void entityManagerDispose(void) {
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
if((ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) == 0) continue;
|
||||
entityDispose(i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entity.h"
|
||||
|
||||
typedef struct {
|
||||
entity_t entities[ENTITY_COUNT_MAX];
|
||||
component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX];
|
||||
componentid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX];
|
||||
} entitymanager_t;
|
||||
|
||||
extern entitymanager_t ENTITY_MANAGER;
|
||||
|
||||
/**
|
||||
* Initializes the entity manager.
|
||||
*/
|
||||
void entityManagerInit(void);
|
||||
|
||||
/**
|
||||
* Adds / Reserves a new entity ID.
|
||||
*
|
||||
* @return The new entity ID.
|
||||
*/
|
||||
entityid_t entityManagerAdd();
|
||||
|
||||
/**
|
||||
* Runs fixedUpdate on every active component that defines one. Should be
|
||||
* called once per fixed timestep (see time.h's DUSK_TIME_DYNAMIC/
|
||||
* TIME.dynamicUpdate) - i.e. every frame on platforms without dynamic
|
||||
* timing, or only on non-dynamic frames on platforms with it.
|
||||
*/
|
||||
void entityManagerFixedUpdate(void);
|
||||
|
||||
/**
|
||||
* Disposes of the entity manager, in turn freeing all entities and components.
|
||||
*/
|
||||
void entityManagerDispose(void);
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entityrender.h"
|
||||
#include "entity.h"
|
||||
#include "entitymanager.h"
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "entity/component/display/entitycamera.h"
|
||||
#include "entity/component/display/entityrenderable.h"
|
||||
#include "display/display.h"
|
||||
#include "display/displaystate.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "display/mesh/mesh.h"
|
||||
|
||||
errorret_t entityRenderAll(void) {
|
||||
entityid_t camId = entityCameraGetCurrent();
|
||||
if(camId == ENTITY_COUNT_MAX) errorOk();
|
||||
|
||||
componentid_t camPosComp = entityGetComponent(camId, COMPONENT_TYPE_POSITION);
|
||||
componentid_t camCompId = entityGetComponent(camId, COMPONENT_TYPE_CAMERA);
|
||||
if(camPosComp == COMPONENT_ID_INVALID) errorOk();
|
||||
|
||||
mat4 view, proj;
|
||||
entityPositionGetTransform(camId, camPosComp, view);
|
||||
entityCameraGetProjection(camId, camCompId, proj);
|
||||
|
||||
errorChain(shaderBind(&SHADER_UNLIT));
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
|
||||
|
||||
errorChain(displaySetState((displaystate_t){
|
||||
.flags = DISPLAY_STATE_FLAG_DEPTH_TEST | DISPLAY_STATE_FLAG_CULL
|
||||
}));
|
||||
|
||||
entityid_t rendEnts[ENTITY_COUNT_MAX];
|
||||
componentid_t rendComps[ENTITY_COUNT_MAX];
|
||||
entityid_t count = componentGetEntitiesWithComponent(
|
||||
COMPONENT_TYPE_RENDERABLE, rendEnts, rendComps
|
||||
);
|
||||
|
||||
for(entityid_t i = 0; i < count; i++) {
|
||||
entityrenderable_t *r = (entityrenderable_t *)componentGetData(
|
||||
rendEnts[i], rendComps[i], COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
if(r->type != ENTITY_RENDERABLE_TYPE_MATERIAL) continue;
|
||||
if(r->mesh == NULL) continue;
|
||||
|
||||
mat4 model;
|
||||
componentid_t posComp = entityGetComponent(rendEnts[i], COMPONENT_TYPE_POSITION);
|
||||
if(posComp != COMPONENT_ID_INVALID) {
|
||||
entityPositionGetTransform(rendEnts[i], posComp, model);
|
||||
} else {
|
||||
glm_mat4_identity(model);
|
||||
}
|
||||
|
||||
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
|
||||
errorChain(shaderSetMaterial(&SHADER_UNLIT, &r->material));
|
||||
errorChain(meshDraw(r->mesh, 0, -1));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
/**
|
||||
* Draws every entity with a RENDERABLE component, from the perspective of
|
||||
* whichever entity currently holds the active CAMERA component. A no-op if
|
||||
* no camera is active. Entities and their components are created and
|
||||
* managed entirely from script - this is the native side of that contract.
|
||||
*
|
||||
* @return An error if rendering failed, or errorOk() if it succeeded.
|
||||
*/
|
||||
errorret_t entityRenderAll(void);
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "event.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void eventInit(
|
||||
event_t *event,
|
||||
eventcallback_t *callbacks,
|
||||
void **users,
|
||||
size_t size
|
||||
) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
assertNotNull((void *)callbacks, "callbacks must not be NULL");
|
||||
assertTrue(size > 0, "size must be greater than 0");
|
||||
|
||||
event->callbacks = callbacks;
|
||||
event->users = users;
|
||||
event->size = size;
|
||||
event->count = 0;
|
||||
memoryZero(callbacks, sizeof(eventcallback_t) * size);
|
||||
if(users) memoryZero(users, sizeof(void *) * size);
|
||||
}
|
||||
|
||||
void eventSubscribe(event_t *event, eventcallback_t callback, void *user) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
assertNotNull(callback, "callback must not be NULL");
|
||||
|
||||
// Ensure callback isn't already susbcribed
|
||||
for(uint32_t i = 0; i < event->count; i++) {
|
||||
if(event->callbacks[i] != callback) continue;
|
||||
assertUnreachable("Callback already registered, cannot subscribe twice.");
|
||||
}
|
||||
|
||||
assertTrue(event->count < event->size, "event subscriber capacity exceeded");
|
||||
|
||||
event->callbacks[event->count] = callback;
|
||||
if(user) {
|
||||
assertNotNull(event->users, "Cannot add user pointer.");
|
||||
event->users[event->count] = user;
|
||||
}
|
||||
event->count++;
|
||||
}
|
||||
|
||||
void eventUnsubscribe(event_t *event, eventcallback_t callback) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
assertNotNull(callback, "callback must not be NULL");
|
||||
|
||||
for(uint32_t i = 0; i < event->count; i++) {
|
||||
if(event->callbacks[i] != callback) continue;
|
||||
|
||||
uint32_t last = event->count - 1;
|
||||
if(i != last) {
|
||||
event->callbacks[i] = event->callbacks[last];
|
||||
if(event->users) event->users[i] = event->users[last];
|
||||
}
|
||||
event->callbacks[last] = NULL;
|
||||
if(event->users) event->users[last] = NULL;
|
||||
event->count--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void eventInvoke(const event_t *event, void *params) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
|
||||
for(uint32_t i = 0; i < event->count; i++) {
|
||||
void *u = event->users ? event->users[i] : NULL;
|
||||
event->callbacks[i](params, u);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +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"
|
||||
|
||||
typedef void (*eventcallback_t)(void *params, void *user);
|
||||
|
||||
typedef struct {
|
||||
eventcallback_t *callbacks;
|
||||
void **users;
|
||||
size_t size;
|
||||
uint32_t count;
|
||||
} event_t;
|
||||
|
||||
/**
|
||||
* Initializes an event, binding it to the provided backing arrays and clearing
|
||||
* all subscribers. May also be called to reset an event (re-clears subscribers
|
||||
* without changing the backing arrays or size).
|
||||
*
|
||||
* @param event The event to initialize.
|
||||
* @param callbacks Caller-owned array of at least `size` callback slots.
|
||||
* @param users Array of user pointers, matching each callback, or NULL.
|
||||
* @param size Capacity of both arrays, must match.
|
||||
*/
|
||||
void eventInit(
|
||||
event_t *event,
|
||||
eventcallback_t *callbacks,
|
||||
void **users,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscribes a callback to an event. The callback is invoked with params and
|
||||
* the provided user pointer each time the event fires. The same (callback,
|
||||
* user) pair may only be subscribed once.
|
||||
*
|
||||
* @param event The event to subscribe to.
|
||||
* @param callback The function to call when the event fires.
|
||||
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||
*/
|
||||
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
|
||||
|
||||
/**
|
||||
* Removes a previously subscribed (callback, user) pair. Does nothing if the
|
||||
* pair is not currently subscribed.
|
||||
*
|
||||
* @param event The event to unsubscribe from.
|
||||
* @param callback The callback that was passed to eventSubscribe.
|
||||
*/
|
||||
void eventUnsubscribe(event_t *event, eventcallback_t callback);
|
||||
|
||||
/**
|
||||
* Invokes all subscribed callbacks, passing params and each subscriber's user
|
||||
* pointer.
|
||||
*
|
||||
* @param event The event to invoke.
|
||||
* @param params Arbitrary pointer forwarded to every callback unchanged.
|
||||
*/
|
||||
void eventInvoke(const event_t *event, void *params);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user