Compare commits

125 Commits

Author SHA1 Message Date
YourWishes 82c300b077 Add some script modules 2026-06-02 12:55:32 -05:00
YourWishes 0f8b629e20 Add logging to Wii 2026-06-02 11:01:54 -05:00
YourWishes 36f6ac65f2 Builds and works on Gamecube 2026-06-02 09:53:56 -05:00
YourWishes a25871a849 Test sprite from script 2026-06-02 09:32:07 -05:00
YourWishes 57766a9104 Merge branch 'main' into scriptentity 2026-06-02 07:36:44 -05:00
YourWishes 3770ae1645 Fix tests? 2026-06-02 07:35:28 -05:00
YourWishes d73edb403f Example Camera 2026-06-01 23:04:55 -05:00
YourWishes b14196ff0d Basic entity script 2026-06-01 22:56:37 -05:00
YourWishes 88903fee94 No need for asset batching on text.c 2026-06-01 22:36:02 -05:00
YourWishes 1e8311fc04 Add asset batch 2026-06-01 22:34:44 -05:00
YourWishes 2b78370cb8 Add asset reaping 2026-06-01 22:20:57 -05:00
YourWishes 8f78bba9e9 Restoring JerryScript a bit cleaner 2026-06-01 21:52:36 -05:00
YourWishes 41a4be678e Added a tiny sleep on assets to stop pegging the CPU 2026-06-01 15:48:10 -05:00
YourWishes 8b2b4b7c3d Fixed JSON loader, added some tests 2026-06-01 15:31:22 -05:00
YourWishes 1f3a29f89d Asyncify other loaders 2026-06-01 15:10:58 -05:00
YourWishes c4c93097cd Add async texture loading 2026-06-01 14:53:18 -05:00
YourWishes eedb7769e6 Add some extra tests 2026-06-01 13:48:29 -05:00
YourWishes 98db62a4bc Add some more tests, prepping for asset testing 2026-06-01 13:37:14 -05:00
YourWishes df48c8e500 Consistency and fixing thread unit tests 2026-06-01 11:33:27 -05:00
YourWishes db9cc0f4c6 Add thread tests 2026-06-01 10:59:56 -05:00
YourWishes a79ee429b4 Prepping for async 2026-06-01 10:57:40 -05:00
YourWishes 6acfca6d48 Consistent 2026-05-30 20:30:13 -05:00
YourWishes 1cd6f4cb72 First refactor of new asset system 2026-05-30 08:21:58 -05:00
YourWishes 3271e8c7d6 FInished porting last asset loader types 2026-05-30 07:59:06 -05:00
YourWishes 0bcde064af Asset refactor, phase one. 2026-05-29 14:27:40 -05:00
YourWishes 957980b3c5 Updating event handler 2026-05-28 14:22:13 -05:00
YourWishes 03eb328d81 Allow dynamic trace on any platform that can support it. 2026-05-28 11:21:36 -05:00
YourWishes e1716a741f Trigger test 2026-05-26 22:18:41 -05:00
YourWishes e24707c847 Scene loading example 2026-05-26 21:42:37 -05:00
YourWishes 7c4b8c307f Fix flocking bug 2026-05-26 20:24:34 -05:00
YourWishes 109318aeaf Remove useless void checks 2026-05-26 19:24:17 -05:00
YourWishes 1f2657cea0 Spritebatch cleanup 2026-05-26 19:07:07 -05:00
YourWishes 382c435bac Entity refactoring 2026-05-22 23:01:45 -05:00
YourWishes 130fe4ca5d Delete JS assets 2026-05-22 00:00:23 -05:00
YourWishes 31ba3fe127 add build to corner of screen 2026-05-21 23:59:26 -05:00
YourWishes f68b31158f Asset refactor 2026-05-21 23:42:56 -05:00
YourWishes 653ca9a72d PSP rendering fix 2026-05-21 22:07:56 -05:00
YourWishes ba7857f4df Fix rendering 2026-05-21 18:24:18 -05:00
YourWishes 23e617ea21 Optimizing entityposition as much as possible. 2026-05-21 13:17:48 -05:00
YourWishes cdf5a5229c Refactor cleaned a few things 2026-05-21 12:52:23 -05:00
YourWishes f841a35a53 Revert "Disable old ent code"
This reverts commit efd31237be.
2026-05-21 11:07:21 -05:00
YourWishes efd31237be Disable old ent code 2026-05-21 10:18:20 -05:00
YourWishes 6502822583 Update render, spritebatch and input stuffs. 2026-05-21 09:51:56 -05:00
YourWishes a9e6f2b2a5 Scene rendering native. 2026-05-20 23:45:27 -05:00
YourWishes 510a94b42c Remove Jerryscript further 2026-05-20 21:34:00 -05:00
YourWishes d805be47ce No script 2026-05-20 20:58:56 -05:00
YourWishes f9ea8e380a Temporarily disable save code 2026-05-20 09:52:07 -05:00
YourWishes 5cb05beb30 Fix dolphin compile 2026-05-19 23:24:27 -05:00
YourWishes 677768e6ab Map Base 2026-05-19 23:13:41 -05:00
YourWishes ed6c951783 Script improvements 2026-05-17 23:40:42 -05:00
YourWishes 54254348b8 Add parent/child 2026-05-17 21:46:08 -05:00
YourWishes 782fd07a8d Savestream update 2026-05-16 17:51:00 -05:00
YourWishes a8fd55cb38 Save file update (incomplete) 2026-05-10 11:20:09 -05:00
YourWishes d7f515575a Working on burned DVD for gamecube 2026-05-09 00:14:28 -05:00
YourWishes bafbf2ec2f Fix compile error 2026-05-08 23:11:20 -05:00
YourWishes 7415944e0a scripting improvements 2026-05-08 22:46:24 -05:00
YourWishes 1ff990ff44 Add strided memory pushing and improved spritebatching 2026-05-08 20:53:05 -05:00
YourWishes 73e73d8772 Cleanup animation 2026-05-08 15:44:55 -05:00
YourWishes 6d876bb767 Anim tweak 2026-05-07 19:37:30 -05:00
YourWishes 2be0fe9f06 Fix wii build 2026-05-07 17:54:10 -05:00
YourWishes e1fb082927 Increase spritebatch flushing count 2026-05-07 17:47:39 -05:00
YourWishes 65ca5ae4c4 Dolphin Bootable ISO working! 2026-05-07 17:38:41 -05:00
YourWishes 2cea43dc70 Switch to ogc2 2026-05-07 17:21:52 -05:00
YourWishes 44a0700800 Fixed memalign again 2026-05-07 14:11:59 -05:00
YourWishes 1613a378f1 Added memalign 2026-05-07 12:39:07 -05:00
YourWishes deed98a27d 2026-05-07 12:31:22 -05:00
YourWishes 9d0cb8fb46 ISO build (partial) 2026-05-07 12:18:30 -05:00
YourWishes d8fe0f6923 textbox 2026-05-06 22:42:28 -05:00
YourWishes 581dbc2b3c Update linux docker 2026-05-06 20:31:50 -05:00
YourWishes 7301d2ad76 luce bree 2026-05-06 20:24:16 -05:00
YourWishes 3232a14d1d Consistent build 2026-05-06 14:40:06 -05:00
YourWishes 84c1f88d42 Fix PSP blending issues 2026-05-06 11:17:34 -05:00
YourWishes 3695b10e4b Easing test 2026-05-05 22:24:25 -05:00
YourWishes 6da02b25fa Testing cutscenes 2026-05-05 22:10:47 -05:00
YourWishes 3bc544fba1 Re-enable build pulling fetched modules 2026-05-05 19:29:48 -05:00
YourWishes 368d370f49 Cleanup modules 2026-05-05 19:29:29 -05:00
YourWishes bb29c0edef Working on some script modules 2026-05-05 16:22:04 -05:00
YourWishes 6edcf75a0c add display state 2026-05-04 22:16:30 -05:00
YourWishes 31cc186424 Fixed small compile bugs 2026-05-04 08:39:47 -05:00
YourWishes 0e94c1fa6d Fixed a bunch of messy over 80 char lines 2026-05-04 08:29:43 -05:00
YourWishes 6d9e2dd3e1 UI first pass 2026-05-03 21:52:12 -05:00
YourWishes 4a4adeb3c8 Finally fixed linux asset weirdness 2026-05-02 15:18:49 -05:00
YourWishes ff77f8cfa0 Fix dolphin rendering 2026-05-01 23:11:59 -05:00
YourWishes 36db89c36e Nuke the old input system, use the new UI system 2026-05-01 15:21:46 -05:00
YourWishes a9948142ad Fix linux warning 2026-05-01 14:00:24 -05:00
YourWishes 8d05510584 Fix linux building 2026-05-01 13:58:05 -05:00
YourWishes d373de7a29 Some adjustments 2026-05-01 13:44:51 -05:00
YourWishes 1efa9a9f7b More cleanup 2026-05-01 09:43:50 -05:00
YourWishes 0fb3ba2f91 Cleanup, prepping for example game stuff 2026-04-30 23:43:49 -05:00
YourWishes 3b4c5b5153 Added FPS meter 2026-04-30 23:34:32 -05:00
YourWishes 9293aeeec8 Fix position 2026-04-30 23:18:36 -05:00
YourWishes 03ae83b119 More cleanup? 2026-04-30 23:07:17 -05:00
YourWishes abd63cc6cf More cleanup 2026-04-30 22:40:32 -05:00
YourWishes 2e43aa2c44 Bit more cleanup 2026-04-30 20:03:44 -05:00
YourWishes 3d984e13c2 Module input improvements 2026-04-29 23:40:01 -05:00
YourWishes 010900fe21 Better again. 2026-04-29 23:26:21 -05:00
YourWishes ffed626447 More cleanup 2026-04-29 22:39:47 -05:00
YourWishes 61f69af35a Refactor pass 1 2026-04-29 14:53:35 -05:00
YourWishes bd248ee91c Build script on PSP, Dolphin and Engine. 2026-04-28 21:34:09 -05:00
YourWishes 194255bffe Fix merge conflcits 2026-04-28 14:02:59 -05:00
YourWishes 52ee627079 Merge branch 'jerryscript' into playertest 2026-04-28 14:02:53 -05:00
YourWishes bd4200e707 Finished getting JerryScript on all the platforms. 2026-04-28 13:59:46 -05:00
YourWishes 73e7d6c7f3 Add epoch 2026-04-28 10:33:23 -05:00
YourWishes a41b0e916b prog 2026-04-28 08:04:01 -05:00
YourWishes 19f2a2c616 Bit more consistent but still far from perfect 2026-04-27 09:14:14 -05:00
YourWishes 998601f722 Playertest: scene/script system refactor and Wii ABI fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 23:30:08 -05:00
YourWishes 7c3386cf3e Add entity scripting 2026-04-20 17:01:12 -05:00
YourWishes d161182997 Entity modules 2026-04-20 16:50:16 -05:00
YourWishes 1646dc2dbd Fixed build 2026-04-20 15:43:18 -05:00
YourWishes b640295be2 Scene script 2026-04-20 15:34:24 -05:00
YourWishes b89ae2391b Reg console. 2026-04-20 14:31:22 -05:00
YourWishes a0fad441d0 Updated bind command 2026-04-20 12:59:25 -05:00
YourWishes 340084dac3 Removed console aliases 2026-04-20 12:49:06 -05:00
YourWishes c78135aa09 Fixed bugs with console 2026-04-20 12:05:35 -05:00
YourWishes d19f8bbd30 Restored console, has a bug 2026-04-20 09:26:25 -05:00
YourWishes 4205899f5a No idea why gamecube is crashing, disabling this for now 2026-04-18 21:57:57 -05:00
YourWishes 7dd3940770 Moved code to dolphin for network 2026-04-18 17:41:30 -05:00
YourWishes 00d94e3015 Slight wii improvements 2026-04-18 16:01:53 -05:00
YourWishes 7bacb3ee2b Testing on real wii hardware some more 2026-04-18 15:59:25 -05:00
YourWishes 8e49be5ac4 Testing some wii rendering bugs 2026-04-18 15:29:40 -05:00
YourWishes 3b94598d2c Fixed dolphin matricies the ugly way 2026-04-18 00:36:35 -05:00
YourWishes bddc9af3b6 "Improved" Dolphin matricies slightly 2026-04-18 00:32:50 -05:00
YourWishes 2451d73a7c Improved Wii aspect ratio significantly 2026-04-17 23:49:39 -05:00
YourWishes 1dd2efa182 Dolphin compiles, network untested 2026-04-17 22:53:49 -05:00
YourWishes acea610773 Disable curl on linux 2026-04-17 22:53:29 -05:00
463 changed files with 20100 additions and 5791 deletions
+61 -17
View File
@@ -53,21 +53,21 @@ jobs:
path: ./git-artifcats/Dusk path: ./git-artifcats/Dusk
if-no-files-found: error if-no-files-found: error
build-vita: # build-vita:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- name: Checkout repository # - name: Checkout repository
uses: actions/checkout@v6 # uses: actions/checkout@v6
- name: Set up Docker # - name: Set up Docker
uses: docker/setup-docker-action@v5 # uses: docker/setup-docker-action@v5
- name: Build Vita # - name: Build Vita
run: ./scripts/build-vita-docker.sh # run: ./scripts/build-vita-docker.sh
- name: Upload Vita binary # - name: Upload Vita binary
uses: actions/upload-artifact@v6 # uses: actions/upload-artifact@v6
with: # with:
name: dusk-vita # name: dusk-vita
path: build-vita/Dusk.vpk # path: build-vita/Dusk.vpk
if-no-files-found: error # if-no-files-found: error
build-knulli: build-knulli:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -110,6 +110,28 @@ jobs:
path: ./git-artifcats/Dusk path: ./git-artifcats/Dusk
if-no-files-found: error if-no-files-found: error
build-gamecube-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build GameCube ISO
run: ./scripts/build-gamecube-iso-docker.sh
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk
cp build-gamecube-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
cp build-gamecube-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
cp build-gamecube-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
- name: Upload GameCube ISO
uses: actions/upload-artifact@v6
with:
name: dusk-gamecube-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii: build-wii:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -122,12 +144,34 @@ jobs:
- name: Copy output files. - name: Copy output files.
run: | run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/Dusk.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
cp docker/dolphin/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
- name: Upload Wii binary - name: Upload Wii binary
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
with: with:
name: dusk-wii name: dusk-wii
path: ./git-artifcats/Dusk path: ./git-artifcats/Dusk
if-no-files-found: error if-no-files-found: error
build-wii-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker
uses: docker/setup-docker-action@v5
- name: Build Wii ISO
run: ./scripts/build-wii-iso-docker.sh
- name: Copy output files.
run: |
mkdir -p ./git-artifcats/Dusk
cp build-wii-iso/Dusk-NTSC-J.iso ./git-artifcats/Dusk/Dusk-NTSC-J.iso
cp build-wii-iso/Dusk-NTSC-U.iso ./git-artifcats/Dusk/Dusk-NTSC-U.iso
cp build-wii-iso/Dusk-PAL.iso ./git-artifcats/Dusk/Dusk-PAL.iso
- name: Upload Wii ISO
uses: actions/upload-artifact@v6
with:
name: dusk-wii-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
+1
View File
@@ -106,3 +106,4 @@ yarn.lock
/build* /build*
/assets/test /assets/test
/tools_old /tools_old
/assets/test.png
+18 -1
View File
@@ -4,14 +4,22 @@
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Setup # Setup
cmake_minimum_required(VERSION 3.18) cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
option(DUSK_BUILD_TESTS "Enable tests" OFF) option(DUSK_BUILD_TESTS "Enable tests" OFF)
# Game identity — override these per-project
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YouWish" CACHE STRING "Game author / coder")
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
set(DUSK_GAME_LONG_DESCRIPTION "No description yet." CACHE STRING "Full description")
# Prep cache # Prep cache
set(DUSK_CACHE_TARGET "dusk-target") set(DUSK_CACHE_TARGET "dusk-target")
@@ -68,10 +76,19 @@ else()
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET}) set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET})
endif() endif()
if(NOT DEFINED DUSK_VERSION)
string(TIMESTAMP DUSK_VERSION "debug-%y%m%d%H%M%S")
endif()
# Definitions # Definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}" DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}"
DUSK_GAME_NAME="${DUSK_GAME_NAME}"
DUSK_GAME_AUTHOR="${DUSK_GAME_AUTHOR}"
DUSK_GAME_SHORT_DESCRIPTION="${DUSK_GAME_SHORT_DESCRIPTION}"
DUSK_GAME_LONG_DESCRIPTION="${DUSK_GAME_LONG_DESCRIPTION}"
DUSK_VERSION="${DUSK_VERSION}"
) )
# Toolchains # Toolchains
+12
View File
@@ -0,0 +1,12 @@
Console.print('This is called from JavaScript');
const platformNames = {
[System.PLATFORM_LINUX]: 'Linux',
[System.PLATFORM_KNULLI]: 'Knulli',
[System.PLATFORM_PSP]: 'PSP',
[System.PLATFORM_GAMECUBE]: 'GameCube',
[System.PLATFORM_WII]: 'Wii',
};
const platformName = platformNames[System.platform] || 'Unknown';
Console.print('Platform: ' + platformName);
-76
View File
@@ -1,76 +0,0 @@
module('input')
module('platform')
module('scene')
module('locale')
-- Default Input bindings.
if PSP then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("accept", INPUT_ACTION_ACCEPT)
inputBind("cancel", INPUT_ACTION_CANCEL)
inputBind("select", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif DOLPHIN then
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("b", INPUT_ACTION_CANCEL)
inputBind("a", INPUT_ACTION_ACCEPT)
inputBind("z", INPUT_ACTION_RAGEQUIT)
inputBind("lstick_up", INPUT_ACTION_UP)
inputBind("lstick_down", INPUT_ACTION_DOWN)
inputBind("lstick_left", INPUT_ACTION_LEFT)
inputBind("lstick_right", INPUT_ACTION_RIGHT)
elseif LINUX then
if INPUT_KEYBOARD then
inputBind("w", INPUT_ACTION_UP)
inputBind("s", INPUT_ACTION_DOWN)
inputBind("a", INPUT_ACTION_LEFT)
inputBind("d", INPUT_ACTION_RIGHT)
inputBind("left", INPUT_ACTION_LEFT)
inputBind("right", INPUT_ACTION_RIGHT)
inputBind("up", INPUT_ACTION_UP)
inputBind("down", INPUT_ACTION_DOWN)
inputBind("enter", INPUT_ACTION_ACCEPT)
inputBind("e", INPUT_ACTION_ACCEPT)
inputBind("q", INPUT_ACTION_CANCEL)
inputBind("escape", INPUT_ACTION_RAGEQUIT)
end
if INPUT_GAMEPAD then
inputBind("gamepad_up", INPUT_ACTION_UP)
inputBind("gamepad_down", INPUT_ACTION_DOWN)
inputBind("gamepad_left", INPUT_ACTION_LEFT)
inputBind("gamepad_right", INPUT_ACTION_RIGHT)
inputBind("gamepad_a", INPUT_ACTION_ACCEPT)
inputBind("gamepad_b", INPUT_ACTION_CANCEL)
inputBind("gamepad_back", INPUT_ACTION_RAGEQUIT)
inputBind("gamepad_lstick_up", INPUT_ACTION_UP)
inputBind("gamepad_lstick_down", INPUT_ACTION_DOWN)
inputBind("gamepad_lstick_left", INPUT_ACTION_LEFT)
inputBind("gamepad_lstick_right", INPUT_ACTION_RIGHT)
end
if INPUT_POINTER then
inputBind("mouse_x", INPUT_ACTION_POINTERX)
inputBind("mouse_y", INPUT_ACTION_POINTERY)
end
else
print("Unknown platform, no default input bindings set.")
end
+24
View File
@@ -0,0 +1,24 @@
// Load rosa.
Console.print('Asset time');
const entry = Asset.lock('test.png', Asset.TYPE_TEXTURE, Texture.FORMAT_RGBA);
Asset.requireLoaded(entry);
Console.print('Asset loaded');
// Camera at (3,3,3) looking at origin
const cam = Entity.create();
const camPos = cam.add(Component.POSITION);
cam.add(Component.CAMERA);
camPos.localPosition = new Vec3(3, 3, 3);
camPos.lookAt(new Vec3(0, 0, 0));
// Test entity at origin
const testEntity = Entity.create();
const testPos = testEntity.add(Component.POSITION);
/** @type {RenderableSpritebatch} */
const testRenderable = testEntity.add(Component.RENDERABLE);
testRenderable.texture = entry.texture;
testRenderable.sprites = [
[0, 0, 1, 1, 0, 1, 1, 0]
];
testPos.localPosition = new Vec3(0, 0, 0);
+6
View File
@@ -0,0 +1,6 @@
module = {
render() {
Text.draw(0, 0, "Hello World");
SpriteBatch.flush();
}
};
+96
View File
@@ -0,0 +1,96 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Turn things off we don't need
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
set(JERRY_EXT ON CACHE BOOL "" FORCE)
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
# Fetch Jerry
include(FetchContent)
FetchContent_Declare(
jerryscript
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
GIT_TAG float32-fix
)
FetchContent_MakeAvailable(jerryscript)
# Mark found
set(jerryscript_FOUND ON)
# Define targets
if(TARGET jerryscript-core)
set(JERRY_CORE_TARGET jerryscript-core)
elseif(TARGET jerry-core)
set(JERRY_CORE_TARGET jerry-core)
endif()
if(TARGET jerryscript-ext)
set(JERRY_EXT_TARGET jerryscript-ext)
elseif(TARGET jerry-ext)
set(JERRY_EXT_TARGET jerry-ext)
endif()
if(TARGET jerryscript-port-default)
set(JERRY_PORT_TARGET jerryscript-port-default)
elseif(TARGET jerry-port-default)
set(JERRY_PORT_TARGET jerry-port-default)
elseif(TARGET jerryscript-port)
set(JERRY_PORT_TARGET jerryscript-port)
elseif(TARGET jerry-port)
set(JERRY_PORT_TARGET jerry-port)
endif()
if(NOT JERRY_CORE_TARGET)
message(FATAL_ERROR "JerryScript core target not found")
endif()
if(NOT JERRY_EXT_TARGET)
message(FATAL_ERROR "JerryScript ext target not found")
endif()
if(NOT JERRY_PORT_TARGET)
message(FATAL_ERROR "JerryScript port target not found")
endif()
foreach(tgt IN ITEMS
${JERRY_CORE_TARGET}
${JERRY_EXT_TARGET}
${JERRY_PORT_TARGET}
)
if(TARGET ${tgt})
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
JERRY_NUMBER_TYPE_FLOAT64=0
JERRY_BUILTIN_DATE=0
)
endif()
endforeach()
# Export include dirs through the targets
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-core/include
)
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-ext/include
)
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
${jerryscript_SOURCE_DIR}/jerry-port/default/include
)
# Suppress JerryScript-only warning
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
-Wno-error
)
endif()
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
+41 -32
View File
@@ -1,57 +1,66 @@
# Build type: FAT (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "FAT" CACHE STRING "Dolphin asset source: FAT (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "FAT" "ISO")
# Target definitions # Target definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD DUSK_INPUT_GAMEPAD
DUSK_DISPLAY_WIDTH=640 DUSK_DISPLAY_WIDTH=640
DUSK_DISPLAY_HEIGHT=480 DUSK_DISPLAY_HEIGHT=480
DUSK_THREAD_PTHREAD
DUSK_DOLPHIN_BUILD_TYPE="${DUSK_DOLPHIN_BUILD_TYPE}"
) )
# Custom compiler flags # Custom compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
# Need PkgConfig
find_package(PkgConfig REQUIRED)
pkg_check_modules(zip IMPORTED_TARGET libzip)
# Disable all warnings # Disable all warnings
target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w) target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w)
# Custom flags for cglm # cglm: fetched at source level via Findcglm.cmake (FetchContent, headers only)
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED) find_package(cglm REQUIRED)
# Compile lua # Pre-create ZLIB::ZLIB so any downstream cmake module that resolves it
include(FetchContent) # (FindZLIB, pkg-config IMPORTED_TARGET Requires processing) gets plain -lz
FetchContent_Declare( # rather than an unresolvable IMPORTED target that the PPC linker rejects.
liblua if(NOT TARGET ZLIB::ZLIB)
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL)
) set_target_properties(ZLIB::ZLIB PROPERTIES INTERFACE_LINK_LIBRARIES "z")
FetchContent_MakeAvailable(liblua) endif()
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
# Link libraries # Mark libzip as found so src/dusk/CMakeLists.txt skips Findlibzip.cmake.
# Findlibzip.cmake calls find_package(ZLIB) which can recreate a broken
# ZLIB::ZLIB IMPORTED target, bypassing the shim above.
set(libzip_FOUND TRUE CACHE BOOL "libzip found (devkitpro portlibs)" FORCE)
# Locate zip.h in the devkitpro sysroot (respects CMAKE_FIND_ROOT_PATH).
find_path(_dusk_zip_inc NAMES zip.h)
if(_dusk_zip_inc)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE "${_dusk_zip_inc}")
endif()
# Link libraries.
# zip/z/lzma use target_link_options (raw flags) to bypass cmake target
# resolution — pkg-config-generated targets for these carry ZLIB::ZLIB in
# INTERFACE_LINK_LIBRARIES which breaks the PPC link step.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
cglm cglm
liblua
m m
fat zip
PkgConfig::zip bz2
zstd
z
lzma
) )
# Postbuild if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_DOLPHIN_BUILD_ISO)
else()
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE fat)
endif()
# Postbuild: ELF -> DOL
set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol") set(DUSK_BINARY_TARGET_NAME_DOL "${DUSK_BUILD_DIR}/Dusk.dol")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND elf2dol COMMAND elf2dol
+19
View File
@@ -3,3 +3,22 @@ include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_GAMECUBE DUSK_GAMECUBE
) )
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
# bba
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"GCN"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building GameCube ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
+1
View File
@@ -34,6 +34,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_OPENGL DUSK_OPENGL
DUSK_OPENGL_ES DUSK_OPENGL_ES
DUSK_LINUX DUSK_LINUX
DUSK_KNULLI
DUSK_DISPLAY_SIZE_DYNAMIC DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640 DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_HEIGHT_DEFAULT=480 DUSK_DISPLAY_HEIGHT_DEFAULT=480
+6 -3
View File
@@ -1,7 +1,7 @@
# Find link platform-specific libraries # Find link platform-specific libraries
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(CURL REQUIRED) # find_package(CURL REQUIRED)
# Setup endianess at compile time to optimize. # Setup endianess at compile time to optimize.
include(TestBigEndian) include(TestBigEndian)
@@ -23,13 +23,16 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
OpenGL::GL OpenGL::GL
GL GL
m m
CURL::libcurl # CURL::libcurl
) )
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
# Define platform-specific macros. # Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2 DUSK_SDL2
DUSK_OPENGL DUSK_OPENGL
DUSK_CONSOLE_POSIX
# DUSK_OPENGL_LEGACY # DUSK_OPENGL_LEGACY
DUSK_LINUX DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC DUSK_DISPLAY_SIZE_DYNAMIC
@@ -41,5 +44,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_INPUT_GAMEPAD DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC DUSK_TIME_DYNAMIC
DUSK_NETWORK_IPV6 DUSK_NETWORK_IPV6
THREAD_PTHREAD=1 DUSK_THREAD_PTHREAD
) )
+14 -4
View File
@@ -1,6 +1,16 @@
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_ARCHIVE_CREATE "$ENV{PSPDEV}/bin/psp-ar qc <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_APPEND "$ENV{PSPDEV}/bin/psp-ar q <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_FINISH "$ENV{PSPDEV}/bin/psp-ranlib <TARGET>")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE)
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES} ${SDL2_LIBRARIES}
SDL2 SDL2
pthread pthread
@@ -31,11 +41,11 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
pspssl pspssl
) )
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS} ${SDL2_INCLUDE_DIRS}
) )
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_SDL2 DUSK_SDL2
DUSK_OPENGL DUSK_OPENGL
DUSK_PSP DUSK_PSP
@@ -44,7 +54,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_OPENGL_LEGACY DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=480 DUSK_DISPLAY_WIDTH=480
DUSK_DISPLAY_HEIGHT=272 DUSK_DISPLAY_HEIGHT=272
THREAD_PTHREAD=1 DUSK_THREAD_PTHREAD
) )
# Postbuild, create .pbp file for PSP. # Postbuild, create .pbp file for PSP.
-22
View File
@@ -20,31 +20,9 @@ set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE) set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED) find_package(cglm REQUIRED)
# Compile lua
include(FetchContent)
FetchContent_Declare(
liblua
URL https://www.lua.org/ftp/lua-5.5.0.tar.gz
)
FetchContent_MakeAvailable(liblua)
set(LUA_SRC_DIR "${liblua_SOURCE_DIR}/src")
set(LUA_C_FILES
lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c ldebug.c
ldo.c ldump.c lfunc.c lgc.c linit.c liolib.c llex.c lmathlib.c lmem.c
loadlib.c lobject.c lopcodes.c loslib.c lparser.c lstate.c lstring.c
lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c lzio.c
)
list(TRANSFORM LUA_C_FILES PREPEND "${LUA_SRC_DIR}/")
add_library(liblua STATIC ${LUA_C_FILES})
target_include_directories(liblua PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(liblua PRIVATE LUA_USE_C89)
add_library(lua::lua ALIAS liblua)
set(Lua_FOUND TRUE CACHE BOOL "Lua found" FORCE)
# Link libraries # Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES} ${SDL2_LIBRARIES}
liblua
cglm cglm
SDL2 SDL2
SDL2main SDL2main
+22
View File
@@ -3,3 +3,25 @@ include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_WII DUSK_WII
) )
# Generate Homebrew Channel meta.xml from project identity variables
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
configure_file(
"${CMAKE_SOURCE_DIR}/docker/dolphin/meta.xml.in"
"${DUSK_BUILD_DIR}/meta.xml"
@ONLY
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"WII"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building Wii ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
+4 -3
View File
@@ -1,6 +1,7 @@
FROM devkitpro/devkitppc FROM ghcr.io/extremscorner/libogc2
WORKDIR /workdir WORKDIR /workdir
RUN apt update && \ RUN apt update && \
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl && \ dkp-pacman -Syu --noconfirm && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd
VOLUME ["/workdir"] VOLUME ["/workdir"]
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>Dusk</name>
<version>1.00</version>
<release_date></release_date>
<coder>YouWish</coder>
<short_description>Dusk game</short_description>
<long_description>No description yet.</long_description>
<ahb_access/>
</app>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<app version="1">
<name>@DUSK_GAME_NAME@</name>
<version>@PROJECT_VERSION@</version>
<release_date>@DUSK_BUILD_DATE@</release_date>
<coder>@DUSK_GAME_AUTHOR@</coder>
<short_description>@DUSK_GAME_SHORT_DESCRIPTION@</short_description>
<long_description>@DUSK_GAME_LONG_DESCRIPTION@</long_description>
<ahb_access/>
</app>
+1 -1
View File
@@ -14,8 +14,8 @@ RUN apt-get install -y \
python3-dotenv \ python3-dotenv \
python3-pyqt5 \ python3-pyqt5 \
python3-opengl \ python3-opengl \
liblua5.3-dev \
xz-utils \ xz-utils \
liblzma-dev \
libbz2-dev \ libbz2-dev \
zlib1g-dev \ zlib1g-dev \
libzip-dev \ libzip-dev \
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh" docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube.sh"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-gamecube-iso.sh"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
if [ -z "$DEVKITPRO" ]; then
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
exit 1
fi
mkdir -p build-gamecube-iso
cmake -S. -Bbuild-gamecube-iso \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake"
cd build-gamecube-iso
make -j$(nproc) VERBOSE=1
+4 -1
View File
@@ -5,6 +5,9 @@ if [ -z "$DEVKITPRO" ]; then
fi fi
mkdir -p build-gamecube mkdir -p build-gamecube
cmake -S. -Bbuild-gamecube -DDUSK_TARGET_SYSTEM=gamecube -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" cmake -S. -Bbuild-gamecube \
-DDUSK_TARGET_SYSTEM=gamecube \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \
-DDKP_OGC_PLATFORM_LIBRARY=libogc2
cd build-gamecube cd build-gamecube
make -j$(nproc) VERBOSE=1 make -j$(nproc) VERBOSE=1
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile . docker build -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-knulli /bin/bash -c "./scripts/build-knulli.sh" docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile . docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/build-linux.sh" docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-psp -f docker/psp/Dockerfile . docker build -t dusk-psp -f docker/psp/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-psp /bin/bash -c "./scripts/build-psp.sh" docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile . docker build -t dusk-vita -f docker/vita/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-vita /bin/bash -c "./scripts/build-vita.sh" docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile . docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-dolphin /bin/bash -c "./scripts/build-wii.sh" docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii.sh"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
docker build -t dusk-dolphin -f docker/dolphin/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-dolphin /bin/bash -c "./scripts/build-wii-iso.sh"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
if [ -z "$DEVKITPRO" ]; then
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
exit 1
fi
mkdir -p build-wii-iso
cmake -S. -Bbuild-wii-iso \
-DDUSK_TARGET_SYSTEM=wii \
-DDUSK_DOLPHIN_BUILD_TYPE=ISO \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cd build-wii-iso
make -j$(nproc) VERBOSE=1
+1
View File
@@ -8,3 +8,4 @@ mkdir -p build-wii
cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" cmake -S. -Bbuild-wii -DDUSK_TARGET_SYSTEM=wii -DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake"
cd build-wii cd build-wii
make -j$(nproc) VERBOSE=1 make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile . docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v $(pwd):/workdir dusk-linux /bin/bash -c "./scripts/test-linux.sh" docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/test-linux.sh"
+2
View File
@@ -1,4 +1,6 @@
#!/bin/bash #!/bin/bash
set -e
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j$(nproc) cmake --build build-tests -- -j$(nproc)
ctest --output-on-failure --test-dir build-tests ctest --output-on-failure --test-dir build-tests
+1 -2
View File
@@ -4,7 +4,6 @@
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
add_subdirectory(dusk) add_subdirectory(dusk)
add_subdirectory(duskrpg)
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli") if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
add_subdirectory(dusklinux) add_subdirectory(dusklinux)
@@ -21,7 +20,7 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
add_subdirectory(dusksdl2) add_subdirectory(dusksdl2)
add_subdirectory(duskgl) add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "gamecube" OR DUSK_TARGET_SYSTEM STREQUAL "wii") elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
add_subdirectory(duskdolphin) add_subdirectory(duskdolphin)
endif() endif()
+23 -14
View File
@@ -32,18 +32,20 @@ if(NOT yyjson_FOUND)
endif() endif()
endif() endif()
if(NOT Lua_FOUND) if(NOT jerryscript_FOUND)
find_package(Lua REQUIRED) find_package(jerryscript REQUIRED)
if(Lua_FOUND AND NOT TARGET Lua::Lua) target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
add_library(Lua::Lua INTERFACE IMPORTED) jerryscript::core
set_target_properties( jerryscript::ext
Lua::Lua jerryscript::port
PROPERTIES )
INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}" endif()
INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
) if(DUSK_BACKTRACE)
endif() target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Lua::Lua) target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_BACKTRACE
)
endif() endif()
# Includes # Includes
@@ -59,14 +61,19 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
) )
# Subdirs # Subdirs
add_subdirectory(animation)
add_subdirectory(event)
add_subdirectory(assert) add_subdirectory(assert)
add_subdirectory(asset) add_subdirectory(asset)
add_subdirectory(log) add_subdirectory(cutscene)
add_subdirectory(item)
add_subdirectory(story)
add_subdirectory(console)
add_subdirectory(display) add_subdirectory(display)
add_subdirectory(log)
add_subdirectory(engine) add_subdirectory(engine)
add_subdirectory(entity) add_subdirectory(entity)
add_subdirectory(error) add_subdirectory(error)
add_subdirectory(event)
add_subdirectory(input) add_subdirectory(input)
add_subdirectory(locale) add_subdirectory(locale)
add_subdirectory(physics) add_subdirectory(physics)
@@ -76,5 +83,7 @@ add_subdirectory(system)
add_subdirectory(time) add_subdirectory(time)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(network) add_subdirectory(network)
add_subdirectory(overworld)
add_subdirectory(save)
add_subdirectory(util) add_subdirectory(util)
add_subdirectory(thread) add_subdirectory(thread)
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
easing.c
animation.c
)
+52
View File
@@ -0,0 +1,52 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "animation.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
anim->keyframes = keyframes;
anim->keyframeCount = keyframeCount;
}
float_t animationGetValue(animation_t *anim, const float_t time) {
assertNotNull(anim, "Animation pointer cannot be null.");
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
assertTrue(time >= 0, "Time must be non-negative.");
keyframe_t *start;
keyframe_t *end;
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
keyframe_t *current = anim->keyframes;
start = current;
do {
if(current->time > time) {
end = current;
break;
}
start = current;
current++;
if(current > last) {
end = start;
break;
}
} while(true);
float_t t = (time - start->time) / (end->time - start->time);
return mathLerp(start->value, end->value, easingApply(start->easing, t));
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "keyframe.h"
typedef struct {
keyframe_t *keyframes;
uint16_t keyframeCount;
} animation_t;
/**
* Initializes an animation.
*
* @param anim The animation to initialize.
* @param keyframes The keyframes to use for the animation.
* @param keyframeCount The number of keyframes in the animation.
*/
void animationInit(
animation_t *anim,
keyframe_t *keyframes,
uint16_t keyframeCount
);
/**
* Gets the value of the animation at a given time.
*
* @param anim The animation to get the value from.
* @param time The time at which to get the value, in seconds.
* @return The value of the animation at the given time.
*/
float_t animationGetValue(animation_t *anim, const float_t time);
+111
View File
@@ -0,0 +1,111 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "easing.h"
#include "assert/assert.h"
#include <math.h>
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
easingLinear,
easingInSine,
easingOutSine,
easingInOutSine,
easingInQuad,
easingOutQuad,
easingInOutQuad,
easingInCubic,
easingOutCubic,
easingInOutCubic,
easingInQuart,
easingOutQuart,
easingInOutQuart,
easingInBack,
easingOutBack,
easingInOutBack,
};
float_t easingApply(const easingtype_t type, const float_t t) {
assertTrue(type < EASING_COUNT, "Invalid easing type");
return EASING_FUNCTIONS[type](t);
}
float_t easingLinear(const float_t t) {
return t;
}
float_t easingInSine(const float_t t) {
return 1.0f - cosf(t * EASING_PI * 0.5f);
}
float_t easingOutSine(const float_t t) {
return sinf(t * EASING_PI * 0.5f);
}
float_t easingInOutSine(const float_t t) {
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f;
}
float_t easingInQuad(const float_t t) {
return t * t;
}
float_t easingOutQuad(const float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u;
}
float_t easingInOutQuad(const float_t t) {
if(t < 0.5f) return 2.0f * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * 0.5f;
}
float_t easingInCubic(const float_t t) {
return t * t * t;
}
float_t easingOutCubic(const float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u * u;
}
float_t easingInOutCubic(const float_t t) {
if(t < 0.5f) return 4.0f * t * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * u * 0.5f;
}
float_t easingInQuart(const float_t t) {
return t * t * t * t;
}
float_t easingOutQuart(const float_t t) {
float_t u = 1.0f - t;
return 1.0f - u * u * u * u;
}
float_t easingInOutQuart(const float_t t) {
if(t < 0.5f) return 8.0f * t * t * t * t;
float_t u = -2.0f * t + 2.0f;
return 1.0f - u * u * u * u * 0.5f;
}
float_t easingInBack(const float_t t) {
return EASING_C3 * t * t * t - EASING_C1 * t * t;
}
float_t easingOutBack(const float_t t) {
float_t u = t - 1.0f;
return 1.0f + EASING_C3 * u * u * u + EASING_C1 * u * u;
}
float_t easingInOutBack(const float_t t) {
if(t < 0.5f) {
float_t u = 2.0f * t;
return u * u * ((EASING_C2 + 1.0f) * u - EASING_C2) * 0.5f;
}
float_t u = 2.0f * t - 2.0f;
return (u * u * ((EASING_C2 + 1.0f) * u + EASING_C2) + 2.0f) * 0.5f;
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "dusk.h"
#define EASING_PI 3.14159265358979323846f
#define EASING_C1 1.70158f
#define EASING_C2 (EASING_C1 * 1.525f)
#define EASING_C3 (EASING_C1 + 1.0f)
typedef enum {
EASING_LINEAR,
EASING_IN_SINE,
EASING_OUT_SINE,
EASING_IN_OUT_SINE,
EASING_IN_QUAD,
EASING_OUT_QUAD,
EASING_IN_OUT_QUAD,
EASING_IN_CUBIC,
EASING_OUT_CUBIC,
EASING_IN_OUT_CUBIC,
EASING_IN_QUART,
EASING_OUT_QUART,
EASING_IN_OUT_QUART,
EASING_IN_BACK,
EASING_OUT_BACK,
EASING_IN_OUT_BACK,
EASING_COUNT
} easingtype_t;
typedef float_t (*easingfn_t)(const float_t t);
extern const easingfn_t EASING_FUNCTIONS[EASING_COUNT];
/**
* Applies the specified easing function to t.
*
* @param type The easing type to apply.
* @param t The input time, in the range [0, 1].
* @return The eased value, in the range [0, 1].
*/
float_t easingApply(const easingtype_t type, const float_t t);
float_t easingLinear(const float_t t);
float_t easingInSine(const float_t t);
float_t easingOutSine(const float_t t);
float_t easingInOutSine(const float_t t);
float_t easingInQuad(const float_t t);
float_t easingOutQuad(const float_t t);
float_t easingInOutQuad(const float_t t);
float_t easingInCubic(const float_t t);
float_t easingOutCubic(const float_t t);
float_t easingInOutCubic(const float_t t);
float_t easingInQuart(const float_t t);
float_t easingOutQuart(const float_t t);
float_t easingInOutQuart(const float_t t);
float_t easingInBack(const float_t t);
float_t easingOutBack(const float_t t);
float_t easingInOutBack(const float_t t);
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "easing.h"
typedef struct {
float_t time;
float_t value;
easingtype_t easing;
} keyframe_t;
+24 -1
View File
@@ -8,6 +8,7 @@
#include "assert.h" #include "assert.h"
#include "log/log.h" #include "log/log.h"
#include "util/string.h" #include "util/string.h"
#include "util/memory.h"
#ifndef DUSK_ASSERTIONS_FAKED #ifndef DUSK_ASSERTIONS_FAKED
#ifdef DUSK_TEST_ASSERT #ifdef DUSK_TEST_ASSERT
@@ -25,6 +26,25 @@
); );
} }
#else #else
#ifdef DUSK_BACKTRACE
#include <execinfo.h>
#include <stdlib.h>
static void assertLogBacktrace(void) {
void *frames[64];
int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count);
memoryTrack(symbols);
logError("Stack trace:\n");
if(symbols) {
for(int i = 0; i < count; i++) {
logError(" %s\n", symbols[i]);
}
memoryFree(symbols);
}
}
#endif
void assertTrueImpl( void assertTrueImpl(
const char *file, const char *file,
const int32_t line, const int32_t line,
@@ -33,11 +53,14 @@
) { ) {
if(x != true) { if(x != true) {
logError( logError(
"Assertion Failed in %s:%i\n\n%s\n", "Assertion Failed in %s:%i\n\n%s\n\n",
file, file,
line, line,
message message
); );
#ifdef DUSK_BACKTRACE
assertLogBacktrace();
#endif
abort(); abort();
} }
} }
+2
View File
@@ -218,6 +218,8 @@
#endif #endif
// Static Assertions
#define assertStructSize(struct, size) \ #define assertStructSize(struct, size) \
_Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size) _Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
+1
View File
@@ -7,6 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
asset.c asset.c
assetbatch.c
assetfile.c assetfile.c
) )
+314 -13
View File
@@ -11,45 +11,346 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "engine/engine.h" #include "engine/engine.h"
#include "util/string.h" #include "util/string.h"
#include "console/console.h"
#include <unistd.h>
asset_t ASSET; asset_t ASSET;
errorret_t assetInit(void) { errorret_t assetInit(void) {
memoryZero(&ASSET, sizeof(asset_t)); memoryZero(&ASSET, sizeof(asset_t));
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
// assetInitPlatform must either define ASSET.zip or throw an error. // assetInitPlatform must either define ASSET.zip or throw an error.
errorChain(assetInitPlatform()); errorChain(assetInitPlatform());
assertNotNull(ASSET.zip, "Asset zip null without error."); assertNotNull(ASSET.zip, "Asset zip null without error.");
threadInit(&ASSET.loadThread, assetUpdateAsync);
threadStart(&ASSET.loadThread);
errorOk(); errorOk();
} }
bool_t assetFileExists(const char_t *filename) { bool_t assetFileExists(const char_t *filename) {
assertStrLenMax(filename, FILENAME_MAX, "Filename too long."); assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0); zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
if(idx < 0) return false; if(idx < 0) return false;
return true; return true;
} }
errorret_t assetLoad( assetentry_t * assetGetEntry(
const char_t *filename, const char_t *name,
assetfileloader_t loader, const assetloadertype_t type,
void *params, assetloaderinput_t *input
void *output
) { ) {
assertStrLenMax(filename, FILENAME_MAX, "Filename too long."); // Is there an existing asset?
assertNotNull(output, "Output pointer cannot be NULL."); assetentry_t *entry = ASSET.entries;
assertNotNull(loader, "Asset file loader cannot be NULL."); do {
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(stringEquals(entry->name, name)) {
assertTrue(entry->type == type, "Asset entry type mismatch.");
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assetfile_t file; // We did not find one existing, Find first available slot.
errorChain(assetFileInit(&file, filename, params, output)); entry = ASSET.entries;
errorChain(loader(&file)); do {
errorChain(assetFileDispose(&file)); if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("No available asset entry slots.");
return NULL;
}
errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
}
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->state != ASSET_ENTRY_STATE_LOADED) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk(); errorOk();
} }
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assetentry_t *entry = assetGetEntry(name, type, input);
assetEntryLock(entry);
return entry;
}
void assetUnlock(const char_t *name) {
assertNotNull(name, "Name cannot be NULL.");
assetentry_t *entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL && stringEquals(entry->name, name)) {
assetEntryUnlock(entry);
return;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
assertUnreachable("Asset entry not found for unlock.");
}
void assetUnlockEntry(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assetEntryUnlock(entry);
}
errorret_t assetUpdate(void) {
// Determine how many available loading slots we have.
assetloading_t *availableLoading[ASSET_LOADING_COUNT_MAX];
uint8_t availableLoadingCount = 0;
assetloading_t *loading = ASSET.loading;
assetentry_t *entry;
do {
// We only care about NULL entry references. Nothing async touches this so
// it's fine to use raw here.
if(loading->entry != NULL) {
loading++;
continue;
}
availableLoading[availableLoadingCount++] = loading;
loading++;
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Now we can check for pending asset entries, we can't do anything if there
// is no available slots though.
if(availableLoadingCount > 0) {
entry = ASSET.entries;
do {
// Is this asset "ready to start loading" ?
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
// We only care about assets not started.
if(entry->state != ASSET_ENTRY_STATE_NOT_STARTED) {
entry++;
continue;
}
// Pop a loading slot for this asset entry.
loading = availableLoading[--availableLoadingCount];
// Start loading this asset.
assetEntryStartLoading(entry, loading);
entry++;
// Did we run out of loading slots?
if(availableLoadingCount == 0) {
break;
}
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
}
// Now walk over all the loading slots and see what needs to be done.
loading = ASSET.loading;
do {
// Is the loading slot in use? Entry can only be modified synchronously.
if(loading->entry == NULL) {
loading++;
continue;
}
// Lock the loading slot. This will prevent any async modifications.
threadMutexLock(&loading->mutex);
// Check the state of the entry.
switch(loading->entry->state) {
// This thing is pending synchronous loading.
case ASSET_ENTRY_STATE_PENDING_SYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_SYNC;
// Unlock before calling loadSync. The sync loader may re-enter
// assetUpdate (e.g. a script loading another asset), and the async
// thread never touches LOADING_SYNC entries, so this is safe.
threadMutexUnlock(&loading->mutex);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadSync(loading)
);
// After a sync load, these are the only valid states.
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_LOADED ||
loading->entry->state == ASSET_ENTRY_STATE_ERROR ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_SYNC ||
loading->entry->state == ASSET_ENTRY_STATE_PENDING_ASYNC,
"Loader did not set entry state to loaded or error on finished load."
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
}
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_SYNC:
// A re-entrant assetUpdate call (e.g. from a script loading another
// asset) will see this entry mid-sync-load. Skip it.
threadMutexUnlock(&loading->mutex);
loading++;
continue;
// Done loading, we can just free it up.
case ASSET_ENTRY_STATE_LOADED:
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_ERROR:
threadMutexUnlock(&loading->mutex);
errorThrow("Failed to load asset asynchronously.");
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
// Reap entries that have no external locks (refs.count == 0) AND have been
// explicitly locked at least once. Entries that were never locked are left
// alone — raw-pointer callers hold no ref but should not lose the entry.
entry = ASSET.entries;
do {
if(entry->state != ASSET_ENTRY_STATE_LOADED) {
entry++;
continue;
}
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(!entry->wasLocked) {
entry++;
continue;
}
if(entry->refs.count > 0) {
entry++;
continue;
}
consolePrint("Reaping asset %s", entry->name);
errorChain(assetEntryDispose(entry));
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
errorOk();
}
void assetUpdateAsync(thread_t *thread) {
while(!threadShouldStop(thread)) {
// Walk over each asset
assetloading_t *loading;
loading = ASSET.loading;
do {
threadMutexLock(&loading->mutex);
if(loading->entry == NULL) {
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
switch(loading->entry->state) {
case ASSET_ENTRY_STATE_PENDING_ASYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC;
assertNotNull(
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
"Loader does not support async loading."
);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_ASYNC:
assertUnreachable(
"Entry is in a pending async state still?"
);
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
if(threadShouldStop(thread)) break;
usleep(1000);
}
}
errorret_t assetDispose(void) { errorret_t assetDispose(void) {
threadStop(&ASSET.loadThread);
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexDispose(&ASSET.loading[i].mutex);
}
// Cleanup zip file.
if(ASSET.zip != NULL) { if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) { if(zip_close(ASSET.zip) != 0) {
errorThrow("Failed to close asset zip archive."); errorThrow("Failed to close asset zip archive.");
+79 -11
View File
@@ -9,6 +9,9 @@
#include "error/error.h" #include "error/error.h"
#include "asset/assetplatform.h" #include "asset/assetplatform.h"
#include "assetfile.h" #include "assetfile.h"
#include "thread/thread.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloading.h"
#ifndef assetInitPlatform #ifndef assetInitPlatform
#error "Platform must define assetInitPlatform function." #error "Platform must define assetInitPlatform function."
@@ -20,15 +23,27 @@
#define ASSET_FILE_NAME "dusk.dsk" #define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3 #define ASSET_HEADER_SIZE 3
#define ASSET_LOADING_COUNT_MAX 4
#define ASSET_ENTRY_COUNT_MAX 128
typedef struct asset_s { typedef struct asset_s {
zip_t *zip; zip_t *zip;
assetplatform_t platform; assetplatform_t platform;
// Background loading thread.
thread_t loadThread;
// Assets that are mid loading.
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
} asset_t; } asset_t;
extern asset_t ASSET; extern asset_t ASSET;
/** /**
* Initializes the asset system. * Initializes the asset system.
*
* @return An error code if the asset system could not be initialized properly.
*/ */
errorret_t assetInit(void); errorret_t assetInit(void);
@@ -41,21 +56,74 @@ errorret_t assetInit(void);
bool_t assetFileExists(const char_t *filename); bool_t assetFileExists(const char_t *filename);
/** /**
* Loads an asset by its filename, * Gets, or creates, a new asset entry. Internal — prefer assetLock.
* *
* @param filename The filename of the asset to retrieve. * @param name Filename of the asset.
* @param loader Loader to use for loading the asset file. * @param type Type of the asset.
* @param params Parameters to pass to the loader. * @param input Loader-specific parameters.
* @param output The output pointer to store the loaded asset data.
* @return An error code if the asset could not be loaded.
*/ */
errorret_t assetLoad( assetentry_t * assetGetEntry(
const char_t *filename, const char_t *name,
assetfileloader_t loader, const assetloadertype_t type,
void *params, assetloaderinput_t *input
void *output
); );
/**
* Gets, creates, and locks an asset entry. The asset will begin loading on
* the next assetUpdate. Call assetUnlock when done to allow the entry to be
* reclaimed.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
* @return The locked asset entry.
*/
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Releases a lock on an asset entry by name. When all locks are released the
* entry will be reclaimed at the start of the next assetUpdate.
*
* @param name Filename of the asset to unlock.
*/
void assetUnlock(const char_t *name);
/**
* Releases a lock on an asset entry by pointer. When all locks are released
* the entry will be reclaimed at the start of the next assetUpdate.
*
* @param entry The asset entry to unlock.
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
*
* @param entry The asset entry to require.
* @return An error code if the asset entry could not be loaded properly.
*/
errorret_t assetRequireLoaded(assetentry_t *entry);
/**
* Updates the asset system.
*
* @return An error code if the asset system could not be updated properly.
*/
errorret_t assetUpdate(void);
/**
* Starts the background asset loading thread. The thread runs assetUpdate
* in a loop with a short sleep until stopped.
*
* @param thread The thread runner.
*/
void assetUpdateAsync(thread_t *thread);
/** /**
* Disposes/cleans up the asset system. * Disposes/cleans up the asset system.
* *
+94
View File
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetbatch.h"
#include "asset.h"
#include "assert/assert.h"
#include "util/memory.h"
#include <unistd.h>
void assetBatchInit(
assetbatch_t *batch,
const uint16_t count,
const assetbatchdesc_t *descs
) {
assertNotNull(batch, "Batch cannot be NULL.");
assertNotNull(descs, "Descs cannot be NULL.");
assertTrue(count > 0, "Count must be greater than 0.");
assertTrue(count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX.");
memoryZero(batch, sizeof(assetbatch_t));
batch->count = count;
for(uint16_t i = 0; i < count; i++) {
// Copy input into batch-owned storage so the descriptor need not persist.
batch->inputs[i] = descs[i].input;
batch->entries[i] = assetLock(descs[i].path, descs[i].type, &batch->inputs[i]);
}
}
void assetBatchLock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryLock(batch->entries[i]);
}
}
void assetBatchUnlock(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetEntryUnlock(batch->entries[i]);
}
}
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
}
return true;
}
bool_t assetBatchHasError(const assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
}
return false;
}
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
bool_t allDone;
do {
allDone = true;
for(uint16_t i = 0; i < batch->count; i++) {
const assetentrystate_t state = batch->entries[i]->state;
if(state == ASSET_ENTRY_STATE_ERROR) {
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
}
if(state != ASSET_ENTRY_STATE_LOADED) {
allDone = false;
}
}
if(!allDone) {
usleep(1000);
errorChain(assetUpdate());
}
} while(!allDone);
errorOk();
}
void assetBatchDispose(assetbatch_t *batch) {
assertNotNull(batch, "Batch cannot be NULL.");
for(uint16_t i = 0; i < batch->count; i++) {
assetUnlockEntry(batch->entries[i]);
}
memoryZero(batch, sizeof(assetbatch_t));
}
+82
View File
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#define ASSET_BATCH_COUNT_MAX 64
typedef struct {
const char_t *path;
assetloadertype_t type;
assetloaderinput_t input;
} assetbatchdesc_t;
typedef struct {
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
uint16_t count;
} assetbatch_t;
/**
* Initialises the batch from an array of descriptors. Each entry is locked
* and queued for loading immediately.
*
* @param batch Batch to initialise.
* @param descs Array of entry descriptors (need not outlive this call).
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
*/
void assetBatchInit(
assetbatch_t *batch,
uint16_t count,
const assetbatchdesc_t *descs
);
/**
* Acquires one additional lock on every entry in the batch.
*
* @param batch Batch to lock.
*/
void assetBatchLock(assetbatch_t *batch);
/**
* Releases one lock from every entry in the batch. When an entry's lock
* count reaches zero it will be reaped on the next assetUpdate.
*
* @param batch Batch to unlock.
*/
void assetBatchUnlock(assetbatch_t *batch);
/**
* Returns true if every entry in the batch has finished loading.
*
* @param batch Batch to query.
*/
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
/**
* Returns true if any entry in the batch is in an error state.
*
* @param batch Batch to query.
*/
bool_t assetBatchHasError(const assetbatch_t *batch);
/**
* Blocks until every entry is loaded. Returns an error if any entry fails.
*
* @param batch Batch to wait on.
*/
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
/**
* Releases the batch's lock on every entry and clears the batch. After this
* call the batch struct may be reused with assetBatchInit.
*
* @param batch Batch to dispose.
*/
void assetBatchDispose(assetbatch_t *batch);
+18 -6
View File
@@ -16,9 +16,11 @@ errorret_t assetFileInit(
void *params, void *params,
void *output void *output
) { ) {
memoryZero(file, sizeof(assetfile_t)); assertNotNull(file, "Asset file cannot be NULL.");
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
file->filename = filename; memoryZero(file, sizeof(assetfile_t));
memoryCopy(file->filename, filename, ASSET_FILE_NAME_MAX);
file->params = params; file->params = params;
file->output = output; file->output = output;
@@ -146,7 +148,9 @@ size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader) {
return reader->bufferEnd - reader->bufferStart; return reader->bufferEnd - reader->bufferStart;
} }
const uint8_t *assetFileLineReaderUnreadPtr(const assetfilelinereader_t *reader) { const uint8_t *assetFileLineReaderUnreadPtr(
const assetfilelinereader_t *reader
) {
assertNotNull(reader, "Reader cannot be NULL."); assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL."); assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
return reader->readBuffer + reader->bufferStart; return reader->readBuffer + reader->bufferStart;
@@ -177,11 +181,16 @@ static errorret_t assetFileLineReaderAppend(
static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) { static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL."); assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL."); assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
assertTrue(reader->lineLength < reader->outBufferSize, "Line length exceeds out buffer."); assertTrue(
reader->lineLength < reader->outBufferSize,
"Line length exceeds out buffer."
);
reader->outBuffer[reader->lineLength] = '\0'; reader->outBuffer[reader->lineLength] = '\0';
} }
static ssize_t assetFileLineReaderFindNewline(const assetfilelinereader_t *reader) { static ssize_t assetFileLineReaderFindNewline(
const assetfilelinereader_t *reader
) {
size_t i; size_t i;
assertNotNull(reader, "Reader cannot be NULL."); assertNotNull(reader, "Reader cannot be NULL.");
@@ -284,7 +293,10 @@ errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
errorret_t ret; errorret_t ret;
/* strip CR in CRLF */ /* strip CR in CRLF */
if(chunkLength > 0 && reader->readBuffer[(size_t)newlineIndex - 1] == '\r') { if(
chunkLength > 0 &&
reader->readBuffer[(size_t)newlineIndex - 1] == '\r'
) {
chunkLength--; chunkLength--;
} }
+4 -1
View File
@@ -9,12 +9,15 @@
#include "error/error.h" #include "error/error.h"
#include <zip.h> #include <zip.h>
#define ASSET_FILE_NAME_MAX 48
typedef struct assetfile_s assetfile_t; typedef struct assetfile_s assetfile_t;
typedef errorret_t (*assetfileloader_t)(assetfile_t *file); typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
// Describes a file not yet loaded.
typedef struct assetfile_s { typedef struct assetfile_s {
const char_t *filename; char_t filename[ASSET_FILE_NAME_MAX];
void *params; void *params;
void *output; void *output;
+7 -1
View File
@@ -4,9 +4,15 @@
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Sources # Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetentry.c
assetloader.c
)
# Subdirs # Subdirs
add_subdirectory(display) add_subdirectory(display)
add_subdirectory(locale) add_subdirectory(locale)
add_subdirectory(script)
add_subdirectory(json) add_subdirectory(json)
add_subdirectory(script)
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetentry.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertNotNull(entry, "Entry cannot be NULL");
assertStrLenMin(name, 1, "Name cannot be empty");
assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long");
assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
if(input) {
entry->inputData = *input;
entry->input = &entry->inputData;
}
refInit(&entry->refs, entry, NULL, NULL, NULL);
}
void assetEntryLock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
entry->wasLocked = true;
refLock(&entry->refs);
}
void assetEntryUnlock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refUnlock(&entry->refs);
}
void assetEntryStartLoading(
assetentry_t *entry,
assetloading_t *loading
) {
assertNotNull(entry, "Entry cannot be NULL");
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED,
"Can only start loading from NOT_STARTED state."
);
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
memoryZero(&loading->loading, sizeof(assetloaderloading_t));
loading->type = entry->type;
loading->entry = entry;
// At this point the asset manager will manage this thing's loading
}
errorret_t assetEntryDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t));
errorOk();
}
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/assetloading.h"
#include "util/ref.h"
typedef enum {
ASSET_ENTRY_STATE_NOT_STARTED,
ASSET_ENTRY_STATE_PENDING_ASYNC,
ASSET_ENTRY_STATE_LOADING_ASYNC,
ASSET_ENTRY_STATE_PENDING_SYNC,
ASSET_ENTRY_STATE_LOADING_SYNC,
ASSET_ENTRY_STATE_LOADED,
ASSET_ENTRY_STATE_ERROR
} assetentrystate_t;
typedef struct assetentry_s {
// Filename and cache key
char_t name[ASSET_FILE_NAME_MAX];
// What type of asset is this?
assetloadertype_t type;
// Data
assetloaderoutput_t data;
// What state is this asset entry in currently?
assetentrystate_t state;
// What is referencing this asset entry.
ref_t refs;
// True once assetEntryLock has been called at least once. The reaper only
// collects entries that have been explicitly locked (and later unlocked to
// zero). Entries that nobody has ever locked are left alone so raw-pointer
// callers (tests, requireLoaded before locking) are not surprised.
bool_t wasLocked;
// Owned copy of the loader input. input points here when non-NULL.
assetloaderinput_t inputData;
// Pointer to inputData, or NULL if no input was provided.
assetloaderinput_t *input;
} assetentry_t;
/**
* Initializes an asset entry with the given name and type. This does not load
* the asset.
*
* @param entry The asset entry to initialize.
* @param name The name of the asset, used as a key for loading and caching.
* @param type The type of asset this entry represents.
* @param input Data that will be passed to the loader about how it should load.
*/
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Locks an asset entry, preventing it from being freed until it is unlocked.
*
* @param entry The asset entry to lock.
*/
void assetEntryLock(assetentry_t *entry);
/**
* Unlocks an asset entry, allowing it to be freed if there are no more locks.
*
* @param entry The asset entry to unlock.
*/
void assetEntryUnlock(assetentry_t *entry);
/**
* Starts loading the given asset entry using an assetloading slot. This will
* be called by the asset manager when it deems it's a good time to begin the
* loading of this asset entry.
*
* Currently we return the error but in future this will not be returned.
*
* @param entry The asset entry to start loading.
* @param loading The assetloading slot to use for loading this asset entry.
* @return Any error that occurs during loading.
*/
void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading);
/**
* Disposes an asset entry, freeing any resources it holds.
*
* @param entry The asset entry to dispose.
* @return Any error that occurs during disposal.
*/
errorret_t assetEntryDispose(assetentry_t *entry);
+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
*/
#include "assetloader.h"
assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
[ASSET_LOADER_TYPE_NULL] = { 0 },
[ASSET_LOADER_TYPE_MESH] = {
.loadSync = assetMeshLoaderSync,
.loadAsync = assetMeshLoaderAsync,
.dispose = assetMeshDispose
},
[ASSET_LOADER_TYPE_TEXTURE] = {
.loadSync = assetTextureLoaderSync,
.loadAsync = assetTextureLoaderAsync,
.dispose = assetTextureDispose
},
[ASSET_LOADER_TYPE_TILESET] = {
.loadSync = assetTilesetLoaderSync,
.loadAsync = assetTilesetLoaderAsync,
.dispose = assetTilesetDispose
},
[ASSET_LOADER_TYPE_LOCALE] = {
.loadSync = assetLocaleLoaderSync,
.loadAsync = assetLocaleLoaderAsync,
.dispose = assetLocaleDispose
},
[ASSET_LOADER_TYPE_JSON] = {
.loadSync = assetJsonLoaderSync,
.loadAsync = assetJsonLoaderAsync,
.dispose = assetJsonDispose
},
[ASSET_LOADER_TYPE_SCRIPT] = {
.loadSync = assetScriptLoaderSync,
.loadAsync = assetScriptLoaderAsync,
.dispose = assetScriptDispose
},
};
+96
View File
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/display/assetmeshloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/script/assetscriptloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_SCRIPT,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
typedef union {
assetmeshloaderinput_t mesh;
assettextureloaderinput_t texture;
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
assetscriptloaderinput_t script;
} assetloaderinput_t;
typedef union {
assetmeshloaderloading_t mesh;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
assetscriptloaderloading_t script;
} assetloaderloading_t;
typedef union {
assetmeshoutput_t mesh;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetscriptoutput_t script;
} assetloaderoutput_t;
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef errorret_t (assetloadersynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderasynccallback_t)(assetloading_t *loading);
typedef errorret_t (assetloaderdisposecallback_t)(assetentry_t *entry);
typedef struct {
assetloadersynccallback_t *loadSync;
assetloaderasynccallback_t *loadAsync;
assetloaderdisposecallback_t *dispose;
} assetloadercallbacks_t;
extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
/**
* Shorthand method to both chain an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ret The error return value to check and chain if it's an error.
*/
#define assetLoaderErrorChain(loading, _expr) {\
errorret_t _alec = (_expr); \
if(errorIsNotOk(_alec)) { \
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorChain(_alec); \
} \
}
/**
* Shorthand method to both throw an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ... Format string and arguments for the error message.
*/
#define assetLoaderErrorThrow(loading, ...) {\
loading->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorThrow(__VA_ARGS__); \
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "assetloader.h"
#include "asset/assetfile.h"
#include "thread/threadmutex.h"
typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s {
// Protects entry pointer and entry->state from concurrent access.
threadmutex_t mutex;
// What type of asset is being loaded.
assetloadertype_t type;
// Referral back to the asset entry that will be kept alive after load done.
assetentry_t *entry;
// Information used during the load operation only.
assetloaderloading_t loading;
} assetloading_t;
typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading);
typedef struct {
assetloadingcallback_t *loadSync;
} assetloadingcallbacks_t;
+111 -119
View File
@@ -5,178 +5,170 @@
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#include "asset/loader/display/assetmeshloader.h" #include "assetmeshloader.h"
#include "asset/asset.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/endian.h" #include "util/endian.h"
#include "util/memory.h" #include "util/memory.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetMeshLoader(assetfile_t *file) { errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assertNotNull(file, "Asset file cannot be null"); assertNotNull(loading, "Loading cannot be NULL");
assetmeshoutput_t *output = (assetmeshoutput_t *)file->output; if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
assertNotNull(output, "Output cannot be null"); errorOk();
assertNotNull(output->outMesh, "Output mesh cannot be null"); }
assertNotNull(output->outVertices, "Output vertices cannot be null");
// STL file loading assetmeshoutput_t *out = &loading->entry->data.mesh;
errorChain(assetFileOpen(file)); assetfile_t *file = &loading->loading.mesh.file;
assetmeshinputaxis_t axis = loading->entry->input->mesh;
// Skip the 80 byte header assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
errorChain(assetFileRead(file, NULL, 80)); assetLoaderErrorChain(loading, assetFileOpen(file));
if(file->lastRead != 80) errorThrow("Failed to skip STL header");
// Skip the 80-byte STL header.
assetLoaderErrorChain(loading, assetFileRead(file, NULL, 80));
if(file->lastRead != 80) {
assetLoaderErrorThrow(loading, "Failed to skip STL header.");
}
uint32_t triangleCount; uint32_t triangleCount;
errorChain(assetFileRead(file, &triangleCount, sizeof(uint32_t))); assetLoaderErrorChain(loading, assetFileRead(file, &triangleCount, sizeof(uint32_t)));
if(file->lastRead != sizeof(uint32_t)) errorThrow("Failed read tri count"); if(file->lastRead != sizeof(uint32_t)) {
// normalize assetLoaderErrorThrow(loading, "Failed to read tri count");
}
triangleCount = endianLittleToHost32(triangleCount); triangleCount = endianLittleToHost32(triangleCount);
// Allocate mesh and vertex data out->vertices = memoryAllocate(sizeof(meshvertex_t) * triangleCount * 3);
errorret_t ret; meshvertex_t *verts = out->vertices;
meshvertex_t *verts = memoryAllocate(
sizeof(meshvertex_t) * triangleCount * 3
);
*output->outVertices = verts;
// Read triangle data errorret_t ret;
for(uint32_t i = 0; i < triangleCount; i++) { for(uint32_t i = 0; i < triangleCount; i++) {
assetmeshstltriangle_t triData; assetmeshstltriangle_t triData;
ret = assetFileRead(file, &triData, sizeof(triData)); ret = assetFileRead(file, &triData, sizeof(triData));
if(ret.code != ERROR_OK) { if(errorIsNotOk(ret)) {
memoryFree(verts); memoryFree(verts);
errorChain(ret); out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
} }
if(file->lastRead != sizeof(triData)) { if(file->lastRead != sizeof(triData)) {
memoryFree(verts); memoryFree(verts);
errorThrow("Failed to read triangle data"); out->vertices = NULL;
assetLoaderErrorThrow(loading, "Failed to read triangle data");
} }
// Skip normals, we don't use them
// Fix endianess of of data
for(uint8_t j = 0; j < 3; j++) { for(uint8_t j = 0; j < 3; j++) {
#if MESH_ENABLE_COLOR #if MESH_ENABLE_COLOR
verts[i * 3 + j].color.r = (uint8_t)(endianLittleToHostFloat( verts[i * 3 + j].color.r = (
triData.normal[0] (uint8_t)(endianLittleToHostFloat(triData.normal[0]) * 255.0f)
) * 255.0f); );
verts[i * 3 + j].color.g = (uint8_t)(endianLittleToHostFloat( verts[i * 3 + j].color.g = (
triData.normal[1] (uint8_t)(endianLittleToHostFloat(triData.normal[1]) * 255.0f)
) * 255.0f); );
verts[i * 3 + j].color.b = (uint8_t)(endianLittleToHostFloat( verts[i * 3 + j].color.b = (
triData.normal[2] (uint8_t)(endianLittleToHostFloat(triData.normal[2]) * 255.0f)
) * 255.0f); );
verts[i * 3 + j].color.a = 0xFF; verts[i * 3 + j].color.a = 0xFF;
#endif #endif
verts[i * 3 + j].uv[0] = 0.0f; // No UV data in STL, just set to 0 verts[i * 3 + j].uv[0] = 0.0f;
verts[i * 3 + j].uv[1] = 0.0f; verts[i * 3 + j].uv[1] = 0.0f;
for(uint8_t k = 0; k < 3; k++) { for(uint8_t k = 0; k < 3; k++) {
verts[i * 3 + j].pos[k] = endianLittleToHostFloat( verts[i * 3 + j].pos[k] = endianLittleToHostFloat(triData.positions[j][k]);
triData.positions[j][k]
);
} }
switch(output->inputAxis) { switch(axis) {
case MESH_INPUT_AXIS_Z_UP: case MESH_INPUT_AXIS_Z_UP: {
// Convert Z-Up to Y-Up float_t temp = verts[i * 3 + j].pos[1];
{ verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
float_t temp = verts[i * 3 + j].pos[1]; verts[i * 3 + j].pos[2] = temp;
verts[i * 3 + j].pos[1] = verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
}
break; break;
}
case MESH_INPUT_AXIS_X_UP: case MESH_INPUT_AXIS_X_UP: {
// Convert X-Up to Y-Up float_t temp = verts[i * 3 + j].pos[0];
{ verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
float_t temp = verts[i * 3 + j].pos[0]; verts[i * 3 + j].pos[1] = temp;
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = temp;
}
break; break;
}
case MESH_INPUT_AXIS_Y_DOWN: case MESH_INPUT_AXIS_Y_DOWN:
// Invert Y axis
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[1]; verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[1];
break; break;
case MESH_INPUT_AXIS_Z_DOWN: {
case MESH_INPUT_AXIS_Z_DOWN: float_t temp = verts[i * 3 + j].pos[1];
// Convert Z-Up to Y-Up and invert Y axis verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
{ verts[i * 3 + j].pos[2] = temp;
float_t temp = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -verts[i * 3 + j].pos[2];
verts[i * 3 + j].pos[2] = temp;
}
break; break;
}
case MESH_INPUT_AXIS_X_DOWN: case MESH_INPUT_AXIS_X_DOWN: {
// Convert X-Up to Y-Up and invert Y axis float_t temp = verts[i * 3 + j].pos[0];
{ verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
float_t temp = verts[i * 3 + j].pos[0]; verts[i * 3 + j].pos[1] = -temp;
verts[i * 3 + j].pos[0] = verts[i * 3 + j].pos[1];
verts[i * 3 + j].pos[1] = -temp;
}
break; break;
}
case MESH_INPUT_AXIS_Y_UP: case MESH_INPUT_AXIS_Y_UP:
default: default:
// No covnersion possible / Needed
break; break;
} }
} }
} }
// Finally, init mesh
ret = meshInit(
output->outMesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
triangleCount * 3,
verts
);
if(ret.code != ERROR_OK) {
memoryFree(verts);
errorChain(ret);
}
ret = assetFileClose(file); ret = assetFileClose(file);
if(ret.code != ERROR_OK) { if(errorIsNotOk(ret)) {
errorCatch(errorPrint(meshDispose(output->outMesh)));
memoryFree(verts); memoryFree(verts);
errorChain(ret); out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
} }
assetFileDispose(file);
loading->loading.mesh.triangleCount = triangleCount;
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_CREATE_MESH;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
} }
errorret_t assetMeshLoadToOutput( errorret_t assetMeshLoaderSync(assetloading_t *loading) {
const char_t *path, assertNotNull(loading, "Loading cannot be NULL");
assetmeshoutput_t *output assertTrue(loading->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
) {
assertNotNull(path, "Path cannot be null");
assertNotNull(output, "Output cannot be null");
assertNotNull(output->outMesh, "Output mesh cannot be null");
assertNotNull(output->outVertices, "Output vertices cannot be null");
return assetLoad(path, &assetMeshLoader, NULL, output); switch(loading->loading.mesh.state) {
case ASSET_MESH_LOADING_STATE_INITIAL:
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_MESH_LOADING_STATE_CREATE_MESH:
break;
default:
errorOk();
}
assetmeshoutput_t *out = &loading->entry->data.mesh;
assertNotNull(out->vertices, "Mesh vertices should have been loaded by now.");
errorret_t ret = meshInit(
&out->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
loading->loading.mesh.triangleCount * 3,
out->vertices
);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
memoryFree(out->vertices);
out->vertices = NULL;
assetLoaderErrorChain(loading, ret);
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
} }
errorret_t assetMeshLoad( errorret_t assetMeshDispose(assetentry_t *entry) {
const char_t *path, assertNotNull(entry, "Asset entry cannot be NULL");
mesh_t *outMesh, assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
meshvertex_t **outVertices, errorChain(meshDispose(&entry->data.mesh.mesh));
const assetmeshinputaxis_t inputAxis memoryFree(entry->data.mesh.vertices);
) { errorOk();
assertNotNull(path, "Path cannot be null");
assertNotNull(outMesh, "Output mesh cannot be null");
assertNotNull(outVertices, "Output vertices cannot be null");
assetmeshoutput_t output = {
outMesh,
outVertices,
inputAxis
};
return assetMeshLoadToOutput(path, &output);
} }
+25 -28
View File
@@ -6,12 +6,15 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h" #include "asset/assetfile.h"
#include "display/mesh/mesh.h" #include "display/mesh/mesh.h"
#include "assert/assert.h" #include "assert/assert.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef enum { typedef enum {
MESH_INPUT_AXIS_Y_UP,// Default MESH_INPUT_AXIS_Y_UP,
MESH_INPUT_AXIS_Z_UP, MESH_INPUT_AXIS_Z_UP,
MESH_INPUT_AXIS_X_UP, MESH_INPUT_AXIS_X_UP,
@@ -20,10 +23,24 @@ typedef enum {
MESH_INPUT_AXIS_X_DOWN, MESH_INPUT_AXIS_X_DOWN,
} assetmeshinputaxis_t; } assetmeshinputaxis_t;
typedef assetmeshinputaxis_t assetmeshloaderinput_t;
typedef enum {
ASSET_MESH_LOADING_STATE_INITIAL,
ASSET_MESH_LOADING_STATE_READ_FILE,
ASSET_MESH_LOADING_STATE_CREATE_MESH,
ASSET_MESH_LOADING_STATE_DONE
} assetmeshloadingstate_t;
typedef struct { typedef struct {
mesh_t *outMesh; assetfile_t file;
meshvertex_t **outVertices; assetmeshloadingstate_t state;
assetmeshinputaxis_t inputAxis; uint32_t triangleCount;
} assetmeshloaderloading_t;
typedef struct {
mesh_t mesh;
meshvertex_t *vertices;
} assetmeshoutput_t; } assetmeshoutput_t;
#pragma pack(push, 1) #pragma pack(push, 1)
@@ -36,26 +53,6 @@ typedef struct {
assertStructSize(assetmeshstltriangle_t, 50); assertStructSize(assetmeshstltriangle_t, 50);
/** errorret_t assetMeshLoaderAsync(assetloading_t *loading);
* Loader for mesh assets. errorret_t assetMeshLoaderSync(assetloading_t *loading);
* errorret_t assetMeshDispose(assetentry_t *entry);
* @param file Asset file to load the mesh from.
* @return Any error that occurs during loading.
*/
errorret_t assetMeshLoader(assetfile_t *file);
/**
* Handler for mesh assets.
*
* @param file Asset file to load the mesh from.
* @param outMesh Output mesh to load the data into.
* @param outVertices Output pointer to the vertex data, used for cleanup.
* @param inputAxis The axis orientation of the input mesh data.
* @return Any error that occurs during loading.
*/
errorret_t assetMeshLoad(
const char_t *path,
mesh_t *outMesh,
meshvertex_t **outVertices,
const assetmeshinputaxis_t inputAxis
);
@@ -11,6 +11,8 @@
#include "stb_image.h" #include "stb_image.h"
#include "log/log.h" #include "log/log.h"
#include "util/endian.h" #include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = { stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = {
.read = assetTextureReader, .read = assetTextureReader,
@@ -25,7 +27,7 @@ int assetTextureReader(void *user, char *data, int size) {
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL."); assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, data, (size_t)size); errorret_t ret = assetFileRead(file, data, (size_t)size);
if(ret.code != ERROR_OK) { if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret)); errorCatch(errorPrint(ret));
return -1; return -1;
} }
@@ -38,7 +40,7 @@ void assetTextureSkipper(void *user, int n) {
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL."); assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, NULL, (size_t)n); errorret_t ret = assetFileRead(file, NULL, (size_t)n);
if(ret.code != ERROR_OK) { if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret)); errorCatch(errorPrint(ret));
} }
} }
@@ -50,66 +52,114 @@ int assetTextureEOF(void *user) {
return file->position >= file->size; return file->position >= file->size;
} }
errorret_t assetTextureLoader(assetfile_t *file) { errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(loading, "Loading cannot be NULL");
assertNotNull(file->params, "Asset file parameters cannot be NULL.");
assertNotNull(file->output, "Asset file output cannot be NULL.");
assettextureloaderparams_t *p = (assettextureloaderparams_t*)file->params; // Only care about loading pixels.
assertNotNull(p, "Asset texture loader parameters cannot be NULL."); if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){
errorOk();
}
// Init the file
assertNull(
loading->loading.texture.data, "Pixels already defined?"
);
assetfile_t *file = &loading->loading.texture.file;
assetLoaderErrorChain(loading, assetFileInit(
file,
loading->entry->name,
NULL,
&loading->entry->data.texture
));
assetLoaderErrorChain(loading, assetFileOpen(file));
// Determine channels
int channelsDesired; int channelsDesired;
switch(p->format) { switch(loading->entry->input->texture) {
case TEXTURE_FORMAT_RGBA: case TEXTURE_FORMAT_RGBA:
channelsDesired = 4; channelsDesired = 4;
break; break;
default: default:
errorThrow("Bad texture format."); assetLoaderErrorThrow(loading, "Bad texture format.");
} }
int width, height, channels; // Load image pixels.
errorChain(assetFileOpen(file)); loading->loading.texture.data = stbi_load_from_callbacks(
uint8_t *data = stbi_load_from_callbacks(
&ASSET_TEXTURE_STB_CALLBACKS, &ASSET_TEXTURE_STB_CALLBACKS,
file, file,
&width, &loading->loading.texture.width,
&height, &loading->loading.texture.height,
&channels, &loading->loading.texture.channels,
channelsDesired channelsDesired
); );
errorChain(assetFileClose(file));
if(data == NULL) { // Close out the file.
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
// Ensure we loaded correctly.
if(loading->loading.texture.data == NULL) {
const char_t *errorStr = stbi_failure_reason(); const char_t *errorStr = stbi_failure_reason();
errorThrow("Failed to load texture from file %s.", errorStr); assetLoaderErrorThrow(loading, "Failed to load texture from file %s.", errorStr);
} }
// Fixes a specific bug probably with Dolphin but for now just assuming endian // Fixes a specific bug probably with Dolphin but for now just assuming endian
if(!isHostLittleEndian()) { if(!isHostLittleEndian()) {
stbi__vertical_flip(data, width, height, channelsDesired); stbi__vertical_flip(
loading->loading.texture.data,
loading->loading.texture.width,
loading->loading.texture.height,
loading->loading.texture.channels
);
} }
errorChain(textureInit( loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE;
(texture_t*)file->output, loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
(int32_t)width, (int32_t)height,
p->format,
(texturedata_t){
.rgbaColors = (color_t*)data
}
));
stbi_image_free(data);
errorOk(); errorOk();
} }
errorret_t assetTextureLoad( errorret_t assetTextureLoaderSync(assetloading_t *loading) {
const char_t *path, assertNotNull(loading, "Loading cannot be NULL");
texture_t *out,
const textureformat_t format switch(loading->loading.texture.state) {
) { case ASSET_TEXTURE_LOADING_STATE_INITIAL:
assettextureloaderparams_t params = { loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS;
.format = format loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
}; errorOk();
return assetLoad(path, assetTextureLoader, &params, out); break;
case ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE:
break;
default:
errorOk();
}
// Create the texture.
assertNotNull(
loading->loading.texture.data, "Pixels should have been loaded by now."
);
assetLoaderErrorChain(loading, textureInit(
(texture_t*)&loading->entry->data.texture,
loading->loading.texture.width,
loading->loading.texture.height,
loading->entry->input->texture,
(texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data
}
));
// Free the pixels.
stbi_image_free(loading->loading.texture.data);
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTextureDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
return textureDispose(&entry->data.texture);
} }
@@ -6,12 +6,29 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h" #include "asset/assetfile.h"
#include "display/texture/texture.h" #include "display/texture/texture.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef textureformat_t assettextureloaderinput_t;
typedef enum {
ASSET_TEXTURE_LOADING_STATE_INITIAL,
ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS,
ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE,
ASSET_TEXTURE_LOADING_STATE_DONE
} assettextureloadingstate_t;
typedef struct { typedef struct {
textureformat_t format; assetfile_t file;
} assettextureloaderparams_t; assettextureloadingstate_t state;
int channels, width, height;
uint8_t *data;
} assettextureloaderloading_t;
typedef texture_t assettextureoutput_t;
/** /**
* STB image read callback for asset files. * STB image read callback for asset files.
@@ -23,28 +40,42 @@ typedef struct {
*/ */
int assetTextureReader(void *user, char *data, int size); int assetTextureReader(void *user, char *data, int size);
/**
* STB image skip callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param n Number of bytes to skip in the file.
*/
void assetTextureSkipper(void *user, int n); void assetTextureSkipper(void *user, int n);
/**
* STB image EOF callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @return Non-zero if end of file has been reached, zero otherwise.
*/
int assetTextureEOF(void *user); int assetTextureEOF(void *user);
/** /**
* Handler for texture assets. * Synchronous loader for texture assets.
* *
* @param file Asset file to load the texture from. * @param loading Loading information for the asset being loaded.
* @return Any error that occurs during loading. * @return Error code indicating success or failure of the load operation.
*/ */
errorret_t assetTextureLoader(assetfile_t *file); errorret_t assetTextureLoaderAsync(assetloading_t *loading);
/** /**
* Loads a texture from the specified path. * Synchronous loader for texture assets.
* *
* @param path Path to the texture asset. * @param loading Loading information for the asset being loaded.
* @param out Output texture to load into. * @return Error code indicating success or failure of the load operation.
* @param format Format of the texture to load.
* @return Any error that occurs during loading.
*/ */
errorret_t assetTextureLoad( errorret_t assetTextureLoaderSync(assetloading_t *loading);
const char_t *path,
texture_t *out, /**
const textureformat_t format * Disposer for texture assets.
); *
* @param entry Asset entry containing the texture to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTextureDispose(assetentry_t *entry);
@@ -9,72 +9,107 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/endian.h" #include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetTilesetLoader(assetfile_t *file) { errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(file, "Asset file pointer for tileset loader is null."); assertNotNull(loading, "Loading cannot be NULL");
tileset_t *out = (tileset_t *)file->output; if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
assertNotNull(out, "Output pointer for tileset loader is null."); errorOk();
uint8_t *entire = memoryAllocate(file->size);
errorChain(assetFileOpen(file));
errorChain(assetFileRead(file, entire, file->size));
errorChain(assetFileClose(file));
assertTrue(file->lastRead == file->size, "Failed to read entire file.");
if(
entire[0] != 'D' ||
entire[1] != 'T' ||
entire[2] != 'F'
) {
errorThrow("Invalid tileset header");
} }
if(entire[3] != 0x00) { assertNull(loading->loading.tileset.data, "Data already defined?");
errorThrow("Unsupported tileset version");
}
// Fix endianness assetfile_t *file = &loading->loading.tileset.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
out->tileWidth = endianLittleToHost16(*(uint16_t *)(entire + 4)); uint8_t *data = memoryAllocate(file->size);
out->tileHeight = endianLittleToHost16(*(uint16_t *)(entire + 6)); assetLoaderErrorChain(loading, assetFileOpen(file));
out->columns = endianLittleToHost16(*(uint16_t *)(entire + 8)); assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
out->rows = endianLittleToHost16(*(uint16_t *)(entire + 10)); assetLoaderErrorChain(loading, assetFileClose(file));
// out->right = endianLittleToHost16(*(uint16_t *)(entire + 12)); assetLoaderErrorChain(loading, assetFileDispose(file));
// out->bottom = endianLittleToHost16(*(uint16_t *)(entire + 14)); assertTrue(file->lastRead == file->size, "Failed to read entire tileset file.");
if(out->tileWidth == 0) {
errorThrow("Tile width cannot be 0");
}
if(out->tileHeight == 0) {
errorThrow("Tile height cannot be 0");
}
if(out->columns == 0) {
errorThrow("Column count cannot be 0");
}
if(out->rows == 0) {
errorThrow("Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(entire + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(entire + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
errorThrow("Invalid v0 value in tileset");
}
// Setup tileset
out->tileCount = out->columns * out->rows;
memoryFree(entire);
loading->loading.tileset.data = data;
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
} }
errorret_t assetTilesetLoad( errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
const char_t *path, assertNotNull(loading, "Loading cannot be NULL");
tileset_t *out assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
) {
return assetLoad(path, assetTilesetLoader, NULL, out); switch(loading->loading.tileset.state) {
case ASSET_TILESET_LOADING_STATE_INITIAL:
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TILESET_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.tileset.data;
assertNotNull(data, "Tileset data should have been loaded by now.");
tileset_t *out = &loading->entry->data.tileset;
if(data[0] != 'D' || data[1] != 'T' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid tileset header");
}
if(data[3] != 0x00) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Unsupported tileset version");
}
out->tileWidth = endianLittleToHost16(*(uint16_t *)(data + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(data + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(data + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(data + 10));
if(out->tileWidth == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile width cannot be 0");
}
if(out->tileHeight == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile height cannot be 0");
}
if(out->columns == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Column count cannot be 0");
}
if(out->rows == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
}
out->tileCount = out->columns * out->rows;
memoryFree(data);
loading->loading.tileset.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
errorOk();
} }
@@ -6,25 +6,55 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h" #include "asset/assetfile.h"
#include "display/texture/tileset.h" #include "display/texture/tileset.h"
/** typedef struct assetloading_s assetloading_t;
* Handler for tileset assets. typedef struct assetentry_s assetentry_t;
*
* @param file Asset file to load the tileset from. typedef struct {
* @return Any error that occurs during loading. void *nothing;
*/ } assettilesetloaderinput_t;
errorret_t assetTilesetLoader(assetfile_t *file);
typedef enum {
ASSET_TILESET_LOADING_STATE_INITIAL,
ASSET_TILESET_LOADING_STATE_READ_FILE,
ASSET_TILESET_LOADING_STATE_PARSE,
ASSET_TILESET_LOADING_STATE_DONE
} assettilesetloadingstate_t;
typedef struct {
assetfile_t file;
assettilesetloadingstate_t state;
uint8_t *data;
} assettilesetloaderloading_t;
typedef tileset_t assettilesetoutput_t;
/** /**
* Loads a tileset from the specified path. * Asynchronous loader for tileset assets. Reads the raw DTF file bytes into
* the loading buffer so the sync phase can parse without blocking the main
* thread on I/O.
* *
* @param path Path to the tileset asset. * @param loading Loading information for the asset being loaded.
* @param out Output tileset to load into. * @return Error code indicating success or failure of the load operation.
* @return Any error that occurs during loading.
*/ */
errorret_t assetTilesetLoad( errorret_t assetTilesetLoaderAsync(assetloading_t *loading);
const char_t *path,
tileset_t *out /**
); * Synchronous loader for tileset assets. Parses the DTF binary previously
* read by the async phase and populates the output tileset_t.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTilesetLoaderSync(assetloading_t *loading);
/**
* Disposer for tileset assets.
*
* @param entry Asset entry containing the tileset to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTilesetDispose(assetentry_t *entry);
+64 -29
View File
@@ -8,46 +8,81 @@
#include "assetjsonloader.h" #include "assetjsonloader.h"
#include "util/memory.h" #include "util/memory.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetJsonLoadFileToDoc(assetfile_t *file, yyjson_doc **outDoc) { errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
assertNotNull(file, "Asset file pointer for JSON loader is null."); assertNotNull(loading, "Loading cannot be NULL");
assertNotNull(outDoc, "Output pointer for JSON loader is null.");
if(file->size > ASSET_JSON_FILE_SIZE_MAX) { if(loading->loading.json.state != ASSET_JSON_LOADING_STATE_READ_FILE) {
errorThrow("JSON exceeds maximum allowed size"); errorOk();
} }
// Create buffer assertNull(loading->loading.json.buffer, "Buffer already defined?");
uint8_t *buffer = memoryAllocate(file->size);
errorChain(assetFileOpen(file)); assetfile_t *file = &loading->loading.json.file;
assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
// Read entire file if(file->size > ASSET_JSON_FILE_SIZE_MAX) {
errorChain(assetFileRead(file, buffer, file->size)); assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
}
size_t fileSize = (size_t)file->size;
uint8_t *buffer = memoryAllocate(fileSize);
assetLoaderErrorChain(loading, assetFileOpen(file));
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file."); assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
errorChain(assetFileClose(file)); loading->loading.json.buffer = buffer;
loading->loading.json.size = fileSize;
*outDoc = yyjson_read( loading->loading.json.state = ASSET_JSON_LOADING_STATE_PARSE;
buffer, loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
file->size,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
if(!*outDoc) errorThrow("Failed to parse JSON");
errorOk(); errorOk();
} }
errorret_t assetJsonLoader(assetfile_t *file) { errorret_t assetJsonLoaderSync(assetloading_t *loading) {
yyjson_doc **outDoc = (yyjson_doc **)file->output; assertNotNull(loading, "Loading cannot be NULL");
assertNotNull(outDoc, "Output pointer for JSON loader is null."); assertTrue(loading->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
return assetJsonLoadFileToDoc(file, outDoc);
switch(loading->loading.json.state) {
case ASSET_JSON_LOADING_STATE_INITIAL:
loading->loading.json.state = ASSET_JSON_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_JSON_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *buffer = loading->loading.json.buffer;
assertNotNull(buffer, "JSON buffer should have been loaded by now.");
loading->entry->data.json = yyjson_read(
(char *)buffer,
loading->loading.json.size,
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
);
memoryFree(buffer);
loading->loading.json.buffer = NULL;
if(!loading->entry->data.json) {
assetLoaderErrorThrow(loading, "Failed to parse JSON");
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
} }
errorret_t assetJsonLoad( errorret_t assetJsonDispose(assetentry_t *entry) {
const char_t *path, assertNotNull(entry, "Asset entry cannot be NULL");
yyjson_doc **outDoc assertTrue(entry->type == ASSET_LOADER_TYPE_JSON, "Invalid type.");
) { yyjson_doc_free(entry->data.json);
return assetLoad(path, assetJsonLoader, NULL, outDoc); entry->data.json = NULL;
errorOk();
} }
+22 -30
View File
@@ -6,40 +6,32 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h" #include "asset/assetfile.h"
#include "yyjson.h" #include "yyjson.h"
#define ASSET_JSON_FILE_SIZE_MAX 1024*256 #define ASSET_JSON_FILE_SIZE_MAX 1024*256
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetjsonloaderinput_t;
typedef enum {
ASSET_JSON_LOADING_STATE_INITIAL,
ASSET_JSON_LOADING_STATE_READ_FILE,
ASSET_JSON_LOADING_STATE_PARSE,
ASSET_JSON_LOADING_STATE_DONE
} assetjsonloadingstate_t;
typedef struct { typedef struct {
void *nothing; assetfile_t file;
} assetjsonloaderparams_t; assetjsonloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetjsonloaderloading_t;
/** typedef yyjson_doc * assetjsonoutput_t;
* Loads a JSON document from the specified asset file.
*
* @param file Asset file to load the JSON document from.
* @param outDoc Pointer to store the loaded JSON document.
* @return Any error that occurs during loading.
*/
errorret_t assetJsonLoadFileToDoc(assetfile_t *file, yyjson_doc **outDoc);
/** errorret_t assetJsonLoaderAsync(assetloading_t *loading);
* Handler for locale assets. errorret_t assetJsonLoaderSync(assetloading_t *loading);
* errorret_t assetJsonDispose(assetentry_t *entry);
* @param file Asset file to load the locale from.
* @return Any error that occurs during loading.
*/
errorret_t assetJsonLoader(assetfile_t *file);
/**
* Loads a locale from the specified path.
*
* @param path Path to the locale asset.
* @param outDoc Pointer to store the loaded JSON document.
* @return Any error that occurs during loading.
*/
errorret_t assetJsonLoad(
const char_t *path,
yyjson_doc **outDoc
);
@@ -11,38 +11,60 @@
#include "util/string.h" #include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
errorret_t assetLocaleFileInit( #include "asset/loader/assetloading.h"
assetlocalefile_t *localeFile, #include "asset/loader/assetentry.h"
const char_t *path
) {
assertNotNull(localeFile, "Locale file cannot be NULL.");
assertNotNull(path, "Locale file path cannot be NULL.");
errorret_t assetLocaleLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
if(loading->loading.locale.state != ASSET_LOCALE_LOADER_STATE_LOAD_HEADER) {
errorOk();
}
assetlocalefile_t *localeFile = &loading->entry->data.locale;
memoryZero(localeFile, sizeof(assetlocalefile_t)); memoryZero(localeFile, sizeof(assetlocalefile_t));
assetLoaderErrorChain(loading, assetFileInit(&localeFile->file, loading->entry->name, NULL, NULL));
assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file));
// Init the asset file.
errorChain(assetFileInit(&localeFile->file, path, NULL, NULL));
// Open the file handle
errorChain(assetFileOpen(&localeFile->file));
// Get the blank key, this is basically the header info for po files
char_t buffer[1024]; char_t buffer[1024];
errorChain(assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer))); assetLoaderErrorChain(loading, assetLocaleGetString(localeFile, "", 0, buffer, sizeof(buffer)));
errorChain(assetLocaleParseHeader(localeFile, buffer, sizeof(buffer))); assetLoaderErrorChain(loading, assetLocaleParseHeader(localeFile, buffer, sizeof(buffer)));
loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_DONE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk(); errorOk();
} }
errorret_t assetLocaleFileDispose(assetlocalefile_t *localeFile) { errorret_t assetLocaleLoaderSync(assetloading_t *loading) {
assertNotNull(localeFile, "Locale file cannot be NULL."); assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
errorChain(assetFileClose(&localeFile->file)); switch(loading->loading.locale.state) {
errorChain(assetFileDispose(&localeFile->file)); case ASSET_LOCALE_LOADER_STATE_INITIAL:
loading->loading.locale.state = ASSET_LOCALE_LOADER_STATE_LOAD_HEADER;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_LOCALE_LOADER_STATE_DONE:
break;
default:
errorOk();
}
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk(); errorOk();
} }
errorret_t assetLocaleDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_LOCALE, "Invalid type.");
assetlocalefile_t *localeFile = &entry->data.locale;
errorChain(assetFileClose(&localeFile->file));
return assetFileDispose(&localeFile->file);
}
errorret_t assetLocaleParseHeader( errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile, assetlocalefile_t *localeFile,
char_t *headerBuffer, char_t *headerBuffer,
@@ -152,7 +174,7 @@ errorret_t assetLocaleParseHeader(
if(pluralIndex >= localeFile->pluralStateCount - 1) { if(pluralIndex >= localeFile->pluralStateCount - 1) {
errorThrow( errorThrow(
"Too many plural conditions. Expected %d conditional clauses for nplurals=%d.", "Too many plural conditions. Expected %d clauses for nplurals=%d.",
localeFile->pluralStateCount - 1, localeFile->pluralStateCount - 1,
localeFile->pluralStateCount localeFile->pluralStateCount
); );
@@ -307,13 +329,21 @@ errorret_t assetLocaleLineSkipBlanks(
while(!reader->eof) { while(!reader->eof) {
// Skip blank lines // Skip blank lines
if(lineBuffer[0] == '\0') { if(lineBuffer[0] == '\0') {
errorChain(assetFileLineReaderNext(reader)); errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue; continue;
} }
// Skip comment lines // Skip comment lines
if(lineBuffer[0] == '#') { if(lineBuffer[0] == '#') {
errorChain(assetFileLineReaderNext(reader)); errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue; continue;
} }
@@ -329,7 +359,11 @@ errorret_t assetLocaleLineSkipBlanks(
} }
if(onlySpaces) { if(onlySpaces) {
errorChain(assetFileLineReaderNext(reader)); errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
continue; continue;
} }
break; break;
@@ -365,10 +399,18 @@ errorret_t assetLocaleLineUnbuffer(
// Now start buffering lines out // Now start buffering lines out
while(!reader->eof) { while(!reader->eof) {
errorChain(assetFileLineReaderNext(reader)); errorret_t r = assetFileLineReaderNext(reader);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
// Skip blank lines // Skip blank lines
errorChain(assetLocaleLineSkipBlanks(reader, lineBuffer)); r = assetLocaleLineSkipBlanks(reader, lineBuffer);
if(errorIsNotOk(r)) {
errorCatch(r);
break;
}
// Skip starting spaces // Skip starting spaces
char_t *ptr = (char_t *)lineBuffer; char_t *ptr = (char_t *)lineBuffer;
@@ -593,7 +635,7 @@ errorret_t assetLocaleGetStringWithVA(
tempBuffer, tempBuffer,
bufferSize bufferSize
); );
if(ret.code != ERROR_OK) { if(errorIsNotOk(ret)) {
memoryFree(tempBuffer); memoryFree(tempBuffer);
return ret; return ret;
} }
@@ -641,7 +683,7 @@ errorret_t assetLocaleGetStringWithArgs(
format, format,
bufferSize bufferSize
); );
if(ret.code != ERROR_OK) { if(errorIsNotOk(ret)) {
memoryFree(format); memoryFree(format);
return ret; return ret;
} }
+172 -52
View File
@@ -6,10 +6,35 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h" #include "asset/assetfile.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
/** Input passed to the locale loader — currently unused. */
typedef struct { void *nothing; } assetlocaleloaderinput_t;
typedef enum {
ASSET_LOCALE_LOADER_STATE_INITIAL,
ASSET_LOCALE_LOADER_STATE_LOAD_HEADER,
ASSET_LOCALE_LOADER_STATE_DONE
} assetlocaleloaderstate_t;
/** Per-slot scratch data used while the locale file is loading. */
typedef struct {
assetlocaleloaderstate_t state;
} assetlocaleloaderloading_t;
/** Maximum number of distinct plural forms a locale file may declare. */
#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6 #define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6
/**
* Comparison operator used in a plural-form expression.
*
* Each condition in the plural-form header is evaluated as
* `n <op> value`
* where `n` is the runtime plural count.
*/
typedef enum { typedef enum {
ASSET_LOCALE_PLURAL_OP_EQUAL, ASSET_LOCALE_PLURAL_OP_EQUAL,
ASSET_LOCALE_PLURAL_OP_NOT_EQUAL, ASSET_LOCALE_PLURAL_OP_NOT_EQUAL,
@@ -19,13 +44,24 @@ typedef enum {
ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL
} assetlocalepluraloperation_t; } assetlocalepluraloperation_t;
/**
* Discriminator tag for a locale string argument.
* @see assetlocalearg_t
*/
typedef enum { typedef enum {
ASSET_LOCALE_ARG_STRING, ASSET_LOCALE_ARG_STRING,
ASSET_LOCALE_ARG_INT, ASSET_LOCALE_ARG_INT,
ASSET_LOCALE_ARG_FLOAT ASSET_LOCALE_ARG_FLOAT
} assetlocaleargtype_t; } assetlocaleargtype_t;
/**
* A single typed argument for locale string substitution.
*
* Used with @ref assetLocaleGetStringWithArgs to fill `%s`, `%d`, or `%f`
* placeholders inside a translated string.
*/
typedef struct { typedef struct {
/** Which union member is active. */
assetlocaleargtype_t type; assetlocaleargtype_t type;
union { union {
const char_t *stringValue; const char_t *stringValue;
@@ -34,35 +70,81 @@ typedef struct {
}; };
} assetlocalearg_t; } assetlocalearg_t;
/**
* Runtime state for an open locale file.
*
* Loaded once by @ref assetLocaleLoaderSync and kept alive for the lifetime
* of the asset entry. The plural-form fields are populated from the PO header
* by @ref assetLocaleParseHeader.
*
* Plural evaluation works as a linear scan over `pluralStateCount - 1`
* conditions; if none match, `pluralDefaultIndex` is returned.
*/
typedef struct { typedef struct {
/** Underlying file handle used to rewind and re-read the PO data. */
assetfile_t file; assetfile_t file;
/** Comparison operator for each conditional plural clause. */
assetlocalepluraloperation_t pluralOps[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT]; assetlocalepluraloperation_t pluralOps[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
/** Right-hand value for each conditional plural clause. */
int32_t pluralValues[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT]; int32_t pluralValues[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
/** Form index returned when the corresponding condition is true. */
int32_t pluralIndices[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT]; int32_t pluralIndices[ASSET_LOCALE_FILE_PLURAL_FORM_COUNT];
/** Total number of plural forms declared by `nplurals=`. */
uint8_t pluralStateCount; uint8_t pluralStateCount;
/** Form index used when no conditional clause matches. */
uint8_t pluralDefaultIndex; uint8_t pluralDefaultIndex;
} assetlocalefile_t; } assetlocalefile_t;
/** /** Convenience alias — the loaded output type of a locale asset entry. */
* Initialize a locale asset file. typedef assetlocalefile_t assetlocaleoutput_t;
*
* @param localeFile The locale file to initialize.
* @param path The path to the locale file.
* @return An error code if a failure occurs.
*/
errorret_t assetLocaleFileInit(
assetlocalefile_t *localeFile,
const char_t *path
);
/** /**
* Dispose of a locale asset file. * Asynchronous loader callback. Opens the locale file, reads the PO header,
* and parses plural-form rules. All I/O happens here so the main thread is
* not blocked. Sets entry state to `ASSET_ENTRY_STATE_PENDING_SYNC` on
* success or `ASSET_ENTRY_STATE_ERROR` on failure.
* *
* @param localeFile The locale file to dispose of. * @param loading The loading slot for this asset entry.
* @return An error code if a failure occurs. * @return OK on success, error otherwise.
*/ */
errorret_t assetLocaleFileDispose(assetlocalefile_t *localeFile); errorret_t assetLocaleLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader callback. Confirms the async phase completed and marks
* the entry as `ASSET_ENTRY_STATE_LOADED`.
*
* @param loading The loading slot for this asset entry.
* @return OK on success, error otherwise.
*/
errorret_t assetLocaleLoaderSync(assetloading_t *loading);
/**
* Dispose callback. Closes the open file handle and zeros the locale data.
*
* @param entry The asset entry to dispose.
* @return OK on success, error otherwise.
*/
errorret_t assetLocaleDispose(assetentry_t *entry);
/**
* Parses the `Plural-Forms:` line from a PO header string and populates the
* plural-form fields of `localeFile`. If no `Plural-Forms:` key is present
* the function returns OK without modifying the struct.
*
* Supports the `nplurals=N; plural=(<expr>);` syntax where `<expr>` is a
* chain of `n <op> value ? index : ...` ternary conditions ending with a
* fallback index.
*
* @param localeFile Struct whose plural fields will be filled in.
* @param headerBuffer The raw PO header string (msgstr of msgid "").
* @param headerBufferSize Size of `headerBuffer` in bytes.
* @return OK on success, error if the plural expression is malformed.
*/
errorret_t assetLocaleParseHeader( errorret_t assetLocaleParseHeader(
assetlocalefile_t *localeFile, assetlocalefile_t *localeFile,
char_t *headerBuffer, char_t *headerBuffer,
@@ -70,11 +152,29 @@ errorret_t assetLocaleParseHeader(
); );
/** /**
* Skips blank lines and comment lines in the line reader. * Evaluates which plural form index to use for a given count.
* *
* @param reader Line reader to read from. * Walks the conditions stored in `file` in order; returns the index for the
* @param lineBuffer Buffer to use for reading lines. * first condition that matches `pluralCount`. Falls back to
* @return Any error that occurs during skipping. * `file->pluralDefaultIndex` if none match.
*
* @param file Locale file with parsed plural rules.
* @param pluralCount The runtime count (e.g. number of items).
* @return Zero-based plural form index.
*/
uint8_t assetLocaleEvaluatePlural(
assetlocalefile_t *file,
const int32_t pluralCount
);
/**
* Advances the line reader past blank lines, comment lines (starting with
* `#`), and lines containing only spaces. Stops at the first content line or
* end of file.
*
* @param reader Line reader positioned at the current line.
* @param lineBuffer Output buffer that the reader writes each line into.
* @return OK on success, error if reading fails.
*/ */
errorret_t assetLocaleLineSkipBlanks( errorret_t assetLocaleLineSkipBlanks(
assetfilelinereader_t *reader, assetfilelinereader_t *reader,
@@ -82,16 +182,19 @@ errorret_t assetLocaleLineSkipBlanks(
); );
/** /**
* Unbuffers a potentially multi-line quoted string from the line reader. * Reads a PO quoted string value from the current line and any immediately
* following continuation lines that begin with `"`.
* *
* This will read lines until it finds a line that starts with a quote, then * Handles standard C escape sequences (`\n`, `\t`, `\\`, `\"`).
* read until the closing quote.
* *
* @param reader Line reader to read from. * @param reader Line reader positioned at the line containing the opening
* @param lineBuffer Buffer to use for reading lines. * quote (e.g. `msgstr "..."`).
* @param stringBuffer Buffer to write the unbuffered string to. * @param lineBuffer Buffer the reader fills on each @ref assetFileLineReaderNext
* @param stringBufferSize Size of the string buffer. * call; also used to detect continuation lines.
* @return Any error that occurs during unbuffering. * @param stringBuffer Destination for the unescaped string content.
* @param stringBufferSize Capacity of `stringBuffer` in bytes.
* @return OK on success, error if a quote or escape sequence is malformed or
* the buffer would overflow.
*/ */
errorret_t assetLocaleLineUnbuffer( errorret_t assetLocaleLineUnbuffer(
assetfilelinereader_t *reader, assetfilelinereader_t *reader,
@@ -101,14 +204,19 @@ errorret_t assetLocaleLineUnbuffer(
); );
/** /**
* Test function for locale asset loading. * Looks up a translated string by message ID from the open locale file.
* *
* @param file Asset file to test loading from. * Rewinds the file and scans from the beginning on every call. For plural
* @param messageId The message ID to retrieve. * entries (`msgid_plural`) the `pluralCount` is evaluated against the loaded
* @param pluralCount Count for formulating the plural variant. * plural rules to select the correct `msgstr[N]` form.
* @param stringBuffer Buffer to write the retrieved string to. *
* @param stringBufferSize Size of the string buffer. * @param file Locale file to search. Must be open.
* @return Any error that occurs during testing. * @param messageId PO message ID to find (`""` retrieves the header entry).
* @param pluralCount Count used to select the plural form (ignored for
* singular entries).
* @param stringBuffer Buffer to receive the translated string.
* @param stringBufferSize Capacity of `stringBuffer` in bytes.
* @return OK on success, error if the message ID is not found or I/O fails.
*/ */
errorret_t assetLocaleGetString( errorret_t assetLocaleGetString(
assetlocalefile_t *file, assetlocalefile_t *file,
@@ -119,15 +227,21 @@ errorret_t assetLocaleGetString(
); );
/** /**
* Test function for locale asset loading with a variable argument list. * Looks up a translated string and formats it with a `printf`-style variadic
* argument list.
* *
* @param file Asset file to test loading from. * Retrieves the raw format string via @ref assetLocaleGetString then applies
* @param messageId The message ID to retrieve. * `vsnprintf` with the provided arguments.
* @param pluralCount Count for formulating the plural variant. *
* @param buffer Buffer to write the retrieved string to. * @param file Locale file to search. Must be open.
* @param bufferSize Size of the buffer. * @param messageId PO message ID to find.
* @param ... Additional arguments for formatting the string. * @param pluralCount Count used to select the plural form.
* @return Any error that occurs during testing. * @param buffer Buffer to receive the formatted string.
* @param bufferSize Capacity of `buffer` in bytes.
* @param ... Format arguments matching the specifiers in the translated
* string.
* @return OK on success, error if the message is not found or formatting
* fails.
*/ */
errorret_t assetLocaleGetStringWithVA( errorret_t assetLocaleGetStringWithVA(
assetlocalefile_t *file, assetlocalefile_t *file,
@@ -139,16 +253,22 @@ errorret_t assetLocaleGetStringWithVA(
); );
/** /**
* Test function for locale asset loading with a list of arguments. * Looks up a translated string and substitutes typed arguments into its
* `%s`, `%d`, and `%f` placeholders.
* *
* @param file Asset file to test loading from. * Unlike @ref assetLocaleGetStringWithVA this variant accepts an explicit
* @param messageId The message ID to retrieve. * array of @ref assetlocalearg_t values, which is safer to use from
* @param pluralCount Count for formulating the plural variant. * generated or scripted call sites.
* @param buffer Buffer to write the retrieved string to. *
* @param bufferSize Size of the buffer. * @param file Locale file to search. Must be open.
* @param args List of arguments for formatting the string. * @param messageId PO message ID to find.
* @param argCount Number of arguments in the list. * @param pluralCount Count used to select the plural form.
* @return Any error that occurs during testing. * @param buffer Buffer to receive the formatted string.
* @param bufferSize Capacity of `buffer` in bytes.
* @param args Array of typed arguments; may be NULL when `argCount` is 0.
* @param argCount Number of elements in `args`.
* @return OK on success, error if the message is not found, an argument type
* mismatches the specifier, or the buffer would overflow.
*/ */
errorret_t assetLocaleGetStringWithArgs( errorret_t assetLocaleGetStringWithArgs(
assetlocalefile_t *file, assetlocalefile_t *file,
@@ -3,7 +3,6 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
assetscriptloader.c assetscriptloader.c
@@ -6,77 +6,111 @@
*/ */
#include "assetscriptloader.h" #include "assetscriptloader.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
#include "asset/loader/assetloader.h"
#include "util/memory.h"
#include "assert/assert.h" #include "assert/assert.h"
#include <jerryscript.h>
errorret_t assetScriptLoader(assetfile_t *file) { errorret_t assetScriptLoaderAsync(assetloading_t *loading) {
assertNotNull(file, "Asset file cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNull(file->zipFile, "Asset file zip handle must be NULL");
assertNotNull(file->output, "Asset file output cannot be NULL");
assetscript_t *script = (assetscript_t *)file->output; if(loading->loading.script.state != ASSET_SCRIPT_LOADING_STATE_READ_FILE) {
errorOk();
// Open the asset for buffering
errorChain(assetFileOpen(file));
// Request loading
if(!lua_load(
script->ctx->luaState,
assetScriptReader,
file,
file->filename,
NULL
) == LUA_OK) {
const char_t *strErr = lua_tostring(script->ctx->luaState, -1);
lua_pop(script->ctx->luaState, 1);
errorThrow("Failed to load Lua script: %s", strErr);
} }
// Now loaded, exec assertNull(loading->loading.script.buffer, "Buffer already defined?");
if(lua_pcall(script->ctx->luaState, 0, LUA_MULTRET, 0) != LUA_OK) {
const char_t *strErr = lua_tostring(script->ctx->luaState, -1); assetfile_t *file = &loading->loading.script.file;
lua_pop(script->ctx->luaState, 1); assetLoaderErrorChain(loading, assetFileInit(file, loading->entry->name, NULL, NULL));
errorThrow("Failed to execute Lua script: %s", strErr); assetLoaderErrorChain(loading, assetFileOpen(file));
size_t capacity = ASSET_SCRIPT_CHUNK_SIZE;
uint8_t *buffer = memoryAllocate(capacity + 1);
size_t offset = 0;
while(1) {
if(offset + ASSET_SCRIPT_CHUNK_SIZE > capacity) {
size_t oldCapacity = capacity + 1;
capacity += ASSET_SCRIPT_CHUNK_SIZE;
memoryResize((void **)&buffer, oldCapacity, capacity + 1);
}
assetLoaderErrorChain(loading, assetFileRead(
file, buffer + offset, ASSET_SCRIPT_CHUNK_SIZE
));
size_t chunk = (size_t)file->lastRead;
offset += chunk;
if(chunk == 0) break;
} }
// Close the file buffer[offset] = '\0';
return assetFileClose(file); assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
loading->loading.script.buffer = buffer;
loading->loading.script.size = offset;
loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_EXEC;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
} }
errorret_t assetScriptLoad(const char_t *path, scriptcontext_t *ctx) { errorret_t assetScriptLoaderSync(assetloading_t *loading) {
assertNotNull(path, "Script path cannot be NULL"); assertNotNull(loading, "Loading cannot be NULL");
assertNotNull(ctx, "Script context cannot be NULL"); assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
assetscript_t script; switch(loading->loading.script.state) {
script.ctx = ctx; case ASSET_SCRIPT_LOADING_STATE_INITIAL:
loading->loading.script.state = ASSET_SCRIPT_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
return assetLoad( case ASSET_SCRIPT_LOADING_STATE_EXEC:
path, break;
assetScriptLoader,
NULL, default:
&script errorOk();
}
uint8_t *buffer = loading->loading.script.buffer;
assertNotNull(buffer, "Script buffer should have been loaded by now.");
jerry_value_t result = jerry_eval(
(const jerry_char_t *)buffer,
loading->loading.script.size,
JERRY_PARSE_NO_OPTS
); );
} memoryFree(buffer);
loading->loading.script.buffer = NULL;
const char_t * assetScriptReader(lua_State* L, void* data, size_t* size) { if(jerry_value_is_exception(result)) {
assetfile_t *file = (assetfile_t*)data; jerry_value_t errVal = jerry_exception_value(result, false);
assertNotNull(file, "Script asset file cannot be NULL"); jerry_value_t errStr = jerry_value_to_string(errVal);
assertNotNull(file->zipFile, "Script asset zip handle cannot be NULL"); char_t buf[256];
assertNotNull(file->output, "Script asset output cannot be NULL"); jerry_size_t len = jerry_string_to_buffer(
errStr, JERRY_ENCODING_UTF8, (jerry_char_t *)buf, sizeof(buf) - 1
assetscript_t *script = (assetscript_t *)file->output; );
assertNotNull(script, "Script asset output cannot be NULL"); buf[len] = '\0';
jerry_value_free(errStr);
zip_int64_t read = zip_fread( jerry_value_free(errVal);
file->zipFile, jerry_value_free(result);
script->buffer, assetLoaderErrorThrow(loading, "Script error in '%s': %s",
sizeof(script->buffer) loading->entry->name, buf
); );
if(read < 0) {
*size = 0;
return NULL;
} }
*size = (size_t)read; loading->entry->data.script = (assetscriptoutput_t)result;
return script->buffer; loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetScriptDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
if(entry->data.script != 0) {
jerry_value_free((jerry_value_t)entry->data.script);
entry->data.script = 0;
}
errorOk();
} }
@@ -6,43 +6,30 @@
*/ */
#pragma once #pragma once
#include "asset/asset.h" #include "asset/assetfile.h"
#include "script/scriptcontext.h"
#define ASSET_SCRIPT_BUFFER_SIZE 1024 #define ASSET_SCRIPT_CHUNK_SIZE 1024
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct { void *nothing; } assetscriptloaderinput_t;
typedef uint32_t assetscriptoutput_t;
typedef enum {
ASSET_SCRIPT_LOADING_STATE_INITIAL,
ASSET_SCRIPT_LOADING_STATE_READ_FILE,
ASSET_SCRIPT_LOADING_STATE_EXEC,
ASSET_SCRIPT_LOADING_STATE_DONE
} assetscriptloadingstate_t;
typedef struct { typedef struct {
void *nothing; assetfile_t file;
} assetscriptloaderparams_t; assetscriptloadingstate_t state;
uint8_t *buffer;
size_t size;
} assetscriptloaderloading_t;
typedef struct { errorret_t assetScriptLoaderAsync(assetloading_t *loading);
scriptcontext_t *ctx; errorret_t assetScriptLoaderSync(assetloading_t *loading);
char_t buffer[ASSET_SCRIPT_BUFFER_SIZE]; errorret_t assetScriptDispose(assetentry_t *entry);
} assetscript_t;
/**
* Handler for script assets.
*
* @param file Asset file to load the script from.
* @return Any error that occurs during loading.
*/
errorret_t assetScriptLoader(assetfile_t *file);
/**
* Loads a script from the specified path.
*
* @param path Path to the script asset.
* @param ctx Script context to load the script into.
* @return Any error that occurs during loading.
*/
errorret_t assetScriptLoad(const char_t *path, scriptcontext_t *ctx);
/**
* Reader function for Lua to read script data from the asset.
*
* @param L Lua state.
* @param data Pointer to the scriptcontext_t structure.
* @param size Pointer to store the size of the read data.
* @return Pointer to the read data buffer.
*/
const char_t * assetScriptReader(lua_State* L, void* data, size_t* size);
@@ -3,8 +3,7 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
moduleui.c console.c
) )
+83
View File
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "console.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "input/input.h"
#include "log/log.h"
#include "engine/engine.h"
#include "display/shader/shaderunlit.h"
#include "display/text/text.h"
#include "display/spritebatch/spritebatch.h"
console_t CONSOLE;
void consoleInit(void) {
memoryZero(&CONSOLE, sizeof(console_t));
#ifdef DUSK_CONSOLE_POSIX
threadMutexInit(&CONSOLE.printMutex);
#endif
}
void consolePrint(const char_t *message, ...) {
char_t buffer[CONSOLE_LINE_MAX];
va_list args;
va_start(args, message);
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
va_end(args);
#ifdef DUSK_CONSOLE_POSIX
threadMutexLock(&CONSOLE.printMutex);
#endif
memoryMove(
CONSOLE.line[0],
CONSOLE.line[1],
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
);
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
#ifdef DUSK_CONSOLE_POSIX
threadMutexUnlock(&CONSOLE.printMutex);
#endif
logDebug("%s\n", buffer);
}
void consoleUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) return;
#endif
if(inputPressed(INPUT_ACTION_CONSOLE)) {
CONSOLE.visible = !CONSOLE.visible;
}
}
errorret_t consoleDraw(void) {
if(!CONSOLE.visible) errorOk();
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
errorChain(textDraw(
0, FONT_DEFAULT.tileset->tileHeight * i,
CONSOLE.line[i],
COLOR_WHITE,
&FONT_DEFAULT
));
}
return spriteBatchFlush();
}
void consoleDispose(void) {
#ifdef DUSK_CONSOLE_POSIX
threadMutexDispose(&CONSOLE.printMutex);
#endif
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "consoledefs.h"
#include "error/error.h"
#include "dusk.h"
#ifdef DUSK_CONSOLE_POSIX
#include "thread/thread.h"
#include <poll.h>
#include <unistd.h>
#define CONSOLE_POSIX_POLL_RATE 75
#endif
typedef struct {
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
bool_t visible;
#ifdef DUSK_CONSOLE_POSIX
threadmutex_t printMutex;
#endif
} console_t;
extern console_t CONSOLE;
/**
* Initializes the console.
*/
void consoleInit(void);
/**
* Prints a message to the console history.
*
* @param message The message to print (printf-style).
*/
void consolePrint(const char_t *message, ...);
/**
* Processes pending queued script lines. Call once per frame from main thread.
*/
void consoleUpdate(void);
/**
* Renders the console history to the screen (UI space).
*
* @return The error return value.
*/
errorret_t consoleDraw(void);
/**
* Disposes of the console.
*/
void consoleDispose(void);
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#define CONSOLE_LINE_MAX 512
#define CONSOLE_HISTORY_MAX 16
#define CONSOLE_EXEC_BUFFER_MAX 32
@@ -3,8 +3,7 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
moduleitem.c cutscene.c
) )
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) 2026 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#include "cutscene.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "console/console.h"
#include "time/time.h"
cutscene_t CUTSCENE;
errorret_t cutsceneInit(void) {
memoryZero(&CUTSCENE, sizeof(cutscene_t));
errorOk();
}
errorret_t cutsceneUpdate(void) {
#ifdef DUSK_TIME_DYNAMIC
if(TIME.dynamicUpdate) {
errorOk();
}
#endif
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *event = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(event->onUpdate) errorChain(event->onUpdate());
errorOk();
}
errorret_t cutscenePlay(
const cutsceneevent_t *events,
const uint8_t eventCount
) {
assertNotNull(events, "Events cannot be null");
assertTrue(eventCount > 0, "Event count must be greater than zero");
assertTrue(
eventCount <= CUTSCENE_EVENT_COUNT_MAX,
"Event count exceeds CUTSCENE_EVENT_COUNT_MAX"
);
if(CUTSCENE.active) {
errorChain(cutsceneStop());
}
memoryCopy(CUTSCENE.events, events, sizeof(cutsceneevent_t) * eventCount);
CUTSCENE.eventCount = eventCount;
CUTSCENE.eventCurrent = 0;
CUTSCENE.active = true;
cutsceneevent_t *firstEvent = &CUTSCENE.events[0];
if(firstEvent->onStart) errorChain(firstEvent->onStart());
errorOk();
}
errorret_t cutsceneAdvance(void) {
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *currentEvent = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(currentEvent->onEnd) errorChain(currentEvent->onEnd());
CUTSCENE.eventCurrent++;
if(CUTSCENE.eventCurrent >= CUTSCENE.eventCount) {
if(CUTSCENE.onStop) errorChain(CUTSCENE.onStop());
CUTSCENE.active = false;
errorOk();
}
cutsceneevent_t *nextEvent = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(nextEvent->onStart) errorChain(nextEvent->onStart());
consolePrint("Cutscene advance");
errorOk();
}
errorret_t cutsceneStop(void) {
if(!CUTSCENE.active) errorOk();
cutsceneevent_t *currentEvent = &CUTSCENE.events[CUTSCENE.eventCurrent];
if(currentEvent->onEnd) errorChain(currentEvent->onEnd());
if(CUTSCENE.onStop) errorChain(CUTSCENE.onStop());
CUTSCENE.active = false;
errorOk();
}
errorret_t cutsceneDispose(void) {
errorChain(cutsceneStop());
errorOk();
}
bool_t cutsceneIsActive(void) {
return CUTSCENE.active;
}
+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"
#define CUTSCENE_EVENT_COUNT_MAX 16
typedef struct {
errorret_t (*onStart)(void);
errorret_t (*onEnd)(void);
errorret_t (*onUpdate)(void);
} cutsceneevent_t;
typedef struct {
cutsceneevent_t events[CUTSCENE_EVENT_COUNT_MAX];
uint8_t eventCount;
uint8_t eventCurrent;
errorret_t (*onStop)(void);
bool_t active;
} cutscene_t;
extern cutscene_t CUTSCENE;
/**
* Initializes the cutscene manager.
*
* @return Any error state that happened.
*/
errorret_t cutsceneInit(void);
/**
* Ticks the active cutscene event, calling its onUpdate callback.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneUpdate(void);
/**
* Copies the given event array and begins playing from the first
* event. If a cutscene is already playing it is stopped first.
*
* @param events Array of events to copy.
* @param eventCount Number of events. Must be > 0 and
* <= CUTSCENE_EVENT_COUNT_MAX.
* @return Any error state that happened.
*/
errorret_t cutscenePlay(
const cutsceneevent_t *events,
const uint8_t eventCount
);
/**
* Ends the current event and starts the next one.
* Marks the cutscene as inactive after the last event ends.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneAdvance(void);
/**
* Ends the current event and stops the cutscene immediately.
* Does nothing when no cutscene is playing.
*
* @return Any error state that happened.
*/
errorret_t cutsceneStop(void);
/**
* Disposes of the cutscene manager, stopping any active cutscene.
*
* @return Any error state that happened.
*/
errorret_t cutsceneDispose(void);
/**
* Returns whether a cutscene is currently playing.
*
* @return true if a cutscene is active.
*/
bool_t cutsceneIsActive(void);
+11 -31
View File
@@ -22,11 +22,9 @@
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "asset/asset.h" #include "asset/asset.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderlist.h"
#include "time/time.h" #include "time/time.h"
#include "script/module/display/moduleshader.h"
display_t DISPLAY = { 0 }; display_t DISPLAY = { 0 };
errorret_t displayInit(void) { errorret_t displayInit(void) {
@@ -35,6 +33,7 @@ errorret_t displayInit(void) {
#ifdef displayPlatformInit #ifdef displayPlatformInit
errorChain(displayPlatformInit()); errorChain(displayPlatformInit());
#endif #endif
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
errorChain(textureInit( errorChain(textureInit(
&TEXTURE_WHITE, 4, 4, &TEXTURE_WHITE, 4, 4,
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS } TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
@@ -54,31 +53,8 @@ errorret_t displayInit(void) {
errorChain(screenInit()); errorChain(screenInit());
// Setup initial shader with default values // Setup initial shader with default values
mat4 view, proj, model;
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
view
);
glm_perspective( errorChain(shaderListInit());
glm_rad(45.0f),
(float_t)SCREEN.width / (float_t)SCREEN.height,
0.1f,
100.0f,
proj
);
glm_mat4_identity(model);
errorChain(shaderInit(&SHADER_UNLIT, &SHADER_UNLIT_DEFINITION));
errorChain(shaderBind(&SHADER_UNLIT));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view));
errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model));
errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL));
errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE));
errorOk(); errorOk();
} }
@@ -101,9 +77,6 @@ errorret_t displayUpdate(void) {
errorChain(sceneRender()); errorChain(sceneRender());
// Render UI
// uiRender();
// Finish up // Finish up
screenUnbind(); screenUnbind();
screenRender(); screenRender();
@@ -115,8 +88,15 @@ errorret_t displayUpdate(void) {
errorOk(); errorOk();
} }
errorret_t displaySetState(displaystate_t state) {
#ifdef displayPlatformSetState
errorChain(displayPlatformSetState(state));
#endif
errorOk();
}
errorret_t displayDispose(void) { errorret_t displayDispose(void) {
errorChain(shaderDispose(&SHADER_UNLIT)); errorChain(shaderListDispose());
errorChain(spriteBatchDispose()); errorChain(spriteBatchDispose());
screenDispose(); screenDispose();
errorChain(textDispose()); errorChain(textDispose());
+11
View File
@@ -40,15 +40,26 @@ extern display_t DISPLAY;
/** /**
* Initializes the display system. * Initializes the display system.
* @return An errorret_t indicating success or failure.
*/ */
errorret_t displayInit(void); errorret_t displayInit(void);
/** /**
* Tells the display system to actually draw the frame. * Tells the display system to actually draw the frame.
* @return An errorret_t indicating success or failure.
*/ */
errorret_t displayUpdate(void); errorret_t displayUpdate(void);
/**
* Sets the display state.
*
* @param state The state to set.
* @return An errorret_t indicating success or failure.
*/
errorret_t displaySetState(displaystate_t state);
/** /**
* Disposes of the display system. * Disposes of the display system.
* @return An errorret_t indicating success or failure.
*/ */
errorret_t displayDispose(void); errorret_t displayDispose(void);
+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
*/
#pragma once
#include "dusk.h"
#define DISPLAY_STATE_FLAG_CULL (1 << 0)
#define DISPLAY_STATE_FLAG_DEPTH_TEST (1 << 1)
#define DISPLAY_STATE_FLAG_BLEND (1 << 2)
typedef struct {
uint8_t flags;
} displaystate_t;
@@ -40,6 +40,17 @@ uint32_t frameBufferGetHeight(const framebuffer_t *framebuffer) {
return frameBufferPlatformGetHeight(framebuffer); return frameBufferPlatformGetHeight(framebuffer);
} }
float_t frameBufferGetAspect(const framebuffer_t *framebuffer) {
#ifdef frameBufferPlatformGetAspect
return frameBufferPlatformGetAspect(framebuffer);
#endif
uint32_t width = frameBufferGetWidth(framebuffer);
uint32_t height = frameBufferGetHeight(framebuffer);
if(height == 0) return 1.0f; // Avoid divide by zero, just return 1:1 aspect.
return (float_t)width / (float_t)height;
}
void frameBufferClear(const uint8_t flags, const color_t color) { void frameBufferClear(const uint8_t flags, const color_t color) {
frameBufferPlatformClear(flags, color); frameBufferPlatformClear(flags, color);
} }
@@ -58,6 +58,16 @@ uint32_t frameBufferGetWidth(const framebuffer_t *framebuffer);
*/ */
uint32_t frameBufferGetHeight(const framebuffer_t *framebuffer); uint32_t frameBufferGetHeight(const framebuffer_t *framebuffer);
/**
* Returns the aspect ratio of the framebuffer. This is ALMOST always just
* the width / height, however some platforms may choose to override this if
* they have stretched styled back buffers, e.g. 640x480 stretched.
*
* @param framebuffer The framebuffer to get the aspect ratio of.
* @return The aspect ratio of the framebuffer.
*/
float_t frameBufferGetAspect(const framebuffer_t *framebuffer);
/** /**
* Binds the framebuffer for rendering, or the backbuffer if the framebuffer * Binds the framebuffer for rendering, or the backbuffer if the framebuffer
* provided is NULL. * provided is NULL.
+12 -4
View File
@@ -119,8 +119,12 @@ void capsuleBuffer(
{ {
const float_t yTop = cy + halfHeight; const float_t yTop = cy + halfHeight;
const float_t yBot = cy - halfHeight; const float_t yBot = cy - halfHeight;
const float_t vTop = 1.0f - (float_t)capRings / (float_t)(2 * capRings + 1); const float_t vTop = (
const float_t vBot = 1.0f - (float_t)(capRings + 1) / (float_t)(2 * capRings + 1); 1.0f - (float_t)capRings / (float_t)(2 * capRings + 1)
);
const float_t vBot = (
1.0f - (float_t)(capRings + 1) / (float_t)(2 * capRings + 1)
);
for(int32_t j = 0; j < sectors; j++) { for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep; const float_t t1 = (float_t)j * sectorStep;
@@ -152,8 +156,12 @@ void capsuleBuffer(
const float_t lxz1 = radius * cosf(phi1); const float_t lxz1 = radius * cosf(phi1);
const float_t lxz2 = radius * cosf(phi2); const float_t lxz2 = radius * cosf(phi2);
const float_t v1 = 1.0f - (float_t)(capRings + 1 + i) / (float_t)(2 * capRings + 1); const float_t v1 = (
const float_t v2 = 1.0f - (float_t)(capRings + 1 + i + 1) / (float_t)(2 * capRings + 1); 1.0f - (float_t)(capRings + 1 + i) / (float_t)(2 * capRings + 1)
);
const float_t v2 = (
1.0f - (float_t)(capRings + 1 + i + 1) / (float_t)(2 * capRings + 1)
);
for(int32_t j = 0; j < sectors; j++) { for(int32_t j = 0; j < sectors; j++) {
const float_t t1 = (float_t)j * sectorStep; const float_t t1 = (float_t)j * sectorStep;
+1 -1
View File
@@ -28,7 +28,7 @@ errorret_t cubeInit();
* Buffers a 3D axis-aligned cube into the provided vertex array. * Buffers a 3D axis-aligned cube into the provided vertex array.
* Writes CUBE_VERTEX_COUNT vertices (6 faces x 6 vertices, CCW winding). * Writes CUBE_VERTEX_COUNT vertices (6 faces x 6 vertices, CCW winding).
* *
* @param vertices The vertex array to buffer into (must hold CUBE_VERTEX_COUNT). * @param vertices The vertex array to buffer into.
* @param min The minimum XYZ corner of the cube. * @param min The minimum XYZ corner of the cube.
* @param max The maximum XYZ corner of the cube. * @param max The maximum XYZ corner of the cube.
* @param color The color applied to all vertices. * @param color The color applied to all vertices.
+6 -2
View File
@@ -34,10 +34,14 @@ errorret_t meshFlush(
#ifdef meshFlushPlatform #ifdef meshFlushPlatform
assertNotNull(mesh, "Mesh cannot be NULL"); assertNotNull(mesh, "Mesh cannot be NULL");
assertTrue(vertexOffset >= 0, "Vertex offset must be non-negative."); assertTrue(vertexOffset >= 0, "Vertex offset must be non-negative.");
assertTrue(vertexCount == -1 || vertexCount > 0, "Vertex count incorrect."); assertTrue(
vertexCount == -1 || vertexCount > 0, "Vertex count incorrect."
);
int32_t vertCount = meshGetVertexCount(mesh); int32_t vertCount = meshGetVertexCount(mesh);
assertTrue(vertexOffset < (vertCount - 1), "Need at least one vert to draw"); assertTrue(
vertexOffset < (vertCount - 1), "Need at least one vert to draw"
);
int32_t drawCount = vertexCount; int32_t drawCount = vertexCount;
if(vertexCount == -1) { if(vertexCount == -1) {
+7 -13
View File
@@ -9,12 +9,6 @@
#include "display/mesh/mesh.h" #include "display/mesh/mesh.h"
#include "display/color.h" #include "display/color.h"
/**
* Vertex layout:
* 2 triangular end-caps (3 verts each) = 6
* 3 rectangular side faces (6 verts each) = 18
* Total = 24
*/
#define TRIPRISM_VERTEX_COUNT 24 #define TRIPRISM_VERTEX_COUNT 24
#define TRIPRISM_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES #define TRIPRISM_PRIMITIVE_TYPE MESH_PRIMITIVE_TYPE_TRIANGLES
@@ -35,13 +29,13 @@ errorret_t triPrismInit();
* the prism is extruded along the Z axis between minZ and maxZ. * the prism is extruded along the Z axis between minZ and maxZ.
* Writes TRIPRISM_VERTEX_COUNT (24) vertices (CCW winding). * Writes TRIPRISM_VERTEX_COUNT (24) vertices (CCW winding).
* *
* @param vertices Vertex array to write into (must hold TRIPRISM_VERTEX_COUNT). * @param vertices Vertex array to write into.
* @param x0,y0 First triangle vertex (XY). * @param x0,y0 First triangle vertex (XY).
* @param x1,y1 Second triangle vertex (XY). * @param x1,y1 Second triangle vertex (XY).
* @param x2,y2 Third triangle vertex (XY). * @param x2,y2 Third triangle vertex (XY).
* @param minZ Near Z extent of the prism. * @param minZ Near Z extent of the prism.
* @param maxZ Far Z extent of the prism. * @param maxZ Far Z extent of the prism.
* @param color Color applied to all vertices. * @param color Color applied to all vertices.
*/ */
void triPrismBuffer( void triPrismBuffer(
meshvertex_t *vertices, meshvertex_t *vertices,
+4 -4
View File
@@ -52,7 +52,7 @@ errorret_t screenBind() {
// Screen mode backbuffer uses the full display size // Screen mode backbuffer uses the full display size
SCREEN.width = frameBufferGetWidth(FRAMEBUFFER_BOUND); SCREEN.width = frameBufferGetWidth(FRAMEBUFFER_BOUND);
SCREEN.height = frameBufferGetHeight(FRAMEBUFFER_BOUND); SCREEN.height = frameBufferGetHeight(FRAMEBUFFER_BOUND);
SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; SCREEN.aspect = frameBufferGetAspect(FRAMEBUFFER_BOUND);
// No needd for a framebuffer. // No needd for a framebuffer.
#ifdef DUSK_DISPLAY_SIZE_DYNAMIC #ifdef DUSK_DISPLAY_SIZE_DYNAMIC
@@ -100,8 +100,7 @@ errorret_t screenBind() {
int32_t fbWidth, fbHeight; int32_t fbWidth, fbHeight;
fbWidth = frameBufferGetWidth(FRAMEBUFFER_BOUND); fbWidth = frameBufferGetWidth(FRAMEBUFFER_BOUND);
fbHeight = frameBufferGetHeight(FRAMEBUFFER_BOUND); fbHeight = frameBufferGetHeight(FRAMEBUFFER_BOUND);
float_t currentAspect = frameBufferGetAspect(FRAMEBUFFER_BOUND);
float_t currentAspect = (float_t)fbWidth / (float_t)fbHeight;
if(currentAspect == SCREEN.aspectRatio.ratio) { if(currentAspect == SCREEN.aspectRatio.ratio) {
// No need to use framebuffer. // No need to use framebuffer.
SCREEN.width = fbWidth; SCREEN.width = fbWidth;
@@ -129,13 +128,14 @@ errorret_t screenBind() {
if(SCREEN.framebufferReady) { if(SCREEN.framebufferReady) {
// Is current framebuffer the correct size? // Is current framebuffer the correct size?
int32_t curFbWidth, curFbHeight; int32_t curFbWidth, curFbHeight;
float_t curFbAspect = frameBufferGetAspect(&SCREEN.framebuffer);
curFbWidth = frameBufferGetWidth(&SCREEN.framebuffer); curFbWidth = frameBufferGetWidth(&SCREEN.framebuffer);
curFbHeight = frameBufferGetHeight(&SCREEN.framebuffer); curFbHeight = frameBufferGetHeight(&SCREEN.framebuffer);
if(curFbWidth == newFbWidth && curFbHeight == newFbHeight) { if(curFbWidth == newFbWidth && curFbHeight == newFbHeight) {
// Correct size, nothing to do. // Correct size, nothing to do.
SCREEN.width = newFbWidth; SCREEN.width = newFbWidth;
SCREEN.height = newFbHeight; SCREEN.height = newFbHeight;
SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; SCREEN.aspect = curFbAspect;
errorChain(frameBufferBind(&SCREEN.framebuffer)); errorChain(frameBufferBind(&SCREEN.framebuffer));
errorOk(); errorOk();
} }
+1
View File
@@ -7,5 +7,6 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
shader.c shader.c
shaderlist.c
shaderunlit.c shaderunlit.c
) )
+2
View File
@@ -8,6 +8,7 @@
#include "shader.h" #include "shader.h"
#include "shadermaterial.h" #include "shadermaterial.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "log/log.h"
shader_t *bound = NULL; shader_t *bound = NULL;
@@ -57,6 +58,7 @@ errorret_t shaderSetColor(
) { ) {
assertNotNull(shader, "Shader cannot be null"); assertNotNull(shader, "Shader cannot be null");
assertStrLenMin(name, 1, "Uniform name cannot be empty"); assertStrLenMin(name, 1, "Uniform name cannot be empty");
assertTrue(bound == shader, "Shader must be bound.");
errorChain(shaderSetColorPlatform(shader, name, color)); errorChain(shaderSetColorPlatform(shader, name, color));
errorOk(); errorOk();
} }
+87
View File
@@ -0,0 +1,87 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "shaderlist.h"
#include "display/screen/screen.h"
#include "assert/assert.h"
shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT] = {
[SHADER_LIST_SHADER_UNLIT] = {
.shader = &SHADER_UNLIT,
.definition = &SHADER_UNLIT_DEFINITION
},
};
errorret_t shaderListInit() {
mat4 view, proj, model;
glm_lookat(
(vec3){ 0.0f, 0.0f, 1.0f },
(vec3){ 0.0f, 0.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
view
);
glm_perspective(
glm_rad(45.0f),
SCREEN.aspect,
0.1f,
100.0f,
proj
);
glm_mat4_identity(model);
for(shaderlistshader_t i = 0; i < SHADER_LIST_SHADER_COUNT; i++) {
if(i == SHADER_LIST_SHADER_NULL) {
continue;
}
assertNotNull(
SHADER_LIST_DEFS[i].shader, "Shader cannot be null"
);
assertNotNull(
SHADER_LIST_DEFS[i].definition, "Shader definition cannot be null"
);
errorChain(shaderInit(
SHADER_LIST_DEFS[i].shader, SHADER_LIST_DEFS[i].definition
));
errorChain(shaderBind(SHADER_LIST_DEFS[i].shader));
errorChain(shaderSetMatrix(
SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_PROJECTION, proj
));
errorChain(shaderSetMatrix(
SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_VIEW, view
));
errorChain(shaderSetMatrix(
SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_MODEL, model
));
errorChain(shaderSetTexture(
SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_TEXTURE, NULL
));
errorChain(shaderSetColor(
SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_COLOR, COLOR_WHITE
));
}
errorOk();
}
errorret_t shaderListDispose(void) {
for(shaderlistshader_t i = 0; i < SHADER_LIST_SHADER_COUNT; i++) {
if(i == SHADER_LIST_SHADER_NULL) {
continue;
}
assertNotNull(
SHADER_LIST_DEFS[i].shader, "Shader cannot be null"
);
errorChain(shaderDispose(SHADER_LIST_DEFS[i].shader));
}
errorOk();
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "display/shader/shader.h"
#include "display/shader/shaderunlit.h"
typedef enum {
SHADER_LIST_SHADER_NULL,
SHADER_LIST_SHADER_UNLIT,
SHADER_LIST_SHADER_COUNT
} shaderlistshader_t;
typedef struct {
shader_t *shader;
shaderdefinition_t *definition;
} shaderlistdef_t;
extern shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT];
/**
* Initializes all default shaders and uploads the initial view, projection,
* and model matrices to each.
*
* @return Error state.
*/
errorret_t shaderListInit();
/**
* Disposes all default shaders.
*
* @return Error state.
*/
errorret_t shaderListDispose(void);
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
#pragma once #pragma once
#include "display/shader/shaderunlit.h" #include "display/shader/shaderlist.h"
typedef union shadermaterial_u { typedef union shadermaterial_u {
shaderunlitmaterial_t unlit; shaderunlitmaterial_t unlit;
+90 -47
View File
@@ -8,13 +8,14 @@
#include "spritebatch.h" #include "spritebatch.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/math.h"
#include "display/shader/shadermaterial.h"
meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT];
spritebatch_t SPRITEBATCH; spritebatch_t SPRITEBATCH;
errorret_t spriteBatchInit() { errorret_t spriteBatchInit() {
memoryZero(&SPRITEBATCH, sizeof(spritebatch_t)); memoryZero(&SPRITEBATCH, sizeof(spritebatch_t));
errorChain(meshInit( errorChain(meshInit(
&SPRITEBATCH.mesh, &SPRITEBATCH.mesh,
QUAD_PRIMITIVE_TYPE, QUAD_PRIMITIVE_TYPE,
@@ -24,63 +25,102 @@ errorret_t spriteBatchInit() {
errorOk(); errorOk();
} }
errorret_t spriteBatchPush( errorret_t spriteBatchBuffer(
const float_t minX, const spritebatchsprite_t *sprites,
const float_t minY, const uint32_t count,
const float_t maxX, shader_t *shader,
const float_t maxY, const shadermaterial_t material
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const float_t u0,
const float_t v0,
const float_t u1,
const float_t v1
) { ) {
return spriteBatchPush3D( assertNotNull(sprites, "Sprites cannot be null");
(vec3){ minX, minY, 0 }, assertTrue(count > 0, "Count must be greater than zero");
(vec3){ maxX, maxY, 0 }, assertNotNull(shader, "Shader cannot be null");
#if MESH_ENABLE_COLOR
color,
#endif
(vec2){ u0, v0 },
(vec2){ u1, v1 }
);
}
errorret_t spriteBatchPush3D( // Did the shader or material data change?
const vec3 min, if(shader != SPRITEBATCH.shader) {
const vec3 max,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const vec2 uv0,
const vec2 uv1
) {
// Need to flush?
if(SPRITEBATCH.spriteCount >= SPRITEBATCH_SPRITES_MAX_PER_FLUSH) {
errorChain(spriteBatchFlush()); errorChain(spriteBatchFlush());
SPRITEBATCH.shader = shader;
SPRITEBATCH.material = material;
} else if(memoryCompare(
&material, &SPRITEBATCH.material, sizeof(shadermaterial_t)
) != 0) {
// Did the material data change?
errorChain(spriteBatchFlush());
SPRITEBATCH.shader = shader;
SPRITEBATCH.material = material;
}
// Buffer the vertices.
for(uint32_t i = 0; i < count; i++ ){
spritebatchsprite_t sprite = sprites[i];
meshvertex_t *v = &SPRITEBATCH_VERTICES[
(SPRITEBATCH.spriteCount + (SPRITEBATCH.spriteFlush *
SPRITEBATCH_SPRITES_MAX_PER_FLUSH)) * QUAD_VERTEX_COUNT
];
// Buffer the quad
v[0].pos[0] = sprite.min[0];
v[0].pos[1] = sprite.min[1];
v[0].pos[2] = sprite.min[2];
v[0].uv[0] = sprite.uvMin[0];
v[0].uv[1] = sprite.uvMin[1];
v[1].pos[0] = sprite.max[0];
v[1].pos[1] = sprite.min[1];
v[1].pos[2] = sprite.min[2];
v[1].uv[0] = sprite.uvMax[0];
v[1].uv[1] = sprite.uvMin[1];
v[2].pos[0] = sprite.max[0];
v[2].pos[1] = sprite.max[1];
v[2].pos[2] = sprite.max[2];
v[2].uv[0] = sprite.uvMax[0];
v[2].uv[1] = sprite.uvMax[1];
v[3].pos[0] = sprite.min[0];
v[3].pos[1] = sprite.min[1];
v[3].pos[2] = sprite.min[2];
v[3].uv[0] = sprite.uvMin[0];
v[3].uv[1] = sprite.uvMin[1];
v[4].pos[0] = sprite.max[0];
v[4].pos[1] = sprite.max[1];
v[4].pos[2] = sprite.max[2];
v[4].uv[0] = sprite.uvMax[0];
v[4].uv[1] = sprite.uvMax[1];
v[5].pos[0] = sprite.min[0];
v[5].pos[1] = sprite.max[1];
v[5].pos[2] = sprite.max[2];
v[5].uv[0] = sprite.uvMin[0];
v[5].uv[1] = sprite.uvMax[1];
// Do we need to flush?
SPRITEBATCH.spriteCount++;
if(SPRITEBATCH.spriteCount >= SPRITEBATCH_SPRITES_MAX_PER_FLUSH) {
errorChain(spriteBatchFlush());
}
} }
size_t vertexOffset = (
SPRITEBATCH.spriteCount +
(SPRITEBATCH.spriteFlush * SPRITEBATCH_SPRITES_MAX_PER_FLUSH)
) * QUAD_VERTEX_COUNT;
quadBuffer3D(
&SPRITEBATCH_VERTICES[vertexOffset],
min, max,
uv0, uv1
#if MESH_ENABLE_COLOR
, color
#endif
);
SPRITEBATCH.spriteCount++;
errorOk(); errorOk();
} }
void spriteBatchClear() { void spriteBatchClear() {
SPRITEBATCH.spriteCount = 0; SPRITEBATCH.spriteCount = 0;
SPRITEBATCH.spriteFlush = 0; SPRITEBATCH.spriteFlush = 0;
SPRITEBATCH.shader = NULL;
memoryZero(&SPRITEBATCH.material, sizeof(shadermaterial_t));
} }
errorret_t spriteBatchFlush() { errorret_t spriteBatchFlush() {
@@ -93,6 +133,9 @@ errorret_t spriteBatchFlush() {
SPRITEBATCH.spriteFlush * SPRITEBATCH_SPRITES_MAX_PER_FLUSH * SPRITEBATCH.spriteFlush * SPRITEBATCH_SPRITES_MAX_PER_FLUSH *
QUAD_VERTEX_COUNT QUAD_VERTEX_COUNT
); );
errorChain(shaderBind(SPRITEBATCH.shader));
errorChain(shaderSetMaterial(SPRITEBATCH.shader, &SPRITEBATCH.material));
errorChain(meshFlush(&SPRITEBATCH.mesh, vertexOffset, vertexCount)); errorChain(meshFlush(&SPRITEBATCH.mesh, vertexOffset, vertexCount));
errorChain(meshDraw(&SPRITEBATCH.mesh, vertexOffset, vertexCount)); errorChain(meshDraw(&SPRITEBATCH.mesh, vertexOffset, vertexCount));
+40 -60
View File
@@ -7,101 +7,81 @@
#pragma once #pragma once
#include "display/mesh/quad.h" #include "display/mesh/quad.h"
#include "display/texture/texture.h"
#include "display/shader/shadermaterial.h"
#define SPRITEBATCH_SPRITES_MAX 32 #define SPRITEBATCH_SPRITES_MAX 512
#define SPRITEBATCH_VERTEX_COUNT (SPRITEBATCH_SPRITES_MAX * QUAD_VERTEX_COUNT) #define SPRITEBATCH_VERTEX_COUNT (SPRITEBATCH_SPRITES_MAX * QUAD_VERTEX_COUNT)
#define SPRITEBATCH_FLUSH_COUNT 4 #define SPRITEBATCH_FLUSH_COUNT 16
#define SPRITEBATCH_SPRITES_MAX_PER_FLUSH (\ #define SPRITEBATCH_SPRITES_MAX_PER_FLUSH (\
SPRITEBATCH_SPRITES_MAX / SPRITEBATCH_FLUSH_COUNT \ SPRITEBATCH_SPRITES_MAX / SPRITEBATCH_FLUSH_COUNT \
) )
typedef struct {
vec3 min;
vec3 max;
vec2 uvMin;
vec2 uvMax;
} spritebatchsprite_t;
typedef struct { typedef struct {
mesh_t mesh; mesh_t mesh;
int32_t spriteCount; int32_t spriteCount;
int32_t spriteFlush; int32_t spriteFlush;
shader_t *shader;
shadermaterial_t material;
} spritebatch_t; } spritebatch_t;
// Have to define these seperately because of alignment in certain platforms. // Have to define these separately because of alignment on certain platforms.
extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT];
extern spritebatch_t SPRITEBATCH; extern spritebatch_t SPRITEBATCH;
/** /**
* Initializes a sprite batch. * Initializes the global sprite batch and its internal mesh buffer.
* *
* @param spriteBatch The sprite batch to initialize. * @return Error state.
* @return An error code indicating success or failure.
*/ */
errorret_t spriteBatchInit(); errorret_t spriteBatchInit();
/** /**
* Pushes a sprite to the batch. This basically "queues" it to render (well * Lowest-level buffer function. Writes sprites into the internal vertex buffer.
* technically it is buffering the vertices to the mesh at the moment, but * Flushes automatically when the per-flush capacity is reached. Does not
* that is likely to change when we switch to VAOs or VBOs or even Shader UBOs). * modify material state — call spriteBatchSetState or use a high-level push
* function before buffering.
* *
* Currently changing texture pointer will cause the buffer to flush but this is * @param sprites Pointer to the sprite array.
* also likely to change in the future. * @param count Number of sprites to buffer.
* * @param shader Shader to use when flushing.
* @param minX The minimum x coordinate of the sprite. * @param material Material information passed to the shader when flushing.
* @param minY The minimum y coordinate of the sprite. * @return Error state.
* @param maxX The maximum x coordinate of the sprite.
* @param maxY The maximum y coordinate of the sprite.
* @param color The color to tint the sprite with.
* @param u0 The texture coordinate for the top-left corner of the sprite.
* @param v0 The texture coordinate for the top-left corner of the sprite.
* @param u1 The texture coordinate for the bottom-right corner of the sprite.
* @param v1 The texture coordinate for the bottom-right corner of the sprite.
* @return An error code indicating success or failure.
*/ */
errorret_t spriteBatchPush( errorret_t spriteBatchBuffer(
const float_t minX, const spritebatchsprite_t *sprites,
const float_t minY, const uint32_t count,
const float_t maxX, shader_t *shader,
const float_t maxY, const shadermaterial_t material
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const float_t u0,
const float_t v0,
const float_t u1,
const float_t v1
); );
/** /**
* Pushes a 3D sprite to the batch. This is like spriteBatchPush but takes * Resets sprite and flush counters and clears the current material state.
* 3D coordinates instead of 2D. * Calling spriteBatchFlush after this renders nothing.
*
* @param min The minimum (x,y,z) coordinate of the sprite.
* @param max The maximum (x,y,z) coordinate of the sprite.
* @param color The color to tint the sprite with.
* @param uvMin The texture coordinate for the top-left corner of the sprite.
* @param uvMax The texture coordinate for the bottom-right corner of the sprite
* @return An error code indicating success or failure.
*/
errorret_t spriteBatchPush3D(
const vec3 min,
const vec3 max,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const vec2 uvMin,
const vec2 uvMax
);
/**
* Clears the sprite batch. This will mean calling flush renders nothing.
*/ */
void spriteBatchClear(); void spriteBatchClear();
/** /**
* Flushes the sprite batch, rendering all queued sprites. * Uploads and draws all buffered sprites. If a material type has been set via
* spriteBatchSetState or spriteBatchCheckState, the shader is bound and the
* material is applied first. If matType is NULL the caller is responsible for
* having the correct shader already bound. Does nothing if the buffer is empty.
* *
* @return An error code indicating success or failure. * @return Error state.
*/ */
errorret_t spriteBatchFlush(); errorret_t spriteBatchFlush();
/** /**
* Disposes of the sprite batch, freeing any allocated resources. * Disposes of the sprite batch and frees its internal mesh buffer.
* *
* @return An error code indicating success or failure. * @return Error state.
*/ */
errorret_t spriteBatchDispose(); errorret_t spriteBatchDispose();
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "display/texture/texture.h"
#include "display/texture/tileset.h"
typedef struct {
texture_t *texture;
tileset_t *tileset;
} font_t;
+67 -66
View File
@@ -9,118 +9,123 @@
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "asset/asset.h"
#include "asset/loader/display/assettextureloader.h" #include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h" #include "asset/loader/display/assettilesetloader.h"
#include "display/shader/shaderunlit.h" #include "display/shader/shaderunlit.h"
texture_t DEFAULT_FONT_TEXTURE; font_t FONT_DEFAULT;
tileset_t DEFAULT_FONT_TILESET;
errorret_t textInit(void) { errorret_t textInit(void) {
errorChain(assetTextureLoad( assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
"ui/minogram.png", &DEFAULT_FONT_TEXTURE, TEXTURE_FORMAT_RGBA assetentry_t *entryTexture = assetLock(
)); "ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
errorChain(assetTilesetLoad("ui/minogram.dtf", &DEFAULT_FONT_TILESET)); );
assetentry_t *entryTileset = assetLock(
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
);
errorChain(assetRequireLoaded(entryTexture));
errorChain(assetRequireLoaded(entryTileset));
FONT_DEFAULT.texture = &entryTexture->data.texture;
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
errorOk(); errorOk();
} }
errorret_t textDispose(void) { errorret_t textDispose(void) {
errorChain(textureDispose(&DEFAULT_FONT_TEXTURE)); FONT_DEFAULT.texture = NULL;
FONT_DEFAULT.tileset = NULL;
assetUnlock("ui/minogram.png");
assetUnlock("ui/minogram.dtf");
errorOk(); errorOk();
} }
errorret_t textDrawChar( spritebatchsprite_t textGetSprite(
const float_t x, const vec2 pos, const char_t c, const font_t *font
const float_t y,
const char_t c,
#if MESH_ENABLE_COLOR
const color_t color,
#endif
const tileset_t *tileset,
texture_t *texture
) { ) {
assertNotNull(font, "Font cannot be NULL");
// Change char from ASCII to a tile index.
int32_t tileIndex = (int32_t)(c) - TEXT_CHAR_START; int32_t tileIndex = (int32_t)(c) - TEXT_CHAR_START;
if(tileIndex < 0 || tileIndex >= tileset->tileCount) { if(tileIndex < 0 || tileIndex >= font->tileset->tileCount) {
tileIndex = ((int32_t)'@') - TEXT_CHAR_START; tileIndex = ((int32_t)'@') - TEXT_CHAR_START;
} }
assertTrue( assertTrue(
tileIndex >= 0 && tileIndex <= tileset->tileCount, tileIndex >= 0 && tileIndex <= font->tileset->tileCount,
"Character is out of bounds for font tiles" "Character is out of bounds for font tiles"
); );
// Create sprite.
vec4 uv; vec4 uv;
tilesetTileGetUV(tileset, tileIndex, uv); tilesetTileGetUV(font->tileset, tileIndex, uv);
spritebatchsprite_t sprite;
errorChain(spriteBatchPush( sprite.min[0] = pos[0];
// texture, sprite.min[1] = pos[1];
x, y, sprite.min[2] = 0.0f;
x + tileset->tileWidth, sprite.max[0] = pos[0] + font->tileset->tileWidth;
y + tileset->tileHeight, sprite.max[1] = pos[1] + font->tileset->tileHeight;
#if MESH_ENABLE_COLOR sprite.max[2] = 0.0f;
color, sprite.uvMin[0] = uv[0];
#endif sprite.uvMin[1] = uv[1];
uv[0], uv[1], uv[2], uv[3] sprite.uvMax[0] = uv[2];
)); sprite.uvMax[1] = uv[3];
errorOk(); return sprite;
} }
errorret_t textDraw( errorret_t textDraw(
const float_t x, const float_t x,
const float_t y, const float_t y,
const char_t *text, const char_t *text,
#if MESH_ENABLE_COLOR const color_t color,
const color_t color, font_t *font
#endif
const tileset_t *tileset,
texture_t *texture
) { ) {
assertNotNull(text, "Text cannot be NULL"); assertNotNull(text, "Text cannot be NULL");
spritebatchsprite_t sprite;
shadermaterial_t material = {
.unlit = {
.color = color,
.texture = font->texture
}
};
float_t posX = x; float_t posX = x;
float_t posY = y; float_t posY = y;
errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, texture)); errorChain(shaderSetTexture(
&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, font->texture
));
// errorChain(spriteBatchPush( #if MESH_ENABLE_COLOR
// // texture, #else
// posX, posY, errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, color));
// posX + texture->width * 1, posY + texture->height * 1, #endif
// color,
// 0.0f, 0.0f, 1.0f, 1.0f
// ));
// errorOk();
char_t c; char_t c;
int32_t i = 0; int32_t i = 0;
while((c = text[i++]) != '\0') { while((c = text[i++]) != '\0') {
if(c == '\n') { if(c == '\n') {
posX = x; posX = x;
posY += tileset->tileHeight; posY += font->tileset->tileHeight;
continue; continue;
} }
if(c == ' ') { if(c == ' ') {
posX += tileset->tileWidth; posX += font->tileset->tileWidth;
continue; continue;
} }
errorChain(textDrawChar( sprite = textGetSprite((vec2){posX, posY}, c, font);
posX, posY, c, errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
#if MESH_ENABLE_COLOR posX += font->tileset->tileWidth;
color,
#endif
tileset, texture
));
posX += tileset->tileWidth;
} }
errorOk(); errorOk();
} }
void textMeasure( void textMeasure(
const char_t *text, const char_t *text,
const tileset_t *tileset, const font_t *font,
int32_t *outWidth, int32_t *outWidth,
int32_t *outHeight int32_t *outHeight
) { ) {
@@ -129,27 +134,23 @@ void textMeasure(
assertNotNull(outHeight, "Output height pointer cannot be NULL"); assertNotNull(outHeight, "Output height pointer cannot be NULL");
int32_t width = 0; int32_t width = 0;
int32_t height = tileset->tileHeight; int32_t height = font->tileset->tileHeight;
int32_t lineWidth = 0; int32_t lineWidth = 0;
char_t c; char_t c;
int32_t i = 0; int32_t i = 0;
while((c = text[i++]) != '\0') { while((c = text[i++]) != '\0') {
if(c == '\n') { if(c == '\n') {
if(lineWidth > width) { if(lineWidth > width) width = lineWidth;
width = lineWidth;
}
lineWidth = 0; lineWidth = 0;
height += tileset->tileHeight; height += font->tileset->tileHeight;
continue; continue;
} }
lineWidth += tileset->tileWidth; lineWidth += font->tileset->tileWidth;
} }
if(lineWidth > width) { if(lineWidth > width) width = lineWidth;
width = lineWidth;
}
*outWidth = width; *outWidth = width;
*outHeight = height; *outHeight = height;
+16 -29
View File
@@ -7,13 +7,12 @@
#pragma once #pragma once
#include "asset/asset.h" #include "asset/asset.h"
#include "display/texture/texture.h" #include "display/text/font.h"
#include "display/texture/tileset.h" #include "display/spritebatch/spritebatch.h"
#define TEXT_CHAR_START '!' #define TEXT_CHAR_START '!'
extern texture_t DEFAULT_FONT_TEXTURE; extern font_t FONT_DEFAULT;
extern tileset_t DEFAULT_FONT_TILESET;
/** /**
* Initializes the text system. * Initializes the text system.
@@ -30,25 +29,17 @@ errorret_t textInit(void);
errorret_t textDispose(void); errorret_t textDispose(void);
/** /**
* Draws a single character at the specified position. * Builds a sprite for a single character at the given position.
* *
* @param x The x-coordinate to draw the character at. * @param pos The (x, y) position of the character in screen/world space.
* @param y The y-coordinate to draw the character at. * @param c The character to build a sprite for.
* @param c The character to draw. * @param font Font to use for tile lookup.
* @param color The color to draw the character in. * @return The populated sprite ready for spriteBatchBuffer.
* @param tileset Font tileset to use for rendering.
* @param texture Texture containing the font tileset image.
* @return Either an error or success result.
*/ */
errorret_t textDrawChar( spritebatchsprite_t textGetSprite(
const float_t x, const vec2 pos,
const float_t y,
const char_t c, const char_t c,
#if MESH_ENABLE_COLOR const font_t *font
const color_t color,
#endif
const tileset_t *tileset,
texture_t *texture
); );
/** /**
@@ -58,32 +49,28 @@ errorret_t textDrawChar(
* @param y The y-coordinate to draw the text at. * @param y The y-coordinate to draw the text at.
* @param text The null-terminated string of text to draw. * @param text The null-terminated string of text to draw.
* @param color The color to draw the text in. * @param color The color to draw the text in.
* @param tileset Font tileset to use for rendering. * @param font Font to use for rendering.
* @param texture Texture containing the font tileset image.
* @return Either an error or success result. * @return Either an error or success result.
*/ */
errorret_t textDraw( errorret_t textDraw(
const float_t x, const float_t x,
const float_t y, const float_t y,
const char_t *text, const char_t *text,
#if MESH_ENABLE_COLOR const color_t color,
const color_t color, font_t *font
#endif
const tileset_t *tileset,
texture_t *texture
); );
/** /**
* Measures the width and height of the given text string when rendered. * Measures the width and height of the given text string when rendered.
* *
* @param text The null-terminated string of text to measure. * @param text The null-terminated string of text to measure.
* @param tileset Font tileset to use for measurement. * @param font Font to use for measurement.
* @param outWidth Pointer to store the measured width in pixels. * @param outWidth Pointer to store the measured width in pixels.
* @param outHeight Pointer to store the measured height in pixels. * @param outHeight Pointer to store the measured height in pixels.
*/ */
void textMeasure( void textMeasure(
const char_t *text, const char_t *text,
const tileset_t *tileset, const font_t *font,
int32_t *outWidth, int32_t *outWidth,
int32_t *outHeight int32_t *outHeight
); );
+2
View File
@@ -17,6 +17,8 @@
#include <ctype.h> #include <ctype.h>
#include <stdarg.h> #include <stdarg.h>
#include <float.h> #include <float.h>
#include <memory.h>
#include <malloc.h>
#include <cglm/cglm.h> #include <cglm/cglm.h>
#include <cglm/types.h> #include <cglm/types.h>
+30 -148
View File
@@ -12,196 +12,73 @@
#include "locale/localemanager.h" #include "locale/localemanager.h"
#include "display/display.h" #include "display/display.h"
#include "scene/scene.h" #include "scene/scene.h"
#include "cutscene/cutscene.h"
#include "asset/asset.h" #include "asset/asset.h"
#include "ui/ui.h" #include "ui/ui.h"
#include "script/scriptmanager.h" #include "ui/uitextbox.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "entity/entitymanager.h" #include "entity/entitymanager.h"
#include "entity/component/physics/entityphysics.h" #include "entity/component/physics/entityphysics.h"
#include "game/game.h"
#include "physics/physicsmanager.h" #include "physics/physicsmanager.h"
#include "network/network.h" #include "network/network.h"
#include "network/networkinfo.h"
#include "system/system.h" #include "system/system.h"
#include "console/console.h"
#include "display/mesh/cube.h" #include "script/script.h"
#include "display/mesh/plane.h" #include "item/backpack.h"
#include "save/save.h"
engine_t ENGINE; engine_t ENGINE;
entityid_t phBoxEnt;
componentid_t phBoxPhys;
float_t onlineSwapTime = FLT_MAX;
void goOnline();
void goOffline();
void onNetworkConnected(void *user) {
onlineSwapTime = TIME.time + 3.0f;
networkinfo_t info = networkGetInfo();
if(info.type == NETWORK_TYPE_IPV4) {
printf(
"Connected to network with IPv4 address: " NETWORK_INFO_FORMAT_IPV4 "\n",
info.ipv4.ip[0], info.ipv4.ip[1], info.ipv4.ip[2], info.ipv4.ip[3]
);
#ifdef DUSK_NETWORK_IPV6
} else if(info.type == NETWORK_TYPE_IPV6) {
printf(
"Connected to network with IPv6 address: " NETWORK_INFO_FORMAT_IPV6 "\n",
info.ipv6.ip[0], info.ipv6.ip[1], info.ipv6.ip[2], info.ipv6.ip[3],
info.ipv6.ip[4], info.ipv6.ip[5], info.ipv6.ip[6], info.ipv6.ip[7],
info.ipv6.ip[8], info.ipv6.ip[9], info.ipv6.ip[10], info.ipv6.ip[11],
info.ipv6.ip[12], info.ipv6.ip[13], info.ipv6.ip[14], info.ipv6.ip[15]
);
#endif
}
printf("Network connected, I will disconnect at: %.2f1.\n", onlineSwapTime);
}
void onNetworkFailed(errorret_t error, void *user) {
onlineSwapTime = TIME.time + 3.0f;
printf("Failed to connect to network, will try again at %.2f1.\n", onlineSwapTime);
}
void onNetworkDisconnected(errorret_t error, void *user) {
onlineSwapTime = TIME.time + 3.0f;
printf("Network disconnected, will go online at %.2f1.\n", onlineSwapTime);
errorCatch(errorPrint(error));
}
void onNetworkDisconnectFinished(void *user) {
onlineSwapTime = TIME.time + 3.0f;
printf("Finished disconnecting from network, will go online at %.2f1.\n", onlineSwapTime);
}
void goOnline() {
printf("Going online...\n");
networkRequestConnection(
onNetworkConnected,
onNetworkFailed,
onNetworkDisconnected,
NULL
);
}
void goOffline() {
printf("Going offline...\n");
networkRequestDisconnection(onNetworkDisconnectFinished, NULL);
}
errorret_t engineInit(const int32_t argc, const char_t **argv) { errorret_t engineInit(const int32_t argc, const char_t **argv) {
memoryZero(&ENGINE, sizeof(engine_t)); memoryZero(&ENGINE, sizeof(engine_t));
ENGINE.running = true; ENGINE.running = true;
ENGINE.argc = argc; ENGINE.argc = argc;
ENGINE.argv = argv; ENGINE.argv = argv;
ENGINE.version = DUSK_VERSION;
// Init systems. Order is important. // Init systems. Order is important.
errorChain(systemInit()); errorChain(systemInit());
timeInit(); timeInit();
consoleInit();
errorChain(inputInit()); errorChain(inputInit());
errorChain(assetInit()); errorChain(assetInit());
// errorChain(saveInit());
errorChain(localeManagerInit()); errorChain(localeManagerInit());
errorChain(scriptManagerInit());
errorChain(displayInit()); errorChain(displayInit());
errorChain(uiInit()); errorChain(uiInit());
errorChain(uiTextboxInit());
errorChain(cutsceneInit());
errorChain(sceneInit()); errorChain(sceneInit());
entityManagerInit(); entityManagerInit();
backpackInit();
physicsManagerInit(); physicsManagerInit();
errorChain(networkInit()); errorChain(networkInit());
errorChain(gameInit()); errorChain(scriptInit());
printf("Init done, going to queue online in 3 seconds...\n"); consolePrint("Engine initialized");
onlineSwapTime = TIME.time + 3.0f;
// Camera errorChain(scriptExecFile("init.js"));
entityid_t cam = entityManagerAdd(); sceneSet(SCENE_TYPE_INITIAL);
componentid_t camPos = entityAddComponent(cam, COMPONENT_TYPE_POSITION);
float_t distance = 6.0f;
entityPositionLookAt(
cam, camPos,
(vec3){ 0.0f, 1.0f, 0.0f },
(vec3){ 0.0f, 1.0f, 0.0f },
(vec3){ distance, distance, distance }
);
componentid_t camCam = entityAddComponent(cam, COMPONENT_TYPE_CAMERA);
entityCameraSetZFar(cam, camCam, distance * 6.0f);
// Floor
entityid_t floorEnt = entityManagerAdd();
componentid_t floorPos = entityAddComponent(floorEnt, COMPONENT_TYPE_POSITION);
componentid_t floorMesh = entityAddComponent(floorEnt, COMPONENT_TYPE_MESH);
componentid_t floorMat = entityAddComponent(floorEnt, COMPONENT_TYPE_MATERIAL);
componentid_t floorPhys = entityAddComponent(floorEnt, COMPONENT_TYPE_PHYSICS);
entityPositionSetPosition(floorEnt, floorPos, (vec3){ -5.0f, 0.0f, -5.0f });
entityPositionSetScale(floorEnt, floorPos, (vec3){ 10.0f, 1.0f, 10.0f });
entityMeshSetMesh(floorEnt, floorMesh, &PLANE_MESH_SIMPLE);
entityMaterialGetShaderMaterial(floorEnt, floorMat)->unlit.color = COLOR_GREEN;
entityphysics_t *floorPhysData = entityPhysicsGet(floorEnt, floorPhys);
floorPhysData->type = PHYSICS_BODY_STATIC;
floorPhysData->shape.type = PHYSICS_SHAPE_PLANE;
floorPhysData->shape.data.plane.normal[0] = 0.0f;
floorPhysData->shape.data.plane.normal[1] = 1.0f;
floorPhysData->shape.data.plane.normal[2] = 0.0f;
floorPhysData->shape.data.plane.distance = 0.0f;
/* ---- Dynamic box ---- */
phBoxEnt = entityManagerAdd();
componentid_t boxPos = entityAddComponent(phBoxEnt, COMPONENT_TYPE_POSITION);
componentid_t boxMesh = entityAddComponent(phBoxEnt, COMPONENT_TYPE_MESH);
componentid_t boxMat = entityAddComponent(phBoxEnt, COMPONENT_TYPE_MATERIAL);
phBoxPhys = entityAddComponent(phBoxEnt, COMPONENT_TYPE_PHYSICS);
entityMeshSetMesh(phBoxEnt, boxMesh, &CUBE_MESH_SIMPLE);
entityMaterialGetShaderMaterial(phBoxEnt, boxMat)->unlit.color = COLOR_RED;
entityPositionSetPosition(phBoxEnt, boxPos, (vec3){ 0.0f, 4.0f, 0.0f });
/* Run the init script. */
scriptcontext_t ctx;
errorChain(scriptContextInit(&ctx));
errorChain(scriptContextExecFile(&ctx, "init.lua"));
scriptContextDispose(&ctx);
errorOk(); errorOk();
} }
errorret_t engineUpdate(void) { errorret_t engineUpdate(void) {
// Order here is important.
errorChain(networkUpdate()); errorChain(networkUpdate());
timeUpdate(); timeUpdate();
inputUpdate(); inputUpdate();
consoleUpdate();
entityManagerUpdate();
uiUpdate(); uiUpdate();
errorChain(sceneUpdate()); errorChain(uiTextboxUpdate());
/* Reset the box to its start position on demand. */
if(inputIsDown(INPUT_ACTION_ACCEPT)) {
componentid_t posComp = entityGetComponent(phBoxEnt, COMPONENT_TYPE_POSITION);
entityPositionSetPosition(phBoxEnt, posComp, (vec3){ 0.0f, 4.0f, 0.0f });
entityPhysicsSetVelocity(phBoxEnt, phBoxPhys, (vec3){ 0.0f, 0.0f, 0.0f });
}
/* Step physics: positions are updated directly on POSITION components. */
physicsManagerUpdate(); physicsManagerUpdate();
errorChain(gameUpdate());
errorChain(displayUpdate()); errorChain(displayUpdate());
errorChain(cutsceneUpdate());
errorChain(sceneUpdate());
errorChain(assetUpdate());
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false; if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
if(TIME.time >= onlineSwapTime) {
onlineSwapTime = FLT_MAX;
if(NETWORK.state == NETWORK_STATE_CONNECTED) {
goOffline();
} else {
goOnline();
}
}
errorOk(); errorOk();
} }
@@ -210,13 +87,18 @@ void engineExit(void) {
} }
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
errorChain(networkDispose()); uiTextboxDispose();
cutsceneDispose();
sceneDispose(); sceneDispose();
errorChain(gameDispose()); errorChain(networkDispose());
entityManagerDispose(); entityManagerDispose();
localeManagerDispose(); localeManagerDispose();
uiDispose(); uiDispose();
consoleDispose();
errorChain(displayDispose()); errorChain(displayDispose());
// errorChain(saveDispose());
errorChain(assetDispose()); errorChain(assetDispose());
errorChain(scriptDispose());
errorOk(); errorOk();
} }
+2
View File
@@ -15,6 +15,7 @@ typedef struct {
bool_t running; bool_t running;
int32_t argc; int32_t argc;
const char_t **argv; const char_t **argv;
const char_t *version;
} engine_t; } engine_t;
extern engine_t ENGINE; extern engine_t ENGINE;
@@ -36,3 +37,4 @@ errorret_t engineUpdate(void);
* Shuts down the engine. * Shuts down the engine.
*/ */
errorret_t engineDispose(void); errorret_t engineDispose(void);
+24 -4
View File
@@ -12,8 +12,15 @@
componentdefinition_t COMPONENT_DEFINITIONS[] = { componentdefinition_t COMPONENT_DEFINITIONS[] = {
[COMPONENT_TYPE_NULL] = { 0 }, [COMPONENT_TYPE_NULL] = { 0 },
#define X(enumName, type, field, iMethod, dMethod) \ #define X(enm, type, field, iMethod, dMethod, rMethod) \
[COMPONENT_TYPE_##enumName] = { .init = iMethod, .dispose = dMethod }, [COMPONENT_TYPE_##enm] = { \
.enumName = #enm, \
.name = #field, \
.init = iMethod, \
.dispose = dMethod, \
.render = rMethod \
},
#include "componentlist.h" #include "componentlist.h"
#undef X #undef X
@@ -81,7 +88,7 @@ entityid_t componentGetEntitiesWithComponent(
componentid_t used = ENTITY_MANAGER.entitiesWithComponent[ componentid_t used = ENTITY_MANAGER.entitiesWithComponent[
type * ENTITY_COUNT_MAX + i type * ENTITY_COUNT_MAX + i
]; ];
if(used == 0xFF) continue; if(used == COMPONENT_ID_INVALID) continue;
assertTrue( assertTrue(
ENTITY_MANAGER.components[componentGetIndex(i, used)].type == type, ENTITY_MANAGER.components[componentGetIndex(i, used)].type == type,
"Component type mismatch in entitiesWithComponent lookup" "Component type mismatch in entitiesWithComponent lookup"
@@ -95,7 +102,7 @@ entityid_t componentGetEntitiesWithComponent(
"Component ID OOB in entitiesWithComponent lookup" "Component ID OOB in entitiesWithComponent lookup"
); );
assertTrue( assertTrue(
componentGetIndex(i, used) < ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX, componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
"Component index OOB in entitiesWithComponent lookup" "Component index OOB in entitiesWithComponent lookup"
); );
assertTrue( assertTrue(
@@ -108,6 +115,19 @@ entityid_t componentGetEntitiesWithComponent(
return written; return written;
} }
errorret_t componentRenderAll(void) {
for(entityid_t eid = 0; eid < ENTITY_COUNT_MAX; eid++) {
if(!(ENTITY_MANAGER.entities[eid].state & ENTITY_STATE_ACTIVE)) continue;
for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) {
component_t *cmp = &ENTITY_MANAGER.components[componentGetIndex(eid, cid)];
if(cmp->type == COMPONENT_TYPE_NULL) continue;
if(!COMPONENT_DEFINITIONS[cmp->type].render) continue;
errorChain(COMPONENT_DEFINITIONS[cmp->type].render(eid, cid));
}
}
errorOk();
}
void componentDispose( void componentDispose(
const entityid_t entityId, const entityid_t entityId,
const componentid_t componentId const componentid_t componentId

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