54 Commits

Author SHA1 Message Date
YourWishes b639bc6c4f Reimplement the archived UI widget stack on the new element-tree system
The old widget system (archive/dusk/ui/) depended on infrastructure this
rewrite deleted -- a static X-macro element list, a 9-slice UI_FRAME, and
a global gamepad/keyboard focus stack -- so everything below is rebuilt
on top of the new native uielement_t pool + parent/child tree instead,
gamepad/keyboard-only (no mouse support exists in the input system).

New JS widgets (assets/scripts/), all pure composites of Label/Rectangle
following the Button.js pattern: Checkbox, Tab, Slider, Dropdown. Slider/
Dropdown expose a directionInput(dx, dy) hook so a menu can hand them
LEFT/RIGHT before falling back to cursor movement.

New Menu.js replaces the old global uifocus_t stack: a small push/pop
stack of Menu instances where only the topmost consumes CANCEL/ACCEPT/
direction input each tick (needed for nested modals -- a Settings
sub-page menu plus a "discard changes?" Confirm can be open at once).
Held-direction repeat timing matches the old 0.5s delay / 0.1s repeat.

New overlay composites (added via a new UI.addOverlay()/UI.removeOverlay(),
always drawn/updated after normal roots): FpsCounter, ConsoleOverlay
(one Label per console history line, driven directly rather than via
add() since History (16) exceeds the 8-children-per-element cap), Crop
(letterbox/pillarbox bars), and Fullbox (a reusable tweened full-screen
fade covering both the old fullbox and transition effects -- reuses
shared instances rather than allocate-and-dispose, since disposing an
element from inside its own render() callback risks the same JerryScript
refcount corruption hit earlier in this rewrite).

New Confirm.js (Yes/No dialog) and Settings.js (+ SettingsGeneral/
SettingsInput sub-pages) built on Menu. Display/Audio settings pages are
intentionally not ported -- neither had a real backing engine system
even in the old code. Frame is a flat Rectangle for now, not real 9-slice
(a genuinely bigger native lift, deliberately deferred).

Small native binding additions needed by the above, each with its own
unit test: Time.renderDelta, Console.lineCount/getLine/visible/
consumeDirty, a new Screen module, a new Locale module, Input.deadzone,
a new Save module, and Label.color (was missing entirely). Also fixes
a real gap from the UI.addOverlay() work: uiElementDispose() wasn't
removing disposed elements from the new overlay root array, which would
have left dangling ids behind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 09:05:11 -05:00
YourWishes 7ee04c78cd Rebuild UI as a scriptable element tree, archive the old system
The old UI system (X-macro static element list, hand-authored C
screens/widgets/focus stack) is archived under archive/ rather than
deleted, since it's a useful reference during the rewrite.

New native UI element pool (src/dusk/ui): a flat UI_ELEMENTS[128] pool
of tagged-union elements (Label, Rectangle, Scripted), a real
persistent parent/child tree (children[8] per element, cycle- and
capacity-checked uiElementSetParent), always-fresh worldX/worldY
(cheap enough to recompute on every read, no dirty-flag cache needed),
and cascading dispose. Rendering stays manual/immediate: a scripted
element with no render() override auto-renders its children by
default, but overriding render() takes full control (an override must
call renderChildren() itself to opt back in) -- this is deliberately
preserved end to end via a render()-shadow trampoline so overriding
render() always keeps working the same way regardless of how a node
is reached.

New scripting layer (src/dusk/script/module/ui): UIElement/Label/
Rectangle JS classes (Label/Rectangle share UIElement's prototype via
manual chaining, not JS `extends`), exposing x/y/worldX/worldY/parent/
add()/remove()/render()/renderChildren()/dispose(). UI.add()/
UI.remove() manage top-level render roots, mutually exclusive with
being someone's child.

Also: Scene gains a lateUpdate() hook (called once per frame after
every other update, for things like camera-follow that need to react
to where everything else ended up); several duskrpg call sites
(cutscene items, entityinteractable, entityplayer) that depended on
the now-archived RPG textbox are stubbed to console output instead of
a dialogue box, pending the new UI reaching that far.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:13:23 -05:00
YourWishes 7b58addf7e Prepping UI overhaul 2026-08-11 23:33:29 -05:00
YourWishes ae52be591b Add Input/InputBind/InputButton JS bindings, move default binds to JS
Adds a new script module exposing Input.bind/value/lastValue/isDown/
wasDown/pressed/released, InputBind.* (the inputbind_t action enum),
and InputButton.* (every named button in the compiled platform's
INPUT_BUTTON_DATA table, e.g. InputButton.SELECT on PSP).

Renames the PLATFORM global to Platform.current for consistency with
Time/Console/Input.

Uppercases every INPUT_BUTTON_DATA .name across inputlinux.c/
inputpsp.c/inputdolphin.c to match the InputButton.* JS constants
(inputButtonGetByName was already case-insensitive, so this doesn't
change matching behavior).

Moves the default key/button-to-action bindings out of
src/duskrpg/input/inputbindmap.h (deleted) and into
assets/scripts/input.js, branching on Platform.current instead of the
old per-platform #ifdefs. Wired in via require('./input.js') from
init.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 16:03:32 -05:00
YourWishes 93ab7690ba Add asset/event test coverage, fix caching-breaking eventSubscribe bug
Adds missing test coverage for the asset pipeline's binary loaders
(mesh/model/texture), assetfile.c, and assetbatch.c, plus a new
test/event suite for the shared event primitive.

Found and fixed two real bugs while writing this:
- assetFileRead's NULL-buffer skip path double-counted file->position,
  which could trip stb_image's EOF check early on images that skip
  bytes mid-decode.
- eventSubscribe/eventUnsubscribe matched only on the callback pointer
  instead of the (callback, user) pair the docs already promised, so
  two independent consumers of the same cached asset (e.g. two
  assetbatch_t's) would abort. Covered by dedicated caching tests in
  both test_assetbatch.c and test_assetmodelloader.c.

Also drops test_overworldscene.c, broken by the in-progress
overworldscene.js/init.js export-contract rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 14:34:51 -05:00
YourWishes 68c3f88181 Build on PSP 2026-08-11 13:11:22 -05:00
YourWishes 830864aa8a Gating network behind compile flag 2026-08-11 12:52:38 -05:00
YourWishes 5f34cb34b2 Add a thin declarative API for defining script methods/properties
scriptvalue.h/.c holds the scriptvalue_t tagged-union type and its
JS<->C decode/encode helpers; scriptdef.h/.c holds the declarative
scriptfuncdef_t/scriptpropdef_t definitions, the binding registry,
and the JerryScript trampolines. Lets a module declare its JS-facing
methods/properties/globals as typed data instead of hand-writing
argument-checking boilerplate per method. Framework only for now --
no existing module has been migrated onto it yet.
2026-08-03 19:36:08 -05:00
YourWishes b9d2fe60fd Split overworldscene.js's player/plane/camera into their own classes
Extracts Player (extends Entity), TestPlane, and PlayerCamera into
their own files; the camera now tracks the player's live position
at a fixed offset instead of orbiting the world origin over time.
Updates test_overworldscene.c to match the new entity order and to
build an in-memory zip fixture, since overworldscene.js now
require()s these sibling files and require() always resolves
through ASSET.zip.
2026-08-03 19:35:54 -05:00
YourWishes 06bc4fcd55 .MD remove 2026-08-03 14:35:30 -05:00
YourWishes 42cb84b610 Extract player entity setup into a Player class
Moves the capsule/physics/renderable/PLAYER setup out of
overworldscene.js's inline init() into assets/scripts/Player.js so the
scene module just instantiates it.
2026-08-02 21:06:31 -05:00
YourWishes f8607d114c Add UDP socket client/server multiplayer protocol
Implements the multiplayer transport layer from ROADMAP.md: UUID-based
client identity, a reliable-packet channel over UDP, handshake/ack/
ping/disconnect/player-joined/player-left/player-state packet types,
and split online (networked) vs offline (in-process singleplayer)
server modes wired into the engine's update/dispose loop.
2026-08-02 21:06:23 -05:00
YourWishes 56230dd340 Fix ref-count underflow, PSP timezone units, thread restart race, color codegen
- util/ref.c: refUnlock's assert(count >= 0) on an unsigned count was
  tautological, so a double-unlock silently underflowed to UINT32_MAX
  instead of asserting. Now asserts count > 0 before decrementing.
- duskpsp/time/timepsp.c: timeGetRealTimeZonePSP returned hours while
  every other platform (and timeepoch.c's math) expects seconds.
- thread.c: threadHandler reset threadId outside the mutex, after
  signaling STOPPED, letting a caller's immediate threadStart() race
  threadStartRequest()'s "thread id not 0" assert. threadId is now
  reset inside the same locked section.
- tools/color.py: whole-number CSV channels (0, 1) produced invalid C
  float literals (0f, 1f) for the generated COLOR_*_3F/_4F macros.
  Route through float() so they always stringify with a decimal point.

Also adds .claude/code-check.md: a full review pass over every
subsystem in src/dusk/, with the above (plus several other findings
not yet acted on) written up in detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 15:02:17 -05:00
YourWishes df9fdf26c8 Refresh STATUS.md/ROADMAP.md for the Scene.set()/UI caching/CI work
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:53:06 -05:00
YourWishes 8f8fa8f8d1 Scene.set() JS lifecycle, UI sprite caching, emulator test scripts
- Add Scene.set(module) so JerryScript can switch scenes via
  require()'d {init, update, dispose} modules; wire it into
  engineUpdate() and rework overworldscene.js/init.js to the new shape.
- Fix scriptManagerExecFile() never pushing its own file's directory
  onto the require() dir stack, breaking relative require() calls from
  a top-level entry script (e.g. init.js -> ./overworldscene.js).
- Cache sprite geometry across frames for uislider/uitab/uiframe
  (dialogs/textbox/settings) and give uitextbox a per-page glyph cache
  instead of one spriteBatchBuffer call per visible character. uiconsole
  drops its alloc/free vertex buffer for a fixed-size one. uifps skips
  rebuilding its label when the text hasn't changed.
- Add PSP_OPTIMIZATION_PLAN.md and a handful of UI/spritebatch unit
  tests covering the new caching logic.
- Add Dolphin/PPSSPP emulator smoke-test scripts (+ Docker variants and
  CI jobs) for GameCube/Wii/PSP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 13:21:57 -05:00
YourWishes 07f98c119a Script stuff 2026-07-31 09:40:53 -05:00
YourWishes d49194fc4d Embed font into binary 2026-07-30 15:18:07 -05:00
YourWishes e61914bad4 Finish JerryScript Entity/Scene wiring; remove JSON serialize/deserialize
Register Entity, the generic Component wrapper, and Scene as JerryScript
classes (modulelist.c ties them together, plus their .d.ts stubs), so
scenes/entities can be built from script going forward instead of the
JSON scene format.

With scripting now the intended authoring path, drop the JSON
serialize/deserialize machinery entirely: componentdefinition_t's
serialize/deserialize callbacks, every component's *Serialize/
*Deserialize function, entitySerialize/entityDeserialize,
sceneSerialize/sceneDeserialize, and the "prefabs/<name>.json"/
"scenes/<name>.json" asset fallback in entityPrefabResolveAndApply/
scenePrefabResolveAndApply (C-coded prefabs only now). physicsshape.c
and its test are removed outright since they only existed for this.
game.c's test scene now comes from the existing overworldSceneCreate()
C builder instead of loading the now-deleted assets/scenes/test.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 11:19:08 -05:00
YourWishes 015d90519f WIP: start JerryScript integration (core engine + Component wrapper)
Ports the core scripting engine (scriptmanager, scriptproto, modulebase,
moduleplatform) and a generic Component JS wrapper from the old we-ball
branch, adapted to the current entitymanager_t/multi-scene architecture.

Not yet buildable: moduleentity.c, modulescene, modulelist.h/.c, and the
CMakeLists.txt for the new module subdirectories are still missing, and
engine.c isn't wired up yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 09:06:34 -05:00
YourWishes 7b4f0abfb8 Cutscene progress 2026-07-24 15:57:13 -05:00
YourWishes 0667a44e14 Restored cutscene code 2026-07-23 09:15:47 -05:00
YourWishes 0852b8463b ANIM 2026-07-21 18:55:23 -05:00
YourWishes 1436fda858 Some animation work 2026-07-21 09:29:14 -05:00
YourWishes 0826908068 First round of animation improvements 2026-07-20 16:07:38 -05:00
YourWishes 7ceb9e571d Scene prefabs 2026-07-20 15:11:38 -05:00
YourWishes 38c2f6b6f9 Add prefab support 2026-07-20 14:44:14 -05:00
YourWishes b9f06fef7a Add area and flag on physics 2026-07-20 12:31:14 -05:00
YourWishes 373f1c1010 test 2026-07-19 22:12:53 -05:00
YourWishes e80d693d85 More ui optimization 2026-07-19 17:39:38 -05:00
YourWishes ef60ddf6a8 UI Optim 2026-07-19 17:31:29 -05:00
YourWishes de0ff2e263 Fix collide issue on PSP 2026-07-19 14:51:57 -05:00
YourWishes 13a435e9c1 Test 2026-07-19 14:05:46 -05:00
YourWishes 725338cd5a Game code start 2026-07-19 11:49:13 -05:00
YourWishes acd14f46a8 Optimize PSP slightly 2026-07-19 11:15:22 -05:00
YourWishes ef78f023ed yeah whatever 2026-07-18 23:07:45 -05:00
YourWishes 85b6109792 Fixed save crash 2026-07-18 21:17:24 -05:00
YourWishes 6c5738c863 Testing physics 2026-07-18 20:53:22 -05:00
YourWishes 1174e471f6 Time fixes 2026-07-18 20:31:42 -05:00
YourWishes a618ff46fe Physics 2026-07-18 20:01:44 -05:00
YourWishes 1fa5cd316e Restore ECS 2026-07-18 19:05:51 -05:00
YourWishes 11e5d37563 Cleanup more. 2026-07-18 17:38:25 -05:00
YourWishes 9f86c20129 remove rpg 2026-07-18 17:15:45 -05:00
YourWishes 8fb1c9fb42 physics and rendering fixes 2026-07-17 21:06:21 -05:00
YourWishes 5f08337726 physics stuff 2026-07-17 19:07:32 -05:00
YourWishes 0bc80d5df3 Roadmap and physics 2026-07-17 13:09:58 -05:00
YourWishes 2432443ea6 Cleaned settings a bit 2026-07-17 09:11:19 -05:00
YourWishes fa138971e7 Console optimized slightly for rendering 2026-07-16 23:52:47 -05:00
YourWishes d7982599d1 Simplified PSP network 2026-07-16 22:55:47 -05:00
YourWishes f16c80aa0f Fixed PSP rendering 2026-07-16 21:20:49 -05:00
YourWishes ef5adf8274 PSP Network connect 2026-07-16 20:09:39 -05:00
YourWishes 2ffc65b1b1 Network conn and disconn 2026-07-16 19:32:44 -05:00
YourWishes d4a17bd98d Net 2026-07-16 19:08:32 -05:00
YourWishes dcf5f434c5 Remove vita for now 2026-07-16 15:50:19 -05:00
YourWishes 78cd973a33 Remove story and battle stuff 2026-07-16 15:37:55 -05:00
883 changed files with 35013 additions and 14552 deletions
+71
View File
@@ -33,3 +33,74 @@ jobs:
libssl-dev
- name: Run tests
run: ./scripts/test-linux.sh
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
# confirm the emulator doesn't crash. Not yet verified against a real
# runner (no CI run has exercised these) -- continue-on-error so a
# flaky/broken emulator step doesn't block the required Linux test job.
run-tests-gamecube-dolphin:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ghcr.io/extremscorner/libogc2:latest
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install additional dependencies
run: |
apt-get install -y \
python3-pip python3-polib python3-pil \
python3-dotenv python3-pyqt5 python3-opengl xorriso \
dolphin-emu xvfb
dkp-pacman -Syu --noconfirm
dkp-pacman -S --needed --noconfirm \
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build GameCube ISO and boot it in Dolphin
run: ./scripts/test-gamecube-dolphin.sh
run-tests-wii-dolphin:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ghcr.io/extremscorner/libogc2:latest
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install additional dependencies
run: |
apt-get install -y \
python3-pip python3-polib python3-pil \
python3-dotenv python3-pyqt5 python3-opengl xorriso \
dolphin-emu xvfb
dkp-pacman -Syu --noconfirm
dkp-pacman -S --needed --noconfirm \
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build Wii ISO and boot it in Dolphin
run: ./scripts/test-wii-dolphin.sh
run-tests-psp-ppsspp:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pspdev
uses: ./.github/actions/setup-pspdev
- name: Install PPSSPPHeadless build dependencies
run: |
sudo apt-get update
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
- name: Build PPSSPPHeadless
run: |
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
run: ./scripts/test-psp-ppsspp.sh
-455
View File
@@ -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+0000U+007F).
Non-ASCII characters are banned even in comments and string literals.
Use ASCII-only substitutes instead:
- `--` or `-` instead of `` (em dash)
- `->` instead of `` (arrow)
- `x` or `*` instead of `×` (multiplication)
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
### Indentation
2 spaces. No tabs.
### Keyword and operator spacing
No space between a keyword or function name and its opening parenthesis:
```c
if(!ptr) return;
for(uint8_t i = 0; i < count; i++) {
while(entry->state != DONE) {
switch(type) {
sizeof(assetbatch_t)
memoryZero(ptr, size)
```
Spaces around all binary operators and after every comma:
```c
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
(size_t)end - (size_t)start
foo(a, b, c)
```
### Braces
Opening brace on the **same line** as the statement (K&R style) for all
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
```c
void assetEntryLock(assetentry_t *entry) {
...
}
if(dirty) {
...
} else {
...
}
```
### Guard returns
Short guards go on one line with no braces:
```c
if(!ptr) return;
if(!b || !b->batch) return jerry_undefined();
if(!(flags & DIRTY)) return;
```
### Blank lines
- One blank line between functions; no blank line at the start or end of
a function body.
- One blank line between logical blocks inside a function body.
- No trailing blank lines at the end of a file.
### Pointer placement
`*` is attached to the variable name, not the type:
```c
assetentry_t *entry
const char_t *name
void *ptr
uint8_t *d = (uint8_t *)dest;
```
### Casts
Space between cast and operand:
```c
(assetbatch_t *)user
(uint8_t *)dest
(textureformat_t)v
```
### Return
No parentheses around the return value:
```c
return ptr;
return MEMORY_POINTERS_IN_USE;
```
### switch / case
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
```c
switch(type) {
case ASSET_LOADER_TYPE_TEXTURE:
descs[i].input.texture = (textureformat_t)v;
break;
default:
break;
}
```
### Multi-line function signatures
When parameters don't fit on one line, put each on its own line indented
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
its own line at column 0:
```c
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
errorret_t memoryCompare(
const void *a,
const void *b,
const size_t size
);
```
### Structs and enums
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
brace and name on the same line:
```c
typedef struct {
errorcode_t code;
char_t *message;
} errorstate_t;
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
```
### Designated initialisers
Spaces inside braces; `.field = value`:
```c
jsassetentry_t e = { .entry = entry };
assetbatchloadedpend_t init = { .batch = batch };
```
### Ternary operator
Spaces around `?` and `:`:
```c
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
```
### const placement
`const` before the type, `*` attached to the variable:
```c
const char_t *name
const void *src
const size_t size
```
### Comments in `.c` files
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
functions follow one another with a single blank line between them.
- Multi-line explanatory comments inside function bodies use `//` lines:
```c
// Script modules are freed; orphaned JS wrapper objects now get GC'd
// so their finalizers fire before assetDispose() checks ref counts.
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
```
- Do not use `/* */` for inline or inline-block comments inside `.c`
function bodies.
### Comments in `.h` files
Every public declaration gets a Javadoc block (`/** … */`) with
`@param` and `@returns` where relevant. Keep it on the lines immediately
above the declaration with no blank line in between.
---
## 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`.
+12 -1
View File
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF)
option(DUSK_NETWORKING "Enable networking support" OFF)
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
DUSK_VERSION="${DUSK_VERSION}"
)
if(DUSK_NETWORKING)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_NETWORKING
)
endif()
# Toolchains
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
@@ -116,7 +123,11 @@ if(DUSK_BUILD_TESTS)
endif()
# Build assets
file(GLOB_RECURSE DUSK_ASSET_FILES CONFIGURE_DEPENDS "${DUSK_ASSETS_DIR}/*")
# Deliberately not CONFIGURE_DEPENDS: that reruns the full CMake configure
# step (which invalidates generated headers and forces a huge rebuild) on
# every single asset edit. Re-run cmake manually when assets are added or
# removed.
file(GLOB_RECURSE DUSK_ASSET_FILES "${DUSK_ASSETS_DIR}/*")
add_custom_command(
OUTPUT "${DUSK_ASSETS_ZIP}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
+18
View File
@@ -0,0 +1,18 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(debug)
add_subdirectory(frame)
add_subdirectory(focus)
add_subdirectory(overlay)
add_subdirectory(transition)
add_subdirectory(widget)
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
ui.c
uielement.c
)
@@ -7,5 +7,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uiconsole.c
uifps.c
uiplayerpos.c
)
+138
View File
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiconsole.h"
#include "assert/assert.h"
#include "console/console.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/spritebatch/spritebatch.h"
#include "display/shader/shaderunlit.h"
#include "display/mesh/mesh.h"
#include "util/memory.h"
typedef struct {
mesh_t mesh;
bool_t built;
meshvertex_t vertices[UI_CONSOLE_CACHE_VERTEX_MAX];
int32_t vertexCount;
int32_t cachedScanX;
int32_t cachedScanY;
} uiconsolecache_t;
uiconsolecache_t UI_CONSOLE_CACHE;
errorret_t uiConsoleDraw(void) {
if(!CONSOLE.visible) errorOk();
if(
CONSOLE.dirty ||
UI_CONSOLE_CACHE.cachedScanX != SCREEN.scanX ||
UI_CONSOLE_CACHE.cachedScanY != SCREEN.scanY
) {
errorChain(uiConsoleRebuild());
}
if(UI_CONSOLE_CACHE.vertexCount == 0) errorOk();
shadermaterial_t material = {
.unlit = {
.color = COLOR_RED,
.texture = FONT_DEFAULT.texture
}
};
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMaterial(&SHADER_UNLIT, &material));
return meshDraw(&UI_CONSOLE_CACHE.mesh, 0, UI_CONSOLE_CACHE.vertexCount);
}
errorret_t uiConsoleRebuild(void) {
int32_t spriteCount = 0;
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
char_t c;
int32_t j = 0;
while((c = CONSOLE.line[i][j++]) != '\0') {
if(c != ' ' && c != '\n') spriteCount++;
}
}
assertTrue(
spriteCount <= UI_CONSOLE_CACHE_GLYPH_MAX,
"Console history exceeds fixed cache capacity"
);
const int32_t vertexCount = spriteCount * QUAD_VERTEX_COUNT;
UI_CONSOLE_CACHE.vertexCount = vertexCount;
if(vertexCount > 0) {
const float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight;
const float_t charW = (float_t)FONT_DEFAULT.tileset->tileWidth;
int32_t index = 0;
int32_t row = 0;
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
float_t posX = (float_t)SCREEN.scanX;
float_t posY = (float_t)SCREEN.scanY + lineH * (float_t)row;
char_t c;
int32_t j = 0;
while((c = CONSOLE.line[i][j++]) != '\0') {
if(c == '\n') {
posX = (float_t)SCREEN.scanX;
posY += lineH;
row++;
continue;
}
if(c == ' ') {
posX += charW;
continue;
}
const spritebatchsprite_t sprite = textGetSprite(
(vec2){ posX, posY }, c, &FONT_DEFAULT
);
spriteBatchBufferToMesh(
&sprite, 1,
&UI_CONSOLE_CACHE.vertices[index], QUAD_VERTEX_COUNT
);
index += QUAD_VERTEX_COUNT;
posX += charW;
}
row++;
}
if(!UI_CONSOLE_CACHE.built) {
errorChain(meshInit(
&UI_CONSOLE_CACHE.mesh,
QUAD_PRIMITIVE_TYPE,
UI_CONSOLE_CACHE_VERTEX_MAX,
UI_CONSOLE_CACHE.vertices
));
UI_CONSOLE_CACHE.built = true;
} else {
errorChain(meshFlush(&UI_CONSOLE_CACHE.mesh, 0, vertexCount));
}
}
UI_CONSOLE_CACHE.cachedScanX = SCREEN.scanX;
UI_CONSOLE_CACHE.cachedScanY = SCREEN.scanY;
CONSOLE.dirty = false;
errorOk();
}
errorret_t uiConsoleDispose(void) {
if(UI_CONSOLE_CACHE.built) {
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
}
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
errorOk();
}
+48
View File
@@ -0,0 +1,48 @@
/**
* 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/mesh/quad.h"
// Fixed capacity for the console's cached glyph mesh, in glyphs -- the
// mesh/vertex buffer is sized to this once and never grown/shrunk, so
// history producing more non-space characters than this asserts (see
// uiConsoleRebuild) instead of reallocating.
#define UI_CONSOLE_CACHE_GLYPH_MAX 512
#define UI_CONSOLE_CACHE_VERTEX_MAX (\
UI_CONSOLE_CACHE_GLYPH_MAX * QUAD_VERTEX_COUNT \
)
/**
* Renders the console history into the scan-safe area, drawing a mesh
* that is only rebuilt when the history changes or the scan-safe origin
* moves, rather than rebuffering vertices every frame. No-ops when the
* console is not visible.
*
* @return Any error that occurs.
*/
errorret_t uiConsoleDraw(void);
/**
* Rebuilds the cached mesh for the console's current history and
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
* needed; only needs calling directly to force an immediate rebuild.
* The underlying vertex buffer is a fixed-size array (see
* UI_CONSOLE_CACHE_VERTEX_MAX) -- asserts if the history produces more
* glyphs than that capacity, rather than growing it.
*
* @return Any error that occurs.
*/
errorret_t uiConsoleRebuild(void);
/**
* Frees the mesh and vertex buffer built up by uiConsoleDraw().
*
* @return Any error that occurs.
*/
errorret_t uiConsoleDispose(void);
@@ -7,13 +7,21 @@
#include "uifps.h"
#include "time/time.h"
#include "display/spritebatch/spritebatch.h"
#include "util/string.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "engine/engine.h"
uifps_t UIFPS;
errorret_t uiFPSInit() {
uiLabelInit(&UIFPS.fpsLabel, &FONT_DEFAULT);
uiLabelInit(&UIFPS.versionLabel, &FONT_DEFAULT);
uiLabelSetText(&UIFPS.versionLabel, ENGINE.version);
uiLabelSetColor(&UIFPS.versionLabel, color(255, 255, 255, 128));
errorOk();
}
errorret_t uiFPSDraw() {
char_t fpsText[32];
@@ -50,23 +58,21 @@ errorret_t uiFPSDraw() {
textColor = COLOR_RED;
}
errorChain(textDraw(
(float_t)SCREEN.scanX,
(float_t)SCREEN.scanY,
fpsText, textColor,
&FONT_DEFAULT
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
if(stringCompare(fpsText, UIFPS.fpsLabel.text) != 0) {
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
}
errorChain(uiLabelDraw(
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
));
errorChain(spriteBatchFlush());
int32_t versionWidth, versionHeight;
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
errorChain(textDraw(
uiLabelGetSize(&UIFPS.versionLabel, &versionWidth, &versionHeight);
errorChain(uiLabelDraw(
&UIFPS.versionLabel,
(float_t)(SCREEN.scanX + SCREEN.scanWidth - versionWidth),
(float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight),
ENGINE.version,
color(255, 255, 255, 128),
&FONT_DEFAULT
(float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight)
));
return spriteBatchFlush();
errorOk();
}
@@ -8,17 +8,27 @@
#pragma once
#include "error/error.h"
#include "time/timeepoch.h"
#include "ui/widget/uilabel.h"
typedef struct {
dusktimeepoch_t lastTick;
float_t fpsAverage;
uilabel_t fpsLabel;
uilabel_t versionLabel;
} uifps_t;
extern uifps_t UIFPS;
/**
* Initializes the FPS counter, caching the (static) version label.
*
* @return Any error that occurs.
*/
errorret_t uiFPSInit();
/**
* Draws the FPS counter on the screen, and also does the update (for now).
*
*
* @return Any error that occurs.
*/
errorret_t uiFPSDraw();
@@ -12,11 +12,11 @@
#include "time/time.h"
const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = {
{ INPUT_ACTION_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
{ INPUT_ACTION_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
{ INPUT_ACTION_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
{ INPUT_ACTION_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
{ INPUT_ACTION_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
{ INPUT_BIND_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
{ INPUT_BIND_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
{ INPUT_BIND_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
{ INPUT_BIND_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
{ INPUT_BIND_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
};
uifocus_t UI_FOCUS;
@@ -108,7 +108,7 @@ void uiFocusMoveDirection(
) {
for(
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
m->action != INPUT_ACTION_NULL;
m->action != INPUT_BIND_NULL;
m++
) {
if(m->direction != dir) continue;
@@ -142,19 +142,19 @@ void uiFocusUpdate(void) {
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1];
if(inputPressed(INPUT_ACTION_ACCEPT)) {
if(inputPressed(INPUT_BIND_ACCEPT)) {
if(item->selected != NULL) item->selected(item);
return;
}
if(inputPressed(INPUT_ACTION_CANCEL)) {
if(inputPressed(INPUT_BIND_CANCEL)) {
uiFocusPop();
return;
}
for(
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
m->action != INPUT_ACTION_NULL;
m->action != INPUT_BIND_NULL;
m++
) {
if(!inputPressed(m->action)) continue;
@@ -167,7 +167,7 @@ void uiFocusUpdate(void) {
bool_t held = false;
for(
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
m->action != INPUT_ACTION_NULL;
m->action != INPUT_BIND_NULL;
m++
) {
if(m->direction != UI_FOCUS.direction) continue;
@@ -23,7 +23,7 @@
#define UI_FOCUS_HOLD_REPEAT 0.1f
typedef struct {
inputaction_t action;
inputbind_t action;
uifocusdirection_t direction;
int8_t dx;
int8_t dy;
@@ -31,7 +31,7 @@ typedef struct {
/**
* Mapping of input actions to focus directions, terminated by an
* entry with action == INPUT_ACTION_NULL.
* entry with action == INPUT_BIND_NULL.
*/
extern const uifocusdirmap_t UI_FOCUS_DIR_MAP[];
@@ -3,10 +3,10 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
battle.c
battlefighter.c
party.c
uiframe.c
uiconfirm.c
)
add_subdirectory(settings)
@@ -13,7 +13,6 @@
#include "ui/frame/uiframe.h"
#include "ui/frame/uiconfirm.h"
#include "util/memory.h"
#include "display/spritebatch/spritebatch.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "assert/assert.h"
@@ -149,7 +148,7 @@ errorret_t uiSettingsDraw(void) {
const float_t y = (float_t)SCREEN.scanY +
((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
errorChain(uiFrameDrawCached(&UI_SETTINGS.frameCache, x, y, width, height));
const float_t contentX = x + UI_FRAME_START_X;
const float_t contentY = y + UI_FRAME_START_Y;
@@ -174,7 +173,6 @@ errorret_t uiSettingsDraw(void) {
));
}
errorChain(spriteBatchFlush());
errorOk();
}
@@ -8,6 +8,7 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "ui/frame/uiframe.h"
#include "uisettingsdata.h"
#define UI_SETTINGS_TAB_COUNT 4
@@ -29,6 +30,7 @@ typedef struct {
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
uisettingsdata_t data;
uiframecache_t frameCache;
} uisettings_t;
extern uisettings_t UI_SETTINGS;
@@ -12,6 +12,7 @@
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "input/input.h"
#include "save/savesettings.h"
void uiSettingsInputSelected(
const uimenu_t *menu,
@@ -66,6 +67,7 @@ void uiSettingsInputApply(void) {
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
);
#endif
errorCatch(errorPrint(saveSettingsWrite()));
uiMenuClose(&UI_SETTINGS.data.input.menu);
}
@@ -9,7 +9,6 @@
#include "uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
@@ -39,6 +38,7 @@ void uiConfirmClosed(const uimenu_t *menu) {
errorret_t uiConfirmInit(void) {
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
uiLabelInit(&UI_CONFIRM.textLabel, &FONT_DEFAULT);
MENU_BEGIN(
&UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL
@@ -69,10 +69,9 @@ errorret_t uiConfirmDraw(void) {
errorChain(
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
);
errorChain(spriteBatchFlush());
int32_t textW, textH;
textMeasure(UI_CONFIRM.text, &FONT_DEFAULT, &textW, &textH);
uiLabelGetSize(&UI_CONFIRM.textLabel, &textW, &textH);
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t width = mathMax(
@@ -83,18 +82,17 @@ errorret_t uiConfirmDraw(void) {
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
errorChain(uiFrameDrawCached(&UI_CONFIRM.frameCache, x, y, width, height));
float_t contentX = x + UI_FRAME_START_X;
float_t contentY = y + UI_FRAME_START_Y;
float_t contentWidth = width - (UI_FRAME_START_X * 2);
errorChain(textDraw(contentX, contentY, UI_CONFIRM.text, COLOR_WHITE, &FONT_DEFAULT));
errorChain(uiLabelDraw(&UI_CONFIRM.textLabel, contentX, contentY));
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
errorChain(spriteBatchFlush());
errorOk();
}
@@ -112,7 +110,7 @@ void uiConfirmOpen(
void *user
) {
assertNotNull(question, "Question cannot be NULL");
stringCopy(UI_CONFIRM.text, question, UI_CONFIRM_TEXT_MAX);
uiLabelSetText(&UI_CONFIRM.textLabel, question);
UI_CONFIRM.callback = callback;
UI_CONFIRM.user = user;
UI_CONFIRM.result = false;
@@ -8,8 +8,9 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#include "ui/widget/uilabel.h"
#include "ui/frame/uiframe.h"
#define UI_CONFIRM_TEXT_MAX 256
#define UI_CONFIRM_MIN_WIDTH 160.0f
#define UI_CONFIRM_INDEX_CONFIRM 0
#define UI_CONFIRM_INDEX_CANCEL 1
@@ -25,12 +26,13 @@
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
typedef struct {
char_t text[UI_CONFIRM_TEXT_MAX];
uilabel_t textLabel;
uimenu_t menu;
uimenuitem_t items[UI_CONFIRM_ITEM_COUNT];
uiconfirmcallback_t callback;
void *user;
bool_t result;
uiframecache_t frameCache;
} uiconfirm_t;
extern uiconfirm_t UI_CONFIRM;
@@ -72,6 +72,17 @@ errorret_t uiFrameDraw(
}
};
spritebatchsprite_t sprites[9];
uiFrameBuildSprites(sprites, x, y, width, height);
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
}
void uiFrameBuildSprites(
spritebatchsprite_t sprites[9],
const float_t x,
const float_t y,
const float_t width,
const float_t height
) {
float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
@@ -112,8 +123,37 @@ errorret_t uiFrameDraw(
&UI_FRAME.tileset, 2, 2,
x + width - tileW, y + height - tileH, tileW, tileH
);
}
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
errorret_t uiFrameDrawCached(
uiframecache_t *cache,
const float_t x,
const float_t y,
const float_t width,
const float_t height
) {
assertNotNull(cache, "Frame cache cannot be NULL");
if(
!cache->built ||
cache->lastX != x || cache->lastY != y ||
cache->lastWidth != width || cache->lastHeight != height
) {
uiFrameBuildSprites(cache->sprites, x, y, width, height);
cache->lastX = x;
cache->lastY = y;
cache->lastWidth = width;
cache->lastHeight = height;
cache->built = true;
}
shadermaterial_t material = {
.unlit = {
.color = COLOR_WHITE,
.texture = &UI_FRAME.texture
}
};
return spriteBatchBuffer(cache->sprites, 9, &SHADER_UNLIT, material);
}
errorret_t uiFrameDispose(void) {
+122
View File
@@ -0,0 +1,122 @@
/**
* 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/texture/texture.h"
#include "display/texture/tileset.h"
#include "display/spritebatch/spritebatchsprite.h"
#define UI_FRAME_BORDER_WIDTH 6
#define UI_FRAME_BORDER_HEIGHT 6
#define UI_FRAME_PADDING_X 2
#define UI_FRAME_PADDING_Y 2
#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
#define UI_FRAME_TILE_WIDTH 1
#define UI_FRAME_TILE_HEIGHT 1
#define UI_FRAME_TEXTURE_WIDTH (UI_FRAME_TILE_WIDTH * 3)
#define UI_FRAME_TEXTURE_HEIGHT (UI_FRAME_TILE_HEIGHT * 3)
#define UI_FRAME_TEXTURE_WIDTH_POW2 4
#define UI_FRAME_TEXTURE_HEIGHT_POW2 4
typedef struct {
tileset_t tileset;
texture_t texture;
color_t pixels[UI_FRAME_BORDER_WIDTH * UI_FRAME_BORDER_HEIGHT];
} uiframe_t;
extern uiframe_t UI_FRAME;
/**
* Initializes the global UI_FRAME: builds the dummy texture and
* configures the tileset. Call once after displayInit().
*
* @return Any error that occurs.
*/
errorret_t uiFrameInit(void);
/**
* Draws UI_FRAME using 9-slice rendering from its 3x3 tileset.
* Pushes quads to the sprite batch without flushing.
*
* @param x Screen x position.
* @param y Screen y position.
* @param width Total width of the frame.
* @param height Total height of the frame.
* @return Any error that occurs.
*/
errorret_t uiFrameDraw(
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
/**
* Builds the 9 sprites for a 9-slice frame at the given rect. Used
* internally by uiFrameDraw and uiFrameDrawCached -- call directly only
* if you need the raw sprites instead of buffering them.
*
* @param sprites Destination array of exactly 9 sprites.
* @param x Screen x position.
* @param y Screen y position.
* @param width Total width of the frame.
* @param height Total height of the frame.
*/
void uiFrameBuildSprites(
spritebatchsprite_t sprites[9],
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
/**
* A frame's cached 9-slice sprites, plus the rect they were built for.
* Owned by whichever widget/screen draws a frame repeatedly (dialogs,
* textboxes, settings panels) so uiFrameDrawCached can skip rebuilding
* the sprites when the rect hasn't moved since the last draw.
*/
typedef struct {
spritebatchsprite_t sprites[9];
bool_t built;
float_t lastX;
float_t lastY;
float_t lastWidth;
float_t lastHeight;
} uiframecache_t;
/**
* Draws a 9-slice frame using a caller-owned cache, only rebuilding the
* sprites (via uiFrameBuildSprites) when x/y/width/height differ from
* the last call -- unlike uiFrameDraw, which rebuilds unconditionally
* every time. Prefer this for frames redrawn every frame at a fixed or
* rarely-changing rect (dialogs, panels); use plain uiFrameDraw for
* genuinely one-off or per-frame-varying rects.
*
* @param cache Caller-owned cache, zero-initialized before first use.
* @param x Screen x position.
* @param y Screen y position.
* @param width Total width of the frame.
* @param height Total height of the frame.
* @return Any error that occurs.
*/
errorret_t uiFrameDrawCached(
uiframecache_t *cache,
const float_t x,
const float_t y,
const float_t width,
const float_t height
);
/**
* Disposes of the global UI_FRAME, releasing its GPU texture.
*
* @return Any error that occurs.
*/
errorret_t uiFrameDispose(void);
@@ -85,5 +85,5 @@ errorret_t uiCropDraw(void) {
}
};
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material));
return spriteBatchFlush();
errorOk();
}
@@ -75,7 +75,7 @@ errorret_t uiFullboxDraw(uifullbox_t *fullbox) {
}
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
return spriteBatchFlush();
errorOk();
}
void uiFullboxTransition(
@@ -11,7 +11,6 @@
#include "time/time.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "display/spritebatch/spritebatch.h"
#include "uifullbox.h"
#define UI_LOADING_TEXT "loading"
@@ -20,6 +19,8 @@ uiloading_t UI_LOADING;
errorret_t uiLoadingInit(void) {
memoryZero(&UI_LOADING, sizeof(uiloading_t));
uiLabelInit(&UI_LOADING.textLabel, &FONT_DEFAULT);
uiLabelSetText(&UI_LOADING.textLabel, UI_LOADING_TEXT);
eventInit(
&UI_LOADING.onTransitionEnd,
UI_LOADING.onTransitionEndCallbacks,
@@ -53,7 +54,7 @@ errorret_t uiLoadingDraw(void) {
if(alpha <= 0.0f) errorOk();
int32_t textW, textH;
textMeasure(UI_LOADING_TEXT, &FONT_DEFAULT, &textW, &textH);
uiLabelGetSize(&UI_LOADING.textLabel, &textW, &textH);
float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) -
(float_t)textW - UI_LOADING_MARGIN;
@@ -62,9 +63,10 @@ errorret_t uiLoadingDraw(void) {
color_t color = COLOR_WHITE;
color.a = (uint8_t)(alpha * 255.0f);
uiLabelSetColor(&UI_LOADING.textLabel, color);
errorChain(textDraw(x, y, UI_LOADING_TEXT, color, &FONT_DEFAULT));
return spriteBatchFlush();
errorChain(uiLabelDraw(&UI_LOADING.textLabel, x, y));
errorOk();
}
static void uiLoadingTransition(
@@ -8,6 +8,7 @@
#pragma once
#include "error/error.h"
#include "event/event.h"
#include "ui/widget/uilabel.h"
#define UI_LOADING_FADE_DURATION 0.5f
#define UI_LOADING_MARGIN 8.0f
@@ -20,6 +21,7 @@ typedef struct {
eventcallback_t onTransitionEndCallbacks[4];
void *onTransitionEndUsers[4];
event_t onTransitionEnd;
uilabel_t textLabel;
} uiloading_t;
extern uiloading_t UI_LOADING;
@@ -52,5 +52,5 @@ errorret_t uiTransitionFadeDraw(const uitransitiondata_t *data) {
}
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
return spriteBatchFlush();
errorOk();
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "ui.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "display/spritebatch/spritebatch.h"
#include "display/screen/screen.h"
#include "ui/uielement.h"
#include "ui/focus/uifocus.h"
ui_t UI;
errorret_t uiInit(void) {
memoryZero(&UI, sizeof(ui_t));
uiFocusInit();
uiElementsSort();
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementInit(element));
element++;
}
errorOk();
}
errorret_t uiUpdate(void) {
uiFocusUpdate();
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementUpdate(element));
element++;
}
errorOk();
}
errorret_t uiRender(void) {
const uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementDraw(element));
element++;
}
errorChain(spriteBatchFlush());
errorOk();
}
errorret_t uiDispose(void) {
uielement_t *element = &UI_ELEMENTS[0];
while(!uiElementIsNull(element)) {
errorChain(uiElementDispose(element));
element++;
}
errorOk();
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
void *nothing;
} ui_t;
extern ui_t UI;
/**
* Initializes the UI system.
*/
errorret_t uiInit(void);
/**
* Updates the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiUpdate(void);
/**
* Renders the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiRender(void);
/**
* Disposes of the UI system.
*
* @return Any error that occurs.
*/
errorret_t uiDispose(void);
+86
View File
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uielement.h"
#include "assert/assert.h"
#include "util/sort.h"
#include "ui/frame/uiframe.h"
#include "ui/debug/uifps.h"
#include "engine/engine.h"
#include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h"
#include "ui/overlay/uicrop.h"
#include "ui/transition/uitransition.h"
#include "ui/debug/uiconsole.h"
#include "ui/frame/uiconfirm.h"
// Priming pass: X does nothing here, so this only exists to process any
// #include directives nested in uielementlist.h/ui/uielementgame.h at
// file scope (each such header's own #pragma once makes it a no-op on
// the real pass below, which happens inside UI_ELEMENTS[]'s braces,
// where a raw #include of a declaration would be invalid).
#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing
#include "uielementlist.h"
#undef X
uielement_t UI_ELEMENTS[] = {
#define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \
{ \
.init = initFn, .update = updateFn, .draw = drawFn, \
.dispose = disposeFn, .order = elementOrder \
},
#include "uielementlist.h"
#undef X
{ 0 } // Null terminator
};
bool_t uiElementIsNull(const uielement_t *element) {
return element->init == NULL &&
element->update == NULL &&
element->draw == NULL &&
element->dispose == NULL;
}
int_t uiElementCompareOrder(const void *a, const void *b) {
const uielement_t *elementA = (const uielement_t *)a;
const uielement_t *elementB = (const uielement_t *)b;
if(elementA->order < elementB->order) return -1;
if(elementA->order > elementB->order) return 1;
return 0;
}
void uiElementsSort(void) {
// The trailing null terminator is always the last static entry -- sort
// everything before it, and leave it in place.
const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1;
sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder);
}
errorret_t uiElementInit(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->init != NULL) errorChain(element->init());
errorOk();
}
errorret_t uiElementUpdate(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->update != NULL) errorChain(element->update());
errorOk();
}
errorret_t uiElementDraw(const uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->draw != NULL) errorChain(element->draw());
errorOk();
}
errorret_t uiElementDispose(uielement_t *element) {
assertNotNull(element, "element must not be NULL");
if(element->dispose != NULL) errorChain(element->dispose());
errorOk();
}
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
// Built-in order tiers. Lower values update/render first; ties preserve
// their relative UI_ELEMENTS declaration order (uiElementsSort uses a
// stable sort). Game-specific elements can use any int32_t value -- these
// are just the ones the engine itself relies on.
#define UI_ELEMENT_ORDER_DEFAULT 0
#define UI_ELEMENT_ORDER_DEBUG 1000
typedef struct {
errorret_t (*init)();
errorret_t (*update)();
errorret_t (*draw)();
errorret_t (*dispose)();
int32_t order;
} uielement_t;
extern uielement_t UI_ELEMENTS[];
/**
* Returns true when all four callbacks on the element are NULL,
* which marks the end of the UI_ELEMENTS array.
*
* @param element The element to test.
* @returns True if the element is the null terminator.
*/
bool_t uiElementIsNull(const uielement_t *element);
/**
* Compares two elements by their .order field, ascending. Matches
* sortcompare_t, for use with the project's sort utilities.
*
* @param a First uielement_t to compare.
* @param b Second uielement_t to compare.
* @return Negative if a < b, zero if a == b, positive if a > b.
*/
int_t uiElementCompareOrder(const void *a, const void *b);
/**
* Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing
* null terminator is never moved. Called once by uiInit -- element order
* is static after that, so there's no need to re-sort every frame.
*/
void uiElementsSort(void);
/**
* Initializes a UI element, invoking its init callback if set.
*
* @param element The element to initialize.
* @return Any error that occurs.
*/
errorret_t uiElementInit(uielement_t *element);
/**
* Updates a UI element, calling its update callback if set.
*
* @param element The element to update.
* @return Any error that occurs.
*/
errorret_t uiElementUpdate(uielement_t *element);
/**
* Draws a UI element, calling its draw callback if set.
*
* @param element The element to render.
* @return Any error that occurs.
*/
errorret_t uiElementDraw(const uielement_t *element);
/**
* Disposes of a UI element, invoking its dispose callback if set.
*
* @param element The element to dispose.
* @return Any error that occurs.
*/
errorret_t uiElementDispose(uielement_t *element);
+47
View File
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
// X(init, update, draw, dispose, order) -- pass NULL for any callback the
// element doesn't need. order controls update/draw sequence (lower first,
// see UI_ELEMENT_ORDER_* in uielement.h); ties preserve declaration order.
// See uielement.c for how this expands.
X(uiFrameInit, NULL, NULL, uiFrameDispose, UI_ELEMENT_ORDER_DEFAULT)
// Fullbox under: above scene, below system UI.
X(
uiFullboxUnderInit, uiFullboxUnderUpdate, uiFullboxUnderDraw, NULL,
UI_ELEMENT_ORDER_DEFAULT
)
// Text stuffs
X(uiConfirmInit, NULL, uiConfirmDraw, uiConfirmDispose, UI_ELEMENT_ORDER_DEFAULT)
X(
uiTransitionInit, uiTransitionUpdate, uiTransitionDraw, NULL,
UI_ELEMENT_ORDER_DEFAULT
)
// Fullbox over: above absolutely everything (except debug).
X(
uiFullboxOverInit, uiFullboxOverUpdate, uiFullboxOverDraw, NULL,
UI_ELEMENT_ORDER_DEFAULT
)
X(
uiLoadingInit, uiLoadingUpdate, uiLoadingDraw, NULL,
UI_ELEMENT_ORDER_DEFAULT
)
X(uiCropInit, NULL, uiCropDraw, NULL, UI_ELEMENT_ORDER_DEFAULT)
// Debug items -- always last.
X(NULL, NULL, uiConsoleDraw, uiConsoleDispose, UI_ELEMENT_ORDER_DEBUG)
X(uiFPSInit, NULL, uiFPSDraw, NULL, UI_ELEMENT_ORDER_DEBUG)
// Game-specific UI elements (duskrpg)
#include "ui/uielementgame.h"
+17
View File
@@ -0,0 +1,17 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uibutton.c
uicheckbox.c
uitab.c
uislider.c
uidropdown.c
uiscrolling.c
uimenu.c
uilabel.c
uiwidgetlabel.c
)
@@ -7,15 +7,15 @@
#include "uibutton.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "display/text/text.h"
#include "display/color.h"
void uiButtonInit(uibutton_t *button, const char_t *label) {
assertNotNull(button, "Button cannot be NULL");
assertNotNull(label, "Label cannot be NULL");
memoryZero(button, sizeof(uibutton_t));
button->label = label;
uiWidgetLabelInit(&button->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&button->label, label);
button->highlighted = false;
}
bool_t uiButtonIsHighlighted(const uibutton_t *button) {
@@ -26,6 +26,9 @@ bool_t uiButtonIsHighlighted(const uibutton_t *button) {
void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) {
assertNotNull(button, "Button cannot be NULL");
button->highlighted = highlighted;
uiWidgetLabelSetColor(
&button->label, highlighted ? COLOR_RED : COLOR_WHITE
);
}
errorret_t uiButtonDraw(
@@ -34,10 +37,6 @@ errorret_t uiButtonDraw(
const float_t y
) {
assertNotNull(button, "Button cannot be NULL");
errorChain(textDraw(
x, y, button->label,
button->highlighted ? COLOR_RED : COLOR_WHITE,
&FONT_DEFAULT
));
errorChain(uiWidgetLabelDraw(&button->label, x, y));
errorOk();
}
@@ -7,9 +7,10 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
typedef struct {
const char_t *label;
uiwidgetlabel_t label;
bool_t highlighted;
} uibutton_t;
@@ -7,6 +7,7 @@
#include "uicheckbox.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h"
#include "display/color.h"
@@ -15,7 +16,9 @@ void uiCheckboxInit(
const char_t *label
) {
memoryZero(checkbox, sizeof(uicheckbox_t));
checkbox->label = label;
checkbox->rawLabel = label;
uiWidgetLabelInit(&checkbox->label, &FONT_DEFAULT);
uiCheckboxRebuildLabel(checkbox);
}
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
@@ -24,6 +27,7 @@ bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) {
checkbox->checked = checked;
uiCheckboxRebuildLabel(checkbox);
}
void uiCheckboxToggle(uicheckbox_t *checkbox) {
@@ -36,6 +40,9 @@ bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox) {
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted) {
checkbox->highlighted = highlighted;
uiWidgetLabelSetColor(
&checkbox->label, highlighted ? COLOR_RED : COLOR_WHITE
);
}
errorret_t uiCheckboxDraw(
@@ -43,15 +50,15 @@ errorret_t uiCheckboxDraw(
const float_t x,
const float_t y
) {
color_t color = checkbox->highlighted ? COLOR_RED : COLOR_WHITE;
const char_t *mark = checkbox->checked ? "Y " : "N ";
errorChain(textDraw(x, y, mark, color, &FONT_DEFAULT));
int32_t markW, markH;
textMeasure(mark, &FONT_DEFAULT, &markW, &markH);
errorChain(textDraw(
x + (float_t)markW, y, checkbox->label, color, &FONT_DEFAULT
));
errorChain(uiWidgetLabelDraw(&checkbox->label, x, y));
errorOk();
}
void uiCheckboxRebuildLabel(uicheckbox_t *checkbox) {
char_t combined[UI_WIDGET_LABEL_TEXT_MAX];
stringFormat(
combined, UI_WIDGET_LABEL_TEXT_MAX - 1, "%s %s",
checkbox->checked ? "Y" : "N", checkbox->rawLabel
);
uiWidgetLabelSetText(&checkbox->label, combined);
}
@@ -7,9 +7,11 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
typedef struct uicheckbox_s {
const char_t *label;
const char_t *rawLabel;
uiwidgetlabel_t label;
bool_t checked;
bool_t highlighted;
} uicheckbox_t;
@@ -77,3 +79,12 @@ errorret_t uiCheckboxDraw(
const float_t x,
const float_t y
);
/**
* Rebuilds the checkbox's cached label ("Y "/"N " mark plus rawLabel)
* from its current checked state. Called internally whenever checked
* changes.
*
* @param checkbox The checkbox to update.
*/
void uiCheckboxRebuildLabel(uicheckbox_t *checkbox);
@@ -25,10 +25,13 @@ void uiDropdownInit(
assertTrue(optionCount > 0, "Dropdown must have at least one option");
memoryZero(dropdown, sizeof(uidropdown_t));
dropdown->label = label;
uiWidgetLabelInit(&dropdown->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&dropdown->label, label);
uiWidgetLabelInit(&dropdown->value, &FONT_DEFAULT);
dropdown->options = options;
dropdown->optionCount = optionCount;
dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0;
uiDropdownRebuildValue(dropdown);
}
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) {
@@ -45,18 +48,21 @@ void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index) {
assertNotNull(dropdown, "Dropdown cannot be NULL");
assertTrue(index < dropdown->optionCount, "Dropdown index out of range");
dropdown->selectedIndex = index;
uiDropdownRebuildValue(dropdown);
}
void uiDropdownStepNext(uidropdown_t *dropdown) {
assertNotNull(dropdown, "Dropdown cannot be NULL");
dropdown->selectedIndex =
(dropdown->selectedIndex + 1) % dropdown->optionCount;
uiDropdownRebuildValue(dropdown);
}
void uiDropdownStepPrev(uidropdown_t *dropdown) {
assertNotNull(dropdown, "Dropdown cannot be NULL");
dropdown->selectedIndex = dropdown->selectedIndex == 0 ?
dropdown->optionCount - 1 : dropdown->selectedIndex - 1;
uiDropdownRebuildValue(dropdown);
}
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) {
@@ -70,6 +76,9 @@ void uiDropdownSetHighlighted(
) {
assertNotNull(dropdown, "Dropdown cannot be NULL");
dropdown->highlighted = highlighted;
color_t color = highlighted ? COLOR_RED : COLOR_WHITE;
uiWidgetLabelSetColor(&dropdown->label, color);
uiWidgetLabelSetColor(&dropdown->value, color);
}
errorret_t uiDropdownDraw(
@@ -79,22 +88,23 @@ errorret_t uiDropdownDraw(
) {
assertNotNull(dropdown, "Dropdown cannot be NULL");
color_t color = dropdown->highlighted ? COLOR_RED : COLOR_WHITE;
errorChain(textDraw(x, y, dropdown->label, color, &FONT_DEFAULT));
errorChain(uiWidgetLabelDraw(&dropdown->label, x, y));
int32_t labelW, labelH;
textMeasure(dropdown->label, &FONT_DEFAULT, &labelW, &labelH);
uiWidgetLabelGetSize(&dropdown->label, &labelW, &labelH);
char_t valueText[UI_DROPDOWN_VALUE_TEXT_MAX];
stringFormat(
valueText, UI_DROPDOWN_VALUE_TEXT_MAX - 1, "< %s >",
uiDropdownGetSelectedOption(dropdown)
);
errorChain(textDraw(
x + (float_t)labelW + UI_DROPDOWN_GAP, y, valueText, color, &FONT_DEFAULT
errorChain(uiWidgetLabelDraw(
&dropdown->value, x + (float_t)labelW + UI_DROPDOWN_GAP, y
));
errorOk();
}
void uiDropdownRebuildValue(uidropdown_t *dropdown) {
char_t valueText[UI_WIDGET_LABEL_TEXT_MAX];
stringFormat(
valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "< %s >",
uiDropdownGetSelectedOption(dropdown)
);
uiWidgetLabelSetText(&dropdown->value, valueText);
}
@@ -7,12 +7,13 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
#define UI_DROPDOWN_GAP 4.0f
#define UI_DROPDOWN_VALUE_TEXT_MAX 64
typedef struct {
const char_t *label;
uiwidgetlabel_t label;
uiwidgetlabel_t value;
const char_t *const *options;
uint8_t optionCount;
uint8_t selectedIndex;
@@ -111,3 +112,12 @@ errorret_t uiDropdownDraw(
const float_t x,
const float_t y
);
/**
* Rebuilds the dropdown's cached value label ("< option >") from its
* current selectedIndex. Called internally whenever selectedIndex
* changes.
*
* @param dropdown The dropdown to update.
*/
void uiDropdownRebuildValue(uidropdown_t *dropdown);
+70
View File
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uilabel.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h"
void uiLabelInit(uilabel_t *label, font_t *font) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
memoryZero(label, sizeof(uilabel_t));
label->font = font;
label->color = COLOR_WHITE;
}
void uiLabelSetText(uilabel_t *label, const char_t *text) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(text, "Text cannot be NULL");
assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long");
stringCopy(label->text, text, UI_LABEL_TEXT_MAX);
label->spriteCount = textBuildSpriteCache(
label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX,
&label->width, &label->height
);
}
void uiLabelSetColor(uilabel_t *label, const color_t color) {
assertNotNull(label, "Label cannot be NULL");
label->color = color;
}
void uiLabelGetSize(
const uilabel_t *label,
int32_t *outWidth,
int32_t *outHeight
) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(outWidth, "Output width cannot be NULL");
assertNotNull(outHeight, "Output height cannot be NULL");
*outWidth = label->width;
*outHeight = label->height;
}
errorret_t uiLabelDraw(
const uilabel_t *label,
const float_t x,
const float_t y
) {
assertNotNull(label, "Label cannot be NULL");
if(label->spriteCount == 0) errorOk();
// Cached sprites are relative to (0,0); textDrawSpriteCache translates
// into the requested screen position here instead of in
// uiLabelSetText, so a label can be repositioned every frame without
// rebuilding the (much more expensive) glyph/UV cache.
spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX];
errorChain(textDrawSpriteCache(
label->sprites, label->spriteCount, scratch, x, y, label->color,
label->font->texture
));
errorOk();
}
+85
View File
@@ -0,0 +1,85 @@
/**
* 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/text/font.h"
#include "display/spritebatch/spritebatchsprite.h"
#include "display/color.h"
#define UI_LABEL_TEXT_MAX 256
#define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX
typedef struct {
char_t text[UI_LABEL_TEXT_MAX];
color_t color;
font_t *font;
spritebatchsprite_t sprites[UI_LABEL_SPRITE_COUNT_MAX];
uint32_t spriteCount;
int32_t width;
int32_t height;
} uilabel_t;
/**
* Initializes a label, defaulting to white text and no text set.
*
* @param label The label to initialize.
* @param font Font to use for rendering. Must outlive the label.
*/
void uiLabelInit(uilabel_t *label, font_t *font);
/**
* Sets the label's text, rebuilding its cached sprites (glyph lookup, UV,
* and layout) immediately. This is the only way the sprite cache gets
* rebuilt -- uiLabelDraw never recomputes it, so call this whenever the
* text changes rather than once up front and expecting it to stay in sync.
*
* @param label The label to update.
* @param text Null-terminated string to display. Must be shorter than
* UI_LABEL_TEXT_MAX.
*/
void uiLabelSetText(uilabel_t *label, const char_t *text);
/**
* Sets the label's tint color. Cheap -- doesn't touch the sprite cache,
* since color is applied via the draw material, not baked into sprites.
*
* @param label The label to update.
* @param color The new tint color.
*/
void uiLabelSetColor(uilabel_t *label, const color_t color);
/**
* Gets the measured size (in pixels) of the label's current text, cached
* from the last uiLabelSetText call.
*
* @param label The label to query.
* @param outWidth Pointer to store the width.
* @param outHeight Pointer to store the height.
*/
void uiLabelGetSize(
const uilabel_t *label,
int32_t *outWidth,
int32_t *outHeight
);
/**
* Draws the label's cached sprites at the given screen position in a
* single batched buffer call. Does not recompute glyph layout -- call
* uiLabelSetText first whenever the text changes.
*
* @param label The label to draw.
* @param x Screen x position.
* @param y Screen y position.
* @return Any error that occurs.
*/
errorret_t uiLabelDraw(
const uilabel_t *label,
const float_t x,
const float_t y
);
@@ -10,6 +10,7 @@
#include "util/memory.h"
#include "display/text/text.h"
#include "display/color.h"
#include "ui/widget/uiwidgetlabel.h"
#include "ui/widget/uibutton.h"
#include "ui/widget/uitab.h"
#include "ui/widget/uislider.h"
@@ -102,12 +103,8 @@ errorret_t uiMenuDraw(
if(item->type == UI_MENU_WIDGET_TYPE_LABEL) {
if(col > 0) { row++; col = 0; }
errorChain(textDraw(
x,
y + (float_t)row * rowHeight,
item->label,
COLOR_WHITE,
&FONT_DEFAULT
errorChain(uiWidgetLabelDraw(
&item->label, x, y + (float_t)row * rowHeight
));
row++;
continue;
@@ -274,6 +271,13 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) {
return 0xFF;
}
void uiMenuLabelInit(uimenuitem_t *item, const char_t *text) {
assertNotNull(item, "Item cannot be NULL");
assertNotNull(text, "Text cannot be NULL");
uiWidgetLabelInit(&item->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&item->label, text);
}
void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) {
if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
uiCheckboxSetHighlighted(&item->checkbox, highlighted);
@@ -8,14 +8,13 @@
#pragma once
#include "error/error.h"
#include "ui/focus/uifocus.h"
#include "ui/widget/uiwidgetlabel.h"
#include "ui/widget/uibutton.h"
#include "ui/widget/uicheckbox.h"
#include "ui/widget/uitab.h"
#include "ui/widget/uislider.h"
#include "ui/widget/uidropdown.h"
#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX
typedef struct uimenu_s uimenu_t;
typedef enum {
@@ -32,7 +31,7 @@ typedef enum {
typedef struct {
uimenuwidgettype_t type;
union {
char_t *label;
uiwidgetlabel_t label;
uicheckbox_t checkbox;
uibutton_t button;
uitab_t tab;
@@ -178,6 +177,15 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot);
*/
void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted);
/**
* Initializes a menu item as a standalone label, caching its text.
* Used internally by the MENU_LABEL helper macro.
*
* @param item The item to initialize.
* @param text Display text.
*/
void uiMenuLabelInit(uimenuitem_t *item, const char_t *text);
/**
* Internal focus callback - forwards selection to the menu's selected handler.
*
@@ -227,7 +235,7 @@ bool_t uiMenuFocusDirection(
#define MENU_LABEL(text) \
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \
menu->items[menuIndex].label = text; \
uiMenuLabelInit(&menu->items[menuIndex], text); \
++menuIndex
#define MENU_SPACER() \
@@ -30,12 +30,16 @@ void uiSliderInitFloat(
assertTrue(step > 0.0f, "Slider step must be greater than zero");
memoryZero(slider, sizeof(uislider_t));
slider->label = label;
uiWidgetLabelInit(&slider->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&slider->label, label);
uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT);
slider->type = UI_SLIDER_TYPE_FLOAT;
slider->min.f = min;
slider->max.f = max;
slider->step.f = step;
slider->value.f = mathClamp(value, min, max);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
void uiSliderInitInt(
@@ -52,12 +56,16 @@ void uiSliderInitInt(
assertTrue(step > 0, "Slider step must be greater than zero");
memoryZero(slider, sizeof(uislider_t));
slider->label = label;
uiWidgetLabelInit(&slider->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&slider->label, label);
uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT);
slider->type = UI_SLIDER_TYPE_INT;
slider->min.i = min;
slider->max.i = max;
slider->step.i = step;
slider->value.i = mathClamp(value, min, max);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
float_t uiSliderGetFloat(const uislider_t *slider) {
@@ -80,6 +88,8 @@ void uiSliderSetFloat(uislider_t *slider, const float_t value) {
slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider"
);
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
@@ -88,6 +98,8 @@ void uiSliderSetInt(uislider_t *slider, const int32_t value) {
slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
);
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
uiSliderRebuildValueLabel(slider);
uiSliderRebuildGeometry(slider);
}
void uiSliderStepUp(uislider_t *slider) {
@@ -138,6 +150,9 @@ bool_t uiSliderIsHighlighted(const uislider_t *slider) {
void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) {
assertNotNull(slider, "Slider cannot be NULL");
slider->highlighted = highlighted;
color_t color = highlighted ? COLOR_RED : COLOR_WHITE;
uiWidgetLabelSetColor(&slider->label, color);
uiWidgetLabelSetColor(&slider->valueLabel, color);
}
errorret_t uiSliderDraw(
@@ -147,47 +162,97 @@ errorret_t uiSliderDraw(
) {
assertNotNull(slider, "Slider cannot be NULL");
color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE;
errorChain(uiWidgetLabelDraw(&slider->label, x, y));
errorChain(textDraw(x, y, slider->label, color, &FONT_DEFAULT));
int32_t labelW, labelH;
textMeasure(slider->label, &FONT_DEFAULT, &labelW, &labelH);
float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP;
float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
spritebatchsprite_t trackSprite = {
.min = { trackX, trackY, 0.0f },
.max = { trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t trackMaterial = {
.unlit = {
.color = COLOR_DARK_GRAY,
.texture = &TEXTURE_WHITE
}
};
errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial));
errorChain(spriteBatchFlush());
spritebatchsprite_t track = spriteBatchSpriteTranslate(
&slider->cachedTrack, x, y
);
errorChain(spriteBatchBuffer(&track, 1, &SHADER_UNLIT, trackMaterial));
if(slider->cachedFillVisible) {
shadermaterial_t fillMaterial = {
.unlit = {
.color = slider->highlighted ? COLOR_RED : COLOR_WHITE,
.texture = &TEXTURE_WHITE
}
};
spritebatchsprite_t fill = spriteBatchSpriteTranslate(
&slider->cachedFill, x, y
);
errorChain(spriteBatchBuffer(&fill, 1, &SHADER_UNLIT, fillMaterial));
}
if(slider->cachedMarkerCount > 0) {
spritebatchsprite_t markers[UI_SLIDER_STEP_MARKERS_MAX];
for(int32_t i = 0; i < slider->cachedMarkerCount; i++) {
markers[i] = spriteBatchSpriteTranslate(&slider->cachedMarkers[i], x, y);
}
shadermaterial_t markerMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_WHITE
}
};
errorChain(spriteBatchBuffer(
markers, slider->cachedMarkerCount, &SHADER_UNLIT, markerMaterial
));
}
errorChain(uiWidgetLabelDraw(
&slider->valueLabel,
x + slider->cachedTrack.max[0] + UI_SLIDER_GAP,
y
));
errorOk();
}
void uiSliderRebuildValueLabel(uislider_t *slider) {
char_t valueText[UI_WIDGET_LABEL_TEXT_MAX];
if(slider->type == UI_SLIDER_TYPE_INT) {
stringFormat(
valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "%d", slider->value.i
);
} else {
stringFormat(
valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "%.2f", slider->value.f
);
}
uiWidgetLabelSetText(&slider->valueLabel, valueText);
}
void uiSliderRebuildGeometry(uislider_t *slider) {
int32_t labelW, labelH;
uiWidgetLabelGetSize(&slider->label, &labelW, &labelH);
float_t trackX = (float_t)labelW + UI_SLIDER_GAP;
float_t trackY = ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
slider->cachedTrack = (spritebatchsprite_t){
.min = { trackX, trackY, 0.0f },
.max = {
trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f
},
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
if(fillWidth > 0.0f) {
spritebatchsprite_t fillSprite = {
slider->cachedFillVisible = fillWidth > 0.0f;
if(slider->cachedFillVisible) {
slider->cachedFill = (spritebatchsprite_t){
.min = { trackX, trackY, 0.0f },
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t fillMaterial = {
.unlit = {
.color = color,
.texture = &TEXTURE_WHITE
}
};
errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial));
errorChain(spriteBatchFlush());
}
int32_t stepCount = uiSliderGetStepCount(slider);
@@ -195,7 +260,7 @@ errorret_t uiSliderDraw(
for(int32_t i = 0; i <= stepCount; i++) {
float_t markerX = trackX +
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
spritebatchsprite_t markerSprite = {
slider->cachedMarkers[i] = (spritebatchsprite_t){
.min = {
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
@@ -209,34 +274,9 @@ errorret_t uiSliderDraw(
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t markerMaterial = {
.unlit = {
.color = COLOR_WHITE,
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(&markerSprite, 1, &SHADER_UNLIT, markerMaterial)
);
errorChain(spriteBatchFlush());
}
}
char_t valueText[UI_SLIDER_VALUE_TEXT_MAX];
if(slider->type == UI_SLIDER_TYPE_INT) {
stringFormat(
valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%d", slider->value.i
);
slider->cachedMarkerCount = stepCount + 1;
} else {
stringFormat(
valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%.2f", slider->value.f
);
slider->cachedMarkerCount = 0;
}
errorChain(textDraw(
trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y,
valueText, color, &FONT_DEFAULT
));
errorOk();
}
@@ -7,13 +7,14 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
#include "display/spritebatch/spritebatchsprite.h"
#define UI_SLIDER_TRACK_WIDTH 80.0f
#define UI_SLIDER_TRACK_HEIGHT 4.0f
#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f
#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f
#define UI_SLIDER_GAP 4.0f
#define UI_SLIDER_VALUE_TEXT_MAX 16
/**
* Number of discrete steps below which an int slider renders individual
@@ -34,13 +35,24 @@ typedef union {
} uislidervalue_t;
typedef struct {
const char_t *label;
uiwidgetlabel_t label;
uiwidgetlabel_t valueLabel;
uislidertype_t type;
uislidervalue_t value;
uislidervalue_t min;
uislidervalue_t max;
uislidervalue_t step;
bool_t highlighted;
// Track/fill/marker geometry, cached relative to origin (0,0) and only
// rebuilt when the value or step configuration changes (see
// uiSliderRebuildGeometry) -- uiSliderDraw just translates these into
// position instead of re-deriving them every frame.
spritebatchsprite_t cachedTrack;
spritebatchsprite_t cachedFill;
bool_t cachedFillVisible;
spritebatchsprite_t cachedMarkers[UI_SLIDER_STEP_MARKERS_MAX];
int32_t cachedMarkerCount;
} uislider_t;
/**
@@ -183,3 +195,21 @@ errorret_t uiSliderDraw(
const float_t x,
const float_t y
);
/**
* Rebuilds the slider's cached value label from its current value.
* Called internally whenever value changes.
*
* @param slider The slider to update.
*/
void uiSliderRebuildValueLabel(uislider_t *slider);
/**
* Rebuilds the slider's cached track/fill/marker geometry (relative to
* origin (0,0)) from its current label size, value, and step
* configuration. Called internally on init and whenever the value
* changes -- uiSliderDraw never recomputes this itself.
*
* @param slider The slider to update.
*/
void uiSliderRebuildGeometry(uislider_t *slider);
@@ -7,7 +7,6 @@
#include "uitab.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
@@ -17,8 +16,10 @@
void uiTabInit(uitab_t *tab, const char_t *label) {
assertNotNull(tab, "Tab cannot be NULL");
assertNotNull(label, "Label cannot be NULL");
memoryZero(tab, sizeof(uitab_t));
tab->label = label;
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
uiWidgetLabelSetText(&tab->label, label);
tab->active = false;
uiTabRebuildBackground(tab);
}
bool_t uiTabIsActive(const uitab_t *tab) {
@@ -38,15 +39,9 @@ errorret_t uiTabDraw(
) {
assertNotNull(tab, "Tab cannot be NULL");
int32_t labelW, labelH;
textMeasure(tab->label, &FONT_DEFAULT, &labelW, &labelH);
spritebatchsprite_t sprite = {
.min = { x, y, 0.0f },
.max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
spritebatchsprite_t sprite = spriteBatchSpriteTranslate(
&tab->cachedBackground, x, y
);
shadermaterial_t material = {
.unlit = {
.color = tab->active ? COLOR_GREEN : COLOR_RED,
@@ -54,9 +49,21 @@ errorret_t uiTabDraw(
}
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
errorChain(spriteBatchFlush());
errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT));
errorChain(uiWidgetLabelDraw(&tab->label, x, y));
errorOk();
}
void uiTabRebuildBackground(uitab_t *tab) {
assertNotNull(tab, "Tab cannot be NULL");
int32_t labelW, labelH;
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
tab->cachedBackground = (spritebatchsprite_t){
.min = { 0.0f, 0.0f, 0.0f },
.max = { (float_t)labelW, (float_t)labelH, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
}
@@ -7,10 +7,17 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uiwidgetlabel.h"
#include "display/spritebatch/spritebatchsprite.h"
typedef struct {
const char_t *label;
uiwidgetlabel_t label;
bool_t active;
// Background quad, cached relative to origin (0,0) from the label's
// size at init -- uiTabDraw only translates this into position, since
// active/inactive only changes the material color, not the geometry.
spritebatchsprite_t cachedBackground;
} uitab_t;
/**
@@ -51,3 +58,13 @@ errorret_t uiTabDraw(
const float_t x,
const float_t y
);
/**
* Rebuilds the tab's cached background quad (relative to origin (0,0))
* from its current label size. Called internally by uiTabInit -- the
* background's geometry never changes afterwards, since the tab has no
* API to change its label text.
*
* @param tab The tab to update.
*/
void uiTabRebuildBackground(uitab_t *tab);
+66
View File
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiwidgetlabel.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "display/text/text.h"
void uiWidgetLabelInit(uiwidgetlabel_t *label, font_t *font) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(font, "Font cannot be NULL");
memoryZero(label, sizeof(uiwidgetlabel_t));
label->font = font;
label->color = COLOR_WHITE;
}
void uiWidgetLabelSetText(uiwidgetlabel_t *label, const char_t *text) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(text, "Text cannot be NULL");
assertStrLenMax(text, UI_WIDGET_LABEL_TEXT_MAX, "Label text too long");
stringCopy(label->text, text, UI_WIDGET_LABEL_TEXT_MAX);
label->spriteCount = textBuildSpriteCache(
label->text, label->font, label->sprites,
UI_WIDGET_LABEL_SPRITE_COUNT_MAX, &label->width, &label->height
);
}
void uiWidgetLabelSetColor(uiwidgetlabel_t *label, const color_t color) {
assertNotNull(label, "Label cannot be NULL");
label->color = color;
}
void uiWidgetLabelGetSize(
const uiwidgetlabel_t *label,
int32_t *outWidth,
int32_t *outHeight
) {
assertNotNull(label, "Label cannot be NULL");
assertNotNull(outWidth, "Output width cannot be NULL");
assertNotNull(outHeight, "Output height cannot be NULL");
*outWidth = label->width;
*outHeight = label->height;
}
errorret_t uiWidgetLabelDraw(
const uiwidgetlabel_t *label,
const float_t x,
const float_t y
) {
assertNotNull(label, "Label cannot be NULL");
if(label->spriteCount == 0) errorOk();
spritebatchsprite_t scratch[UI_WIDGET_LABEL_SPRITE_COUNT_MAX];
errorChain(textDrawSpriteCache(
label->sprites, label->spriteCount, scratch, x, y, label->color,
label->font->texture
));
errorOk();
}
+90
View File
@@ -0,0 +1,90 @@
/**
* 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/text/font.h"
#include "display/spritebatch/spritebatchsprite.h"
#include "display/color.h"
// A cached, cheap-to-redraw label sized for embedding directly inside
// small widgets (buttons, checkboxes, sliders, ...) rather than
// referencing text by pointer and re-deriving glyph geometry every
// frame. Capacity is intentionally much smaller than uilabel_t's -- it
// gets embedded by value in every widget instance (and inside
// uimenuitem_t's union, which is often arrayed), so keeping it small
// matters on memory-constrained targets. See uilabel_t for the
// general-purpose, larger-capacity equivalent.
#define UI_WIDGET_LABEL_TEXT_MAX 64
#define UI_WIDGET_LABEL_SPRITE_COUNT_MAX UI_WIDGET_LABEL_TEXT_MAX
typedef struct {
char_t text[UI_WIDGET_LABEL_TEXT_MAX];
color_t color;
font_t *font;
spritebatchsprite_t sprites[UI_WIDGET_LABEL_SPRITE_COUNT_MAX];
uint32_t spriteCount;
int32_t width;
int32_t height;
} uiwidgetlabel_t;
/**
* Initializes a widget label, defaulting to white text and no text set.
*
* @param label The label to initialize.
* @param font Font to use for rendering. Must outlive the label.
*/
void uiWidgetLabelInit(uiwidgetlabel_t *label, font_t *font);
/**
* Sets the label's text, rebuilding its cached sprites immediately.
* Call this whenever the displayed text changes -- uiWidgetLabelDraw
* never recomputes it.
*
* @param label The label to update.
* @param text Null-terminated string to display. Must be shorter than
* UI_WIDGET_LABEL_TEXT_MAX.
*/
void uiWidgetLabelSetText(uiwidgetlabel_t *label, const char_t *text);
/**
* Sets the label's tint color. Cheap -- doesn't touch the sprite cache.
*
* @param label The label to update.
* @param color The new tint color.
*/
void uiWidgetLabelSetColor(uiwidgetlabel_t *label, const color_t color);
/**
* Gets the measured size (in pixels) of the label's current text, cached
* from the last uiWidgetLabelSetText call.
*
* @param label The label to query.
* @param outWidth Pointer to store the width.
* @param outHeight Pointer to store the height.
*/
void uiWidgetLabelGetSize(
const uiwidgetlabel_t *label,
int32_t *outWidth,
int32_t *outHeight
);
/**
* Draws the label's cached sprites at the given screen position in a
* single batched buffer call. Does not recompute glyph layout.
*
* @param label The label to draw.
* @param x Screen x position.
* @param y Screen y position.
* @return Any error that occurs.
*/
errorret_t uiWidgetLabelDraw(
const uiwidgetlabel_t *label,
const float_t x,
const float_t y
);
@@ -3,8 +3,8 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetchunkloader.c
uitextbox.c
uitextboxmain.c
)
@@ -33,6 +33,7 @@ void uiTextboxInit(
box->maxLength = maxLength;
box->lines = lines;
box->linesMax = linesMax;
box->glyphsBuiltForPage = -1;
}
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
@@ -43,6 +44,7 @@ void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
box->scroll = 0;
box->layoutWidth = 0.0f;
box->layoutHeight = 0.0f;
box->glyphsBuiltForPage = -1;
}
void uiTextboxBuildLayout(
@@ -52,6 +54,7 @@ void uiTextboxBuildLayout(
) {
assertNotNull(box, "Textbox cannot be NULL");
box->glyphsBuiltForPage = -1;
box->layoutWidth = width;
box->layoutHeight = height;
box->lineCount = 0;
@@ -138,9 +141,9 @@ void uiTextboxBuildLayout(
errorret_t uiTextboxUpdate(uitextbox_t *box) {
assertNotNull(box, "Textbox cannot be NULL");
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) errorOk();
#endif
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) errorOk();
#endif
if(!uiTextboxPageIsComplete(box)) {
box->scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK;
@@ -169,13 +172,30 @@ errorret_t uiTextboxDraw(
uiTextboxBuildLayout(box, contentW, contentH);
}
errorChain(uiFrameDraw(x, y, width, height));
errorChain(spriteBatchFlush());
errorChain(uiFrameDrawCached(&box->frameCache, x, y, width, height));
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
if(box->glyphsBuiltForPage != box->currentPage) {
uiTextboxBuildPageGlyphs(box);
}
if(box->glyphCount == 0) errorOk();
int32_t visibleCount = 0;
while(
visibleCount < box->glyphCount &&
box->glyphs[visibleCount].revealAt <= box->scroll
) visibleCount++;
if(visibleCount == 0) errorOk();
spritebatchsprite_t scratch[UI_TEXTBOX_PAGE_GLYPHS_MAX];
for(int32_t i = 0; i < visibleCount; i++) {
scratch[i] = spriteBatchSpriteTranslate(
&box->glyphs[i].sprite, contentX, contentY
);
}
shadermaterial_t material = {
.unlit = {
@@ -183,35 +203,47 @@ errorret_t uiTextboxDraw(
.texture = FONT_DEFAULT.texture
}
};
errorChain(spriteBatchBuffer(scratch, visibleCount, &SHADER_UNLIT, material));
errorOk();
}
void uiTextboxBuildPageGlyphs(uitextbox_t *box) {
assertNotNull(box, "Textbox cannot be NULL");
box->glyphCount = 0;
int32_t pageFirst = box->currentPage * box->linesPerPage;
int32_t pageLast = pageFirst + box->linesPerPage;
if(pageLast > box->lineCount) pageLast = box->lineCount;
int32_t charsLeft = box->scroll;
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) {
int32_t consumed = 0;
for(int32_t li = pageFirst; li < pageLast; li++) {
uitextboxline_t *line = &box->lines[li];
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
float_t lineY = contentY +
float_t lineY =
(float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
for(int32_t ci = 0; ci < visible; ci++) {
for(int32_t ci = 0; ci < line->count; ci++) {
consumed++;
char_t c = box->text[line->start + ci];
if(c == ' ') continue;
spritebatchsprite_t sprite = textGetSprite(
(vec2){ contentX + (float_t)ci * fontW, lineY },
c,
&FONT_DEFAULT
);
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
}
charsLeft -= visible;
assertTrue(
box->glyphCount < UI_TEXTBOX_PAGE_GLYPHS_MAX,
"Textbox page produces too many glyphs"
);
uitextboxglyph_t *glyph = &box->glyphs[box->glyphCount++];
glyph->sprite = textGetSprite(
(vec2){ (float_t)ci * fontW, lineY }, c, &FONT_DEFAULT
);
glyph->revealAt = consumed;
}
}
errorChain(spriteBatchFlush());
errorOk();
box->glyphsBuiltForPage = box->currentPage;
}
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
@@ -7,16 +7,34 @@
#pragma once
#include "error/error.h"
#include "ui/frame/uiframe.h"
#include "display/spritebatch/spritebatchsprite.h"
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
#define UI_TEXTBOX_LINE_SPACING 0.0f
// Fixed capacity for a page's cached glyph sprites (see uitextboxglyph_t).
// Sized generously above UI_TEXTBOX_LINES_PER_PAGE_MAX worth of glyphs at
// typical textbox widths; uiTextboxBuildPageGlyphs asserts if a page
// somehow produces more than this.
#define UI_TEXTBOX_PAGE_GLYPHS_MAX 512
typedef struct {
int32_t start;
int32_t count;
} uitextboxline_t;
// A single visible glyph's cached sprite (relative to the textbox's
// content origin, i.e. (0,0)) plus the scroll value at/after which it
// becomes visible -- lets uiTextboxDraw turn the typewriter scroll into
// a simple prefix-count instead of recomputing glyph geometry every
// frame.
typedef struct {
spritebatchsprite_t sprite;
int32_t revealAt;
} uitextboxglyph_t;
typedef struct {
char_t *text;
uint32_t maxLength;
@@ -34,6 +52,15 @@ typedef struct {
int32_t currentPage;
int32_t scroll;
// Cached glyph sprites for the current page (see uitextboxglyph_t),
// rebuilt only when currentPage no longer matches
// glyphsBuiltForPage -- not every frame/scroll tick.
uitextboxglyph_t glyphs[UI_TEXTBOX_PAGE_GLYPHS_MAX];
int32_t glyphCount;
int32_t glyphsBuiltForPage;
uiframecache_t frameCache;
} uitextbox_t;
/**
@@ -136,3 +163,13 @@ bool_t uiTextboxHasNextPage(const uitextbox_t *box);
* @param box The textbox to advance.
*/
void uiTextboxNextPage(uitextbox_t *box);
/**
* Rebuilds the cached glyph sprites (see uitextboxglyph_t) for the
* current page from its line layout. Called automatically by
* uiTextboxDraw whenever currentPage no longer matches the page the
* cache was last built for.
*
* @param box The textbox to rebuild.
*/
void uiTextboxBuildPageGlyphs(uitextbox_t *box);
@@ -79,7 +79,6 @@ errorret_t uiTextboxMainDraw(void) {
&FONT_DEFAULT
);
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
errorChain(spriteBatchFlush());
errorOk();
}
@@ -6,7 +6,7 @@
*/
#pragma once
#include "ui/rpg/textbox/uitextbox.h"
#include "uitextbox.h"
#include "ui/focus/uifocusitem.h"
#define UI_TEXTBOX_MAIN_LINES 4
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uitestlabel.h"
#include "ui/widget/uilabel.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "engine/engine.h"
uilabel_t UI_TEST_LABEL;
errorret_t uiTestLabelInit() {
uiLabelInit(&UI_TEST_LABEL, &FONT_DEFAULT);
uiLabelSetText(&UI_TEST_LABEL, "duskrpg");
errorOk();
}
errorret_t uiTestLabelDraw() {
int32_t versionWidth, versionHeight;
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
errorChain(uiLabelDraw(
&UI_TEST_LABEL,
(float_t)(SCREEN.scanX + SCREEN.scanWidth - UI_TEST_LABEL.width),
(float_t)(
SCREEN.scanY + SCREEN.scanHeight - versionHeight -
UI_TEST_LABEL.height
)
));
errorOk();
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
/**
* Initializes the test label, a smoke test for the duskrpg-side UI
* element extension point (uielementgame.h).
*
* @return Any error that occurs.
*/
errorret_t uiTestLabelInit();
/**
* Draws the test label above the engine's debug version number.
*
* @return Any error that occurs.
*/
errorret_t uiTestLabelDraw();
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
# Tests
dusktest(test_uiframe.c)
dusktest(test_uislider.c)
dusktest(test_uitab.c)
+68
View File
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "ui/frame/uiframe.h"
static void test_uiFrameBuildSprites_layout(void **state) {
UI_FRAME.tileset = (tileset_t){
.tileWidth = 1,
.tileHeight = 1,
.tileCount = 9,
.columns = 3,
.rows = 3,
.uv = { 0.25f, 0.25f }
};
const float_t x = 100.0f;
const float_t y = 50.0f;
const float_t width = 40.0f;
const float_t height = 30.0f;
const float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
const float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
spritebatchsprite_t sprites[9];
uiFrameBuildSprites(sprites, x, y, width, height);
// Top-left corner sits exactly at the rect's origin.
assert_float_equal(sprites[0].min[0], x, 0.0001f);
assert_float_equal(sprites[0].min[1], y, 0.0001f);
assert_float_equal(sprites[0].max[0], x + tileW, 0.0001f);
assert_float_equal(sprites[0].max[1], y + tileH, 0.0001f);
assert_float_equal(sprites[0].uvMin[0], 0.0f, 0.0001f);
assert_float_equal(sprites[0].uvMin[1], 0.0f, 0.0001f);
assert_float_equal(sprites[0].uvMax[0], 0.25f, 0.0001f);
assert_float_equal(sprites[0].uvMax[1], 0.25f, 0.0001f);
// Top-middle edge stretches to fill the width minus both corners.
assert_float_equal(sprites[1].min[0], x + tileW, 0.0001f);
assert_float_equal(sprites[1].max[0], x + width - tileW, 0.0001f);
assert_float_equal(sprites[1].max[1], y + tileH, 0.0001f);
// Center tile fills the remaining interior on both axes.
assert_float_equal(sprites[4].min[0], x + tileW, 0.0001f);
assert_float_equal(sprites[4].min[1], y + tileH, 0.0001f);
assert_float_equal(sprites[4].max[0], x + width - tileW, 0.0001f);
assert_float_equal(sprites[4].max[1], y + height - tileH, 0.0001f);
// Bottom-right corner sits exactly at the rect's far corner, sampling
// tileset column 2, row 2.
assert_float_equal(sprites[8].min[0], x + width - tileW, 0.0001f);
assert_float_equal(sprites[8].min[1], y + height - tileH, 0.0001f);
assert_float_equal(sprites[8].max[0], x + width, 0.0001f);
assert_float_equal(sprites[8].max[1], y + height, 0.0001f);
assert_float_equal(sprites[8].uvMin[0], 0.5f, 0.0001f);
assert_float_equal(sprites[8].uvMin[1], 0.5f, 0.0001f);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_uiFrameBuildSprites_layout),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "ui/widget/uislider.h"
#include "util/memory.h"
// uiSliderInitFloat/InitInt require a real font (FONT_DEFAULT) to build
// the label's glyph cache, which needs a live GL context this test binary
// doesn't have -- see the label size caching contract for why this is
// safe to bypass here (uiSliderRebuildGeometry only reads slider->label's
// already-measured width/height, it never touches the font itself).
static void sliderSetupFloat(
uislider_t *slider,
const float_t value,
const float_t min,
const float_t max,
const float_t step
) {
memoryZero(slider, sizeof(uislider_t));
slider->label.width = 20;
slider->label.height = 10;
slider->type = UI_SLIDER_TYPE_FLOAT;
slider->min.f = min;
slider->max.f = max;
slider->step.f = step;
slider->value.f = value;
}
static void sliderSetupInt(
uislider_t *slider,
const int32_t value,
const int32_t min,
const int32_t max,
const int32_t step
) {
memoryZero(slider, sizeof(uislider_t));
slider->label.width = 20;
slider->label.height = 10;
slider->type = UI_SLIDER_TYPE_INT;
slider->min.i = min;
slider->max.i = max;
slider->step.i = step;
slider->value.i = value;
}
static void test_uiSliderRebuildGeometry_trackPosition(void **state) {
uislider_t slider;
sliderSetupFloat(&slider, 0.0f, 0.0f, 1.0f, 0.1f);
uiSliderRebuildGeometry(&slider);
const float_t expectedX = (float_t)slider.label.width + UI_SLIDER_GAP;
const float_t expectedY =
((float_t)slider.label.height - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
assert_float_equal(slider.cachedTrack.min[0], expectedX, 0.0001f);
assert_float_equal(slider.cachedTrack.min[1], expectedY, 0.0001f);
assert_float_equal(
slider.cachedTrack.max[0], expectedX + UI_SLIDER_TRACK_WIDTH, 0.0001f
);
}
static void test_uiSliderRebuildGeometry_fillVisibility(void **state) {
uislider_t slider;
// At the minimum value, the ratio is 0 -- no fill should be visible.
sliderSetupFloat(&slider, 0.0f, 0.0f, 10.0f, 1.0f);
uiSliderRebuildGeometry(&slider);
assert_false(slider.cachedFillVisible);
// Halfway to max -- fill should cover half the track.
sliderSetupFloat(&slider, 5.0f, 0.0f, 10.0f, 1.0f);
uiSliderRebuildGeometry(&slider);
assert_true(slider.cachedFillVisible);
assert_float_equal(
slider.cachedFill.max[0] - slider.cachedFill.min[0],
UI_SLIDER_TRACK_WIDTH * 0.5f,
0.0001f
);
}
static void test_uiSliderRebuildGeometry_stepMarkers(void **state) {
uislider_t slider;
// 5 steps between 0 and 10 -- under the marker cap, so markers render.
sliderSetupInt(&slider, 0, 0, 10, 2);
uiSliderRebuildGeometry(&slider);
assert_int_equal(slider.cachedMarkerCount, 6);
assert_float_equal(
slider.cachedMarkers[0].min[0], slider.cachedTrack.min[0] -
UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
0.0001f
);
// 100 steps -- over the marker cap, so it falls back to a smooth fill
// with no markers.
sliderSetupInt(&slider, 0, 0, 100, 1);
uiSliderRebuildGeometry(&slider);
assert_int_equal(slider.cachedMarkerCount, 0);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_uiSliderRebuildGeometry_trackPosition),
cmocka_unit_test(test_uiSliderRebuildGeometry_fillVisibility),
cmocka_unit_test(test_uiSliderRebuildGeometry_stepMarkers),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "ui/widget/uitab.h"
#include "util/memory.h"
// uiTabInit requires a real font (FONT_DEFAULT) to build the label's
// glyph cache, which needs a live GL context this test binary doesn't
// have. uiTabRebuildBackground only reads the label's already-measured
// width/height, so it's safe to set those directly and exercise it here.
static void test_uiTabRebuildBackground_matchesLabelSize(void **state) {
uitab_t tab;
memoryZero(&tab, sizeof(uitab_t));
tab.label.width = 42;
tab.label.height = 12;
uiTabRebuildBackground(&tab);
assert_float_equal(tab.cachedBackground.min[0], 0.0f, 0.0001f);
assert_float_equal(tab.cachedBackground.min[1], 0.0f, 0.0001f);
assert_float_equal(tab.cachedBackground.max[0], 42.0f, 0.0001f);
assert_float_equal(tab.cachedBackground.max[1], 12.0f, 0.0001f);
}
int main(int argc, char **argv) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_uiTabRebuildBackground_matchesLabelSize),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-25
View File
@@ -1,25 +0,0 @@
{
"pause": "DEFAULT",
"items": [
{ "type": "text", "text": "Test Two." },
{ "type": "entityAdd", "entityType": "npc", "position": [4, 4, 0] },
{
"type": "textMini",
"text": "Hello!",
"position": [4, 4, 0],
"duration": 3.0
},
{
"type": "emoji",
"entityIndex": "lastCreated",
"emojiType": "exclamation",
"duration": 2.0
},
{
"type": "entityWalkTo",
"entityIndex": "lastCreated",
"positions": [[8, 2, 0]]
},
{ "type": "text", "text": "Done." }
]
}
-1
View File
@@ -1 +0,0 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_-1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_4_4.json", "offset": [2.0, 1.0, 0.0]}, {"model": "models/buildings/house_6_3.json", "offset": [1.0, 9.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [12.0, 6.0, 0.0]}]}
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_0_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_8_4.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_3_3.json", "offset": [11.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_2.json", "offset": [3.0, 11.0, 0.0]}]}
-1
View File
@@ -1 +0,0 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_0_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_3_2.json", "offset": [1.0, 1.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [7.0, 2.0, 0.0]}, {"model": "models/buildings/house_4_2.json", "offset": [1.0, 8.0, 0.0]}, {"model": "models/buildings/house_1_1.json", "offset": [11.0, 10.0, 0.0]}, {"model": "models/buildings/house_5_2.json", "offset": [13.0, 6.0, 0.0]}]}
-1
View File
@@ -1 +0,0 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_1_1_0_0.json", "offset": [0.0, 0.0, 0.0]}, {"model": "models/buildings/house_5_5.json", "offset": [3.0, 2.0, 0.0]}, {"model": "models/buildings/house_2_3.json", "offset": [10.0, 3.0, 0.0]}, {"model": "models/buildings/house_3_1.json", "offset": [1.0, 12.0, 0.0]}]}
-1
View File
@@ -1 +0,0 @@
{"tiles": [[1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [0, 0], [0, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [4, 1], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [4, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0], [1, 0]], "meshes": [{"model": "models/chunks/chunk_2_0_0_0.json", "offset": [0.0, 0.0, 0.0]}]}
-13
View File
@@ -1,13 +0,0 @@
{
"name": "Test Map",
"entities": [
{ "type": "player", "position": [10, 2, 0], "direction": "north" },
{ "type": "item", "position": [12, 2, 0], "item": "POTION", "quantity": 1 },
{
"type": "npc",
"position": [8, 8, 1],
"path": [[4, 4, 0], [10, 10, 1], [4, 4, 0], [10, 10, 1]],
"cutscene": "test_npc"
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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