4 Commits
Author SHA1 Message Date
YourWishes 5184064a26 w 2026-07-12 10:10:05 -05:00
YourWishes 6a43363539 Optimized 2026-07-11 23:51:39 -05:00
YourWishes b9195fbbad Script ezy 2026-07-11 22:29:57 -05:00
YourWishes f715ad2176 Nuke it all 2026-07-11 20:37:28 -05:00
563 changed files with 35927 additions and 2943 deletions

No files matched your search

@@ -0,0 +1,49 @@
name: Setup devkitPro
description: Install devkitPro with GameCube/Wii packages on an Ubuntu runner
runs:
using: composite
steps:
- name: Install apt dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y \
cmake \
python3 \
python3-pip \
python3-polib \
python3-pil \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
xorriso
- name: Install devkitPro pacman
shell: bash
run: |
sudo ln -sf /proc/self/mounts /etc/mtab
wget https://apt.devkitpro.org/install-devkitpro-pacman \
-O /tmp/install-devkitpro-pacman
echo "=== installer script contents ==="
cat /tmp/install-devkitpro-pacman
echo "================================="
chmod +x /tmp/install-devkitpro-pacman
sudo /tmp/install-devkitpro-pacman
echo "DEVKITPRO=/opt/devkitpro" >> $GITHUB_ENV
echo "DEVKITPPC=/opt/devkitpro/devkitPPC" >> $GITHUB_ENV
echo "/opt/devkitpro/tools/bin" >> $GITHUB_PATH
echo "/opt/devkitpro/devkitPPC/bin" >> $GITHUB_PATH
- name: Install devkitPro GameCube/Wii packages
shell: bash
run: |
sudo dkp-pacman -S --needed --noconfirm \
gamecube-dev \
gamecube-sdl2 \
ppc-liblzma \
ppc-libzip \
libogc2 \
gamecube-tools \
ppc-libmad \
ppc-zlib-ng \
ppc-bzip2 \
ppc-zstd
+20
View File
@@ -0,0 +1,20 @@
name: Setup pspdev
description: Install the pspdev PSP toolchain on an Ubuntu runner
runs:
using: composite
steps:
- name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y cmake python3 python3-pip python3-dotenv
- name: Install pspdev toolchain
shell: bash
run: |
wget -q \
https://github.com/pspdev/pspdev/releases/latest/download/pspdev-ubuntu-latest-x86_64.tar.gz \
-O /tmp/pspdev.tar.gz
sudo tar -xzf /tmp/pspdev.tar.gz -C /usr/local
echo "PSPDEV=/usr/local/pspdev" >> $GITHUB_ENV
echo "/usr/local/pspdev/bin" >> $GITHUB_PATH
+247
View File
@@ -0,0 +1,247 @@
name: Build Dusk
on:
push:
tags:
- '*'
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
python3 \
python3-pip \
python3-polib \
python3-pil \
libsdl2-dev \
libgl1-mesa-dev \
libzip-dev \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
git \
libssl-dev
- name: Build Linux
run: ./scripts/build-linux.sh
- name: Upload Linux binary
uses: actions/upload-artifact@v3
with:
name: dusk-linux
path: build-linux/Dusk
if-no-files-found: error
build-psp:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pspdev
uses: ./.github/actions/setup-pspdev
- name: Build PSP
run: ./scripts/build-psp.sh
- name: Move EBOOT.PBP to Dusk subfolder
run: |
mkdir -p ./git-artifcats/Dusk/PSP/GAME/Dusk
cp build-psp/EBOOT.PBP ./git-artifcats/Dusk/PSP/GAME/Dusk/EBOOT.PBP
- name: Upload PSP binary
uses: actions/upload-artifact@v3
with:
name: dusk-psp
path: ./git-artifcats/Dusk
if-no-files-found: error
build-knulli:
runs-on: ubuntu-latest
container:
image: debian:trixie
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
dpkg --add-architecture arm64
apt-get update
apt-get install -y --no-install-recommends \
crossbuild-essential-arm64 \
ca-certificates \
pkg-config \
cmake \
make \
ninja-build \
git \
file \
python3 \
python3-pip \
python3-polib \
python3-pil \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.4-dev:arm64 \
xz-utils:arm64 \
libbz2-dev:arm64 \
zlib1g-dev:arm64 \
libzip-dev:arm64 \
libssl-dev:arm64 \
libsdl2-dev:arm64 \
liblzma-dev:arm64 \
libopengl0:arm64 \
libgl1:arm64 \
libegl1:arm64 \
libgles2:arm64 \
libgl1-mesa-dev:arm64
- name: Build Knulli
run: ./scripts/build-knulli.sh
- name: Move output to Dusk subfolder
run: |
mkdir -p ./git-artifcats/Dusk
cp -r build-knulli/dusk ./git-artifcats/Dusk
- name: Upload Knulli binary
uses: actions/upload-artifact@v3
with:
name: dusk-knulli
path: ./git-artifcats/Dusk
if-no-files-found: error
build-gamecube:
runs-on: ubuntu-latest
container:
image: ghcr.io/extremscorner/libogc2:latest
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install additional dependencies
run: |
apt-get install -y \
python3-pip python3-polib python3-pil \
python3-dotenv python3-pyqt5 python3-opengl
dkp-pacman -Syu --noconfirm
dkp-pacman -S --needed --noconfirm \
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build GameCube
run: ./scripts/build-gamecube.sh
- name: Copy output files
run: |
mkdir -p ./git-artifcats/Dusk
cp build-gamecube/Dusk.dol ./git-artifcats/Dusk/Dusk.dol
cp build-gamecube/dusk.dsk ./git-artifcats/Dusk/dusk.dsk
- name: Upload GameCube binary
uses: actions/upload-artifact@v3
with:
name: dusk-gamecube
path: ./git-artifcats/Dusk
if-no-files-found: error
build-gamecube-iso:
runs-on: ubuntu-latest
container:
image: ghcr.io/extremscorner/libogc2:latest
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install additional dependencies
run: |
apt-get install -y \
python3-pip python3-polib python3-pil \
python3-dotenv python3-pyqt5 python3-opengl xorriso
dkp-pacman -Syu --noconfirm
dkp-pacman -S --needed --noconfirm \
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build GameCube ISO
run: ./scripts/build-gamecube-iso.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@v3
with:
name: dusk-gamecube-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii:
runs-on: ubuntu-latest
container:
image: ghcr.io/extremscorner/libogc2:latest
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install additional dependencies
run: |
apt-get install -y \
python3-pip python3-polib python3-pil \
python3-dotenv python3-pyqt5 python3-opengl
dkp-pacman -Syu --noconfirm
dkp-pacman -S --needed --noconfirm \
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build Wii
run: ./scripts/build-wii.sh
- name: Copy output files
run: |
mkdir -p ./git-artifcats/Dusk/apps/Dusk
cp build-wii/boot.dol ./git-artifcats/Dusk/apps/Dusk/boot.dol
cp build-wii/dusk.dsk ./git-artifcats/Dusk/apps/Dusk/dusk.dsk
cp build-wii/meta.xml ./git-artifcats/Dusk/apps/Dusk/meta.xml
- name: Upload Wii binary
uses: actions/upload-artifact@v3
with:
name: dusk-wii
path: ./git-artifcats/Dusk
if-no-files-found: error
build-wii-iso:
runs-on: ubuntu-latest
container:
image: ghcr.io/extremscorner/libogc2:latest
steps:
- name: Install Node.js
run: apt-get update && apt-get install -y nodejs
- name: Checkout repository
uses: actions/checkout@v4
- name: Install additional dependencies
run: |
apt-get install -y \
python3-pip python3-polib python3-pil \
python3-dotenv python3-pyqt5 python3-opengl xorriso
dkp-pacman -Syu --noconfirm
dkp-pacman -S --needed --noconfirm \
gamecube-sdl2 ppc-liblzma ppc-libzip \
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
- name: Build Wii ISO
run: ./scripts/build-wii-iso.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@v3
with:
name: dusk-wii-iso
path: ./git-artifcats/Dusk
if-no-files-found: error
+35
View File
@@ -0,0 +1,35 @@
name: Test Dusk
on:
pull_request:
branches:
- main
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
python3 \
python3-pip \
python3-polib \
python3-pil \
libsdl2-dev \
libgl1-mesa-dev \
libzip-dev \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
git \
libssl-dev
- name: Run tests
run: ./scripts/test-linux.sh
-1
View File
@@ -107,4 +107,3 @@ yarn.lock
/assets/test /assets/test
/tools_old /tools_old
/assets/test.png /assets/test.png
/assets/audio/boa.mp3
+89 -57
View File
@@ -1,98 +1,130 @@
# Copyright (c) 2026 Dominic Masters # Copyright (c) 2025 Dominic Masters
# #
# 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
# Setup
cmake_minimum_required(VERSION 3.13) 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)
cmake_policy(SET CMP0079 NEW)
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
# Directories option(DUSK_BUILD_TESTS "Enable tests" OFF)
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
set(DUSK_GAME_SHORT_DESCRIPTION "Dusk game" CACHE STRING "One-line description")
set(DUSK_GAME_LONG_DESCRIPTION "No description yet." CACHE STRING "Full description")
# Prep cache
set(DUSK_CACHE_TARGET "dusk-target")
# Build variables
set(DUSK_ROOT_DIR "${CMAKE_SOURCE_DIR}") set(DUSK_ROOT_DIR "${CMAKE_SOURCE_DIR}")
set(DUSK_BUILD_DIR "${CMAKE_BINARY_DIR}")
set(DUSK_SOURCES_DIR "${DUSK_ROOT_DIR}/src") set(DUSK_SOURCES_DIR "${DUSK_ROOT_DIR}/src")
set(DUSK_TEST_DIR "${DUSK_ROOT_DIR}/test")
set(DUSK_TEMP_DIR "${DUSK_BUILD_DIR}/temp")
set(DUSK_TOOLS_DIR "${DUSK_ROOT_DIR}/tools")
set(DUSK_DATA_DIR "${DUSK_ROOT_DIR}/data")
set(DUSK_ASSETS_DIR "${DUSK_ROOT_DIR}/assets") set(DUSK_ASSETS_DIR "${DUSK_ROOT_DIR}/assets")
set(DUSK_BUILT_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets") set(DUSK_BUILT_ASSETS_DIR "${DUSK_BUILD_DIR}/built_assets" CACHE INTERNAL ${DUSK_CACHE_TARGET})
set(DUSK_GENERATED_HEADERS_DIR "${CMAKE_BINARY_DIR}/generated") set(DUSK_GENERATED_HEADERS_DIR "${DUSK_BUILD_DIR}/generated")
file(MAKE_DIRECTORY ${DUSK_GENERATED_HEADERS_DIR}) set(DUSK_BUILD_BINARY ${DUSK_BUILD_DIR}/Dusk CACHE INTERNAL ${DUSK_CACHE_TARGET})
set(DUSK_ASSETS "" CACHE INTERNAL ${DUSK_CACHE_TARGET})
set(DUSK_LIBRARY_TARGET_NAME "DuskCore" CACHE INTERNAL ${DUSK_CACHE_TARGET})
set(DUSK_BINARY_TARGET_NAME "Dusk" CACHE INTERNAL ${DUSK_CACHE_TARGET})
set(DUSK_ASSETS_ZIP "${DUSK_BUILD_DIR}/dusk.dsk" CACHE INTERNAL ${DUSK_CACHE_TARGET})
project(DuskCore VERSION 1.0.0 LANGUAGES C) if(NOT DEFINED DUSK_TARGET_SYSTEM)
set(DUSK_TARGET_SYSTEM "linux")
# Work out the target platform unless one was given explicitly.
include(${DUSK_ROOT_DIR}/cmake/determineplatform.cmake)
# Platform defaults and definitions (sets DUSK_DEFAULT_* and
# DUSK_PLATFORM_DIRS / DUSK_PLATFORM_DEFINITIONS).
set(DUSK_TARGET_FILE
"${DUSK_ROOT_DIR}/cmake/${DUSK_TARGET_SYSTEM}/target.cmake"
)
if(NOT EXISTS "${DUSK_TARGET_FILE}")
message(FATAL_ERROR "Unknown DUSK_TARGET_SYSTEM: ${DUSK_TARGET_SYSTEM}")
endif() endif()
include("${DUSK_TARGET_FILE}")
# All options and their defaults. # Create directories
include(${DUSK_ROOT_DIR}/cmake/option.cmake) file(MAKE_DIRECTORY ${DUSK_GENERATED_HEADERS_DIR})
file(MAKE_DIRECTORY ${DUSK_TEMP_DIR})
file(MAKE_DIRECTORY ${DUSK_BUILT_ASSETS_DIR})
# Targets. Tests split the code into a library plus the binary, otherwise all # Required build packages
# code is compiled straight into the binary. find_package(Python3 COMPONENTS Interpreter REQUIRED)
set(DUSK_BINARY_TARGET_NAME "Dusk")
add_executable(${DUSK_BINARY_TARGET_NAME}) # Init Project.
project(${DUSK_LIBRARY_TARGET_NAME}
VERSION 1.0.0
LANGUAGES C
)
# Either, create library and binary separately (used for tests), or make them
# one in the same so all code is in the binary only.
# Binary Executable
add_executable(${DUSK_BINARY_TARGET_NAME} ${DUSK_SOURCES_DIR}/dusk/null.c)
if(DUSK_BUILD_TESTS) if(DUSK_BUILD_TESTS)
set(DUSK_LIBRARY_TARGET_NAME "DuskCore") # MainLibrary
add_library(${DUSK_LIBRARY_TARGET_NAME} STATIC) add_library(${DUSK_LIBRARY_TARGET_NAME} STATIC)
target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
# Link library to binary
target_link_libraries(${DUSK_BINARY_TARGET_NAME}
PUBLIC
${DUSK_LIBRARY_TARGET_NAME} ${DUSK_LIBRARY_TARGET_NAME}
) )
else() else()
set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}") set(DUSK_LIBRARY_TARGET_NAME "${DUSK_BINARY_TARGET_NAME}" CACHE INTERNAL ${DUSK_CACHE_TARGET})
endif() endif()
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC if(NOT DEFINED DUSK_VERSION)
string(TIMESTAMP DUSK_VERSION "debug-%y%m%d%H%M%S")
endif()
# Definitions
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}" DUSK_TARGET_SYSTEM="${DUSK_TARGET_SYSTEM}"
DUSK_GAME_NAME="${DUSK_GAME_NAME}" DUSK_GAME_NAME="${DUSK_GAME_NAME}"
DUSK_GAME_AUTHOR="${DUSK_GAME_AUTHOR}" 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}" DUSK_VERSION="${DUSK_VERSION}"
) )
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC # Toolchains
${DUSK_PLATFORM_DEFINITIONS} include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
)
foreach(OPT ASSERTIONS_FAKED BACKTRACE TRACK_MEMORY NETWORK)
if(DUSK_${OPT})
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_${OPT})
endif()
endforeach()
if(DUSK_BACKTRACE) # Add tools
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
endif()
# Tools (run during the build to generate headers)
add_subdirectory(tools) add_subdirectory(tools)
# Include generated headers from tools.
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${DUSK_GENERATED_HEADERS_DIR} ${DUSK_GENERATED_HEADERS_DIR}
) )
# Core engine code # Add main code
add_subdirectory(${DUSK_SOURCES_DIR}/dusk) add_subdirectory(${DUSK_SOURCES_DIR})
# Assets, copied into <build>/assets # Include generated headers
include(${DUSK_ROOT_DIR}/cmake/asset/asset.cmake) target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
# Separate binary dir, so <build>/assets only contains built assets. ${DUSK_GENERATED_HEADERS_DIR}
add_subdirectory(${DUSK_ROOT_DIR}/assets ${CMAKE_BINARY_DIR}/assets_cmake) )
dusk_assets_finalize(DuskAssets)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DuskAssets)
# Game code
add_subdirectory(${DUSK_SOURCES_DIR}/${DUSK_GAME})
# Platform code
foreach(DIR ${DUSK_PLATFORM_DIRS})
add_subdirectory(${DUSK_SOURCES_DIR}/${DIR})
endforeach()
# Handle tests
if(DUSK_BUILD_TESTS) if(DUSK_BUILD_TESTS)
enable_testing() enable_testing()
add_subdirectory(test)
endif() endif()
# Build assets
file(GLOB_RECURSE DUSK_ASSET_FILES CONFIGURE_DEPENDS "${DUSK_ASSETS_DIR}/*")
add_custom_command(
OUTPUT "${DUSK_ASSETS_ZIP}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
COMMAND ${CMAKE_COMMAND} -E rm -f "${DUSK_ASSETS_ZIP}"
COMMAND ${CMAKE_COMMAND} -E tar "cf" "${DUSK_ASSETS_ZIP}" --format=zip -- .
WORKING_DIRECTORY "${DUSK_ASSETS_DIR}"
DEPENDS ${DUSK_ASSET_FILES}
VERBATIM
)
add_custom_target(DUSK_ASSETS_BUILT DEPENDS "${DUSK_ASSETS_ZIP}")
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DUSK_ASSETS_BUILT)
+24
View File
@@ -0,0 +1,24 @@
# Documentation
- [Scripting](docs/SCRIPTING.md) — writing gameplay logic in JavaScript.
- [UI](docs/UI.md) — building buttons/menus/etc from engine/game C code.
# Building
Each build target has different requirements. You can take a look at the git
workflow to see how the builds are done for each target. In addition, for
accessing the editor and building the game on your host system, install the
following packages, depending on your system;
Fedora;
```
sudo dnf install git make gcc cmake python python-polib python3-pillow python3-dotenv python3-numpy python-qt5 python3-pyopengl SDL2-devel zlib-devel libzip-devel bzip2-devel openssl-devel lzma-sdk-devel xz xz-devel lua-devel
```
Ubuntu;
```
sudo apt-get install git build-essential gcc python python-polib python3-pillow python3-dotenv python3-numpy python3-pyqt5 python3-opengl
```
Arch Linux;
```
sudo pacman -S git base-devel gcc python python-polib python-pillow python-dotenv python-numpy python-pyqt5 python-opengl
```
-42
View File
@@ -1,42 +0,0 @@
# Unit cube, centered on the origin, with per face normals and UVs.
o Cube
v -0.5 -0.5 -0.5
v 0.5 -0.5 -0.5
v 0.5 0.5 -0.5
v -0.5 0.5 -0.5
v -0.5 -0.5 0.5
v 0.5 -0.5 0.5
v 0.5 0.5 0.5
v -0.5 0.5 0.5
vt 0.0 0.0
vt 1.0 0.0
vt 1.0 1.0
vt 0.0 1.0
vn 0.0 0.0 -1.0
vn 0.0 0.0 1.0
vn -1.0 0.0 0.0
vn 1.0 0.0 0.0
vn 0.0 -1.0 0.0
vn 0.0 1.0 0.0
# Back (-Z)
f 2/1/1 1/2/1 4/3/1
f 2/1/1 4/3/1 3/4/1
# Front (+Z)
f 5/1/2 6/2/2 7/3/2
f 5/1/2 7/3/2 8/4/2
# Left (-X)
f 1/1/3 5/2/3 8/3/3
f 1/1/3 8/3/3 4/4/3
# Right (+X)
f 6/1/4 2/2/4 3/3/4
f 6/1/4 3/3/4 7/4/4
# Bottom (-Y)
f 1/1/5 2/2/5 6/3/5
f 1/1/5 6/3/5 5/4/5
# Top (+Y)
f 8/1/6 7/2/6 3/3/6
f 8/1/6 3/3/6 4/4/6
+47
View File
@@ -0,0 +1,47 @@
var Actions;
var camera, cameraPosition;
var cube, cubePosition, cubeRenderable, cubeMesh;
var ground, groundPosition, groundRenderable;
// init() is called via scriptManagerCallGlobal(), which pumps the asset
// system + job queue until any promise it returns settles - so it's safe
// to await include() here even though this runs before the main loop.
async function init() {
Actions = await include("input.js");
camera = new Entity();
cameraPosition = camera.add(POSITION);
camera.add(CAMERA);
cameraPosition.position = new Vec3(3, 3, -6);
cameraPosition.lookAt(new Vec3(0, 0, 0));
cube = new Entity();
cubePosition = cube.add(POSITION);
cubeRenderable = cube.add(RENDERABLE);
cubeMesh = Mesh.createCube();
cubeRenderable.mesh = cubeMesh;
cubeRenderable.color = Color.red();
ground = new Entity();
groundPosition = ground.add(POSITION);
groundRenderable = ground.add(RENDERABLE);
groundRenderable.mesh = Mesh.createCube();
groundRenderable.color = Color.dark_gray();
}
// Runs every frame (including dynamic/interpolation frames) - use for
// smooth, purely presentational animation.
function update() {
cubePosition.rotation.y += TIME.delta * 1.5;
cubePosition.rotation.x += TIME.delta * 0.7;
}
// Runs once per fixed timestep only - use for gameplay logic that should
// be deterministic and independent of display refresh rate.
function fixedUpdate() {
}
function deinit() {
cube.dispose();
camera.dispose();
}
+27
View File
@@ -0,0 +1,27 @@
// Binds physical buttons to abstract actions, then exports the action
// constants so other scripts don't need to know raw INPUT_ACTION_* names.
Input.bind("w", INPUT_ACTION_UP);
Input.bind("s", INPUT_ACTION_DOWN);
Input.bind("a", INPUT_ACTION_LEFT);
Input.bind("d", INPUT_ACTION_RIGHT);
Input.bind("space", INPUT_ACTION_ACCEPT);
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
if(typeof INPUT_GAMEPAD !== "undefined") {
Input.bind("gamepad_up", INPUT_ACTION_UP);
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
}
module = {
UP: INPUT_ACTION_UP,
DOWN: INPUT_ACTION_DOWN,
LEFT: INPUT_ACTION_LEFT,
RIGHT: INPUT_ACTION_RIGHT,
ACCEPT: INPUT_ACTION_ACCEPT,
CANCEL: INPUT_ACTION_CANCEL,
RAGEQUIT: INPUT_ACTION_RAGEQUIT
};
+2
View File
@@ -0,0 +1,2 @@
msgid "test.string"
msgstr "This is a test string"
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

-65
View File
@@ -1,65 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Asset building. Include this from the root CMakeLists.txt.
#
# Assets are registered with add_asset() and built by dusk_assets_finalize().
# Each asset is dispatched on its file extension to a handler function named
# dusk_asset_<extension> (e.g. dusk_asset_obj), defined in its own file in this
# directory. Extensions without a handler fall back to a plain copy.
include(${CMAKE_CURRENT_LIST_DIR}/copy.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/obj.cmake)
# Registers an asset to be built into the project. The path is relative to the
# calling CMakeLists.txt, and is kept relative to the root assets directory in
# the output, so assets/models/a.obj ends up at <build>/assets/models/a.obj.
#
# This only records the asset. The build rules are created by
# dusk_assets_finalize() from the root CMakeLists.txt, because a custom command
# output can only be used by targets in the directory it was defined in.
function(add_asset ASSET_PATH)
set(ASSET_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${ASSET_PATH}")
if(NOT EXISTS "${ASSET_SOURCE}")
message(FATAL_ERROR "add_asset: ${ASSET_SOURCE} does not exist")
endif()
set_property(GLOBAL APPEND PROPERTY DUSK_ASSET_SOURCES "${ASSET_SOURCE}")
endfunction()
# Determines the lowercase extension of an asset, without the leading dot.
function(dusk_asset_extension ASSET_SOURCE OUT_VAR)
get_filename_component(ASSET_EXT "${ASSET_SOURCE}" LAST_EXT)
string(TOLOWER "${ASSET_EXT}" ASSET_EXT)
string(REGEX REPLACE "^\\." "" ASSET_EXT "${ASSET_EXT}")
set(${OUT_VAR} "${ASSET_EXT}" PARENT_SCOPE)
endfunction()
# Creates the TARGET_NAME target, which builds every asset registered with
# add_asset(). Call once from the root CMakeLists.txt after adding the assets.
function(dusk_assets_finalize TARGET_NAME)
get_property(ASSET_SOURCES GLOBAL PROPERTY DUSK_ASSET_SOURCES)
set(ASSET_OUTPUTS "")
foreach(ASSET_SOURCE IN LISTS ASSET_SOURCES)
file(RELATIVE_PATH ASSET_RELATIVE "${DUSK_ASSETS_DIR}" "${ASSET_SOURCE}")
dusk_asset_extension("${ASSET_SOURCE}" ASSET_EXT)
# Dispatch on the extension, falling back to a plain copy.
set(ASSET_OUTPUT "")
if(COMMAND dusk_asset_${ASSET_EXT})
cmake_language(CALL dusk_asset_${ASSET_EXT}
"${ASSET_SOURCE}" "${ASSET_RELATIVE}" ASSET_OUTPUT
)
else()
message(STATUS "Asset ${ASSET_RELATIVE}: no handler for '.${ASSET_EXT}', copying")
dusk_asset_copy("${ASSET_SOURCE}" "${ASSET_RELATIVE}" ASSET_OUTPUT)
endif()
list(APPEND ASSET_OUTPUTS "${ASSET_OUTPUT}")
endforeach()
add_custom_target(${TARGET_NAME} ALL DEPENDS ${ASSET_OUTPUTS})
endfunction()
-22
View File
@@ -1,22 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Handler that copies an asset into the built assets directory as is. Every
# handler has the signature (SOURCE RELATIVE OUT_VAR) and must set OUT_VAR, in
# the caller's scope, to the file it produces.
function(dusk_asset_copy ASSET_SOURCE ASSET_RELATIVE OUT_VAR)
set(ASSET_OUTPUT "${DUSK_BUILT_ASSETS_DIR}/${ASSET_RELATIVE}")
add_custom_command(
OUTPUT "${ASSET_OUTPUT}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${ASSET_SOURCE}" "${ASSET_OUTPUT}"
DEPENDS "${ASSET_SOURCE}"
COMMENT "Copying asset ${ASSET_RELATIVE}"
VERBATIM
)
set(${OUT_VAR} "${ASSET_OUTPUT}" PARENT_SCOPE)
endfunction()
-10
View File
@@ -1,10 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Handler for .obj (Wavefront mesh) assets. Currently just copies the file.
function(dusk_asset_obj ASSET_SOURCE ASSET_RELATIVE OUT_VAR)
dusk_asset_copy("${ASSET_SOURCE}" "${ASSET_RELATIVE}" ASSET_OUTPUT)
set(${OUT_VAR} "${ASSET_OUTPUT}" PARENT_SCOPE)
endfunction()
-45
View File
@@ -1,45 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Determines DUSK_TARGET_SYSTEM when it was not explicitly provided with
# -DDUSK_TARGET_SYSTEM=<target>. Must be included after project() so that the
# compiler and CMAKE_SYSTEM_NAME are known.
#
# Console toolchains all cross compile as "Generic", so they are recognised by
# the compiler they configure.
if(DUSK_TARGET_SYSTEM)
message(STATUS "Dusk target: ${DUSK_TARGET_SYSTEM} (explicit)")
return()
endif()
if(CMAKE_C_COMPILER MATCHES "psp-")
set(DUSK_DETECTED_SYSTEM "psp")
elseif(CMAKE_C_COMPILER MATCHES "arm-vita-eabi")
set(DUSK_DETECTED_SYSTEM "vita")
elseif(CMAKE_C_COMPILER MATCHES "powerpc-eabi")
set(DUSK_DETECTED_SYSTEM "dolphin")
elseif(CMAKE_C_COMPILER MATCHES "ps2")
set(DUSK_DETECTED_SYSTEM "ps2")
elseif(CMAKE_C_COMPILER MATCHES "sh-elf")
set(DUSK_DETECTED_SYSTEM "dreamcast")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(DUSK_DETECTED_SYSTEM "windows")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(DUSK_DETECTED_SYSTEM "linux")
else()
message(FATAL_ERROR
"Could not determine the target platform (system: ${CMAKE_SYSTEM_NAME}, "
"compiler: ${CMAKE_C_COMPILER}). Pass -DDUSK_TARGET_SYSTEM=<target>."
)
endif()
set(DUSK_TARGET_SYSTEM "${DUSK_DETECTED_SYSTEM}" CACHE STRING
"Target platform" FORCE
)
set_property(CACHE DUSK_TARGET_SYSTEM PROPERTY STRINGS
linux windows psp dolphin vita ps2 dreamcast
)
message(STATUS "Dusk target: ${DUSK_TARGET_SYSTEM} (detected)")
-27
View File
@@ -1,27 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the dolphin target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS duskdolphin)
# Default values for the shared options declared in the root CMakeLists.txt.
# Assertions are faked (compiled out) unless this is a Debug build.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
else()
set(DUSK_DEFAULT_ASSERTIONS_FAKED ON)
endif()
set(DUSK_DEFAULT_BACKTRACE OFF)
set(DUSK_DEFAULT_TRACK_MEMORY OFF)
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
DUSK_PLATFORM_ENDIAN_BIG
DUSK_DOLPHIN
DUSK_THREAD_PTHREAD
)
-26
View File
@@ -1,26 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the dreamcast target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS duskdreamcast)
# Default values for the shared options declared in the root CMakeLists.txt.
# Assertions are faked (compiled out) unless this is a Debug build.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
else()
set(DUSK_DEFAULT_ASSERTIONS_FAKED ON)
endif()
set(DUSK_DEFAULT_BACKTRACE OFF)
set(DUSK_DEFAULT_TRACK_MEMORY OFF)
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_DREAMCAST
)
-32
View File
@@ -1,32 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the linux target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS dusklinux dusksdl3 duskvulkan)
# Default values for the shared options declared in the root CMakeLists.txt.
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
set(DUSK_DEFAULT_BACKTRACE ON)
set(DUSK_DEFAULT_TRACK_MEMORY ON)
# Linux runs on both endiannesses, so detect it from the target compiler.
include(TestBigEndian)
test_big_endian(DUSK_IS_BIG_ENDIAN)
if(DUSK_IS_BIG_ENDIAN)
set(DUSK_ENDIAN_DEFINITION DUSK_PLATFORM_ENDIAN_BIG)
else()
set(DUSK_ENDIAN_DEFINITION DUSK_PLATFORM_ENDIAN_LITTLE)
endif()
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
${DUSK_ENDIAN_DEFINITION}
DUSK_LINUX
DUSK_THREAD_PTHREAD
DUSK_TIME_DYNAMIC
)
+15
View File
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(FetchContent)
FetchContent_Declare(
cglm
# GIT_REPOSITORY https://git.wish.moe/YourWishes/cglm.git
GIT_REPOSITORY https://github.com/recp/cglm.git
GIT_TAG v0.9.6
)
FetchContent_MakeAvailable(cglm)
set(cglm_FOUND ON)
+14
View File
@@ -0,0 +1,14 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(FetchContent)
FetchContent_Declare(
cmocka
GIT_REPOSITORY https://gitlab.com/cmocka/cmocka.git
GIT_TAG cmocka-2.0.1
)
FetchContent_MakeAvailable(cmocka)
set(cmocka_FOUND ON)
+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})
+61
View File
@@ -0,0 +1,61 @@
find_package(ZLIB REQUIRED)
find_path(LIBZIP_INCLUDE_DIR NAMES zip.h)
mark_as_advanced(LIBZIP_INCLUDE_DIR)
find_library(LIBZIP_LIBRARY NAMES zip)
mark_as_advanced(LIBZIP_LIBRARY)
get_filename_component(_libzip_libdir ${LIBZIP_LIBRARY} DIRECTORY)
find_file(_libzip_pkgcfg libzip.pc
HINTS ${_libzip_libdir} ${LIBZIP_INCLUDE_DIR}/..
PATH_SUFFIXES pkgconfig lib/pkgconfig libdata/pkgconfig
NO_DEFAULT_PATH
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
LIBZIP
REQUIRED_VARS
LIBZIP_LIBRARY
LIBZIP_INCLUDE_DIR
_libzip_pkgcfg
)
if(LIBZIP_FOUND)
if(NOT TARGET libzip::zip)
add_library(libzip::zip UNKNOWN IMPORTED)
set_target_properties(libzip::zip
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${LIBZIP_INCLUDE_DIR}
INTERFACE_LINK_LIBRARIES ZLIB::ZLIB
IMPORTED_LOCATION ${LIBZIP_LIBRARY}
)
# (Ab)use the (always) installed pkgconfig file to check if BZip2 is required
file(STRINGS ${_libzip_pkgcfg} _have_extra_libs REGEX Libs)
if(_have_extra_libs MATCHES "-lbz2")
find_package(BZip2 REQUIRED)
set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES BZip2::BZip2)
endif()
if(_have_extra_libs MATCHES "-lcrypto")
find_package(OpenSSL REQUIRED)
set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES OpenSSL::Crypto)
endif()
if(_have_extra_libs MATCHES "-lgnutls")
find_package(GnuTLS REQUIRED)
set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES GnuTLS::GnuTLS)
endif()
if(_have_extra_libs MATCHES "-lnettle")
find_package(Nettle REQUIRED)
set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES Nettle::Nettle)
endif()
if(_have_extra_libs MATCHES "-llzma")
find_package(LibLZMA REQUIRED)
set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES LibLZMA::LibLZMA)
endif()
if(_have_extra_libs MATCHES "-lz")
find_package(ZLIB REQUIRED)
set_property(TARGET libzip::zip APPEND PROPERTY INTERFACE_LINK_LIBRARIES ZLIB::ZLIB)
endif()
endif()
endif()
+31
View File
@@ -0,0 +1,31 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(FetchContent)
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
)
# Fetch stb if not already done
FetchContent_MakeAvailable(stb)
# Find the stb_image.h header
set(STB_INCLUDE_DIR "${stb_SOURCE_DIR}")
set(STB_IMAGE_HEADER "${stb_SOURCE_DIR}/stb_image.h")
if(EXISTS "${STB_IMAGE_HEADER}")
add_library(stb_image INTERFACE)
target_include_directories(stb_image INTERFACE "${STB_INCLUDE_DIR}")
set(STB_IMAGE_FOUND TRUE)
else()
set(STB_IMAGE_FOUND FALSE)
endif()
# Standard CMake variables
set(STB_IMAGE_INCLUDE_DIRS "${STB_INCLUDE_DIR}")
set(STB_IMAGE_LIBRARIES stb_image)
mark_as_advanced(STB_IMAGE_INCLUDE_DIRS STB_IMAGE_LIBRARIES STB_IMAGE_FOUND)
+18
View File
@@ -0,0 +1,18 @@
include(FetchContent)
if(NOT TARGET yyjson)
FetchContent_Declare(
yyjson
GIT_REPOSITORY https://github.com/ibireme/yyjson.git
GIT_TAG 0.12.0
)
FetchContent_MakeAvailable(yyjson)
endif()
# Provide an imported target if not already available
if(NOT TARGET yyjson::yyjson)
add_library(yyjson::yyjson ALIAS yyjson)
endif()
# Mark yyjson as found for find_package compatibility
set(yyjson_FOUND TRUE)
+32
View File
@@ -0,0 +1,32 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
find_package(cmocka REQUIRED)
# Function to create a test executable from a single .c file
function(dusktest TEST_C_FILE)
# Create target name from file name
get_filename_component(TEST_NAME ${TEST_C_FILE} NAME_WE)
# Create the executable target
add_executable(${TEST_NAME})
# Setup some compiler definitions.
target_compile_definitions(${TEST_NAME} PRIVATE
DUSK_TEST_ASSERT=1
)
# Add the test file
target_sources(${TEST_NAME} PRIVATE ${TEST_C_FILE})
# Add sources, both common and test-specific
target_include_directories(${TEST_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR} ${DUSK_TEST_DIR})
# Link against DuskCore and cmocka
target_link_libraries(${TEST_NAME} PUBLIC ${DUSK_LIBRARY_TARGET_NAME} cmocka)
# Add as a CTest
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
endfunction()
-43
View File
@@ -1,43 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# All user facing CMake options and their defaults. Included by the root
# CMakeLists.txt after the platform's target.cmake, which supplies the
# DUSK_DEFAULT_* values used below. DUSK_TARGET_SYSTEM itself is declared by
# determineplatform.cmake.
# General
option(DUSK_BUILD_TESTS "Enable tests." OFF)
option(DUSK_NETWORK "Enable network support." ON)
# Game metadata
set(DUSK_GAME "duskexample" CACHE STRING
"Directory under src/ containing the game to build (provides game/game.c)."
)
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name.")
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder.")
if(NOT DEFINED DUSK_VERSION)
string(TIMESTAMP DUSK_VERSION "debug-%y%m%d%H%M%S")
endif()
# Platform dependent defaults
option(DUSK_ASSERTIONS_FAKED
"Compile assertions out (they do nothing)."
${DUSK_DEFAULT_ASSERTIONS_FAKED}
)
option(DUSK_BACKTRACE
"Log a stack trace on assertion failure."
${DUSK_DEFAULT_BACKTRACE}
)
option(DUSK_TRACK_MEMORY
"Track memoryAllocate() call sites to help find memory leaks."
${DUSK_DEFAULT_TRACK_MEMORY}
)
# Tests mock assertions, so they can never be faked.
if(DUSK_BUILD_TESTS AND DUSK_ASSERTIONS_FAKED)
message(STATUS "DUSK_BUILD_TESTS: forcing DUSK_ASSERTIONS_FAKED off")
set(DUSK_ASSERTIONS_FAKED OFF CACHE BOOL "" FORCE)
endif()
-26
View File
@@ -1,26 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the ps2 target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS duskps2)
# Default values for the shared options declared in the root CMakeLists.txt.
# Assertions are faked (compiled out) unless this is a Debug build.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
else()
set(DUSK_DEFAULT_ASSERTIONS_FAKED ON)
endif()
set(DUSK_DEFAULT_BACKTRACE OFF)
set(DUSK_DEFAULT_TRACK_MEMORY OFF)
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_PS2
)
-27
View File
@@ -1,27 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the psp target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS duskpsp)
# Default values for the shared options declared in the root CMakeLists.txt.
# Assertions are faked (compiled out) unless this is a Debug build.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
else()
set(DUSK_DEFAULT_ASSERTIONS_FAKED ON)
endif()
set(DUSK_DEFAULT_BACKTRACE OFF)
set(DUSK_DEFAULT_TRACK_MEMORY OFF)
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_PSP
DUSK_THREAD_PTHREAD
)
+79
View File
@@ -0,0 +1,79 @@
# Build type: DOL (SD/USB via libfat) or ISO (DVD disc via libogc DVD driver)
set(DUSK_DOLPHIN_BUILD_TYPE "DOL" CACHE STRING "Dolphin asset source: DOL (SD/USB) or ISO (DVD disc)")
set_property(CACHE DUSK_DOLPHIN_BUILD_TYPE PROPERTY STRINGS "DOL" "ISO")
# Numeric tokens so #if DUSK_DOLPHIN_BUILD_TYPE == DOL works in C.
# DUSK_DOLPHIN_BUILD_TYPE is passed without quotes so it expands to the identifier.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_DOLPHIN
DUSK_INPUT_GAMEPAD
# DUSK_DISPLAY_WIDTH=640
# DUSK_DISPLAY_HEIGHT=480
DUSK_DISPLAY_WIDTH=320
DUSK_DISPLAY_HEIGHT=240
DUSK_DISPLAY_OVERSCAN=16
DUSK_THREAD_PTHREAD
DOL=1
ISO=2
DUSK_DOLPHIN_BUILD_TYPE=${DUSK_DOLPHIN_BUILD_TYPE}
# GameCube/Wii PowerPC is always big-endian; declare it at compile time
# like every other target instead of relying on endian.h's runtime probe.
DUSK_PLATFORM_ENDIAN_BIG
)
# Custom compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -fno-exceptions")
# Disable all warnings
target_compile_options(${DUSK_LIBRARY_TARGET_NAME} PRIVATE -w)
# cglm: fetched at source level via Findcglm.cmake (FetchContent, headers only)
find_package(cglm REQUIRED)
# Pre-create ZLIB::ZLIB so any downstream cmake module that resolves it
# (FindZLIB, pkg-config IMPORTED_TARGET Requires processing) gets plain -lz
# rather than an unresolvable IMPORTED target that the PPC linker rejects.
if(NOT TARGET ZLIB::ZLIB)
add_library(ZLIB::ZLIB INTERFACE IMPORTED GLOBAL)
set_target_properties(ZLIB::ZLIB PROPERTIES INTERFACE_LINK_LIBRARIES "z")
endif()
# 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
cglm
m
zip
bz2
zstd
z
lzma
)
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")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND elf2dol
"$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>"
"${DUSK_BINARY_TARGET_NAME_DOL}"
COMMENT "Generating ${DUSK_BINARY_TARGET_NAME_DOL} from ${DUSK_BINARY_TARGET_NAME}"
)
+24
View File
@@ -0,0 +1,24 @@
include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_GAMECUBE
)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
# bba
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"GCN"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building GameCube ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
+47
View File
@@ -0,0 +1,47 @@
# Find link platform-specific libraries
set(OpenGL_GL_PREFERENCE LEGACY)
find_package(SDL2 REQUIRED)
find_library(EGL_LIB EGL REQUIRED)
find_library(GL_LIB GL REQUIRED)
find_package(OpenGL REQUIRED)
# Setup endianess at compile time to optimize.
include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_BIG
)
else()
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_LITTLE
)
endif()
# Link required libraries.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
SDL2
pthread
OpenGL::GLES2
${GL_LIB}
${EGL_LIB}
m
)
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_OPENGL_ES
DUSK_LINUX
DUSK_KNULLI
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=640
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
DUSK_THREAD_PTHREAD
)
+48
View File
@@ -0,0 +1,48 @@
# Find link platform-specific libraries
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
# find_package(CURL REQUIRED)
# Setup endianess at compile time to optimize.
include(TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_BIG
)
else()
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_PLATFORM_ENDIAN_LITTLE
)
endif()
# Link required libraries.
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
SDL2
pthread
OpenGL::GL
GL
m
# CURL::libcurl
)
set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures.")
# Define platform-specific macros.
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_CONSOLE_POSIX
# DUSK_OPENGL_LEGACY
DUSK_LINUX
DUSK_DISPLAY_SIZE_DYNAMIC
DUSK_DISPLAY_WIDTH_DEFAULT=854
DUSK_DISPLAY_HEIGHT_DEFAULT=480
DUSK_DISPLAY_SCREEN_HEIGHT=240
DUSK_INPUT_KEYBOARD
DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC
DUSK_NETWORK_IPV6
DUSK_THREAD_PTHREAD
)
+77
View File
@@ -0,0 +1,77 @@
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_C_COMPILER_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_C_ARCHIVE_CREATE "$ENV{PSPDEV}/bin/psp-ar qc <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_APPEND "$ENV{PSPDEV}/bin/psp-ar q <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_C_ARCHIVE_FINISH "$ENV{PSPDEV}/bin/psp-ranlib <TARGET>")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_C OFF CACHE BOOL "" FORCE)
find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED)
target_link_libraries(${DUSK_BINARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
SDL2
pthread
OpenGL::GL
zip
bz2
z
mbedtls
mbedcrypto
lzma
m
pspdebug
pspdisplay
pspge
pspctrl
pspgu
pspaudio
pspaudiolib
psputility
pspvfpu
pspvram
psphprm
pspnet
pspnet_inet
pspnet_apctl
psphttp
pspssl
)
target_include_directories(${DUSK_BINARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_PSP
DUSK_INPUT_GAMEPAD
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=480
DUSK_DISPLAY_HEIGHT=272
DUSK_THREAD_PTHREAD
DUSK_TIME_DYNAMIC
DUSK_DISPLAY_OVERSCAN=6
)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_ASSERTIONS_FAKED
)
endif()
# Postbuild, create .pbp file for PSP.
create_pbp_file(
TARGET "${DUSK_BINARY_TARGET_NAME}"
ICON_PATH NULL
BACKGROUND_PATH NULL
PREVIEW_PATH NULL
TITLE "${DUSK_BINARY_TARGET_NAME}"
PSAR_PATH ${DUSK_ASSETS_ZIP}
VERSION 01.00
)
+89
View File
@@ -0,0 +1,89 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
if(NOT DEFINED ENV{VITASDK})
message(FATAL_ERROR "VITASDK environment variable is not set.")
endif()
include("$ENV{VITASDK}/share/vita.cmake" REQUIRED)
set(VITA_APP_NAME "Dusk")
set(VITA_TITLEID "DUSK00001")
set(VITA_VERSION "01.00")
find_package(SDL2 REQUIRED)
# Custom flags for cglm
set(CGLM_SHARED OFF CACHE BOOL "Build cglm shared" FORCE)
set(CGLM_STATIC ON CACHE BOOL "Build cglm static" FORCE)
find_package(cglm REQUIRED)
# Link libraries
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${SDL2_LIBRARIES}
cglm
SDL2
SDL2main
zip
bz2
z
zstd
crypto
lzma
m
pthread
stdc++
vitaGL
mathneon
vitashark
kubridge_stub
SceAppMgr_stub
SceAudio_stub
SceCtrl_stub
SceCommonDialog_stub
SceDisplay_stub
SceKernelDmacMgr_stub
SceGxm_stub
SceShaccCg_stub
SceSysmodule_stub
ScePower_stub
SceTouch_stub
SceVshBridge_stub
SceIofilemgr_stub
SceShaccCgExt
libtaihen_stub.a
# SceKernel_stub
SceAppUtil_stub
SceHid_stub
SceRtc_stub
)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
${SDL2_INCLUDE_DIRS}
)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_SDL2
DUSK_OPENGL
DUSK_VITA
DUSK_INPUT_GAMEPAD
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_OPENGL_LEGACY
DUSK_DISPLAY_WIDTH=960
DUSK_DISPLAY_HEIGHT=544
DUSK_THREAD_PTHREAD
)
# Post-build: create SELF from the ELF binary (UNSAFE = homebrew, no signing)
vita_create_self(${DUSK_BINARY_TARGET_NAME}.self ${DUSK_BINARY_TARGET_NAME} UNSAFE)
# Post-build: package SELF + assets into a .vpk installable on the Vita
vita_create_vpk(${DUSK_BINARY_TARGET_NAME}.vpk ${VITA_TITLEID} ${DUSK_BINARY_TARGET_NAME}.self
VERSION ${VITA_VERSION}
NAME ${VITA_APP_NAME}
FILE ${DUSK_ASSETS_ZIP} dusk.dsk
)
+27
View File
@@ -0,0 +1,27 @@
include(cmake/targets/dolphin.cmake)
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_WII
)
# Generate Homebrew Channel meta.xml from project identity variables
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
configure_file(
"${CMAKE_SOURCE_DIR}/docker/dolphin/meta.xml.in"
"${DUSK_BUILD_DIR}/meta.xml"
@ONLY
)
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
if(DUSK_DOLPHIN_BUILD_TYPE STREQUAL "ISO")
add_custom_command(TARGET ${DUSK_BINARY_TARGET_NAME} POST_BUILD
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/tools/makedolphiniso.py"
"WII"
"${DUSK_BINARY_TARGET_NAME_DOL}"
"${DUSK_ASSETS_ZIP}"
"${DUSK_GAME_NAME}"
"${DUSK_BUILD_DIR}"
COMMENT "Building Wii ISO images (NTSC-J, NTSC-U, PAL)"
)
endif()
+29
View File
@@ -0,0 +1,29 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_ASM_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_SYSROOT /)
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH
/usr/aarch64-linux-gnu
/usr/lib/aarch64-linux-gnu
/usr/include/aarch64-linux-gnu
)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_LIBDIR} "/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "/usr/lib/aarch64-linux-gnu/pkgconfig")
set(CMAKE_PREFIX_PATH "/usr/aarch64-linux-gnu;/usr/lib/aarch64-linux-gnu")
# Optional: helps some Find modules
set(SDL2_DIR "/usr/lib/aarch64-linux-gnu/cmake/SDL2" CACHE PATH "")
-27
View File
@@ -1,27 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the vita target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS duskvita)
# Default values for the shared options declared in the root CMakeLists.txt.
# Assertions are faked (compiled out) unless this is a Debug build.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
else()
set(DUSK_DEFAULT_ASSERTIONS_FAKED ON)
endif()
set(DUSK_DEFAULT_BACKTRACE OFF)
set(DUSK_DEFAULT_TRACK_MEMORY OFF)
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_VITA
DUSK_THREAD_PTHREAD
)
-21
View File
@@ -1,21 +0,0 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Platform setup for the windows target, included by the root CMakeLists.txt.
# Only sets defaults and definitions; the root applies them.
# Source directories to build for this platform.
set(DUSK_PLATFORM_DIRS duskwindows dusksdl3 duskvulkan)
# Default values for the shared options declared in the root CMakeLists.txt.
set(DUSK_DEFAULT_ASSERTIONS_FAKED OFF)
set(DUSK_DEFAULT_BACKTRACE OFF)
set(DUSK_DEFAULT_TRACK_MEMORY ON)
# Platform definitions.
set(DUSK_PLATFORM_DEFINITIONS
DUSK_PLATFORM_ENDIAN_LITTLE
DUSK_WINDOWS
)
+7
View File
@@ -0,0 +1,7 @@
FROM ghcr.io/extremscorner/libogc2
WORKDIR /workdir
RUN apt update && \
dkp-pacman -Syu --noconfirm && \
apt install -y python3 python3-pip python3-polib python3-pil python3-dotenv python3-pyqt5 python3-opengl xorriso && \
dkp-pacman -S --needed --noconfirm gamecube-sdl2 ppc-liblzma ppc-libzip libogc2 gamecube-tools ppc-libmad ppc-zlib-ng ppc-liblzma ppc-bzip2 ppc-zstd
VOLUME ["/workdir"]
+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>
+35
View File
@@ -0,0 +1,35 @@
FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /workdir
RUN dpkg --add-architecture arm64 && \
apt-get update && \
apt-get install -y --no-install-recommends \
crossbuild-essential-arm64 \
ca-certificates \
pkg-config \
cmake \
make \
ninja-build \
git \
file \
python3 \
python3-pip \
python3-polib \
python3-pil \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
liblua5.4-dev:arm64 \
xz-utils:arm64 \
libbz2-dev:arm64 \
zlib1g-dev:arm64 \
libzip-dev:arm64 \
libssl-dev:arm64 \
libsdl2-dev:arm64 \
liblzma-dev:arm64 \
libopengl0:arm64 \
libgl1:arm64 \
libegl1:arm64 \
libgles2:arm64 \
libgl1-mesa-dev:arm64 && \
rm -rf /var/lib/apt/lists/*
+22 -3
View File
@@ -1,5 +1,24 @@
FROM ubuntu:latest FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /workdir WORKDIR /workdir
COPY scripts/linux/install.sh /tmp/install.sh RUN apt-get update
RUN chmod +x /tmp/install.sh && /tmp/install.sh && rm /tmp/install.sh RUN apt-get install -y \
build-essential \
cmake \
python3 \
python3-pip \
python3-polib \
python3-pil \
libsdl2-dev \
libgl1-mesa-dev \
libzip-dev \
python3-dotenv \
python3-pyqt5 \
python3-opengl \
xz-utils \
liblzma-dev \
libbz2-dev \
zlib1g-dev \
libzip-dev \
libbz2-dev \
git \
libssl-dev
+4 -2
View File
@@ -1,5 +1,7 @@
FROM pspdev/pspdev:latest FROM pspdev/pspdev:latest
WORKDIR /workdir WORKDIR /workdir
COPY scripts/psp/install.sh /tmp/install.sh RUN apk add --no-cache \
RUN chmod +x /tmp/install.sh && /tmp/install.sh && rm /tmp/install.sh python3 \
py3-pip \
py3-dotenv
VOLUME ["/workdir"] VOLUME ["/workdir"]
+13
View File
@@ -0,0 +1,13 @@
FROM vitasdk/vitasdk:latest
WORKDIR /workdir
# Install vitaGL and its dependencies (vitashark, SceShaccCg) via vdpm
RUN which vdpm
# Install Python (needed for Dusk code generation tools)
RUN apk add --no-cache \
python3 \
py3-pip \
py3-dotenv
VOLUME ["/workdir"]
+379
View File
@@ -0,0 +1,379 @@
# Scripting
Dusk embeds [JerryScript](https://github.com/jerryscript-project/jerryscript) to
drive gameplay logic from JavaScript. The engine itself (rendering, physics,
asset loading, entity storage) is all C; scripts sit on top and manipulate that
state through a small set of bound objects.
This document covers the JS-facing scripting API. **UI (buttons, sliders,
menus, etc.) is not exposed to scripts** — it's a separate, C-only API. See
[UI.md](UI.md) if you're building screens/menus from engine/game C code.
## Lifecycle
On startup, `engineInit()` loads and evaluates `assets/engine.js` as the main
script, then calls the global `init()` function if one is defined. From then
on, every engine tick calls (in order):
1. `fixedUpdate()` — once per fixed timestep. Use this for gameplay logic that
must be deterministic and independent of display refresh rate (movement,
physics-adjacent input handling, etc). Skipped on interpolation/dynamic
frames when the build has variable-timestep rendering enabled.
2. `update()` — once per rendered frame, including interpolation frames. Use
this for smooth, purely presentational animation (nothing that needs to be
deterministic).
On shutdown, `deinit()` is called once.
All four hooks (`init`, `update`, `fixedUpdate`, `deinit`) are **optional** —
if a script doesn't define one, the engine simply skips it, no error.
```js
function update() {
cubePosition.rotation.y += TIME.delta * 1.5;
}
```
### `async init()` and `include()`
`init` (or any of the other hooks) can be declared `async` and use `await`
freely, including awaiting `include()` (see below), even though the engine
calls these functions synchronously from C with no external JS event loop.
When a hook returns a pending `Promise`, the engine keeps driving the asset
system and JerryScript's job queue until that promise settles before
continuing — so by the time e.g. `init()` "returns" from the engine's point of
view, everything it awaited has actually finished.
```js
async function init() {
Actions = await include("input.js");
// ...
}
```
If the awaited work throws/rejects, it surfaces as a C-level error.
## Loading other scripts: `include(path)`
```js
var Actions = await include("input.js");
```
`include(path)` always returns a `Promise`. The named file is loaded and
evaluated once no matter how many times (or from how many different scripts)
you `include()` it — later calls for the same path are handed the same
in-flight/resolved promise rather than re-running the file.
The included script communicates its result back by assigning to the bare
global `module`:
```js
// input.js
Input.bind("w", INPUT_ACTION_UP);
// ...
module = {
UP: INPUT_ACTION_UP,
DOWN: INPUT_ACTION_DOWN,
// ...
};
```
Whatever `input.js` assigns to `module` becomes the resolved value of the
promise `include("input.js")` returned — that's what `Actions` ends up being
in the example above. If the included script throws, the promise rejects
instead.
## Full example
This is the actual shipped example content (`assets/engine.js` +
`assets/input.js`):
```js
// input.js — binds physical buttons to abstract actions, then exports the
// action constants so other scripts don't need to know raw INPUT_ACTION_* names.
Input.bind("w", INPUT_ACTION_UP);
Input.bind("s", INPUT_ACTION_DOWN);
Input.bind("a", INPUT_ACTION_LEFT);
Input.bind("d", INPUT_ACTION_RIGHT);
Input.bind("space", INPUT_ACTION_ACCEPT);
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
if(typeof INPUT_GAMEPAD !== "undefined") {
Input.bind("gamepad_up", INPUT_ACTION_UP);
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
}
module = {
UP: INPUT_ACTION_UP,
DOWN: INPUT_ACTION_DOWN,
LEFT: INPUT_ACTION_LEFT,
RIGHT: INPUT_ACTION_RIGHT,
ACCEPT: INPUT_ACTION_ACCEPT,
CANCEL: INPUT_ACTION_CANCEL,
RAGEQUIT: INPUT_ACTION_RAGEQUIT
};
```
```js
// engine.js
var Actions;
var camera, cameraPosition;
var cube, cubePosition, cubeRenderable, cubeMesh;
async function init() {
Actions = await include("input.js");
camera = new Entity();
cameraPosition = camera.add(POSITION);
camera.add(CAMERA);
cameraPosition.position = new Vec3(3, 3, -6);
cameraPosition.lookAt(new Vec3(0, 0, 0));
cube = new Entity();
cubePosition = cube.add(POSITION);
cubeRenderable = cube.add(RENDERABLE);
cubeMesh = Mesh.createCube();
cubeRenderable.mesh = cubeMesh;
cubeRenderable.color = Color.red();
}
function update() {
cubePosition.rotation.y += TIME.delta * 1.5;
cubePosition.rotation.x += TIME.delta * 0.7;
}
function fixedUpdate() {
var move = 3.0 * TIME.delta;
if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move;
if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move;
if(Input.isDown(Actions.UP)) cubePosition.position.z += move;
if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move;
if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0);
}
function deinit() {
cube.dispose();
camera.dispose();
}
```
## API reference
### `TIME`
Plain global object, live getters (read fresh engine state every access, not
snapshotted):
| Property | Type | Description |
|---|---|---|
| `TIME.delta` | number | Seconds since the last frame. |
| `TIME.time` | number | Total elapsed engine time, in seconds. |
### `PLATFORM`
A single global string constant — the compile-time target name, e.g.
`"linux"`, `"psp"`, `"vita"`, `"dolphin"`. Individual platform builds may
inject additional platform-specific globals via their own
`modulePlatformPlatform()` hook; those aren't documented here since they vary
per target.
### `Input`
Static namespace (not constructible — there's no `new Input()`).
| Method | Description |
|---|---|
| `Input.bind(buttonName, action)` | Binds a physical button/key (string, e.g. `"w"`, `"space"`, `"gamepad_up"`) to an abstract `INPUT_ACTION_*` constant. Many buttons can bind to the same action. Throws on an empty/unrecognized button name or invalid action. |
| `Input.isDown(action)` → boolean | Is the action currently held. |
| `Input.pressed(action)` → boolean | Action transitioned to down this frame. |
| `Input.released(action)` → boolean | Action transitioned to up this frame. |
| `Input.getValue(action)` → number | Current analog value for the action. |
| `Input.axis(negAction, posAction)` → number | Combined axis value from two opposing actions. |
| `Input.axis2D(negX, posX, negY, posY)` → `Vec2` | Combined 2D axis from four actions. |
Global `INPUT_ACTION_*` constants (names are stable API; treat the numeric
values as opaque/build-specific): `INPUT_ACTION_UP`, `INPUT_ACTION_DOWN`,
`INPUT_ACTION_LEFT`, `INPUT_ACTION_RIGHT`, `INPUT_ACTION_ACCEPT`,
`INPUT_ACTION_CANCEL`, `INPUT_ACTION_PAUSE`, `INPUT_ACTION_RAGEQUIT`,
`INPUT_ACTION_CONSOLE`, `INPUT_ACTION_POINTERX`, `INPUT_ACTION_POINTERY`.
Conditionally-defined boolean globals reflecting build capability — only
present at all if the corresponding input method is compiled in, so
feature-test with `typeof`, don't assume they exist:
`INPUT_KEYBOARD`, `INPUT_GAMEPAD`, `INPUT_POINTER`, `INPUT_TOUCH`.
### `Vec2` / `Vec3` / `Vec4`
`new Vec2(x?, y?)`, `new Vec3(x?, y?, z?)`, `new Vec4(x?, y?, z?, w?)` — all
components optional, default `0`.
Common instance surface across all three: `.dot(other)`, `.length()`,
`.lengthSq()`, `.normalize()`, `.negate()`, `.add(other)`, `.sub(other)`,
`.scale(n)`, `.lerp(other, t)` — each of `add`/`sub`/`scale`/`negate`/
`normalize`/`lerp` returns a **new** vector (non-mutating). `Vec3` additionally
has `.cross(other)` and `.distance(other)`; `Vec2` has `.distance(other)` too;
`Vec4` has neither `.cross()` nor `.distance()`.
`Vec4` also has UV aliases over the same four floats: `.u0` (= `.x`), `.v0`
(= `.y`), `.u1` (= `.z`), `.v1` (= `.w`) — handy for texture-rect style code.
All three have `.x`/`.y`(/`.z`/`.w`) get/set properties and a `.toString()`
like `"Vec3(1, 2, 3)"`.
**"Vec3Ref" — live references.** Several engine properties (entity
`position`/`rotation`/`scale`, physics `velocity`, a mesh vertex's `position`)
return a vector-*like* object instead of a plain `Vec3`. It has the identical
`.x`/`.y`/`.z` surface, but reads/writes go straight into the underlying
native buffer — writing `.x` on `entity.position.position` immediately moves
the entity, no separate assignment needed. Anywhere the API expects a `Vec3`
argument, a Vec3Ref works too. You never construct one directly; you only
ever receive them from properties like the ones above.
### `Mat4`
`new Mat4()` — always constructs identity; no other constructor form.
| Member | Description |
|---|---|
| `.mul(other)` → `Mat4` | `this * other`. |
| `.transpose()` → `Mat4` | |
| `.inverse()` → `Mat4` | |
| `.determinant()` → number | |
| `.mulVec3(vec3, w?)` → `Vec3` | `w` defaults to `1.0` (point); pass `0` for a direction. |
| `.mulVec4(vec4)` → `Vec4` | |
| `.translate(vec3)` → `Mat4` | Non-mutating — returns a translated copy. |
| `.scale(vec3)` → `Mat4` | Non-mutating — returns a scaled copy. |
| `Mat4.identity()` → `Mat4` | Static. |
| `Mat4.perspective(fov, aspect, near, far)` → `Mat4` | Static, all 4 args required. |
| `Mat4.lookAt(eye, center, up)` → `Mat4` | Static, all 3 args required `Vec3`s. |
### `Color`
`new Color(r?, g?, b?, a?)` — each an int `0..255`, default `255` (so
`new Color()` is opaque white). Properties `.r`/`.g`/`.b`/`.a` get/set.
Named factories, each a zero-arg static returning a new opaque `Color`
(alpha `255` unless noted): `Color.black()`, `Color.white()`, `Color.red()`,
`Color.green()`, `Color.blue()`, `Color.yellow()`, `Color.cyan()`,
`Color.magenta()`, `Color.transparent()` (alpha 0), `Color.transparent_white()`
(alpha 0), `Color.transparent_black()` (alpha 0), `Color.gray()`,
`Color.light_gray()`, `Color.dark_gray()`, `Color.orange()`, `Color.purple()`,
`Color.brown()`, `Color.pink()`, `Color.lime()`, `Color.navy()`,
`Color.teal()`, `Color.cornflower_blue()`.
`Color.rainbow(t?, speed?)` → `Color` — `t` defaults to `TIME.time * 4.0`;
produces a shifting rainbow color, useful for debug visuals.
### `Mesh`
`new Mesh(vertexCount)` — allocates an uninitialized CPU-side vertex buffer
(not yet uploaded to the GPU).
| Member | Description |
|---|---|
| `.vertices` | Array of vertex wrappers, each with a `.position` (Vec3Ref, writes straight into that vertex). |
| `.vertexCount` | Read-only. |
| `.flush()` | Uploads to the GPU. First call initializes the GPU mesh; later calls re-upload the current vertex data — call this after editing `.vertices[i].position`. |
| `.dispose()` | Frees GPU + CPU resources. |
Static engine-owned singletons (read-only, not something you dispose):
`Mesh.DEFAULT_CUBE`, `Mesh.DEFAULT_QUAD`, `Mesh.DEFAULT_SPHERE`,
`Mesh.DEFAULT_PLANE`, `Mesh.DEFAULT_CAPSULE`, `Mesh.DEFAULT_TRIPRISM`.
Static factories (each builds and uploads a brand-new `Mesh`):
| Factory | Notes |
|---|---|
| `Mesh.createCube(min?, max?)` | Both `Vec3`, default `(-0.5,-0.5,-0.5)`..`(0.5,0.5,0.5)`. |
| `Mesh.createQuad(minX?, minY?, maxX?, maxY?)` | Default `-0.5..0.5` both axes; UV fixed `0,0`–`1,1`. |
| `Mesh.createSphere(radius?, stacks?, sectors?)` | `radius` default `0.5`. |
| `Mesh.createPlane(width?, height?)` | Defaults `1.0`/`1.0`; XZ-aligned, centered at origin. |
| `Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)` | Defaults `0.5`, `0.5`. |
| `Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)` | All 8 args required — a triangular cross-section extruded along Z. |
### `Entity` and components
```js
var e = new Entity();
var pos = e.add(POSITION);
```
`new Entity()` allocates an entity. `.id` is the read-only numeric engine ID.
`.add(TYPE)` adds a component and returns its wrapper (`TYPE` is one of the
constants below). `.dispose()` removes the entity and all its components.
Component-type constants: `POSITION`, `CAMERA`, `RENDERABLE`, `PHYSICS`,
`TRIGGER`. Each entity also exposes a lowercase getter that returns the
existing wrapper if the component is present, or `undefined` if not (it does
**not** add the component — use `.add()` for that): `entity.position`,
`entity.camera`, `entity.renderable`, `entity.physics`, `entity.trigger`.
#### `entity.add(POSITION)` → position component
| Member | Description |
|---|---|
| `.position` | Vec3Ref. Writing rebuilds the transform automatically. |
| `.rotation` | Vec3Ref, Euler angles. Same rebuild-on-write behavior. |
| `.scale` | Vec3Ref. Same rebuild-on-write behavior. |
| `.parent` | Get/set another position-component wrapper, or `null` to clear parenting. |
| `.lookAt(target, up?)` | `target` a `Vec3`; `up` defaults to `(0,1,0)`. |
#### `entity.add(CAMERA)` → camera component
| Member | Description |
|---|---|
| `.zNear` / `.zFar` | Numbers. |
| `.fov` | Only meaningful when `projectionType` is `CAMERA_TYPE_PERSPECTIVE`; otherwise get returns `undefined` and set is a no-op. |
| `.projectionType` | `CAMERA_TYPE_PERSPECTIVE` or `CAMERA_TYPE_ORTHOGRAPHIC`. |
| `.orthoTop` / `.orthoBottom` / `.orthoLeft` / `.orthoRight` | Only meaningful in orthographic mode, same undefined/no-op rule otherwise. |
#### `entity.add(RENDERABLE)` → renderable component
| Member | Description |
|---|---|
| `.type` | `ENTITY_RENDERABLE_TYPE_MATERIAL`, `_SPRITEBATCH`, or `_CALLBACK`. |
| `.mesh` | Get/set a `Mesh` instance or a `Mesh.DEFAULT_*` singleton. |
| `.color` | Get/set a `Color` instance (throws if given something else). |
| `.addSprite({ min?, max?, uvMin?, uvMax? })` | Adds a sprite to this renderable's sprite batch; all fields optional, default zero. |
| `.clearSprites()` | Clears the sprite batch. |
| `.setCallback(fn?)` | Switches to `ENTITY_RENDERABLE_TYPE_CALLBACK` and calls `fn()` on every render of this entity. Omit/pass non-function to clear. Exceptions inside `fn` surface as a C error. |
#### `entity.add(PHYSICS)` → physics component
| Member | Description |
|---|---|
| `.velocity` | Vec3Ref, plain (no rebuild-on-write). |
| `.onGround` | Read-only boolean. |
| `.bodyType` | `PHYSICS_BODY_STATIC`, `PHYSICS_BODY_DYNAMIC`, `PHYSICS_BODY_KINEMATIC`. |
| `.applyImpulse(vec3)` | Adds to velocity. No-op on static bodies. |
| `.setShapeCube(halfExtents)` | `halfExtents` a `Vec3`. |
| `.setShapeSphere(radius)` | Number. |
| `.setShapeCapsule(radius, halfHeight)` | Two numbers. |
| `.setShapePlane(normal, distance)` | `Vec3` + number. |
Shape-type constants (for reading `.type` on the underlying shape, not for
`.bodyType`): `PHYSICS_SHAPE_CUBE`, `PHYSICS_SHAPE_SPHERE`,
`PHYSICS_SHAPE_CAPSULE`, `PHYSICS_SHAPE_PLANE`.
#### `entity.add(TRIGGER)` → trigger component
| Member | Description |
|---|---|
| `.min` / `.max` | Plain `Vec3` values (copies, not live refs). |
| `.setBounds(min, max)` | Sets both at once. |
| `.contains(point)` → boolean | `point` a `Vec3`. |
## Not yet available to scripts
The following C modules exist and are fully implemented, but aren't currently
wired into script registration (`moduleRegister()` in
`src/dusk/script/module/module.h`), so none of these globals exist in a
script today: `Screen`, `SpriteBatch`, `Text`, `Scene`, `Easing`, `Console`,
`Engine`. If you need one of these from a script, it needs to be registered
in `moduleRegister()` first — see the existing entries there and the modules
under `src/dusk/script/module/` for the pattern to follow.
+255
View File
@@ -0,0 +1,255 @@
# UI
Dusk's UI system (buttons, checkboxes, sliders, dropdowns, tabs, menus, focus
navigation) is a **C-only API**. It is not exposed to JerryScript — see
[SCRIPTING.md](SCRIPTING.md) for what scripts *can* touch. If you need a
script to open/react to a menu, wire it through a C callback or a game-side
flag scripts can poll; there's no bridge for this today.
## Mental model
There is no retained-mode UI tree, no automatic dispatch, no scissor/clip-rect
API. Every widget is a plain struct you own (usually as a global or
scene-owned variable). You call its `xxxInit(...)` once, then call its
`xxxDraw(widget, x, y)` yourself, every frame you want it visible, at whatever
screen position you choose. Nothing draws itself automatically except three
fixed system overlays (overscan bars, debug console, FPS counter) — see
[System overlays](#system-overlays-automatic) below.
### Where UI rendering happens in the frame
- `uiInit()` / `uiDispose()` run once, at engine startup/shutdown.
- `uiUpdate()` runs once per tick (drives focus-navigation input handling).
- `uiRender()` runs once per frame, called from inside `sceneRender()` — i.e.
**after** the active scene's own 3D/game-world rendering, using an
orthographic screen-space projection. Your own widget `xxxDraw()` calls
should happen around the same point — typically from your scene's render
callback, after world content, so UI draws on top.
## Widgets
Every widget follows the same shape: `xxxInit(widget, ...)` zeroes the struct
and sets its fields; `xxxDraw(const widget*, x, y) -> errorret_t` draws it at
that screen position.
> **Init before Draw.** `uislider_t`, `uidropdown_t`, and `uitab_t` cache
> their label's measured width/height at `Init` time (an optimization —
> label text doesn't change after that point). Calling `Draw` before `Init`,
> or mutating `->label` directly instead of re-initializing, leaves stale
> layout. `uibutton_t`/`uicheckbox_t` don't have this restriction.
### Button
```c
void uiButtonInit(uibutton_t *button, const char_t *label);
bool_t uiButtonIsHighlighted(const uibutton_t *button);
void uiButtonSetHighlighted(uibutton_t *button, bool_t highlighted);
errorret_t uiButtonDraw(const uibutton_t *button, float_t x, float_t y);
```
Draws `label` in red when highlighted, white otherwise.
### Checkbox
```c
void uiCheckboxInit(uicheckbox_t *checkbox, const char_t *label);
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox);
void uiCheckboxSetChecked(uicheckbox_t *checkbox, bool_t checked);
void uiCheckboxToggle(uicheckbox_t *checkbox);
bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox);
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, bool_t highlighted);
errorret_t uiCheckboxDraw(const uicheckbox_t *checkbox, float_t x, float_t y);
```
Draws `"Y "`/`"N "` then the label.
### Slider
```c
typedef union { float_t f; int32_t i; } uislidervalue_t;
void uiSliderInitFloat(uislider_t*, const char_t *label,
float_t value, float_t min, float_t max, float_t step);
void uiSliderInitInt(uislider_t*, const char_t *label,
int32_t value, int32_t min, int32_t max, int32_t step);
float_t uiSliderGetFloat(const uislider_t*); // works for either type
int32_t uiSliderGetInt(const uislider_t*); // asserts type == INT
void uiSliderSetFloat(uislider_t*, float_t value); // asserts type == FLOAT, clamps
void uiSliderSetInt(uislider_t*, int32_t value); // asserts type == INT, clamps
void uiSliderStepUp(uislider_t*); // wraps to min past max
void uiSliderStepDown(uislider_t*); // wraps to max past min
float_t uiSliderGetRatio(const uislider_t*); // normalized 0..1
int32_t uiSliderGetStepCount(const uislider_t*); // 0 for float sliders
bool_t uiSliderIsHighlighted(const uislider_t*);
void uiSliderSetHighlighted(uislider_t*, bool_t highlighted);
errorret_t uiSliderDraw(const uislider_t*, float_t x, float_t y);
```
Draws label, a track, a fill proportional to the current ratio, discrete step
markers if it's an int slider with fewer than 10 steps, then the value as
text.
### Dropdown
```c
void uiDropdownInit(uidropdown_t *dropdown, const char_t *label,
const char_t *const *options, uint8_t optionCount,
uint8_t selectedIndex);
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, uint8_t index);
void uiDropdownStepNext(uidropdown_t *dropdown); // wraps
void uiDropdownStepPrev(uidropdown_t *dropdown); // wraps
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
void uiDropdownSetHighlighted(uidropdown_t *dropdown, bool_t highlighted);
errorret_t uiDropdownDraw(const uidropdown_t *dropdown, float_t x, float_t y);
```
`options` is a caller-owned array of strings that must outlive the dropdown
(it isn't copied). Draws `label` then `"< Option >"`.
### Tab
```c
void uiTabInit(uitab_t *tab, const char_t *label);
bool_t uiTabIsActive(const uitab_t *tab);
void uiTabSetActive(uitab_t *tab, bool_t active);
errorret_t uiTabDraw(const uitab_t *tab, float_t x, float_t y);
```
Draws a background box sized to the label (green if active, red if inactive)
with the label on top.
## Menus: assembling widgets into a navigable list
`uimenu_t` is the one aggregate widget — it owns an array of items (labels,
spacers, and any of the widgets above), lays them out in a grid, and wires
keyboard/gamepad navigation via the focus system for you.
```c
typedef enum {
UI_MENU_WIDGET_TYPE_NONE, UI_MENU_WIDGET_TYPE_LABEL,
UI_MENU_WIDGET_TYPE_SPACER, UI_MENU_WIDGET_TYPE_CHECKBOX,
UI_MENU_WIDGET_TYPE_BUTTON, UI_MENU_WIDGET_TYPE_TAB,
UI_MENU_WIDGET_TYPE_SLIDER, UI_MENU_WIDGET_TYPE_DROPDOWN,
} uimenuwidgettype_t;
void uiMenuInit(uimenu_t *menu, uimenuselectedcallback_t selected,
uimenuclosedcallback_t closed, uimenuchangedcallback_t changed);
void uiMenuSetItems(uimenu_t *menu, const uimenuitem_t *items,
uint8_t itemCount, uint8_t columns);
void uiMenuSetPosition(uimenu_t *menu, uint8_t x, uint8_t y); // focus cursor cell, not pixels
void uiMenuOpen(uimenu_t *menu); // pushes onto the focus stack
void uiMenuClose(uimenu_t *menu); // pops it
bool_t uiMenuIsActive(const uimenu_t *menu);
errorret_t uiMenuDraw(const uimenu_t *menu, float_t x, float_t y,
float_t width, float_t height);
```
- `selected(menu, index, item)` fires when the player presses accept on an item.
- `changed(menu, index, item)` fires when the highlighted item changes.
- `closed(menu)` fires when the menu is popped off the focus stack.
- LEFT/RIGHT on a highlighted slider/checkbox/dropdown adjusts its value in
place instead of moving focus off it (handled internally).
### Building a menu with the `MENU_*` macros
`uimenu.h` provides macros that cut the boilerplate of filling in a
`uimenuitem_t` array. They expand into statements using local variables named
`menu`, `menuIndex`, and `menuCapacity`, so use them together, inside one
function, starting with `MENU_BEGIN` and ending with `MENU_END`:
```c
static uimenuitem_t optionsItems[8];
static uimenu_t optionsMenu;
static const char_t *qualityOptions[] = { "Low", "Medium", "High" };
static void onOptionsSelected(
const uimenu_t *menu, const uint8_t index, const uimenuitem_t *item
) {
if(index == 4) uiMenuClose(&optionsMenu); // "Back" button
}
static void onOptionsClosed(const uimenu_t *menu) {
// e.g. return to the previous screen
}
void optionsMenuBuild(void) {
MENU_BEGIN(&optionsMenu, optionsItems, onOptionsSelected, onOptionsClosed, NULL);
MENU_LABEL("Options");
MENU_CHECKBOX("Fullscreen");
MENU_SLIDER_FLOAT("Volume", 0.8f, 0.0f, 1.0f, 0.05f);
MENU_DROPDOWN("Quality", qualityOptions, 3, 1);
MENU_BUTTON("Back");
MENU_END(optionsItems, 1);
}
// Once, when the menu screen becomes active:
uiMenuOpen(&optionsMenu);
// Every frame the menu should be visible:
uiMenuDraw(&optionsMenu, 20.0f, 20.0f, 200.0f, 100.0f);
// When leaving the menu screen:
uiMenuClose(&optionsMenu);
```
`MENU_LABEL`/`MENU_SPACER` force a row break and aren't focusable/selectable.
Every other `MENU_*` macro calls the matching widget's own `Init` for you.
> This example is constructed directly from the widget/menu API surface (all
> function and macro signatures above are verified against the source), but
> there's currently no real menu-building call site anywhere else in the
> engine to cross-check the *pattern* against — treat it as a starting point,
> not a copy of shipped code.
## Focus system: navigation underneath `uimenu`
If you're building a custom widget that needs keyboard/gamepad navigation
without going through `uimenu`, use `ui/focus/uifocus.h` directly. `uimenu`
is implemented entirely in terms of this API, so it's a reasonable reference.
```c
uifocusitem_t * uiFocusPush(
uint8_t cols, uint8_t rows,
uifocusitemcallback_t selected, // fires on accept
uifocusitemcallback_t changed, // fires on cursor move (and once immediately)
uifocusitemcallback_t closed, // fires on pop
uifocusitemdirectioncallback_t direction, // optional pre-empt of a direction press; NULL for default grid movement
void *user
);
void uiFocusPop(void);
void uiFocusPopItem(uifocusitem_t *item);
void uiFocusSetPosition(uifocusitem_t *item, uint8_t x, uint8_t y); // wraps
void uiFocusMoveDirection(uifocusitem_t *item, uifocusdirection_t dir);
```
`uiFocusUpdate()` runs automatically from `uiUpdate()` every tick — you don't
call it yourself. It reads `INPUT_ACTION_ACCEPT` (fires `selected`),
`INPUT_ACTION_CANCEL` (pops the stack), and the four directional actions
(with hold-to-repeat timing) to move the cursor within the topmost pushed
item. Only the topmost stack entry (max depth 8) receives input at a time —
opening a submenu means pushing a new focus item on top; closing it pops back
to the parent.
There's no separate "is this widget focused" query — "focused" is expressed
as the pushed item's current `(x, y)` cursor cell matching a given slot, which
is exactly how `uimenu`'s `changed` callback decides which item to highlight.
## System overlays (automatic)
Three small overlays are wired into a fixed internal list and draw themselves
every frame with no call needed from game code:
- **Overscan bars** (`ui/overlay/uicrop.h`) — draws opaque bars over the
screen area outside `SCREEN.scanX/scanY/scanWidth/scanHeight` (the
overscan-safe viewport). A no-op on platforms/configs where the scan area
already equals the full viewport. `UI_CROP.color` (default black) is the
only thing you'd normally touch here.
- **Debug console** (`ui/debug/uiconsole.h`) — draws console history when
visible.
- **FPS counter** (`ui/debug/uifps.h`) — draws a live FPS/frame-time readout.
None of these have a scissor/clip-rect equivalent for your own widgets —
there is no clipping API in this UI system; everything draws unclipped at
whatever position you give it.
+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.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
+14
View File
@@ -0,0 +1,14 @@
#!/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
cmake -S. -Bbuild-gamecube \
-DDUSK_TARGET_SYSTEM=gamecube \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/GameCube.cmake" \
-DDKP_OGC_PLATFORM_LIBRARY=libogc2
cd build-gamecube
make -j$(nproc) VERBOSE=1
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
docker build -t dusk-knulli -f docker/knulli/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-knulli /bin/bash -c "./scripts/build-knulli.sh"
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
cmake -S . -B build-knulli -G Ninja \
-DDUSK_BUILD_TESTS=ON \
-DDUSK_TARGET_SYSTEM=knulli \
-DCMAKE_TOOLCHAIN_FILE=./cmake/toolchains/aarch64-linux-gnu.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-knulli -- -j$(nproc)
# Copy necessary libs out
mkdir -p ./build-knulli/dusk
cp ./build-knulli/Dusk ./build-knulli/dusk/Dusk
cp ./build-knulli/dusk.dsk ./build-knulli/dusk/dusk.dsk
echo '#!/bin/bash' > build-knulli/dusk/Dusk.sh
echo 'cd "$(dirname "$(readlink -f "$0")")"' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(dirname "$(readlink -f "$0")")' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/gl4es' >> build-knulli/dusk/Dusk.sh
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib' >> build-knulli/dusk/Dusk.sh
echo '$(dirname "$(readlink -f "$0")")/Dusk' >> build-knulli/dusk/Dusk.sh
chmod +x build-knulli/dusk/Dusk.sh
cp /usr/lib/aarch64-linux-gnu/liblua5.4.so.0 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libSDL2-2.0.so.0 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libGL.so.1 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libEGL.so.1 build-knulli/dusk/
# cp /usr/lib/aarch64-linux-gnu/libGLESv2.so.2 build-knulli/dusk/
@@ -1,5 +1,3 @@
#!/bin/bash #!/bin/bash
set -e
cd "$(dirname "$0")/../.."
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/linux/test.sh" docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/build-linux.sh"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cmake -S . -B build-linux -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-linux -- -j$(nproc)
@@ -1,5 +1,3 @@
#!/bin/bash #!/bin/bash
set -e
cd "$(dirname "$0")/../.."
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/psp/build.sh" docker run --rm -v "$(pwd):/workdir" dusk-psp /bin/bash -c "./scripts/build-psp.sh"
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
if [ -z "$PSPDEV" ]; then
echo "PSPDEV environment variable is not set. Please set it to the path of your PSP development environment."
exit 1
fi
mkdir -p build-psp
cd build-psp
psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 .. CMAKE_BUILD_TYPE=Release
make -j$(nproc)
# psp-cmake -DDUSK_TARGET_SYSTEM=psp -DCMAKE_TOOLCHAIN_FILE=$PSPDEV/psp/share/pspdev.cmake -DBUILD_PRX=1 -DCMAKE_BUILD_TYPE=Debug ..
# make
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
docker build -t dusk-vita -f docker/vita/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-vita /bin/bash -c "./scripts/build-vita.sh"
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
if [ -z "$VITASDK" ]; then
echo "VITASDK environment variable is not set. Please set it to the path of your VitaSDK installation."
exit 1
fi
mkdir -p build-vita
cd build-vita
cmake \
-DCMAKE_TOOLCHAIN_FILE=$VITASDK/share/vita.toolchain.cmake \
-DDUSK_TARGET_SYSTEM=vita \
..
make -j$(nproc)
+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.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
+14
View File
@@ -0,0 +1,14 @@
#!/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
cmake -S. -Bbuild-wii \
-DDUSK_TARGET_SYSTEM=wii \
-DCMAKE_TOOLCHAIN_FILE="$DEVKITPRO/cmake/Wii.cmake" \
-DDUSK_DOLPHIN_BUILD_TYPE=DOL
cd build-wii
make -j$(nproc) VERBOSE=1
mv Dusk.dol boot.dol
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/../.."
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run --rm -v "$(pwd):/workdir" dusk-linux /bin/bash -c "./scripts/linux/build.sh"
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/../.."
cmake -S . -B build-linux -DDUSK_TARGET_SYSTEM=linux
cmake --build build-linux -- -j"$(nproc)"
-42
View File
@@ -1,42 +0,0 @@
#!/bin/bash
# Installs the packages required to build and test Dusk on Linux.
set -e
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
if command -v apt-get >/dev/null 2>&1; then
$SUDO apt-get update
$SUDO apt-get install -y \
build-essential \
cmake \
git \
python3 \
python3-pip \
python3-polib \
python3-pil \
python3-dotenv \
python3-numpy \
libsdl3-dev \
libvulkan-dev \
libzip-dev \
libbz2-dev \
liblzma-dev \
zlib1g-dev \
libssl-dev \
xz-utils
elif command -v dnf >/dev/null 2>&1; then
$SUDO dnf install -y \
git make gcc cmake python python-polib python3-pillow python3-dotenv \
python3-numpy SDL2-devel mesa-libGL-devel zlib-devel libzip-devel \
bzip2-devel openssl-devel xz xz-devel
elif command -v pacman >/dev/null 2>&1; then
$SUDO pacman -Sy --noconfirm --needed \
base-devel git cmake python python-polib python-pillow python-dotenv \
python-numpy sdl2 mesa libzip bzip2 openssl xz zlib
else
echo "Unsupported package manager; install dependencies manually." >&2
exit 1
fi
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/../.."
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j"$(nproc)"
ctest --output-on-failure --test-dir build-tests
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/../.."
if [ -z "$PSPDEV" ]; then
echo "PSPDEV environment variable is not set." >&2
exit 1
fi
psp-cmake -S . -B build-psp \
-DDUSK_TARGET_SYSTEM=psp \
-DBUILD_PRX=1 \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-psp -- -j"$(nproc)"
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
# Installs the packages required to build Dusk for the PSP. The PSP toolchain
# itself (pspdev) must already be installed; the pspdev/pspdev docker image
# provides it.
set -e
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
if command -v apk >/dev/null 2>&1; then
$SUDO apk add --no-cache git cmake make python3 py3-pip py3-dotenv
elif command -v apt-get >/dev/null 2>&1; then
$SUDO apt-get update
$SUDO apt-get install -y git cmake make python3 python3-pip python3-dotenv
elif command -v dnf >/dev/null 2>&1; then
$SUDO dnf install -y git cmake make python python3-dotenv
elif command -v pacman >/dev/null 2>&1; then
$SUDO pacman -Sy --noconfirm --needed git cmake make python python-dotenv
else
echo "Unsupported package manager; install dependencies manually." >&2
exit 1
fi
if [ -z "$PSPDEV" ] || ! command -v psp-cmake >/dev/null 2>&1; then
echo "pspdev toolchain not found. Install it from https://pspdev.github.io/" >&2
echo "and set PSPDEV, or use scripts/psp/build-docker.sh." >&2
exit 1
fi
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
docker build -t dusk-linux -f docker/linux/Dockerfile .
docker run \
--rm \
-v "${GITHUB_WORKSPACE}:/workdir" \
dusk-linux \
/bin/bash -c "./scripts/test-linux.sh"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -e
rm -rf build-tests
cmake -S . -B build-tests -DDUSK_BUILD_TESTS=ON -DDUSK_TARGET_SYSTEM=linux
cmake --build build-tests -- -j$(nproc)
ctest --output-on-failure --test-dir build-tests
+26
View File
@@ -0,0 +1,26 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(dusk)
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
add_subdirectory(dusklinux)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
add_subdirectory(duskpsp)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
add_subdirectory(duskvita)
add_subdirectory(dusksdl2)
add_subdirectory(duskgl)
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
add_subdirectory(duskdolphin)
endif()
+70 -9
View File
@@ -1,22 +1,83 @@
# Copyright (c) 2026 Dominic Masters # Copyright (c) 2025 Dominic Masters
# #
# 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
# Headers are included relative to this directory (e.g. "util/string.h"). # Required Libraries
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC if(NOT cglm_FOUND)
${CMAKE_CURRENT_SOURCE_DIR} find_package(cglm REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC cglm)
endif()
if(NOT libzip_FOUND)
find_package(libzip REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC zip)
endif()
if(NOT stb_image_FOUND)
find_package(stb REQUIRED)
if(STB_IMAGE_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC stb_image)
else()
message(FATAL_ERROR "stb_image not found. Please ensure stb is correctly fetched.")
endif()
endif()
if(NOT yyjson_FOUND)
find_package(yyjson REQUIRED)
if(yyjson_FOUND)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC yyjson::yyjson)
else()
message(FATAL_ERROR "yyjson not found. Please ensure yyjson is correctly fetched.")
endif()
endif()
if(NOT jerryscript_FOUND)
find_package(jerryscript REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
jerryscript::core
jerryscript::ext
jerryscript::port
)
endif()
if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
DUSK_BACKTRACE
)
endif()
# Includes
target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
) )
# Modules # Main Binary Source
add_subdirectory(asset) target_sources(${DUSK_BINARY_TARGET_NAME}
PUBLIC
main.c
)
# Subdirs
add_subdirectory(animation)
add_subdirectory(assert) add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(console) add_subdirectory(console)
add_subdirectory(display) add_subdirectory(display)
add_subdirectory(entity)
add_subdirectory(log)
add_subdirectory(engine) add_subdirectory(engine)
add_subdirectory(error) add_subdirectory(error)
add_subdirectory(perf) add_subdirectory(input)
add_subdirectory(resource) add_subdirectory(locale)
add_subdirectory(physics)
add_subdirectory(scene)
add_subdirectory(script)
add_subdirectory(system)
add_subdirectory(time) add_subdirectory(time)
add_subdirectory(thread) add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(util) add_subdirectory(util)
add_subdirectory(thread)
@@ -5,8 +5,6 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME} target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC PUBLIC
resource.c easing.c
resourcemanager.c animation.c
) )
add_subdirectory(loader)
+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);
+110
View File
@@ -0,0 +1,110 @@
// 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"
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;
+1 -1
View File
@@ -40,7 +40,7 @@
#include <execinfo.h> #include <execinfo.h>
#include <stdlib.h> #include <stdlib.h>
void assertLogBacktrace(void) { static void assertLogBacktrace(void) {
void *frames[64]; void *frames[64];
int count = backtrace(frames, 64); int count = backtrace(frames, 64);
char **symbols = backtrace_symbols(frames, count); char **symbols = backtrace_symbols(frames, count);
+2 -28
View File
@@ -22,21 +22,12 @@
#endif #endif
#ifndef DUSK_ASSERTIONS_FAKED #ifndef DUSK_ASSERTIONS_FAKED
#define DUSK_ASSERTIONS 1
/** /**
* Initializes the assert system. Must be the very first call in engine * Initializes the assert system. Must be the very first call in engine
* startup. * startup.
*/ */
void assertInit(void); void assertInit(void);
#ifdef DUSK_BACKTRACE
/**
* Logs a stack trace of the current call stack to the error log.
*/
void assertLogBacktrace(void);
#endif
/** /**
* Assert a given value to be true. * Assert a given value to be true.
* *
@@ -283,25 +274,8 @@
#endif #endif
// Static Assertions. These are compile time only, so they are never faked. // Static Assertions
/**
* Asserts a constant expression at compile time. Never faked, even when
* DUSK_ASSERTIONS_FAKED is set.
*
* @param x Constant expression that must be true.
* @param message Message to show on failure, must be a string literal.
*/
#define assertStatic(x, message) \
_Static_assert(x, message)
/**
* Asserts the size of a struct at compile time.
*
* @param struct The struct type to check.
* @param size The size in bytes the struct must be.
*/
#define assertStructSize(struct, size) \ #define assertStructSize(struct, size) \
assertStatic(sizeof(struct) == size, "Size of " #struct " must be " #size) _Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
// EOF // EOF
+5 -1
View File
@@ -1,10 +1,14 @@
# Copyright (c) 2026 Dominic Masters # Copyright (c) 2025 Dominic Masters
# #
# 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
asset.c asset.c
assetfile.c assetfile.c
) )
# Subdirs
add_subdirectory(loader)
+405 -37
View File
@@ -5,62 +5,430 @@
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
#include "asset/asset.h" #include "asset.h"
#include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h" #include "util/string.h"
#include "assert/assert.h"
#include "engine/engine.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));
// The platform must set ASSET.baseDirectory or throw an error. for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
threadMutexInit(&ASSET.loading[i].mutex);
}
// assetInitPlatform must either define ASSET.zip or throw an error.
errorChain(assetInitPlatform()); errorChain(assetInitPlatform());
assertStrLenMin(ASSET.baseDirectory, 1, "Asset directory empty without error."); assertNotNull(ASSET.zip, "Asset zip null without error.");
threadInit(&ASSET.loadThread, assetUpdateAsync);
errorOk(); threadStart(&ASSET.loadThread);
}
errorret_t assetGetPath(
const char_t *filename,
char_t *out,
const size_t outSize
) {
assertNotNull(filename, "Filename cannot be NULL.");
assertNotNull(out, "Output cannot be NULL.");
if(filename[0] == '\0') {
errorThrow("Asset filename cannot be empty.");
}
if(filename[0] == '/' || stringIncludesString(filename, "..")) {
errorThrow("Invalid asset filename: %s", filename);
}
int32_t len = stringFormat(out, outSize, "%s/%s", ASSET.baseDirectory, filename);
if(len < 0 || (size_t)len >= outSize) {
errorThrow("Asset path too long: %s", filename);
}
errorOk(); errorOk();
} }
bool_t assetFileExists(const char_t *filename) { bool_t assetFileExists(const char_t *filename) {
char_t path[ASSET_PATH_MAX]; assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
errorret_t ret = assetGetPath(filename, path, sizeof(path));
if(errorIsNotOk(ret)) {
errorCatch(ret);
return false;
}
FILE *file = fopen(path, "rb"); zip_int64_t idx = zip_name_locate(ASSET.zip, filename, 0);
if(file == NULL) return false; if(idx < 0) return false;
fclose(file);
return true; return true;
} }
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
// Is there an existing asset?
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(stringEquals(entry->name, name)) {
assertTrue(entry->type == type, "Asset entry type mismatch.");
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
// We did not find one existing. Find first available slot, reaping
// zero-ref entries to make room if none are immediately available.
bool_t reaped = false;
for(;;) {
entry = ASSET.entries;
do {
if(entry->type != ASSET_LOADER_TYPE_NULL) {
entry++;
continue;
}
if(entry->state == ASSET_ENTRY_STATE_NOT_STARTED) {
assetEntryInit(entry, name, type, input);
return entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
if(reaped) break;
reaped = true;
errorCatch(assetReapUnused());
}
assertUnreachable("No available asset entry slots.");
return NULL;
}
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
) {
assertNotNull(outEntries, "Output entries cannot be NULL.");
uint32_t count = 0;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == type) {
outEntries[count++] = entry;
}
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
return count;
}
errorret_t assetRequireLoaded(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->state == ASSET_ENTRY_STATE_LOADED) {
errorOk();
}
// 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();
}
errorret_t assetRequireDisposed(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL.");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertIsMainThread("Currently only works on main thread.");
if(entry->type == ASSET_LOADER_TYPE_NULL) {
errorOk();
}
if(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED ||
entry->state == ASSET_ENTRY_STATE_ERROR
) {
errorOk();
}
assertTrue(
entry->refs.count == 0,
"Cannot require disposal of an entry with active references."
);
// Lock to prevent the reaper from collecting the entry mid-spin.
assetEntryLock(entry);
while(entry->type != ASSET_LOADER_TYPE_NULL) {
usleep(1000);
errorret_t ret = assetUpdate();
if(errorIsNotOk(ret)) {
assetEntryUnlock(entry);
errorChain(ret);
}
}
assetEntryUnlock(entry);
errorOk();
}
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 assetReapUnused(void) {
assertIsMainThread("assetReapUnused must be called from the main thread.");
// Repeatedly find and dispose zero-ref LOADED entries until none remain.
// This handles dependency chains where an entry (e.g. a model) holds refs
// on child entries (mesh, texture): dispose parents first so child ref
// counts drop to zero, then pick up the children on the next pass. Without
// this, a forward-only scan fails when a shared child entry appears before
// a parent that still holds a ref to it.
bool_t any;
do {
any = false;
assetentry_t *entry = ASSET.entries;
do {
if(entry->type == ASSET_LOADER_TYPE_NULL) { entry++; continue; }
if(entry->state != ASSET_ENTRY_STATE_LOADED) { entry++; continue; }
if(entry->refs.count > 0) { entry++; continue; }
errorChain(assetEntryDispose(entry));
any = true;
entry++;
} while(entry < ASSET.entries + ASSET_ENTRY_COUNT_MAX);
} while(any);
errorOk();
}
errorret_t assetUpdate(void) {
assertIsMainThread("assetUpdate must be called from the main thread.");
// 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."
);
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
loading->entry = NULL;
}
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: {
loading->entry = NULL;
threadMutexUnlock(&loading->mutex);
errorThrow("Failed to load asset asynchronously.");
break;
}
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
errorOk();
}
void assetUpdateAsync(thread_t *thread) {
assertNotMainThread("assetUpdateAsync must not run on the main thread.");
while(!threadShouldStop(thread)) {
// Walk over each asset
assetloading_t *loading;
loading = ASSET.loading;
do {
threadMutexLock(&loading->mutex);
if(loading->entry == NULL) {
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
switch(loading->entry->state) {
case ASSET_ENTRY_STATE_PENDING_ASYNC:
loading->entry->state = ASSET_ENTRY_STATE_LOADING_ASYNC;
assertNotNull(
ASSET_LOADER_CALLBACKS[loading->type].loadAsync,
"Loader does not support async loading."
);
errorret_t ret = (
ASSET_LOADER_CALLBACKS[loading->type].loadAsync(loading)
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
assertTrue(
loading->entry->state == ASSET_ENTRY_STATE_ERROR,
"Loader did not set entry state to error on failed load."
);
}
threadMutexUnlock(&loading->mutex);
loading++;
break;
case ASSET_ENTRY_STATE_LOADING_ASYNC:
assertUnreachable(
"Entry is in a pending async state still?"
);
break;
default:
threadMutexUnlock(&loading->mutex);
loading++;
continue;
}
} while(loading < ASSET.loading + ASSET_LOADING_COUNT_MAX);
if(threadShouldStop(thread)) break;
usleep(1000);
}
}
errorret_t assetDispose(void) { errorret_t assetDispose(void) {
assertIsMainThread("Must be called from the main thread.");
threadStop(&ASSET.loadThread);
errorChain(assetReapUnused());
// Cleanup zip file.
if(ASSET.zip != NULL) {
if(zip_close(ASSET.zip) != 0) {
errorThrow("Failed to close asset zip archive.");
}
ASSET.zip = NULL;
}
errorChain(assetDisposePlatform()); errorChain(assetDisposePlatform());
memoryZero(&ASSET, sizeof(asset_t));
errorOk(); errorOk();
} }
+126 -28
View File
@@ -8,7 +8,10 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include "asset/assetplatform.h" #include "asset/assetplatform.h"
#include "asset/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."
@@ -17,49 +20,144 @@
#error "Platform must define assetDisposePlatform function." #error "Platform must define assetDisposePlatform function."
#endif #endif
#define ASSET_BASE_DIRECTORY_MAX 256 #define ASSET_FILE_NAME "dusk.dsk"
#define ASSET_HEADER_SIZE 3
typedef struct { #define ASSET_LOADING_COUNT_MAX 16
// Directory that every asset filename is resolved relative to. Set by the #define ASSET_ENTRY_COUNT_MAX 64
// platform in assetInitPlatform.
char_t baseDirectory[ASSET_BASE_DIRECTORY_MAX]; typedef struct asset_s {
zip_t *zip;
assetplatform_t platform;
// Background loading thread.
thread_t loadThread;
// Assets that are mid loading.
assetloading_t loading[ASSET_LOADING_COUNT_MAX];
assetentry_t entries[ASSET_ENTRY_COUNT_MAX];
} asset_t; } asset_t;
extern asset_t ASSET; extern asset_t ASSET;
/** /**
* Initializes the asset system. The platform locates the assets and sets * Initializes the asset system.
* ASSET.baseDirectory, or throws an error if it cannot.
* *
* @return An errorret_t indicating success or failure. * @return An error code if the asset system could not be initialized properly.
*/ */
errorret_t assetInit(void); errorret_t assetInit(void);
/** /**
* Builds the full path of an asset on disk. * Checks if an asset file exists.
* *
* @param filename Asset filename, relative to the asset directory. * @param filename The filename of the asset to check.
* @param out Buffer to write the null terminated path into. * @return true if the asset file exists, false otherwise.
* @param outSize Size of the buffer in bytes.
* @return An error if the filename is invalid or the path does not fit.
*/
errorret_t assetGetPath(
const char_t *filename,
char_t *out,
const size_t outSize
);
/**
* Checks whether an asset exists.
*
* @param filename Asset filename, relative to the asset directory.
* @return True if the asset exists and can be opened for reading.
*/ */
bool_t assetFileExists(const char_t *filename); bool_t assetFileExists(const char_t *filename);
/** /**
* Disposes of the asset system. * Gets, or creates, a new asset entry. Internal - prefer assetLock.
* *
* @return An errorret_t indicating success or failure. * @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
*/
assetentry_t * assetGetEntry(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Gets all asset entries of a given type.
*
* @param outEntries Output array to write the entries to.
* @param type Type of the asset entries to get.
* @return The number of entries written to outEntries.
*/
uint32_t assetGetEntriesOfType(
assetentry_t **outEntries,
const assetloadertype_t type
);
/**
* Gets, creates, and locks an asset entry. The asset will begin loading on
* the next assetUpdate. Call assetUnlock when done to allow the entry to be
* reclaimed.
*
* @param name Filename of the asset.
* @param type Type of the asset.
* @param input Loader-specific parameters.
* @return The locked asset entry.
*/
assetentry_t * assetLock(
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
);
/**
* Releases a lock on an asset entry by name. When all locks are released the
* entry will be reclaimed at the start of the next assetUpdate.
*
* @param name Filename of the asset to unlock.
*/
void assetUnlock(const char_t *name);
/**
* Releases a lock on an asset entry by pointer. When all locks are released
* the entry will be reclaimed at the start of the next assetUpdate.
*
* @param entry The asset entry to unlock.
*/
void assetUnlockEntry(assetentry_t *entry);
/**
* Frees every currently unreferenced (zero-ref) loaded asset entry. Repeats
* until a full pass frees nothing further, since disposing a parent entry
* (e.g. a model) may drop a child entry's (e.g. a mesh) ref count to zero,
* making it eligible for reaping too.
*
* @return An error code if any entry could not be disposed properly.
*/
errorret_t assetReapUnused(void);
/**
* Requires an asset entry to be loaded. This will block until the asset entry
* is fully loaded.
*
* @param entry The asset entry to require.
* @return An error code if the asset entry could not be loaded properly.
*/
errorret_t assetRequireLoaded(assetentry_t *entry);
/**
* Requires an asset entry to be disposed. This will block until the asset entry
* is fully disposed.
*
* @param entry The asset entry to require disposal of.
* @return An error code if the asset entry could not be disposed properly.
*/
errorret_t assetRequireDisposed(assetentry_t *entry);
/**
* Updates the asset system.
*
* @return An error code if the asset system could not be updated properly.
*/
errorret_t assetUpdate(void);
/**
* Starts the background asset loading thread. The thread runs assetUpdate
* in a loop with a short sleep until stopped.
*
* @param thread The thread runner.
*/
void assetUpdateAsync(thread_t *thread);
/**
* Disposes/cleans up the asset system.
*
* @return An error code if the asset system could not be disposed properly.
*/ */
errorret_t assetDispose(void); errorret_t assetDispose(void);
+301 -79
View File
@@ -8,60 +8,59 @@
#include "asset/asset.h" #include "asset/asset.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h"
#include "util/math.h" #include "util/math.h"
errorret_t assetFileInit(assetfile_t *file, const char_t *filename) { errorret_t assetFileInit(
assetfile_t *file,
const char_t *filename,
void *params,
void *output
) {
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(filename, "Filename cannot be NULL.");
assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long."); assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long.");
memoryZero(file, sizeof(assetfile_t)); memoryZero(file, sizeof(assetfile_t));
stringCopy(file->filename, filename, ASSET_FILE_NAME_MAX); memoryCopy(file->filename, filename, ASSET_FILE_NAME_MAX);
file->params = params;
file->output = output;
// Open temporarily to find the size. // Stat the file
char_t path[ASSET_PATH_MAX]; zip_stat_init(&file->stat);
errorChain(assetGetPath(filename, path, sizeof(path))); if(!zip_stat(ASSET.zip, filename, 0, &file->stat) == 0) {
errorThrow("Failed to stat asset file: %s", filename);
FILE *handle = fopen(path, "rb");
if(handle == NULL) {
errorThrow("Failed to open asset file: %s", filename);
}
if(fseek(handle, 0, SEEK_END) != 0) {
fclose(handle);
errorThrow("Failed to seek asset file: %s", filename);
}
long size = ftell(handle);
fclose(handle);
if(size < 0) {
errorThrow("Failed to get size of asset file: %s", filename);
} }
file->size = (size_t)size; // Minimum file size.
errorOk(); file->size = (zip_int64_t)file->stat.size;
} if(file->size <= 0) {
errorThrow("Invalid asset file size: %s", filename);
errorret_t assetFileOpen(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNull(file->file, "Asset file already open.");
char_t path[ASSET_PATH_MAX];
errorChain(assetGetPath(file->filename, path, sizeof(path)));
file->file = fopen(path, "rb");
if(file->file == NULL) {
errorThrow("Failed to open asset file: %s", file->filename);
} }
file->position = 0;
errorOk(); errorOk();
} }
errorret_t assetFileRewind(assetfile_t *file) { errorret_t assetFileRewind(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->file, "Asset file must be opened before rewinding."); assertNotNull(file->zipFile, "Asset file must be opened before rewinding.");
if(fseek(file->file, 0, SEEK_SET) != 0) { if(file->position == 0) {
errorThrow("Failed to rewind asset file: %s", file->filename); errorOk();
}
errorChain(assetFileClose(file));
errorChain(assetFileOpen(file));
errorOk();
}
errorret_t assetFileOpen(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->filename, "Asset file filename cannot be NULL.");
assertNotNull(ASSET.zip, "Asset zip cannot be NULL.");
assertNull(file->zipFile, "Asset file already open.");
file->zipFile = zip_fopen(ASSET.zip, file->filename, 0);
if(file->zipFile == NULL) {
errorThrow("Failed to open asset file: %s", file->filename);
} }
file->position = 0; file->position = 0;
errorOk(); errorOk();
@@ -73,48 +72,45 @@ errorret_t assetFileRead(
const size_t bufferSize const size_t bufferSize
) { ) {
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->file, "Asset file must be opened before reading."); assertNotNull(file->zipFile, "Asset file must be opened before reading.");
// No buffer, skip the bytes.
if(buffer == NULL) { if(buffer == NULL) {
size_t skipped = 0; size_t bytesRemaining = bufferSize;
uint8_t temp[256]; uint8_t tempBuffer[256];
while(skipped < bufferSize) { while(bytesRemaining > 0) {
size_t chunk = mathMin(bufferSize - skipped, sizeof(temp)); size_t chunkSize = mathMin(bytesRemaining, sizeof(tempBuffer));
size_t got = fread(temp, 1, chunk, file->file); errorChain(assetFileRead(file, tempBuffer, chunkSize));
skipped += got; file->position += chunkSize;
if(got < chunk) break; bytesRemaining -= chunkSize;
} }
file->position += skipped; file->lastRead = bufferSize;
file->lastRead = skipped;
errorOk(); errorOk();
} }
size_t got = fread(buffer, 1, bufferSize, file->file); // I assume zip_fread takes buffer NULL for skipping?
if(got < bufferSize && ferror(file->file)) { zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
if(bytesRead < 0) {
errorThrow("Failed to read from asset file: %s", file->filename); errorThrow("Failed to read from asset file: %s", file->filename);
} }
file->position += got; file->position += bytesRead;
file->lastRead = got; file->lastRead = bytesRead;
errorOk(); errorOk();
} }
errorret_t assetFileClose(assetfile_t *file) { errorret_t assetFileClose(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(file->file, "Asset file must be opened before closing."); assertNotNull(file->zipFile, "Asset file must be opened before closing.");
int result = fclose(file->file); if(zip_fclose(file->zipFile) != 0) {
file->file = NULL;
file->position = 0;
if(result != 0) {
errorThrow("Failed to close asset file: %s", file->filename); errorThrow("Failed to close asset file: %s", file->filename);
} }
file->zipFile = NULL;
file->position = 0;
errorOk(); errorOk();
} }
errorret_t assetFileDispose(assetfile_t *file) { errorret_t assetFileDispose(assetfile_t *file) {
assertNotNull(file, "Asset file cannot be NULL."); if(file->zipFile != NULL) {
if(file->file != NULL) {
errorChain(assetFileClose(file)); errorChain(assetFileClose(file));
} }
memoryZero(file, sizeof(assetfile_t)); memoryZero(file, sizeof(assetfile_t));
@@ -129,30 +125,256 @@ errorret_t assetFileReadEntire(
assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(file, "Asset file cannot be NULL.");
assertNotNull(outBuffer, "outBuffer cannot be NULL."); assertNotNull(outBuffer, "outBuffer cannot be NULL.");
assertNotNull(outSize, "outSize cannot be NULL."); assertNotNull(outSize, "outSize cannot be NULL.");
assertNull(file->file, "Asset file must be closed before reading entire."); assertTrue(
file->size > 0,
if(file->size == 0) { "Asset file has no size; call assetFileInit first."
errorThrow("Asset file is empty: %s", file->filename);
}
errorChain(assetFileOpen(file));
uint8_t *buffer = (uint8_t *)memoryAllocate(file->size);
errorret_t ret = assetFileRead(file, buffer, file->size);
if(errorIsOk(ret) && file->lastRead != file->size) {
ret = errorThrowImpl(
&ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__,
"Failed to read entire asset file: %s", file->filename
); );
}
// File should be closed currently.
assertNull(file->zipFile, "Asset file must be closed before reading entire.");
// Open file
errorret_t ret = assetFileOpen(file);
if(errorIsNotOk(ret)) { if(errorIsNotOk(ret)) {
memoryFree(buffer);
assetFileClose(file);
errorChain(ret); errorChain(ret);
} }
errorChain(assetFileClose(file)); // Set output.
size_t size = (size_t)file->size;
uint8_t *buffer = (uint8_t *)memoryAllocate(size);
// Read entire file.
ret = assetFileRead(file, buffer, size);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
// Close the file.
ret = assetFileClose(file);
if(errorIsNotOk(ret)) {
memoryFree(buffer);
errorChain(ret);
}
*outBuffer = buffer; *outBuffer = buffer;
*outSize = file->size; *outSize = size;
errorOk(); errorOk();
} }
// Line Reader;
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
assetfile_t *file,
uint8_t *readBuffer,
const size_t readBufferSize,
uint8_t *outBuffer,
const size_t outBufferSize
) {
assertNotNull(reader, "Line reader cannot be NULL.");
assertNotNull(file, " File cannot be NULL.");
assertNotNull(readBuffer, "Read buffer cannot be NULL.");
assertNotNull(outBuffer, "Output buffer cannot be NULL.");
assertTrue(readBufferSize > 0, "Read buffer size must be greater than 0.");
assertTrue(outBufferSize > 0, "Output buffer size must be greater than 0.");
memoryZero(reader, sizeof(assetfilelinereader_t));
reader->file = file;
reader->readBuffer = readBuffer;
reader->readBufferSize = readBufferSize;
reader->outBuffer = outBuffer;
reader->outBufferSize = outBufferSize;
}
size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertTrue(reader->bufferEnd >= reader->bufferStart, "Invalid buffer state.");
return reader->bufferEnd - reader->bufferStart;
}
const uint8_t *assetFileLineReaderUnreadPtr(
const assetfilelinereader_t *reader
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
return reader->readBuffer + reader->bufferStart;
}
static errorret_t assetFileLineReaderAppend(
assetfilelinereader_t *reader,
const uint8_t *src,
size_t srcLength
) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
if(srcLength == 0) {
errorOk();
}
/* reserve room for optional NUL terminator */
if(reader->lineLength + srcLength >= reader->outBufferSize) {
errorThrow("Line length exceeds output buffer size.");
}
memoryCopy(reader->outBuffer + reader->lineLength, src, srcLength);
reader->lineLength += srcLength;
errorOk();
}
static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
assertTrue(
reader->lineLength < reader->outBufferSize,
"Line length exceeds out buffer."
);
reader->outBuffer[reader->lineLength] = '\0';
}
static ssize_t assetFileLineReaderFindNewline(
const assetfilelinereader_t *reader
) {
size_t i;
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
for(i = reader->bufferStart; i < reader->bufferEnd; ++i) {
if(reader->readBuffer[i] == '\n') {
return (ssize_t)i;
}
}
return -1;
}
errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->file, "File cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
if(reader->eof) errorOk();
errorret_t ret;
size_t unreadBytes = assetFileLineReaderUnreadBytes(reader);
/* If buffer is fully consumed, refill from start. */
if(unreadBytes == 0) {
reader->bufferStart = 0;
reader->bufferEnd = 0;
errorChain(assetFileRead(
reader->file,
reader->readBuffer,
reader->readBufferSize
));
if(reader->file->lastRead == 0) {
reader->eof = true;
errorOk();
}
reader->bufferStart = 0;
reader->bufferEnd = reader->file->lastRead;
errorOk();
}
/*
* There are unread bytes left but no newline in them.
* If bufferStart > 0, slide unread bytes to front so we can read more.
* This only happens when necessary to make space.
*/
if(reader->bufferEnd == reader->readBufferSize) {
if(reader->bufferStart == 0) {
/*
* Entire read buffer is unread and contains no newline.
* Caller must have a large enough outBuffer to accumulate across fills,
* so we consume these bytes into outBuffer before refilling.
*/
errorOk();
}
memoryMove(
reader->readBuffer,
reader->readBuffer + reader->bufferStart,
unreadBytes
);
reader->bufferStart = 0;
reader->bufferEnd = unreadBytes;
}
errorChain(assetFileRead(
reader->file,
reader->readBuffer + reader->bufferEnd,
reader->readBufferSize - reader->bufferEnd
));
if(reader->file->lastRead == 0) {
reader->eof = true;
errorOk();
}
reader->bufferEnd += reader->file->lastRead;
errorOk();
}
errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader) {
assertNotNull(reader, "Reader cannot be NULL.");
assertNotNull(reader->file, "File cannot be NULL.");
assertNotNull(reader->readBuffer, "Read buffer cannot be NULL.");
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
reader->lineLength = 0;
for(;;) {
ssize_t newlineIndex = assetFileLineReaderFindNewline(reader);
if(newlineIndex >= 0) {
size_t chunkLength = (size_t)newlineIndex - reader->bufferStart;
errorret_t ret;
/* strip CR in CRLF */
if(
chunkLength > 0 &&
reader->readBuffer[(size_t)newlineIndex - 1] == '\r'
) {
chunkLength--;
}
errorChain(assetFileLineReaderAppend(
reader,
reader->readBuffer + reader->bufferStart,
chunkLength
));
reader->bufferStart = (size_t)newlineIndex + 1;
assetFileLineReaderTerminate(reader);
errorOk();
}
if(assetFileLineReaderUnreadBytes(reader) > 0) {
errorChain(assetFileLineReaderAppend(
reader,
assetFileLineReaderUnreadPtr(reader),
assetFileLineReaderUnreadBytes(reader)
));
reader->bufferStart = reader->bufferEnd;
}
if(reader->eof) {
if(reader->lineLength > 0) {
assetFileLineReaderTerminate(reader);
errorOk();
}
errorThrow("End of file reached.");
}
errorChain(assetFileLineReaderFill(reader));
}
}
+99 -39
View File
@@ -7,55 +7,67 @@
#pragma once #pragma once
#include "error/error.h" #include "error/error.h"
#include <zip.h>
#define ASSET_FILE_NAME_MAX 128 #define ASSET_FILE_NAME_MAX 48
// Full on disk path, base directory plus filename. typedef struct assetfile_s assetfile_t;
#define ASSET_PATH_MAX 512
typedef struct { typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
// Describes a file not yet loaded.
typedef struct assetfile_s {
char_t filename[ASSET_FILE_NAME_MAX]; char_t filename[ASSET_FILE_NAME_MAX];
FILE *file; void *params;
size_t size; void *output;
size_t position;
size_t lastRead; zip_stat_t stat;
zip_int64_t size;
zip_int64_t position;
zip_int64_t lastRead;
zip_file_t *zipFile;
} assetfile_t; } assetfile_t;
/** /**
* Initializes an asset file, checking it exists and reading its size. Does not * Initializes the asset file structure in preparation for loading. This will
* open it. * stat the file but not open the handle.
* *
* @param file The asset file to initialize. * @param file The asset file structure to initialize.
* @param filename Asset filename, relative to the asset directory. * @param filename The name of the asset file to load.
* @return An error if the file does not exist or cannot be read. * @param params Optional loader params.
* @param output Output pointer for the loader.
* @return Error indicating success or failure.
*/ */
errorret_t assetFileInit(assetfile_t *file, const char_t *filename); errorret_t assetFileInit(
assetfile_t *file,
const char_t *filename,
void *params,
void *output
);
/** /**
* Opens an asset file for reading, positioned at the start. * Opens the asset file for reading. After opening the loader is responsible
* for closing the file.
* *
* @param file The asset file to open. Must not already be open. * @param file The asset file to open.
* @return An error if the file cannot be opened. * @return An error code if the file could not be opened.
*/ */
errorret_t assetFileOpen(assetfile_t *file); errorret_t assetFileOpen(assetfile_t *file);
/** /**
* Moves an open asset file back to its start. * Rewind the file to the initial position.
* *
* @param file The asset file to rewind. Must be open. * @param file The asset file to rewind.
* @return An error if the file cannot be rewound.
*/ */
errorret_t assetFileRewind(assetfile_t *file); errorret_t assetFileRewind(assetfile_t *file);
/** /**
* Reads bytes from an open asset file. The number of bytes actually read is * Read bytes from the asset file. Assumes the file has already been opened
* stored in file->lastRead, which is less than bufferSize at the end of the * prior to trying to read anything.
* file.
* *
* @param file The asset file to read from. Must be open. * @param file File to read from.
* @param buffer Buffer to read into. If NULL the bytes are skipped. * @param buffer Buffer to read the file data into., or NULL to skip bytes.
* @param bufferSize Number of bytes to read. * @param size Size of the buffer to read into.
* @return An error if reading fails.
*/ */
errorret_t assetFileRead( errorret_t assetFileRead(
assetfile_t *file, assetfile_t *file,
@@ -64,33 +76,81 @@ errorret_t assetFileRead(
); );
/** /**
* Closes an open asset file. * Closes the asset file and releases any resources associated with it.
* *
* @param file The asset file to close. Must be open. * @param file The asset file to close.
* @return An error if the file cannot be closed. * @return An error code if the file could not be closed properly.
*/ */
errorret_t assetFileClose(assetfile_t *file); errorret_t assetFileClose(assetfile_t *file);
/** /**
* Disposes an asset file, closing it first if it is still open. * Disposes the asset file structure, closing any open handles and zeroing
* out the structure.
* *
* @param file The asset file to dispose. * @param file The asset file to dispose.
* @return An error if the file cannot be closed. * @return An error code if the file could not be disposed properly.
*/ */
errorret_t assetFileDispose(assetfile_t *file); errorret_t assetFileDispose(assetfile_t *file);
/** /**
* Reads a whole asset file into a newly allocated buffer. The file is opened * Reads the entire contents of the asset file into a newly allocated buffer.
* and closed by this call, and must not already be open. * The caller is responsible for freeing the buffer with memoryFree.
* *
* @param file The asset file to read, already initialized. * @param file The asset file to read. Must be initialized but not open.
* @param outBuffer Output buffer, allocated with memoryAllocate. The caller * @param outBuffer Receives a pointer to the allocated buffer.
* must memoryFree it. * @param outSize Receives the number of bytes written to the buffer.
* @param outSize Output size of the buffer in bytes. * @return An error code if the file could not be read.
* @return An error if the file cannot be read in full.
*/ */
errorret_t assetFileReadEntire( errorret_t assetFileReadEntire(
assetfile_t *file, assetfile_t *file,
uint8_t **outBuffer, uint8_t **outBuffer,
size_t *outSize size_t *outSize
); );
typedef struct {
assetfile_t *file;
uint8_t *readBuffer;
size_t readBufferSize;
uint8_t *outBuffer;
size_t outBufferSize;
// A
size_t bufferStart;
size_t bufferEnd;
bool_t eof;//?
// Updated each reach:
size_t lineLength;
} assetfilelinereader_t;
/**
* Initializes a line reader for the given asset file. The line reader will read
* lines from the file into the provided line buffer, using the provided buffer
* for reading chunks of the file.
*
* @param file The asset file to read from. Must already be opened.
* @param readBuffer Buffer to use for reading chunks of the file.
* @param readBufferSize Size of the read buffer.
* @param outBuffer Buffer to read lines into. Lines will be null-terminated.
* @param outBufferSize Size of the output buffer.
* @return An initialized line reader structure.
*/
void assetFileLineReaderInit(
assetfilelinereader_t *reader,
assetfile_t *file,
uint8_t *readBuffer,
const size_t readBufferSize,
uint8_t *outBuffer,
const size_t outBufferSize
);
/**
* Reads the next line from the asset file into the line buffer. The line
* buffer is null-terminated and does not include the newline character.
*
* @param reader The line reader to read from.
* @return An error code if a failure occurs, or errorOk() if a line was read
* successfully. If the end of the file is reached, errorEndOfFile() is
* returned.
*/
errorret_t assetFileLineReaderNext(assetfilelinereader_t *reader);
+19
View File
@@ -0,0 +1,19 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assetentry.c
assetloader.c
)
# Subdirs
add_subdirectory(display)
add_subdirectory(locale)
add_subdirectory(json)
add_subdirectory(dmf)
add_subdirectory(script)
+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 "assetentry.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
void assetEntryInit(
assetentry_t *entry,
const char_t *name,
const assetloadertype_t type,
assetloaderinput_t *input
) {
assertNotNull(entry, "Entry cannot be NULL");
assertStrLenMin(name, 1, "Name cannot be empty");
assertStrLenMax(name, ASSET_FILE_NAME_MAX - 1, "Name too long");
assertTrue(type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
memoryZero(entry, sizeof(assetentry_t));
stringCopy(entry->name, name, ASSET_FILE_NAME_MAX);
entry->type = type;
entry->state = ASSET_ENTRY_STATE_NOT_STARTED;
if(input) {
entry->inputData = *input;
entry->input = &entry->inputData;
} else {
memoryZero(&entry->inputData, sizeof(assetloaderinput_t));
entry->input = NULL;
}
refInit(&entry->refs, entry, NULL, NULL, NULL);
}
void assetEntryLock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refLock(&entry->refs);
}
void assetEntryUnlock(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
refUnlock(&entry->refs);
}
void assetEntryStartLoading(
assetentry_t *entry,
assetloading_t *loading
) {
assertNotNull(entry, "Entry cannot be NULL");
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(
entry->state == ASSET_ENTRY_STATE_NOT_STARTED,
"Can only start loading from NOT_STARTED state."
);
assertIsMainThread("Must be called from the main thread.");
entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
memoryZero(&loading->loading, sizeof(assetloaderloading_t));
loading->type = entry->type;
loading->entry = entry;
// At this point the asset manager will manage this thing's loading
}
errorret_t assetEntryDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type != ASSET_LOADER_TYPE_NULL, "Invalid loader type.");
assertTrue(entry->type < ASSET_LOADER_TYPE_COUNT, "Invalid loader type.");
assertIsMainThread("Must be called from the main thread.");
assertTrue(
entry->refs.count == 0,
"Asset entry still refed at dispose time."
);
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
memoryZero(entry, sizeof(assetentry_t));
errorOk();
}
+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
*/
#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 assetentry_t;
struct assetentry_s {
char_t name[ASSET_FILE_NAME_MAX];
assetloadertype_t type;
assetloaderoutput_t data;
assetentrystate_t state;
ref_t refs;
assetloaderinput_t *input;
assetloaderinput_t inputData;
};
/**
* 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);
+53
View File
@@ -0,0 +1,53 @@
/**
* 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_MODEL] = {
.loadSync = assetModelLoaderSync,
.loadAsync = assetModelLoaderAsync,
.dispose = assetModelDispose
},
[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,
.dispose = assetScriptDispose
},
};
+116
View File
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/loader/dmf/assetmeshloader.h"
#include "asset/loader/dmf/assetmodelloader.h"
#include "asset/loader/display/assettextureloader.h"
#include "asset/loader/display/assettilesetloader.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "asset/loader/json/assetjsonloader.h"
#include "asset/loader/script/assetscriptloader.h"
typedef enum {
ASSET_LOADER_TYPE_NULL,
ASSET_LOADER_TYPE_MESH,
ASSET_LOADER_TYPE_MODEL,
ASSET_LOADER_TYPE_TEXTURE,
ASSET_LOADER_TYPE_TILESET,
ASSET_LOADER_TYPE_LOCALE,
ASSET_LOADER_TYPE_JSON,
ASSET_LOADER_TYPE_SCRIPT,
ASSET_LOADER_TYPE_COUNT
} assetloadertype_t;
typedef union {
assetmeshloaderloading_t mesh;
assetmodelloaderloading_t model;
assettextureloaderloading_t texture;
assettilesetloaderloading_t tileset;
assetlocaleloaderloading_t locale;
assetjsonloaderloading_t json;
} assetloaderloading_t;
typedef union {
assetmeshoutput_t mesh;
assetmodeloutput_t model;
assettextureoutput_t texture;
assettilesetoutput_t tileset;
assetlocaleoutput_t locale;
assetjsonoutput_t json;
assetscriptoutput_t script;
} assetloaderoutput_t;
typedef union {
assettextureloaderinput_t texture;
assettilesetloaderinput_t tileset;
assetlocaleloaderinput_t locale;
assetjsonloaderinput_t json;
} assetloaderinput_t;
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
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); \
} \
}
/**
* Like @ref assetLoaderErrorChain, but also frees `_ptr` (via memoryFree)
* before chaining the error if `_expr` failed. Use this for any loader step
* that runs after a buffer has already been allocated, so a later I/O
* failure doesn't leak it.
*
* @param loading The asset loading slot.
* @param _ptr A heap pointer to free if `_expr` fails.
* @param _expr The error return value to check and chain if it's an error.
*/
#define assetLoaderErrorChainFree(loading, _ptr, _expr) {\
errorret_t _alecf = (_expr); \
if(errorIsNotOk(_alecf)) { \
memoryFree(_ptr); \
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorChain(_alecf); \
} \
}
/**
* Shorthand method to both throw an error (against the loader state) and to
* set the asset entry state to error.
*
* @param loading The asset loading slot.
* @param ... Format string and arguments for the error message.
*/
#define assetLoaderErrorThrow(loading, ...) {\
loading->entry->state = ASSET_ENTRY_STATE_ERROR; \
errorThrow(__VA_ARGS__); \
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "assetloader.h"
#include "asset/assetfile.h"
#include "thread/threadmutex.h"
typedef struct assetentry_s assetentry_t;
typedef struct assetloading_s {
threadmutex_t mutex;
assetloadertype_t type;
assetentry_t *entry;
assetloaderloading_t loading;
} assetloading_t;
typedef errorret_t (assetloadingcallback_t)(assetloading_t *loading);
typedef struct {
assetloadingcallback_t *loadSync;
} assetloadingcallbacks_t;
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
assettextureloader.c
assettilesetloader.c
)
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettextureloader.h"
#include "assert/assert.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "log/log.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
stbi_io_callbacks ASSET_TEXTURE_STB_CALLBACKS = {
.read = assetTextureReader,
.skip = assetTextureSkipper,
.eof = assetTextureEOF
};
int assetTextureReader(void *user, char *data, int size) {
assertNotNull(data, "Data buffer for stb_image callbacks cannot be NULL.");
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, data, (size_t)size);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
return -1;
}
return file->lastRead;
}
void assetTextureSkipper(void *user, int n) {
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
errorret_t ret = assetFileRead(file, NULL, (size_t)n);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
}
}
int assetTextureEOF(void *user) {
assetfile_t *file = (assetfile_t*)user;
assertNotNull(file, "Asset file in stb_image callbacks cannot be NULL.");
return file->position >= file->size;
}
errorret_t assetTextureLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
// Only care about loading pixels.
if(loading->loading.texture.state != ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS){
errorOk();
}
// Init the file
assertNull(
loading->loading.texture.data, "Pixels already defined?"
);
assetfile_t *file = &loading->loading.texture.file;
assetLoaderErrorChain(loading, assetFileInit(
file,
loading->entry->name,
NULL,
&loading->entry->data.texture
));
assetLoaderErrorChain(loading, assetFileOpen(file));
// Determine channels
int channelsDesired;
switch(loading->entry->inputData.texture) {
case TEXTURE_FORMAT_RGBA:
channelsDesired = 4;
break;
default:
assetLoaderErrorThrow(loading, "Bad texture format.");
}
// Load image pixels.
loading->loading.texture.data = stbi_load_from_callbacks(
&ASSET_TEXTURE_STB_CALLBACKS,
file,
&loading->loading.texture.width,
&loading->loading.texture.height,
&loading->loading.texture.channels,
channelsDesired
);
// Close out the file.
assetLoaderErrorChain(loading, assetFileClose(file));
assetLoaderErrorChain(loading, assetFileDispose(file));
// Ensure we loaded correctly.
if(loading->loading.texture.data == NULL) {
const char_t *errorStr = stbi_failure_reason();
assetLoaderErrorThrow(
loading, "Failed to load texture from file %s.", errorStr
);
}
// Fixes a specific bug probably with Dolphin but for now just assuming endian
if(!isHostLittleEndian()) {
stbi__vertical_flip(
loading->loading.texture.data,
loading->loading.texture.width,
loading->loading.texture.height,
loading->loading.texture.channels
);
}
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTextureLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.texture.state) {
case ASSET_TEXTURE_LOADING_STATE_INITIAL:
loading->loading.texture.state = ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE:
break;
default:
errorOk();
}
// Create the texture.
assertNotNull(
loading->loading.texture.data, "Pixels should have been loaded by now."
);
assetLoaderErrorChain(loading, textureInit(
(texture_t*)&loading->entry->data.texture,
loading->loading.texture.width,
loading->loading.texture.height,
loading->entry->inputData.texture,
(texturedata_t){
.rgbaColors = (color_t*)loading->loading.texture.data
}
));
// Free the pixels.
stbi_image_free(loading->loading.texture.data);
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTextureDispose(assetentry_t *entry) {
assertNotNull(entry, "Asset entry cannot be NULL");
assertIsMainThread("Must be called from the main thread.");
return textureDispose(&entry->data.texture);
}
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/texture/texture.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef textureformat_t assettextureloaderinput_t;
typedef enum {
ASSET_TEXTURE_LOADING_STATE_INITIAL,
ASSET_TEXTURE_LOADING_STATE_LOAD_PIXELS,
ASSET_TEXTURE_LOADING_STATE_CREATE_TEXTURE,
ASSET_TEXTURE_LOADING_STATE_DONE
} assettextureloadingstate_t;
typedef struct {
assetfile_t file;
assettextureloadingstate_t state;
int channels, width, height;
uint8_t *data;
} assettextureloaderloading_t;
typedef texture_t assettextureoutput_t;
/**
* STB image read callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param data Buffer to read the file data into.
* @param size Size of the buffer to read into.
* @return Number of bytes read, or -1 on error.
*/
int assetTextureReader(void *user, char *data, int size);
/**
* STB image skip callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @param n Number of bytes to skip in the file.
*/
void assetTextureSkipper(void *user, int n);
/**
* STB image EOF callback for asset files.
*
* @param user User data passed to the callback, should be an assetfile_t*.
* @return Non-zero if end of file has been reached, zero otherwise.
*/
int assetTextureEOF(void *user);
/**
* Synchronous loader for texture assets.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for texture assets.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTextureLoaderSync(assetloading_t *loading);
/**
* Disposer for texture assets.
*
* @param entry Asset entry containing the texture to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTextureDispose(assetentry_t *entry);
@@ -0,0 +1,130 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assettilesetloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.tileset.state != ASSET_TILESET_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.tileset.data, "Data already defined?");
assetfile_t *file = &loading->loading.tileset.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
assetLoaderErrorChain(loading, assetFileOpen(file));
uint8_t *data = memoryAllocate(file->size);
assetLoaderErrorChainFree(loading, data, assetFileRead(file, data, file->size));
assetLoaderErrorChainFree(loading, data, assetFileClose(file));
assetLoaderErrorChainFree(loading, data, assetFileDispose(file));
assertTrue(
file->lastRead == file->size,
"Failed to read entire tileset file."
);
loading->loading.tileset.data = data;
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_PARSE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.tileset.state) {
case ASSET_TILESET_LOADING_STATE_INITIAL:
loading->loading.tileset.state = ASSET_TILESET_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_TILESET_LOADING_STATE_PARSE:
break;
default:
errorOk();
}
uint8_t *data = loading->loading.tileset.data;
assertNotNull(data, "Tileset data should have been loaded by now.");
tileset_t *out = &loading->entry->data.tileset;
if(data[0] != 'D' || data[1] != 'T' || data[2] != 'F') {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid tileset header");
}
if(data[3] != 0x00) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Unsupported tileset version");
}
out->tileWidth = endianLittleToHost16(*(uint16_t *)(data + 4));
out->tileHeight = endianLittleToHost16(*(uint16_t *)(data + 6));
out->columns = endianLittleToHost16(*(uint16_t *)(data + 8));
out->rows = endianLittleToHost16(*(uint16_t *)(data + 10));
if(out->tileWidth == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile width cannot be 0");
}
if(out->tileHeight == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Tile height cannot be 0");
}
if(out->columns == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Column count cannot be 0");
}
if(out->rows == 0) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Row count cannot be 0");
}
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
if(out->uv[0] < 0.0f || out->uv[0] > 1.0f) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid u0 value in tileset");
}
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
memoryFree(data);
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
}
out->tileCount = out->columns * out->rows;
memoryFree(data);
loading->loading.tileset.data = NULL;
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetTilesetDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_TILESET, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
errorOk();
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "asset/assetfile.h"
#include "display/texture/tileset.h"
typedef struct assetloading_s assetloading_t;
typedef struct assetentry_s assetentry_t;
typedef struct {
void *nothing;
} assettilesetloaderinput_t;
typedef enum {
ASSET_TILESET_LOADING_STATE_INITIAL,
ASSET_TILESET_LOADING_STATE_READ_FILE,
ASSET_TILESET_LOADING_STATE_PARSE,
ASSET_TILESET_LOADING_STATE_DONE
} assettilesetloadingstate_t;
typedef struct {
assetfile_t file;
assettilesetloadingstate_t state;
uint8_t *data;
} assettilesetloaderloading_t;
typedef tileset_t assettilesetoutput_t;
/**
* Asynchronous loader for tileset assets. Reads the raw DTF file bytes into
* the loading buffer so the sync phase can parse without blocking the main
* thread on I/O.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTilesetLoaderAsync(assetloading_t *loading);
/**
* Synchronous loader for tileset assets. Parses the DTF binary previously
* read by the async phase and populates the output tileset_t.
*
* @param loading Loading information for the asset being loaded.
* @return Error code indicating success or failure of the load operation.
*/
errorret_t assetTilesetLoaderSync(assetloading_t *loading);
/**
* Disposer for tileset assets.
*
* @param entry Asset entry containing the tileset to dispose.
* @return Error code indicating success or failure of the dispose operation.
*/
errorret_t assetTilesetDispose(assetentry_t *entry);
+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
assetmeshloader.c
assetmodelloader.c
)
+158
View File
@@ -0,0 +1,158 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "assetmeshloader.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/endian.h"
#include "asset/loader/assetloading.h"
#include "asset/loader/assetentry.h"
errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertNotMainThread("Should be called from an async thread.");
if(loading->loading.mesh.state != ASSET_MESH_LOADING_STATE_READ_FILE) {
errorOk();
}
assertNull(loading->loading.mesh.data, "Data already defined?");
assetfile_t *file = &loading->loading.mesh.file;
assetLoaderErrorChain(loading,
assetFileInit(file, loading->entry->name, NULL, NULL)
);
assetLoaderErrorChain(loading, assetFileOpen(file));
uint8_t *raw = memoryAllocate(file->size);
assetLoaderErrorChainFree(loading, raw, assetFileRead(file, raw, file->size));
assetLoaderErrorChainFree(loading, raw, assetFileClose(file));
assetLoaderErrorChainFree(loading, raw, assetFileDispose(file));
assertTrue(file->lastRead == file->size, "Failed to read entire DMF file.");
if(raw[0] != 'D' || raw[1] != 'M' || raw[2] != 'F') {
memoryFree(raw);
assetLoaderErrorThrow(loading, "Invalid DMF file header");
}
uint32_t version = endianLittleToHost32(*(uint32_t *)(raw + 4));
if(version != ASSET_MESH_FILE_VERSION) {
memoryFree(raw);
assetLoaderErrorThrow(loading, "Unsupported DMF version %u", version);
}
uint32_t vertCount = endianLittleToHost32(*(uint32_t *)(raw + 8));
meshvertex_t *vertices = NULL;
if(vertCount > 0) {
// 32-byte (cache-line) aligned: GX_SetArray + DCFlushRange on Dolphin
// require this for the DMA'd vertex data to actually reach the GPU
// coherently. Static compiled-in vertex arrays happen to get this from
// the linker; a plain memoryAllocate here would not.
vertices = memoryAlign(32, vertCount * sizeof(meshvertex_t));
memoryCopy(vertices, raw + 12, vertCount * sizeof(meshvertex_t));
for(uint32_t v = 0; v < vertCount; v++) {
vertices[v].uv[0] = endianLittleToHostFloat(vertices[v].uv[0]);
vertices[v].uv[1] = endianLittleToHostFloat(vertices[v].uv[1]);
vertices[v].pos[0] = endianLittleToHostFloat(vertices[v].pos[0]);
vertices[v].pos[1] = endianLittleToHostFloat(vertices[v].pos[1]);
vertices[v].pos[2] = endianLittleToHostFloat(vertices[v].pos[2]);
}
}
memoryFree(raw);
loading->loading.mesh.vertCount = vertCount;
loading->loading.mesh.data = (uint8_t *)vertices;
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_CREATE_MESH;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
errorOk();
}
errorret_t assetMeshLoaderSync(assetloading_t *loading) {
assertNotNull(loading, "Loading cannot be NULL");
assertTrue(loading->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
switch(loading->loading.mesh.state) {
case ASSET_MESH_LOADING_STATE_INITIAL:
loading->loading.mesh.state = ASSET_MESH_LOADING_STATE_READ_FILE;
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
errorOk();
break;
case ASSET_MESH_LOADING_STATE_CREATE_MESH:
break;
default:
errorOk();
}
uint32_t vertCount = loading->loading.mesh.vertCount;
meshvertex_t *vertices = (meshvertex_t *)loading->loading.mesh.data;
loading->loading.mesh.data = NULL;
assetmeshoutput_t *out = &loading->entry->data.mesh;
out->vertices = vertices;
if(vertCount == 0) {
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t ret = meshInit(
&out->mesh,
MESH_PRIMITIVE_TYPE_TRIANGLES,
(int32_t)vertCount,
out->vertices
);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
out->meshInitialized = true;
ret = meshFlush(&out->mesh, 0, (int32_t)vertCount);
if(errorIsNotOk(ret)) {
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
meshDispose(&out->mesh);
out->meshInitialized = false;
memoryFree(out->vertices);
out->vertices = NULL;
errorChain(ret);
}
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
// VBO owns the data now; CPU copy is no longer needed. The platform
// mesh object itself still needs meshDispose later - tracked via
// out->meshInitialized, independent of the CPU buffer's lifetime.
memoryFree(out->vertices);
out->vertices = NULL;
#endif
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
errorOk();
}
errorret_t assetMeshDispose(assetentry_t *entry) {
assertNotNull(entry, "Entry cannot be NULL");
assertTrue(entry->type == ASSET_LOADER_TYPE_MESH, "Invalid type.");
assertIsMainThread("Must be called from the main thread.");
assetmeshoutput_t *out = &entry->data.mesh;
if(out->meshInitialized) {
errorChain(meshDispose(&out->mesh));
out->meshInitialized = false;
}
if(out->vertices != NULL) {
memoryFree(out->vertices);
out->vertices = NULL;
}
errorOk();
}
Loaded 100 of 563 files, more files were not shown because too many files have changed in this diff. Show more