Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5184064a26 | |||
| 6a43363539 | |||
| b9195fbbad | |||
| f715ad2176 |
@@ -33,74 +33,3 @@ jobs:
|
|||||||
libssl-dev
|
libssl-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ./scripts/test-linux.sh
|
run: ./scripts/test-linux.sh
|
||||||
|
|
||||||
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
|
|
||||||
# confirm the emulator doesn't crash. Not yet verified against a real
|
|
||||||
# runner (no CI run has exercised these) -- continue-on-error so a
|
|
||||||
# flaky/broken emulator step doesn't block the required Linux test job.
|
|
||||||
run-tests-gamecube-dolphin:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
container:
|
|
||||||
image: ghcr.io/extremscorner/libogc2:latest
|
|
||||||
steps:
|
|
||||||
- name: Install Node.js
|
|
||||||
run: apt-get update && apt-get install -y nodejs
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install additional dependencies
|
|
||||||
run: |
|
|
||||||
apt-get install -y \
|
|
||||||
python3-pip python3-polib python3-pil \
|
|
||||||
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
|
||||||
dolphin-emu xvfb
|
|
||||||
dkp-pacman -Syu --noconfirm
|
|
||||||
dkp-pacman -S --needed --noconfirm \
|
|
||||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
|
||||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
|
||||||
- name: Build GameCube ISO and boot it in Dolphin
|
|
||||||
run: ./scripts/test-gamecube-dolphin.sh
|
|
||||||
|
|
||||||
run-tests-wii-dolphin:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
container:
|
|
||||||
image: ghcr.io/extremscorner/libogc2:latest
|
|
||||||
steps:
|
|
||||||
- name: Install Node.js
|
|
||||||
run: apt-get update && apt-get install -y nodejs
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install additional dependencies
|
|
||||||
run: |
|
|
||||||
apt-get install -y \
|
|
||||||
python3-pip python3-polib python3-pil \
|
|
||||||
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
|
||||||
dolphin-emu xvfb
|
|
||||||
dkp-pacman -Syu --noconfirm
|
|
||||||
dkp-pacman -S --needed --noconfirm \
|
|
||||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
|
||||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
|
||||||
- name: Build Wii ISO and boot it in Dolphin
|
|
||||||
run: ./scripts/test-wii-dolphin.sh
|
|
||||||
|
|
||||||
run-tests-psp-ppsspp:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup pspdev
|
|
||||||
uses: ./.github/actions/setup-pspdev
|
|
||||||
- name: Install PPSSPPHeadless build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
|
|
||||||
- name: Build PPSSPPHeadless
|
|
||||||
run: |
|
|
||||||
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
|
|
||||||
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
|
|
||||||
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
|
|
||||||
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
|
|
||||||
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
|
|
||||||
run: ./scripts/test-psp-ppsspp.sh
|
|
||||||
|
|||||||
+1
-12
@@ -13,7 +13,6 @@ cmake_policy(SET CMP0079 NEW)
|
|||||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||||
|
|
||||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||||
option(DUSK_NETWORKING "Enable networking support" ON)
|
|
||||||
|
|
||||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||||
@@ -91,12 +90,6 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
DUSK_VERSION="${DUSK_VERSION}"
|
DUSK_VERSION="${DUSK_VERSION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(DUSK_NETWORKING)
|
|
||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|
||||||
DUSK_NETWORKING
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Toolchains
|
# Toolchains
|
||||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||||
|
|
||||||
@@ -123,11 +116,7 @@ if(DUSK_BUILD_TESTS)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build assets
|
# Build assets
|
||||||
# Deliberately not CONFIGURE_DEPENDS: that reruns the full CMake configure
|
file(GLOB_RECURSE DUSK_ASSET_FILES CONFIGURE_DEPENDS "${DUSK_ASSETS_DIR}/*")
|
||||||
# step (which invalidates generated headers and forces a huge rebuild) on
|
|
||||||
# every single asset edit. Re-run cmake manually when assets are added or
|
|
||||||
# removed.
|
|
||||||
file(GLOB_RECURSE DUSK_ASSET_FILES "${DUSK_ASSETS_DIR}/*")
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT "${DUSK_ASSETS_ZIP}"
|
OUTPUT "${DUSK_ASSETS_ZIP}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Dusk
|
# Documentation
|
||||||
RPG Game Project, small and able to run on a PSP.
|
- [Scripting](docs/SCRIPTING.md) — writing gameplay logic in JavaScript.
|
||||||
|
- [UI](docs/UI.md) — building buttons/menus/etc from engine/game C code.
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
Each build target has different requirements. You can take a look at the git
|
Each build target has different requirements. You can take a look at the git
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
-66
@@ -1,66 +1,2 @@
|
|||||||
msgid ""
|
msgid "test.string"
|
||||||
msgstr ""
|
msgstr "This is a test string"
|
||||||
"Project-Id-Version: ExampleApp 1.0\n"
|
|
||||||
"Language: en\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
|
||||||
|
|
||||||
#: ui/menu.c:10
|
|
||||||
msgid "ui.title"
|
|
||||||
msgstr ""
|
|
||||||
"Welcome"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.general"
|
|
||||||
msgstr "General"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.input"
|
|
||||||
msgstr "Input"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.display"
|
|
||||||
msgstr "Display"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.audio"
|
|
||||||
msgstr "Audio"
|
|
||||||
|
|
||||||
msgid "ui.settings.input.deadzone"
|
|
||||||
msgstr "Deadzone"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
|
||||||
msgid "ui.settings.general.language"
|
|
||||||
msgstr "Language"
|
|
||||||
|
|
||||||
msgid "ui.settings.general.language_detail"
|
|
||||||
msgstr "Takes effect after restarting the application."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.apply"
|
|
||||||
msgstr "Apply"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.discard_changes"
|
|
||||||
msgstr "Discard unsaved changes?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.characters"
|
|
||||||
msgstr "Characters"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.items"
|
|
||||||
msgstr "Items"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.settings"
|
|
||||||
msgstr "Settings"
|
|
||||||
|
|
||||||
msgid "item.potion.name"
|
|
||||||
msgstr "Potion"
|
|
||||||
|
|
||||||
msgid "item.potato.name"
|
|
||||||
msgstr "Potato"
|
|
||||||
|
|
||||||
msgid "item.apple.name"
|
|
||||||
msgstr "Apple"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: ExampleApp 1.0\n"
|
|
||||||
"Language: es\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
|
||||||
|
|
||||||
#: ui/menu.c:10
|
|
||||||
msgid "ui.title"
|
|
||||||
msgstr ""
|
|
||||||
"Bienvenido"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.general"
|
|
||||||
msgstr "General"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.input"
|
|
||||||
msgstr "Entrada"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.display"
|
|
||||||
msgstr "Pantalla"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.audio"
|
|
||||||
msgstr "Audio"
|
|
||||||
|
|
||||||
msgid "ui.settings.input.deadzone"
|
|
||||||
msgstr "Deadzone"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
|
||||||
msgid "ui.settings.general.language"
|
|
||||||
msgstr "Idioma"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
|
||||||
msgid "ui.settings.general.language_detail"
|
|
||||||
msgstr "Se aplica después de reiniciar la aplicación."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.apply"
|
|
||||||
msgstr "Aplicar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.discard_changes"
|
|
||||||
msgstr "¿Descartar los cambios no guardados?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.characters"
|
|
||||||
msgstr "Personajes"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.items"
|
|
||||||
msgstr "Objetos"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.settings"
|
|
||||||
msgstr "Configuración"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
|
||||||
msgid "item.potion.name"
|
|
||||||
msgstr "Poción"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
|
||||||
msgid "item.potato.name"
|
|
||||||
msgstr "Papa"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
|
||||||
msgid "item.apple.name"
|
|
||||||
msgstr "Manzana"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: ExampleApp 1.0\n"
|
|
||||||
"Language: ja\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Plural-Forms: nplurals=1; plural=(0);\n"
|
|
||||||
|
|
||||||
#: ui/menu.c:10
|
|
||||||
msgid "ui.title"
|
|
||||||
msgstr ""
|
|
||||||
"歓迎"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.general"
|
|
||||||
msgstr "一般"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.input"
|
|
||||||
msgstr "入力"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.display"
|
|
||||||
msgstr "表示"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.tabs.audio"
|
|
||||||
msgstr "オーディオ"
|
|
||||||
|
|
||||||
msgid "ui.settings.input.deadzone"
|
|
||||||
msgstr "デッドゾーン"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
|
||||||
msgid "ui.settings.general.language"
|
|
||||||
msgstr "言語"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
|
||||||
msgid "ui.settings.general.language_detail"
|
|
||||||
msgstr "アプリケーションを再起動すると適用されます。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettings.c
|
|
||||||
msgid "ui.settings.apply"
|
|
||||||
msgstr "適用"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.discard_changes"
|
|
||||||
msgstr "未保存の変更を破棄しますか?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.characters"
|
|
||||||
msgstr "キャラクター"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.items"
|
|
||||||
msgstr "アイテム"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.settings"
|
|
||||||
msgstr "設定"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
|
||||||
msgid "item.potion.name"
|
|
||||||
msgstr "ポーション"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
|
||||||
msgid "item.potato.name"
|
|
||||||
msgstr "ジャガイモ"
|
|
||||||
|
|
||||||
#: src/dusk/rpg/item/item.json
|
|
||||||
msgid "item.apple.name"
|
|
||||||
msgstr "リンゴ"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
class Player extends Entity {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.position = this.add(POSITION);
|
|
||||||
this.position.setLocalPosition(0.0, 2.0, 0.0);
|
|
||||||
|
|
||||||
this.physics = this.add(PHYSICS);
|
|
||||||
this.physics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
|
||||||
|
|
||||||
this.renderable = this.add(RENDERABLE);
|
|
||||||
this.renderable.setMesh(0, MESH_CAPSULE);
|
|
||||||
this.renderable.setColor(0, 0, 255, 255);
|
|
||||||
|
|
||||||
this.add(PLAYER);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Player;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
var CAMERA_OFFSET_ANGLE = 0.0;
|
|
||||||
var CAMERA_OFFSET_RADIUS = 18.0;
|
|
||||||
var CAMERA_OFFSET_HEIGHT = 10.0;
|
|
||||||
|
|
||||||
class PlayerCamera extends Entity {
|
|
||||||
constructor(target) {
|
|
||||||
super();
|
|
||||||
this.target = target;
|
|
||||||
|
|
||||||
this.position = this.add(POSITION);
|
|
||||||
this.add(CAMERA);
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
var targetPosition = this.target.position.getLocalPosition();
|
|
||||||
|
|
||||||
var eyeX = (
|
|
||||||
targetPosition.x + Math.cos(CAMERA_OFFSET_ANGLE) * CAMERA_OFFSET_RADIUS
|
|
||||||
);
|
|
||||||
var eyeY = (
|
|
||||||
targetPosition.y + CAMERA_OFFSET_HEIGHT
|
|
||||||
);
|
|
||||||
var eyeZ = (
|
|
||||||
targetPosition.z + Math.sin(CAMERA_OFFSET_ANGLE) * CAMERA_OFFSET_RADIUS
|
|
||||||
);
|
|
||||||
|
|
||||||
this.position.lookAt(
|
|
||||||
eyeX, eyeY, eyeZ,
|
|
||||||
targetPosition.x, targetPosition.y, targetPosition.z,
|
|
||||||
0.0, 1.0, 0.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.target = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = PlayerCamera;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
class TestPlane extends Entity {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.position = this.add(POSITION);
|
|
||||||
this.position.setLocalPosition(-10.0, 0.0, -10.0);
|
|
||||||
this.position.setLocalScale(20.0, 1.0, 20.0);
|
|
||||||
|
|
||||||
this.physics = this.add(PHYSICS);
|
|
||||||
this.physics.setBodyType(PHYSICS_BODY_STATIC);
|
|
||||||
this.physics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
|
|
||||||
|
|
||||||
this.renderable = this.add(RENDERABLE);
|
|
||||||
this.renderable.setMesh(0, MESH_PLANE);
|
|
||||||
this.renderable.setColor(128, 128, 128, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TestPlane;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
var OverworldScene = require('./overworldscene.js');
|
|
||||||
|
|
||||||
Scene.set(new OverworldScene());
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
var Player = require('./Player.js');
|
|
||||||
var PlayerCamera = require('./PlayerCamera.js');
|
|
||||||
var TestPlane = require('./TestPlane.js');
|
|
||||||
|
|
||||||
class OverworldScene {
|
|
||||||
constructor() {
|
|
||||||
this.time = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.player = new Player();
|
|
||||||
this.plane = new TestPlane();
|
|
||||||
this.camera = new PlayerCamera(this.player);
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.camera.update();
|
|
||||||
|
|
||||||
this.time += Time.delta;
|
|
||||||
if(this.time > 3.0) {
|
|
||||||
Scene.set(new OverworldScene());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.camera.dispose();
|
|
||||||
this.player.dispose();
|
|
||||||
this.plane.dispose();
|
|
||||||
|
|
||||||
this.camera = null;
|
|
||||||
this.player = null;
|
|
||||||
this.plane = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = OverworldScene;
|
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -6,7 +6,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
|
|
||||||
# Link libraries
|
# Link libraries
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||||
bba
|
# bba
|
||||||
)
|
)
|
||||||
|
|
||||||
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_INPUT_POINTER
|
DUSK_INPUT_POINTER
|
||||||
DUSK_INPUT_GAMEPAD
|
DUSK_INPUT_GAMEPAD
|
||||||
DUSK_TIME_DYNAMIC
|
DUSK_TIME_DYNAMIC
|
||||||
|
DUSK_THREAD_PTHREAD
|
||||||
)
|
)
|
||||||
@@ -32,6 +32,7 @@ set(DUSK_BACKTRACE ON CACHE BOOL "Enable backtrace support for assert failures."
|
|||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
DUSK_SDL2
|
DUSK_SDL2
|
||||||
DUSK_OPENGL
|
DUSK_OPENGL
|
||||||
|
DUSK_CONSOLE_POSIX
|
||||||
# DUSK_OPENGL_LEGACY
|
# DUSK_OPENGL_LEGACY
|
||||||
DUSK_LINUX
|
DUSK_LINUX
|
||||||
DUSK_DISPLAY_SIZE_DYNAMIC
|
DUSK_DISPLAY_SIZE_DYNAMIC
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
if(NOT CMAKE_BUILD_TYPE)
|
|
||||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
set(CMAKE_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
||||||
set(CMAKE_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" 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_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
||||||
@@ -59,9 +55,16 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_DISPLAY_WIDTH=480
|
DUSK_DISPLAY_WIDTH=480
|
||||||
DUSK_DISPLAY_HEIGHT=272
|
DUSK_DISPLAY_HEIGHT=272
|
||||||
DUSK_THREAD_PTHREAD
|
DUSK_THREAD_PTHREAD
|
||||||
|
DUSK_TIME_DYNAMIC
|
||||||
DUSK_DISPLAY_OVERSCAN=6
|
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.
|
# Postbuild, create .pbp file for PSP.
|
||||||
create_pbp_file(
|
create_pbp_file(
|
||||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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 dolphin-emu xvfb && \
|
|
||||||
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"]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
FROM pspdev/pspdev:latest
|
|
||||||
WORKDIR /workdir
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
python3 \
|
|
||||||
py3-pip \
|
|
||||||
py3-dotenv \
|
|
||||||
git \
|
|
||||||
cmake \
|
|
||||||
make \
|
|
||||||
g++ \
|
|
||||||
sdl2-dev \
|
|
||||||
zlib-dev \
|
|
||||||
linux-headers
|
|
||||||
|
|
||||||
# PPSSPP has no Alpine/Linux distro package -- build the "PPSSPPHeadless"
|
|
||||||
# target (the CLI/no-window build PPSSPP's own CI uses for automated runs)
|
|
||||||
# from source instead. The target name and exact CMake options have moved
|
|
||||||
# around across PPSSPP versions -- if this breaks, check
|
|
||||||
# https://github.com/hrydgard/ppsspp's CMakeLists.txt for the current name.
|
|
||||||
RUN git clone --recursive --depth 1 \
|
|
||||||
https://github.com/hrydgard/ppsspp.git /opt/ppsspp && \
|
|
||||||
cd /opt/ppsspp && \
|
|
||||||
cmake -B build -DCMAKE_BUILD_TYPE=Release && \
|
|
||||||
cmake --build build --target PPSSPPHeadless -- -j$(nproc) && \
|
|
||||||
cp build/PPSSPPHeadless /usr/local/bin/PPSSPPHeadless && \
|
|
||||||
rm -rf /opt/ppsspp
|
|
||||||
|
|
||||||
VOLUME ["/workdir"]
|
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# Scripting
|
||||||
|
|
||||||
|
Dusk embeds [JerryScript](https://github.com/jerryscript-project/jerryscript) to
|
||||||
|
drive gameplay logic from JavaScript. The engine itself (rendering, physics,
|
||||||
|
asset loading, entity storage) is all C; scripts sit on top and manipulate that
|
||||||
|
state through a small set of bound objects.
|
||||||
|
|
||||||
|
This document covers the JS-facing scripting API. **UI (buttons, sliders,
|
||||||
|
menus, etc.) is not exposed to scripts** — it's a separate, C-only API. See
|
||||||
|
[UI.md](UI.md) if you're building screens/menus from engine/game C code.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
On startup, `engineInit()` loads and evaluates `assets/engine.js` as the main
|
||||||
|
script, then calls the global `init()` function if one is defined. From then
|
||||||
|
on, every engine tick calls (in order):
|
||||||
|
|
||||||
|
1. `fixedUpdate()` — once per fixed timestep. Use this for gameplay logic that
|
||||||
|
must be deterministic and independent of display refresh rate (movement,
|
||||||
|
physics-adjacent input handling, etc). Skipped on interpolation/dynamic
|
||||||
|
frames when the build has variable-timestep rendering enabled.
|
||||||
|
2. `update()` — once per rendered frame, including interpolation frames. Use
|
||||||
|
this for smooth, purely presentational animation (nothing that needs to be
|
||||||
|
deterministic).
|
||||||
|
|
||||||
|
On shutdown, `deinit()` is called once.
|
||||||
|
|
||||||
|
All four hooks (`init`, `update`, `fixedUpdate`, `deinit`) are **optional** —
|
||||||
|
if a script doesn't define one, the engine simply skips it, no error.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function update() {
|
||||||
|
cubePosition.rotation.y += TIME.delta * 1.5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `async init()` and `include()`
|
||||||
|
|
||||||
|
`init` (or any of the other hooks) can be declared `async` and use `await`
|
||||||
|
freely, including awaiting `include()` (see below), even though the engine
|
||||||
|
calls these functions synchronously from C with no external JS event loop.
|
||||||
|
When a hook returns a pending `Promise`, the engine keeps driving the asset
|
||||||
|
system and JerryScript's job queue until that promise settles before
|
||||||
|
continuing — so by the time e.g. `init()` "returns" from the engine's point of
|
||||||
|
view, everything it awaited has actually finished.
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function init() {
|
||||||
|
Actions = await include("input.js");
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the awaited work throws/rejects, it surfaces as a C-level error.
|
||||||
|
|
||||||
|
## Loading other scripts: `include(path)`
|
||||||
|
|
||||||
|
```js
|
||||||
|
var Actions = await include("input.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
`include(path)` always returns a `Promise`. The named file is loaded and
|
||||||
|
evaluated once no matter how many times (or from how many different scripts)
|
||||||
|
you `include()` it — later calls for the same path are handed the same
|
||||||
|
in-flight/resolved promise rather than re-running the file.
|
||||||
|
|
||||||
|
The included script communicates its result back by assigning to the bare
|
||||||
|
global `module`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// input.js
|
||||||
|
Input.bind("w", INPUT_ACTION_UP);
|
||||||
|
// ...
|
||||||
|
|
||||||
|
module = {
|
||||||
|
UP: INPUT_ACTION_UP,
|
||||||
|
DOWN: INPUT_ACTION_DOWN,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Whatever `input.js` assigns to `module` becomes the resolved value of the
|
||||||
|
promise `include("input.js")` returned — that's what `Actions` ends up being
|
||||||
|
in the example above. If the included script throws, the promise rejects
|
||||||
|
instead.
|
||||||
|
|
||||||
|
## Full example
|
||||||
|
|
||||||
|
This is the actual shipped example content (`assets/engine.js` +
|
||||||
|
`assets/input.js`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// input.js — binds physical buttons to abstract actions, then exports the
|
||||||
|
// action constants so other scripts don't need to know raw INPUT_ACTION_* names.
|
||||||
|
Input.bind("w", INPUT_ACTION_UP);
|
||||||
|
Input.bind("s", INPUT_ACTION_DOWN);
|
||||||
|
Input.bind("a", INPUT_ACTION_LEFT);
|
||||||
|
Input.bind("d", INPUT_ACTION_RIGHT);
|
||||||
|
Input.bind("space", INPUT_ACTION_ACCEPT);
|
||||||
|
Input.bind("escape", INPUT_ACTION_RAGEQUIT);
|
||||||
|
|
||||||
|
if(typeof INPUT_GAMEPAD !== "undefined") {
|
||||||
|
Input.bind("gamepad_up", INPUT_ACTION_UP);
|
||||||
|
Input.bind("gamepad_down", INPUT_ACTION_DOWN);
|
||||||
|
Input.bind("gamepad_left", INPUT_ACTION_LEFT);
|
||||||
|
Input.bind("gamepad_right", INPUT_ACTION_RIGHT);
|
||||||
|
Input.bind("gamepad_a", INPUT_ACTION_ACCEPT);
|
||||||
|
Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
module = {
|
||||||
|
UP: INPUT_ACTION_UP,
|
||||||
|
DOWN: INPUT_ACTION_DOWN,
|
||||||
|
LEFT: INPUT_ACTION_LEFT,
|
||||||
|
RIGHT: INPUT_ACTION_RIGHT,
|
||||||
|
ACCEPT: INPUT_ACTION_ACCEPT,
|
||||||
|
CANCEL: INPUT_ACTION_CANCEL,
|
||||||
|
RAGEQUIT: INPUT_ACTION_RAGEQUIT
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// engine.js
|
||||||
|
var Actions;
|
||||||
|
var camera, cameraPosition;
|
||||||
|
var cube, cubePosition, cubeRenderable, cubeMesh;
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
Actions = await include("input.js");
|
||||||
|
|
||||||
|
camera = new Entity();
|
||||||
|
cameraPosition = camera.add(POSITION);
|
||||||
|
camera.add(CAMERA);
|
||||||
|
cameraPosition.position = new Vec3(3, 3, -6);
|
||||||
|
cameraPosition.lookAt(new Vec3(0, 0, 0));
|
||||||
|
|
||||||
|
cube = new Entity();
|
||||||
|
cubePosition = cube.add(POSITION);
|
||||||
|
cubeRenderable = cube.add(RENDERABLE);
|
||||||
|
cubeMesh = Mesh.createCube();
|
||||||
|
cubeRenderable.mesh = cubeMesh;
|
||||||
|
cubeRenderable.color = Color.red();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
cubePosition.rotation.y += TIME.delta * 1.5;
|
||||||
|
cubePosition.rotation.x += TIME.delta * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixedUpdate() {
|
||||||
|
var move = 3.0 * TIME.delta;
|
||||||
|
if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move;
|
||||||
|
if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move;
|
||||||
|
if(Input.isDown(Actions.UP)) cubePosition.position.z += move;
|
||||||
|
if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move;
|
||||||
|
if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deinit() {
|
||||||
|
cube.dispose();
|
||||||
|
camera.dispose();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API reference
|
||||||
|
|
||||||
|
### `TIME`
|
||||||
|
|
||||||
|
Plain global object, live getters (read fresh engine state every access, not
|
||||||
|
snapshotted):
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `TIME.delta` | number | Seconds since the last frame. |
|
||||||
|
| `TIME.time` | number | Total elapsed engine time, in seconds. |
|
||||||
|
|
||||||
|
### `PLATFORM`
|
||||||
|
|
||||||
|
A single global string constant — the compile-time target name, e.g.
|
||||||
|
`"linux"`, `"psp"`, `"vita"`, `"dolphin"`. Individual platform builds may
|
||||||
|
inject additional platform-specific globals via their own
|
||||||
|
`modulePlatformPlatform()` hook; those aren't documented here since they vary
|
||||||
|
per target.
|
||||||
|
|
||||||
|
### `Input`
|
||||||
|
|
||||||
|
Static namespace (not constructible — there's no `new Input()`).
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `Input.bind(buttonName, action)` | Binds a physical button/key (string, e.g. `"w"`, `"space"`, `"gamepad_up"`) to an abstract `INPUT_ACTION_*` constant. Many buttons can bind to the same action. Throws on an empty/unrecognized button name or invalid action. |
|
||||||
|
| `Input.isDown(action)` → boolean | Is the action currently held. |
|
||||||
|
| `Input.pressed(action)` → boolean | Action transitioned to down this frame. |
|
||||||
|
| `Input.released(action)` → boolean | Action transitioned to up this frame. |
|
||||||
|
| `Input.getValue(action)` → number | Current analog value for the action. |
|
||||||
|
| `Input.axis(negAction, posAction)` → number | Combined axis value from two opposing actions. |
|
||||||
|
| `Input.axis2D(negX, posX, negY, posY)` → `Vec2` | Combined 2D axis from four actions. |
|
||||||
|
|
||||||
|
Global `INPUT_ACTION_*` constants (names are stable API; treat the numeric
|
||||||
|
values as opaque/build-specific): `INPUT_ACTION_UP`, `INPUT_ACTION_DOWN`,
|
||||||
|
`INPUT_ACTION_LEFT`, `INPUT_ACTION_RIGHT`, `INPUT_ACTION_ACCEPT`,
|
||||||
|
`INPUT_ACTION_CANCEL`, `INPUT_ACTION_PAUSE`, `INPUT_ACTION_RAGEQUIT`,
|
||||||
|
`INPUT_ACTION_CONSOLE`, `INPUT_ACTION_POINTERX`, `INPUT_ACTION_POINTERY`.
|
||||||
|
|
||||||
|
Conditionally-defined boolean globals reflecting build capability — only
|
||||||
|
present at all if the corresponding input method is compiled in, so
|
||||||
|
feature-test with `typeof`, don't assume they exist:
|
||||||
|
`INPUT_KEYBOARD`, `INPUT_GAMEPAD`, `INPUT_POINTER`, `INPUT_TOUCH`.
|
||||||
|
|
||||||
|
### `Vec2` / `Vec3` / `Vec4`
|
||||||
|
|
||||||
|
`new Vec2(x?, y?)`, `new Vec3(x?, y?, z?)`, `new Vec4(x?, y?, z?, w?)` — all
|
||||||
|
components optional, default `0`.
|
||||||
|
|
||||||
|
Common instance surface across all three: `.dot(other)`, `.length()`,
|
||||||
|
`.lengthSq()`, `.normalize()`, `.negate()`, `.add(other)`, `.sub(other)`,
|
||||||
|
`.scale(n)`, `.lerp(other, t)` — each of `add`/`sub`/`scale`/`negate`/
|
||||||
|
`normalize`/`lerp` returns a **new** vector (non-mutating). `Vec3` additionally
|
||||||
|
has `.cross(other)` and `.distance(other)`; `Vec2` has `.distance(other)` too;
|
||||||
|
`Vec4` has neither `.cross()` nor `.distance()`.
|
||||||
|
|
||||||
|
`Vec4` also has UV aliases over the same four floats: `.u0` (= `.x`), `.v0`
|
||||||
|
(= `.y`), `.u1` (= `.z`), `.v1` (= `.w`) — handy for texture-rect style code.
|
||||||
|
|
||||||
|
All three have `.x`/`.y`(/`.z`/`.w`) get/set properties and a `.toString()`
|
||||||
|
like `"Vec3(1, 2, 3)"`.
|
||||||
|
|
||||||
|
**"Vec3Ref" — live references.** Several engine properties (entity
|
||||||
|
`position`/`rotation`/`scale`, physics `velocity`, a mesh vertex's `position`)
|
||||||
|
return a vector-*like* object instead of a plain `Vec3`. It has the identical
|
||||||
|
`.x`/`.y`/`.z` surface, but reads/writes go straight into the underlying
|
||||||
|
native buffer — writing `.x` on `entity.position.position` immediately moves
|
||||||
|
the entity, no separate assignment needed. Anywhere the API expects a `Vec3`
|
||||||
|
argument, a Vec3Ref works too. You never construct one directly; you only
|
||||||
|
ever receive them from properties like the ones above.
|
||||||
|
|
||||||
|
### `Mat4`
|
||||||
|
|
||||||
|
`new Mat4()` — always constructs identity; no other constructor form.
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.mul(other)` → `Mat4` | `this * other`. |
|
||||||
|
| `.transpose()` → `Mat4` | |
|
||||||
|
| `.inverse()` → `Mat4` | |
|
||||||
|
| `.determinant()` → number | |
|
||||||
|
| `.mulVec3(vec3, w?)` → `Vec3` | `w` defaults to `1.0` (point); pass `0` for a direction. |
|
||||||
|
| `.mulVec4(vec4)` → `Vec4` | |
|
||||||
|
| `.translate(vec3)` → `Mat4` | Non-mutating — returns a translated copy. |
|
||||||
|
| `.scale(vec3)` → `Mat4` | Non-mutating — returns a scaled copy. |
|
||||||
|
| `Mat4.identity()` → `Mat4` | Static. |
|
||||||
|
| `Mat4.perspective(fov, aspect, near, far)` → `Mat4` | Static, all 4 args required. |
|
||||||
|
| `Mat4.lookAt(eye, center, up)` → `Mat4` | Static, all 3 args required `Vec3`s. |
|
||||||
|
|
||||||
|
### `Color`
|
||||||
|
|
||||||
|
`new Color(r?, g?, b?, a?)` — each an int `0..255`, default `255` (so
|
||||||
|
`new Color()` is opaque white). Properties `.r`/`.g`/`.b`/`.a` get/set.
|
||||||
|
|
||||||
|
Named factories, each a zero-arg static returning a new opaque `Color`
|
||||||
|
(alpha `255` unless noted): `Color.black()`, `Color.white()`, `Color.red()`,
|
||||||
|
`Color.green()`, `Color.blue()`, `Color.yellow()`, `Color.cyan()`,
|
||||||
|
`Color.magenta()`, `Color.transparent()` (alpha 0), `Color.transparent_white()`
|
||||||
|
(alpha 0), `Color.transparent_black()` (alpha 0), `Color.gray()`,
|
||||||
|
`Color.light_gray()`, `Color.dark_gray()`, `Color.orange()`, `Color.purple()`,
|
||||||
|
`Color.brown()`, `Color.pink()`, `Color.lime()`, `Color.navy()`,
|
||||||
|
`Color.teal()`, `Color.cornflower_blue()`.
|
||||||
|
|
||||||
|
`Color.rainbow(t?, speed?)` → `Color` — `t` defaults to `TIME.time * 4.0`;
|
||||||
|
produces a shifting rainbow color, useful for debug visuals.
|
||||||
|
|
||||||
|
### `Mesh`
|
||||||
|
|
||||||
|
`new Mesh(vertexCount)` — allocates an uninitialized CPU-side vertex buffer
|
||||||
|
(not yet uploaded to the GPU).
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.vertices` | Array of vertex wrappers, each with a `.position` (Vec3Ref, writes straight into that vertex). |
|
||||||
|
| `.vertexCount` | Read-only. |
|
||||||
|
| `.flush()` | Uploads to the GPU. First call initializes the GPU mesh; later calls re-upload the current vertex data — call this after editing `.vertices[i].position`. |
|
||||||
|
| `.dispose()` | Frees GPU + CPU resources. |
|
||||||
|
|
||||||
|
Static engine-owned singletons (read-only, not something you dispose):
|
||||||
|
`Mesh.DEFAULT_CUBE`, `Mesh.DEFAULT_QUAD`, `Mesh.DEFAULT_SPHERE`,
|
||||||
|
`Mesh.DEFAULT_PLANE`, `Mesh.DEFAULT_CAPSULE`, `Mesh.DEFAULT_TRIPRISM`.
|
||||||
|
|
||||||
|
Static factories (each builds and uploads a brand-new `Mesh`):
|
||||||
|
|
||||||
|
| Factory | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `Mesh.createCube(min?, max?)` | Both `Vec3`, default `(-0.5,-0.5,-0.5)`..`(0.5,0.5,0.5)`. |
|
||||||
|
| `Mesh.createQuad(minX?, minY?, maxX?, maxY?)` | Default `-0.5..0.5` both axes; UV fixed `0,0`–`1,1`. |
|
||||||
|
| `Mesh.createSphere(radius?, stacks?, sectors?)` | `radius` default `0.5`. |
|
||||||
|
| `Mesh.createPlane(width?, height?)` | Defaults `1.0`/`1.0`; XZ-aligned, centered at origin. |
|
||||||
|
| `Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)` | Defaults `0.5`, `0.5`. |
|
||||||
|
| `Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)` | All 8 args required — a triangular cross-section extruded along Z. |
|
||||||
|
|
||||||
|
### `Entity` and components
|
||||||
|
|
||||||
|
```js
|
||||||
|
var e = new Entity();
|
||||||
|
var pos = e.add(POSITION);
|
||||||
|
```
|
||||||
|
|
||||||
|
`new Entity()` allocates an entity. `.id` is the read-only numeric engine ID.
|
||||||
|
`.add(TYPE)` adds a component and returns its wrapper (`TYPE` is one of the
|
||||||
|
constants below). `.dispose()` removes the entity and all its components.
|
||||||
|
|
||||||
|
Component-type constants: `POSITION`, `CAMERA`, `RENDERABLE`, `PHYSICS`,
|
||||||
|
`TRIGGER`. Each entity also exposes a lowercase getter that returns the
|
||||||
|
existing wrapper if the component is present, or `undefined` if not (it does
|
||||||
|
**not** add the component — use `.add()` for that): `entity.position`,
|
||||||
|
`entity.camera`, `entity.renderable`, `entity.physics`, `entity.trigger`.
|
||||||
|
|
||||||
|
#### `entity.add(POSITION)` → position component
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.position` | Vec3Ref. Writing rebuilds the transform automatically. |
|
||||||
|
| `.rotation` | Vec3Ref, Euler angles. Same rebuild-on-write behavior. |
|
||||||
|
| `.scale` | Vec3Ref. Same rebuild-on-write behavior. |
|
||||||
|
| `.parent` | Get/set another position-component wrapper, or `null` to clear parenting. |
|
||||||
|
| `.lookAt(target, up?)` | `target` a `Vec3`; `up` defaults to `(0,1,0)`. |
|
||||||
|
|
||||||
|
#### `entity.add(CAMERA)` → camera component
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.zNear` / `.zFar` | Numbers. |
|
||||||
|
| `.fov` | Only meaningful when `projectionType` is `CAMERA_TYPE_PERSPECTIVE`; otherwise get returns `undefined` and set is a no-op. |
|
||||||
|
| `.projectionType` | `CAMERA_TYPE_PERSPECTIVE` or `CAMERA_TYPE_ORTHOGRAPHIC`. |
|
||||||
|
| `.orthoTop` / `.orthoBottom` / `.orthoLeft` / `.orthoRight` | Only meaningful in orthographic mode, same undefined/no-op rule otherwise. |
|
||||||
|
|
||||||
|
#### `entity.add(RENDERABLE)` → renderable component
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.type` | `ENTITY_RENDERABLE_TYPE_MATERIAL`, `_SPRITEBATCH`, or `_CALLBACK`. |
|
||||||
|
| `.mesh` | Get/set a `Mesh` instance or a `Mesh.DEFAULT_*` singleton. |
|
||||||
|
| `.color` | Get/set a `Color` instance (throws if given something else). |
|
||||||
|
| `.addSprite({ min?, max?, uvMin?, uvMax? })` | Adds a sprite to this renderable's sprite batch; all fields optional, default zero. |
|
||||||
|
| `.clearSprites()` | Clears the sprite batch. |
|
||||||
|
| `.setCallback(fn?)` | Switches to `ENTITY_RENDERABLE_TYPE_CALLBACK` and calls `fn()` on every render of this entity. Omit/pass non-function to clear. Exceptions inside `fn` surface as a C error. |
|
||||||
|
|
||||||
|
#### `entity.add(PHYSICS)` → physics component
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.velocity` | Vec3Ref, plain (no rebuild-on-write). |
|
||||||
|
| `.onGround` | Read-only boolean. |
|
||||||
|
| `.bodyType` | `PHYSICS_BODY_STATIC`, `PHYSICS_BODY_DYNAMIC`, `PHYSICS_BODY_KINEMATIC`. |
|
||||||
|
| `.applyImpulse(vec3)` | Adds to velocity. No-op on static bodies. |
|
||||||
|
| `.setShapeCube(halfExtents)` | `halfExtents` a `Vec3`. |
|
||||||
|
| `.setShapeSphere(radius)` | Number. |
|
||||||
|
| `.setShapeCapsule(radius, halfHeight)` | Two numbers. |
|
||||||
|
| `.setShapePlane(normal, distance)` | `Vec3` + number. |
|
||||||
|
|
||||||
|
Shape-type constants (for reading `.type` on the underlying shape, not for
|
||||||
|
`.bodyType`): `PHYSICS_SHAPE_CUBE`, `PHYSICS_SHAPE_SPHERE`,
|
||||||
|
`PHYSICS_SHAPE_CAPSULE`, `PHYSICS_SHAPE_PLANE`.
|
||||||
|
|
||||||
|
#### `entity.add(TRIGGER)` → trigger component
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|---|---|
|
||||||
|
| `.min` / `.max` | Plain `Vec3` values (copies, not live refs). |
|
||||||
|
| `.setBounds(min, max)` | Sets both at once. |
|
||||||
|
| `.contains(point)` → boolean | `point` a `Vec3`. |
|
||||||
|
|
||||||
|
## Not yet available to scripts
|
||||||
|
|
||||||
|
The following C modules exist and are fully implemented, but aren't currently
|
||||||
|
wired into script registration (`moduleRegister()` in
|
||||||
|
`src/dusk/script/module/module.h`), so none of these globals exist in a
|
||||||
|
script today: `Screen`, `SpriteBatch`, `Text`, `Scene`, `Easing`, `Console`,
|
||||||
|
`Engine`. If you need one of these from a script, it needs to be registered
|
||||||
|
in `moduleRegister()` first — see the existing entries there and the modules
|
||||||
|
under `src/dusk/script/module/` for the pattern to follow.
|
||||||
+255
@@ -0,0 +1,255 @@
|
|||||||
|
# UI
|
||||||
|
|
||||||
|
Dusk's UI system (buttons, checkboxes, sliders, dropdowns, tabs, menus, focus
|
||||||
|
navigation) is a **C-only API**. It is not exposed to JerryScript — see
|
||||||
|
[SCRIPTING.md](SCRIPTING.md) for what scripts *can* touch. If you need a
|
||||||
|
script to open/react to a menu, wire it through a C callback or a game-side
|
||||||
|
flag scripts can poll; there's no bridge for this today.
|
||||||
|
|
||||||
|
## Mental model
|
||||||
|
|
||||||
|
There is no retained-mode UI tree, no automatic dispatch, no scissor/clip-rect
|
||||||
|
API. Every widget is a plain struct you own (usually as a global or
|
||||||
|
scene-owned variable). You call its `xxxInit(...)` once, then call its
|
||||||
|
`xxxDraw(widget, x, y)` yourself, every frame you want it visible, at whatever
|
||||||
|
screen position you choose. Nothing draws itself automatically except three
|
||||||
|
fixed system overlays (overscan bars, debug console, FPS counter) — see
|
||||||
|
[System overlays](#system-overlays-automatic) below.
|
||||||
|
|
||||||
|
### Where UI rendering happens in the frame
|
||||||
|
|
||||||
|
- `uiInit()` / `uiDispose()` run once, at engine startup/shutdown.
|
||||||
|
- `uiUpdate()` runs once per tick (drives focus-navigation input handling).
|
||||||
|
- `uiRender()` runs once per frame, called from inside `sceneRender()` — i.e.
|
||||||
|
**after** the active scene's own 3D/game-world rendering, using an
|
||||||
|
orthographic screen-space projection. Your own widget `xxxDraw()` calls
|
||||||
|
should happen around the same point — typically from your scene's render
|
||||||
|
callback, after world content, so UI draws on top.
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
Every widget follows the same shape: `xxxInit(widget, ...)` zeroes the struct
|
||||||
|
and sets its fields; `xxxDraw(const widget*, x, y) -> errorret_t` draws it at
|
||||||
|
that screen position.
|
||||||
|
|
||||||
|
> **Init before Draw.** `uislider_t`, `uidropdown_t`, and `uitab_t` cache
|
||||||
|
> their label's measured width/height at `Init` time (an optimization —
|
||||||
|
> label text doesn't change after that point). Calling `Draw` before `Init`,
|
||||||
|
> or mutating `->label` directly instead of re-initializing, leaves stale
|
||||||
|
> layout. `uibutton_t`/`uicheckbox_t` don't have this restriction.
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiButtonInit(uibutton_t *button, const char_t *label);
|
||||||
|
bool_t uiButtonIsHighlighted(const uibutton_t *button);
|
||||||
|
void uiButtonSetHighlighted(uibutton_t *button, bool_t highlighted);
|
||||||
|
errorret_t uiButtonDraw(const uibutton_t *button, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws `label` in red when highlighted, white otherwise.
|
||||||
|
|
||||||
|
### Checkbox
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiCheckboxInit(uicheckbox_t *checkbox, const char_t *label);
|
||||||
|
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox);
|
||||||
|
void uiCheckboxSetChecked(uicheckbox_t *checkbox, bool_t checked);
|
||||||
|
void uiCheckboxToggle(uicheckbox_t *checkbox);
|
||||||
|
bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox);
|
||||||
|
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, bool_t highlighted);
|
||||||
|
errorret_t uiCheckboxDraw(const uicheckbox_t *checkbox, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws `"Y "`/`"N "` then the label.
|
||||||
|
|
||||||
|
### Slider
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef union { float_t f; int32_t i; } uislidervalue_t;
|
||||||
|
|
||||||
|
void uiSliderInitFloat(uislider_t*, const char_t *label,
|
||||||
|
float_t value, float_t min, float_t max, float_t step);
|
||||||
|
void uiSliderInitInt(uislider_t*, const char_t *label,
|
||||||
|
int32_t value, int32_t min, int32_t max, int32_t step);
|
||||||
|
float_t uiSliderGetFloat(const uislider_t*); // works for either type
|
||||||
|
int32_t uiSliderGetInt(const uislider_t*); // asserts type == INT
|
||||||
|
void uiSliderSetFloat(uislider_t*, float_t value); // asserts type == FLOAT, clamps
|
||||||
|
void uiSliderSetInt(uislider_t*, int32_t value); // asserts type == INT, clamps
|
||||||
|
void uiSliderStepUp(uislider_t*); // wraps to min past max
|
||||||
|
void uiSliderStepDown(uislider_t*); // wraps to max past min
|
||||||
|
float_t uiSliderGetRatio(const uislider_t*); // normalized 0..1
|
||||||
|
int32_t uiSliderGetStepCount(const uislider_t*); // 0 for float sliders
|
||||||
|
bool_t uiSliderIsHighlighted(const uislider_t*);
|
||||||
|
void uiSliderSetHighlighted(uislider_t*, bool_t highlighted);
|
||||||
|
errorret_t uiSliderDraw(const uislider_t*, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws label, a track, a fill proportional to the current ratio, discrete step
|
||||||
|
markers if it's an int slider with fewer than 10 steps, then the value as
|
||||||
|
text.
|
||||||
|
|
||||||
|
### Dropdown
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiDropdownInit(uidropdown_t *dropdown, const char_t *label,
|
||||||
|
const char_t *const *options, uint8_t optionCount,
|
||||||
|
uint8_t selectedIndex);
|
||||||
|
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
|
||||||
|
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
|
||||||
|
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, uint8_t index);
|
||||||
|
void uiDropdownStepNext(uidropdown_t *dropdown); // wraps
|
||||||
|
void uiDropdownStepPrev(uidropdown_t *dropdown); // wraps
|
||||||
|
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
|
||||||
|
void uiDropdownSetHighlighted(uidropdown_t *dropdown, bool_t highlighted);
|
||||||
|
errorret_t uiDropdownDraw(const uidropdown_t *dropdown, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
`options` is a caller-owned array of strings that must outlive the dropdown
|
||||||
|
(it isn't copied). Draws `label` then `"< Option >"`.
|
||||||
|
|
||||||
|
### Tab
|
||||||
|
|
||||||
|
```c
|
||||||
|
void uiTabInit(uitab_t *tab, const char_t *label);
|
||||||
|
bool_t uiTabIsActive(const uitab_t *tab);
|
||||||
|
void uiTabSetActive(uitab_t *tab, bool_t active);
|
||||||
|
errorret_t uiTabDraw(const uitab_t *tab, float_t x, float_t y);
|
||||||
|
```
|
||||||
|
|
||||||
|
Draws a background box sized to the label (green if active, red if inactive)
|
||||||
|
with the label on top.
|
||||||
|
|
||||||
|
## Menus: assembling widgets into a navigable list
|
||||||
|
|
||||||
|
`uimenu_t` is the one aggregate widget — it owns an array of items (labels,
|
||||||
|
spacers, and any of the widgets above), lays them out in a grid, and wires
|
||||||
|
keyboard/gamepad navigation via the focus system for you.
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
UI_MENU_WIDGET_TYPE_NONE, UI_MENU_WIDGET_TYPE_LABEL,
|
||||||
|
UI_MENU_WIDGET_TYPE_SPACER, UI_MENU_WIDGET_TYPE_CHECKBOX,
|
||||||
|
UI_MENU_WIDGET_TYPE_BUTTON, UI_MENU_WIDGET_TYPE_TAB,
|
||||||
|
UI_MENU_WIDGET_TYPE_SLIDER, UI_MENU_WIDGET_TYPE_DROPDOWN,
|
||||||
|
} uimenuwidgettype_t;
|
||||||
|
|
||||||
|
void uiMenuInit(uimenu_t *menu, uimenuselectedcallback_t selected,
|
||||||
|
uimenuclosedcallback_t closed, uimenuchangedcallback_t changed);
|
||||||
|
void uiMenuSetItems(uimenu_t *menu, const uimenuitem_t *items,
|
||||||
|
uint8_t itemCount, uint8_t columns);
|
||||||
|
void uiMenuSetPosition(uimenu_t *menu, uint8_t x, uint8_t y); // focus cursor cell, not pixels
|
||||||
|
void uiMenuOpen(uimenu_t *menu); // pushes onto the focus stack
|
||||||
|
void uiMenuClose(uimenu_t *menu); // pops it
|
||||||
|
bool_t uiMenuIsActive(const uimenu_t *menu);
|
||||||
|
errorret_t uiMenuDraw(const uimenu_t *menu, float_t x, float_t y,
|
||||||
|
float_t width, float_t height);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `selected(menu, index, item)` fires when the player presses accept on an item.
|
||||||
|
- `changed(menu, index, item)` fires when the highlighted item changes.
|
||||||
|
- `closed(menu)` fires when the menu is popped off the focus stack.
|
||||||
|
- LEFT/RIGHT on a highlighted slider/checkbox/dropdown adjusts its value in
|
||||||
|
place instead of moving focus off it (handled internally).
|
||||||
|
|
||||||
|
### Building a menu with the `MENU_*` macros
|
||||||
|
|
||||||
|
`uimenu.h` provides macros that cut the boilerplate of filling in a
|
||||||
|
`uimenuitem_t` array. They expand into statements using local variables named
|
||||||
|
`menu`, `menuIndex`, and `menuCapacity`, so use them together, inside one
|
||||||
|
function, starting with `MENU_BEGIN` and ending with `MENU_END`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static uimenuitem_t optionsItems[8];
|
||||||
|
static uimenu_t optionsMenu;
|
||||||
|
static const char_t *qualityOptions[] = { "Low", "Medium", "High" };
|
||||||
|
|
||||||
|
static void onOptionsSelected(
|
||||||
|
const uimenu_t *menu, const uint8_t index, const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
if(index == 4) uiMenuClose(&optionsMenu); // "Back" button
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onOptionsClosed(const uimenu_t *menu) {
|
||||||
|
// e.g. return to the previous screen
|
||||||
|
}
|
||||||
|
|
||||||
|
void optionsMenuBuild(void) {
|
||||||
|
MENU_BEGIN(&optionsMenu, optionsItems, onOptionsSelected, onOptionsClosed, NULL);
|
||||||
|
MENU_LABEL("Options");
|
||||||
|
MENU_CHECKBOX("Fullscreen");
|
||||||
|
MENU_SLIDER_FLOAT("Volume", 0.8f, 0.0f, 1.0f, 0.05f);
|
||||||
|
MENU_DROPDOWN("Quality", qualityOptions, 3, 1);
|
||||||
|
MENU_BUTTON("Back");
|
||||||
|
MENU_END(optionsItems, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once, when the menu screen becomes active:
|
||||||
|
uiMenuOpen(&optionsMenu);
|
||||||
|
|
||||||
|
// Every frame the menu should be visible:
|
||||||
|
uiMenuDraw(&optionsMenu, 20.0f, 20.0f, 200.0f, 100.0f);
|
||||||
|
|
||||||
|
// When leaving the menu screen:
|
||||||
|
uiMenuClose(&optionsMenu);
|
||||||
|
```
|
||||||
|
|
||||||
|
`MENU_LABEL`/`MENU_SPACER` force a row break and aren't focusable/selectable.
|
||||||
|
Every other `MENU_*` macro calls the matching widget's own `Init` for you.
|
||||||
|
|
||||||
|
> This example is constructed directly from the widget/menu API surface (all
|
||||||
|
> function and macro signatures above are verified against the source), but
|
||||||
|
> there's currently no real menu-building call site anywhere else in the
|
||||||
|
> engine to cross-check the *pattern* against — treat it as a starting point,
|
||||||
|
> not a copy of shipped code.
|
||||||
|
|
||||||
|
## Focus system: navigation underneath `uimenu`
|
||||||
|
|
||||||
|
If you're building a custom widget that needs keyboard/gamepad navigation
|
||||||
|
without going through `uimenu`, use `ui/focus/uifocus.h` directly. `uimenu`
|
||||||
|
is implemented entirely in terms of this API, so it's a reasonable reference.
|
||||||
|
|
||||||
|
```c
|
||||||
|
uifocusitem_t * uiFocusPush(
|
||||||
|
uint8_t cols, uint8_t rows,
|
||||||
|
uifocusitemcallback_t selected, // fires on accept
|
||||||
|
uifocusitemcallback_t changed, // fires on cursor move (and once immediately)
|
||||||
|
uifocusitemcallback_t closed, // fires on pop
|
||||||
|
uifocusitemdirectioncallback_t direction, // optional pre-empt of a direction press; NULL for default grid movement
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
void uiFocusPop(void);
|
||||||
|
void uiFocusPopItem(uifocusitem_t *item);
|
||||||
|
void uiFocusSetPosition(uifocusitem_t *item, uint8_t x, uint8_t y); // wraps
|
||||||
|
void uiFocusMoveDirection(uifocusitem_t *item, uifocusdirection_t dir);
|
||||||
|
```
|
||||||
|
|
||||||
|
`uiFocusUpdate()` runs automatically from `uiUpdate()` every tick — you don't
|
||||||
|
call it yourself. It reads `INPUT_ACTION_ACCEPT` (fires `selected`),
|
||||||
|
`INPUT_ACTION_CANCEL` (pops the stack), and the four directional actions
|
||||||
|
(with hold-to-repeat timing) to move the cursor within the topmost pushed
|
||||||
|
item. Only the topmost stack entry (max depth 8) receives input at a time —
|
||||||
|
opening a submenu means pushing a new focus item on top; closing it pops back
|
||||||
|
to the parent.
|
||||||
|
|
||||||
|
There's no separate "is this widget focused" query — "focused" is expressed
|
||||||
|
as the pushed item's current `(x, y)` cursor cell matching a given slot, which
|
||||||
|
is exactly how `uimenu`'s `changed` callback decides which item to highlight.
|
||||||
|
|
||||||
|
## System overlays (automatic)
|
||||||
|
|
||||||
|
Three small overlays are wired into a fixed internal list and draw themselves
|
||||||
|
every frame with no call needed from game code:
|
||||||
|
|
||||||
|
- **Overscan bars** (`ui/overlay/uicrop.h`) — draws opaque bars over the
|
||||||
|
screen area outside `SCREEN.scanX/scanY/scanWidth/scanHeight` (the
|
||||||
|
overscan-safe viewport). A no-op on platforms/configs where the scan area
|
||||||
|
already equals the full viewport. `UI_CROP.color` (default black) is the
|
||||||
|
only thing you'd normally touch here.
|
||||||
|
- **Debug console** (`ui/debug/uiconsole.h`) — draws console history when
|
||||||
|
visible.
|
||||||
|
- **FPS counter** (`ui/debug/uifps.h`) — draws a live FPS/frame-time readout.
|
||||||
|
|
||||||
|
None of these have a scissor/clip-rect equivalent for your own widgets —
|
||||||
|
there is no clipping API in this UI system; everything draws unclipped at
|
||||||
|
whatever position you give it.
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// ChunkTerrain - JS port of the terrain-quad generation performed by
|
|
||||||
// tools/asset/chunk/__main__.py (build_terrain_verts / _tile_quad /
|
|
||||||
// _RAMP_CORNERS) so the editor's 3D preview can show the same geometry the
|
|
||||||
// Python compiler will bake into the chunk's terrain mesh, without waiting
|
|
||||||
// for a compile pass. Vertex layout matches DMF: interleaved [u, v, x, y, z].
|
|
||||||
|
|
||||||
const ChunkTerrain = (() => {
|
|
||||||
const CHUNK_WIDTH = 16;
|
|
||||||
const CHUNK_HEIGHT = 16;
|
|
||||||
const CHUNK_DEPTH = 4;
|
|
||||||
const TILE_COUNT = CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_DEPTH;
|
|
||||||
|
|
||||||
// World-space Z distance of one Z-layer/story, in world-float space.
|
|
||||||
// Must match WORLD_LAYER_HEIGHT in src/dusk/rpg/overworld/worldpos.h.
|
|
||||||
const WORLD_LAYER_HEIGHT = Math.SQRT1_2;
|
|
||||||
|
|
||||||
const TILE_SHAPE_NULL = 0;
|
|
||||||
const TILE_SHAPE_GROUND = 1;
|
|
||||||
const TILE_SHAPE_RAMP_NORTH = 2;
|
|
||||||
const TILE_SHAPE_RAMP_EAST = 3;
|
|
||||||
const TILE_SHAPE_RAMP_SOUTH = 4;
|
|
||||||
const TILE_SHAPE_RAMP_WEST = 5;
|
|
||||||
const TILE_SHAPE_RAMP_NORTHEAST = 6;
|
|
||||||
const TILE_SHAPE_RAMP_NORTHWEST = 7;
|
|
||||||
const TILE_SHAPE_RAMP_SOUTHEAST = 8;
|
|
||||||
const TILE_SHAPE_RAMP_SOUTHWEST = 9;
|
|
||||||
const TILE_SHAPE_RAMP_NORTHEAST_INNER = 10;
|
|
||||||
const TILE_SHAPE_RAMP_NORTHWEST_INNER = 11;
|
|
||||||
const TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12;
|
|
||||||
const TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13;
|
|
||||||
|
|
||||||
// Per-shape corner heights as [sw, se, ne, nw] offsets from tile base Z.
|
|
||||||
// NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1.
|
|
||||||
// Diagonal outer-corner ramps: only the named corner raised, rest at 0.
|
|
||||||
// Diagonal inner-corner ramps: only the corner opposite the named one is
|
|
||||||
// lowered to 0, the rest (including the named corner) stay raised at 1.
|
|
||||||
const RAMP_CORNERS = {
|
|
||||||
[TILE_SHAPE_GROUND]: [0.0, 0.0, 0.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_NORTH]: [0.0, 0.0, 1.0, 1.0],
|
|
||||||
[TILE_SHAPE_RAMP_SOUTH]: [1.0, 1.0, 0.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_EAST]: [0.0, 1.0, 1.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_WEST]: [1.0, 0.0, 0.0, 1.0],
|
|
||||||
[TILE_SHAPE_RAMP_NORTHEAST]: [0.0, 0.0, 1.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_NORTHWEST]: [0.0, 0.0, 0.0, 1.0],
|
|
||||||
[TILE_SHAPE_RAMP_SOUTHEAST]: [0.0, 1.0, 0.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_SOUTHWEST]: [1.0, 0.0, 0.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_NORTHEAST_INNER]: [0.0, 1.0, 1.0, 1.0],
|
|
||||||
[TILE_SHAPE_RAMP_NORTHWEST_INNER]: [1.0, 0.0, 1.0, 1.0],
|
|
||||||
[TILE_SHAPE_RAMP_SOUTHEAST_INNER]: [1.0, 1.0, 1.0, 0.0],
|
|
||||||
[TILE_SHAPE_RAMP_SOUTHWEST_INNER]: [1.0, 1.0, 0.0, 1.0],
|
|
||||||
};
|
|
||||||
|
|
||||||
function tileIndex(x, y, z) {
|
|
||||||
return x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
|
|
||||||
// Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
|
|
||||||
function pushTileQuad(out, u0, u1, v0, v1, fx, fy, swZ, seZ, neZ, nwZ) {
|
|
||||||
out.push(
|
|
||||||
u0, v0, fx, fy, swZ,
|
|
||||||
u1, v0, fx + 1, fy, seZ,
|
|
||||||
u1, v1, fx + 1, fy + 1, neZ,
|
|
||||||
u0, v0, fx, fy, swZ,
|
|
||||||
u1, v1, fx + 1, fy + 1, neZ,
|
|
||||||
u0, v1, fx, fy + 1, nwZ,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate terrain quads for every non-null tile in a flat tiles array
|
|
||||||
// (length TILE_COUNT, indexed via tileIndex). Returns a Float32Array of
|
|
||||||
// interleaved [u, v, x, y, z] vertices, matching DMF.VERTEX_FLOATS.
|
|
||||||
function buildTerrainVerts(tiles) {
|
|
||||||
const out = [];
|
|
||||||
for(let z = 0; z < CHUNK_DEPTH; z++) {
|
|
||||||
for(let y = 0; y < CHUNK_HEIGHT; y++) {
|
|
||||||
for(let x = 0; x < CHUNK_WIDTH; x++) {
|
|
||||||
const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
|
|
||||||
const corners = RAMP_CORNERS[type];
|
|
||||||
if(!corners) continue;
|
|
||||||
|
|
||||||
const u0 = x / CHUNK_WIDTH;
|
|
||||||
const u1 = (x + 1) / CHUNK_WIDTH;
|
|
||||||
const v0 = y / CHUNK_HEIGHT;
|
|
||||||
const v1 = (y + 1) / CHUNK_HEIGHT;
|
|
||||||
const [sw, se, ne, nw] = corners;
|
|
||||||
|
|
||||||
pushTileQuad(
|
|
||||||
out, u0, u1, v0, v1, x, y,
|
|
||||||
(z + sw) * WORLD_LAYER_HEIGHT,
|
|
||||||
(z + se) * WORLD_LAYER_HEIGHT,
|
|
||||||
(z + ne) * WORLD_LAYER_HEIGHT,
|
|
||||||
(z + nw) * WORLD_LAYER_HEIGHT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Float32Array(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Four edges (SW-SE, SE-NE, NE-NW, NW-SW) of a tile's quad as line
|
|
||||||
// segments, lifted slightly above the terrain surface to avoid
|
|
||||||
// z-fighting - same corner-height convention as pushTileQuad().
|
|
||||||
function pushTileGridLines(out, fx, fy, swZ, seZ, neZ, nwZ) {
|
|
||||||
const eps = 0.01;
|
|
||||||
const sw = [fx, fy, swZ + eps];
|
|
||||||
const se = [fx + 1, fy, seZ + eps];
|
|
||||||
const ne = [fx + 1, fy + 1, neZ + eps];
|
|
||||||
const nw = [fx, fy + 1, nwZ + eps];
|
|
||||||
const edges = [sw, se, se, ne, ne, nw, nw, sw];
|
|
||||||
for(const [x, y, z] of edges) out.push(0, 0, x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate grid-outline line segments for every non-null tile, following
|
|
||||||
// the same per-corner heights as buildTerrainVerts() so lines hug ramps
|
|
||||||
// instead of cutting through them. Returns a Float32Array of interleaved
|
|
||||||
// [u, v, x, y, z] vertices meant to be drawn with gl.LINES (pairs).
|
|
||||||
function buildGridLines(tiles) {
|
|
||||||
const out = [];
|
|
||||||
for(let z = 0; z < CHUNK_DEPTH; z++) {
|
|
||||||
for(let y = 0; y < CHUNK_HEIGHT; y++) {
|
|
||||||
for(let x = 0; x < CHUNK_WIDTH; x++) {
|
|
||||||
const type = tiles[tileIndex(x, y, z)] || TILE_SHAPE_NULL;
|
|
||||||
const corners = RAMP_CORNERS[type];
|
|
||||||
if(!corners) continue;
|
|
||||||
|
|
||||||
const [sw, se, ne, nw] = corners;
|
|
||||||
pushTileGridLines(
|
|
||||||
out, x, y,
|
|
||||||
(z + sw) * WORLD_LAYER_HEIGHT,
|
|
||||||
(z + se) * WORLD_LAYER_HEIGHT,
|
|
||||||
(z + ne) * WORLD_LAYER_HEIGHT,
|
|
||||||
(z + nw) * WORLD_LAYER_HEIGHT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Float32Array(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.freeze({
|
|
||||||
CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, TILE_COUNT, WORLD_LAYER_HEIGHT,
|
|
||||||
TILE_SHAPE_NULL, TILE_SHAPE_GROUND,
|
|
||||||
TILE_SHAPE_RAMP_NORTH, TILE_SHAPE_RAMP_EAST,
|
|
||||||
TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST,
|
|
||||||
TILE_SHAPE_RAMP_NORTHEAST, TILE_SHAPE_RAMP_NORTHWEST,
|
|
||||||
TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST,
|
|
||||||
TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
|
||||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
|
||||||
RAMP_CORNERS, tileIndex, buildTerrainVerts, buildGridLines,
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// DMF - Dusk Mesh Format
|
|
||||||
//
|
|
||||||
// Header (12 bytes):
|
|
||||||
// [0-3] "DMF\0" magic
|
|
||||||
// [4-7] uint32 version (little-endian)
|
|
||||||
// [8-11] uint32 vertCount (little-endian)
|
|
||||||
//
|
|
||||||
// Followed by vertCount vertices, each 20 bytes:
|
|
||||||
// [0-7] float32[2] uv (little-endian)
|
|
||||||
// [8-19] float32[3] pos (little-endian)
|
|
||||||
|
|
||||||
const DMF = (() => {
|
|
||||||
const MAGIC = [0x44, 0x4d, 0x46, 0x00]; // "DMF\0"
|
|
||||||
const VERSION = 1;
|
|
||||||
const HEADER_SIZE = 12;
|
|
||||||
const VERTEX_FLOATS = 5; // uv[2] + pos[3]
|
|
||||||
const VERTEX_SIZE = VERTEX_FLOATS * 4;
|
|
||||||
|
|
||||||
// Decode a DMF ArrayBuffer into { version, vertexCount, floats }, where
|
|
||||||
// floats is a Float32Array of tightly interleaved [u, v, x, y, z] per
|
|
||||||
// vertex - ready to hand straight to gl.bufferData().
|
|
||||||
function decode(buffer) {
|
|
||||||
const bytes = new Uint8Array(buffer);
|
|
||||||
const view = new DataView(buffer);
|
|
||||||
|
|
||||||
if(bytes.length < HEADER_SIZE) throw new Error("File too small to be a valid DMF");
|
|
||||||
if(
|
|
||||||
bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] ||
|
|
||||||
bytes[2] !== MAGIC[2] || bytes[3] !== MAGIC[3]
|
|
||||||
) {
|
|
||||||
throw new Error("Invalid DMF magic bytes - not a DMF file");
|
|
||||||
}
|
|
||||||
|
|
||||||
const version = view.getUint32(4, true);
|
|
||||||
if(version !== VERSION) {
|
|
||||||
throw new Error(`Unsupported DMF version: ${version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const vertexCount = view.getUint32(8, true);
|
|
||||||
const expected = HEADER_SIZE + vertexCount * VERTEX_SIZE;
|
|
||||||
if(bytes.length < expected) {
|
|
||||||
throw new Error(`DMF vertex data truncated (expected ${expected} bytes, got ${bytes.length})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const floats = new Float32Array(vertexCount * VERTEX_FLOATS);
|
|
||||||
for(let i = 0; i < floats.length; i++) {
|
|
||||||
floats[i] = view.getFloat32(HEADER_SIZE + i * 4, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { version, vertexCount, floats };
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.freeze({
|
|
||||||
decode,
|
|
||||||
VERSION, HEADER_SIZE, VERTEX_FLOATS, VERTEX_SIZE,
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h1>Dusk Map Editor</h1>
|
|
||||||
<p class="text-muted">Browse and edit project assets.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<link rel="stylesheet" href="/map/map.css">
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex flex-wrap align-items-end gap-2 mb-2">
|
|
||||||
<div>
|
|
||||||
<label class="form-label mb-0">X</label>
|
|
||||||
<input type="number" id="coordX" class="form-control form-control-sm" style="width:5rem" value="0">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="form-label mb-0">Y</label>
|
|
||||||
<input type="number" id="coordY" class="form-control form-control-sm" style="width:5rem" value="0">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="form-label mb-0">Z</label>
|
|
||||||
<input type="number" id="coordZ" class="form-control form-control-sm" style="width:5rem" value="0">
|
|
||||||
</div>
|
|
||||||
<button id="btnLoad" type="button" class="btn btn-sm btn-secondary">Load</button>
|
|
||||||
|
|
||||||
<div class="dropdown">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
|
||||||
type="button" data-bs-toggle="dropdown"
|
|
||||||
>Browse</button>
|
|
||||||
<ul class="dropdown-menu" id="chunkBrowseList"></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dpad" title="Navigate to the neighboring chunk">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary dpad-n" id="navN" title="North (+Y)">N</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary dpad-w" id="navW" title="West (-X)">W</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary dpad-e" id="navE" title="East (+X)">E</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary dpad-s" id="navS" title="South (-Y)">S</button>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-column gap-1">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="navUp" title="Up (+Z)">Up</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="navDown" title="Down (-Z)">Down</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="btnSave" type="button" class="btn btn-sm btn-primary ms-auto">
|
|
||||||
Save & Compile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="statusLine" class="small text-muted mb-2"> </div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-lg-6">
|
|
||||||
<div class="d-flex align-items-center gap-2 mb-2">
|
|
||||||
<label class="form-label mb-0">Z-Level</label>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="zLevelDown">-</button>
|
|
||||||
<input type="range" id="zLevel" class="form-range" style="width:150px" min="0" max="3" value="0">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="zLevelUp">+</button>
|
|
||||||
<span id="zLevelLabel">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-3">
|
|
||||||
<div id="toolPalette" class="tile-palette"></div>
|
|
||||||
<div id="tilePalette" class="tile-palette"></div>
|
|
||||||
<canvas id="gridCanvas" class="border tile-grid" width="512" height="512"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="mt-3">Meshes</h5>
|
|
||||||
<ul id="meshList" class="list-group mb-2"></ul>
|
|
||||||
<div class="d-flex gap-2 align-items-end flex-wrap">
|
|
||||||
<div style="min-width:220px">
|
|
||||||
<label class="form-label mb-0">Model</label>
|
|
||||||
<select id="meshPicker" class="form-select form-select-sm"></select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="form-label mb-0">X</label>
|
|
||||||
<input id="meshX" type="number" class="form-control form-control-sm" style="width:5rem" value="0">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="form-label mb-0">Y</label>
|
|
||||||
<input id="meshY" type="number" class="form-control form-control-sm" style="width:5rem" value="0">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="form-label mb-0">Z</label>
|
|
||||||
<input id="meshZ" type="number" class="form-control form-control-sm" style="width:5rem" value="0">
|
|
||||||
</div>
|
|
||||||
<button id="btnAddMesh" type="button" class="btn btn-sm btn-outline-primary">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-lg-6">
|
|
||||||
<p class="small text-muted mb-2">Preview (scroll to zoom)</p>
|
|
||||||
<canvas id="previewCanvas" class="border" width="512" height="512"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/common/dmf.js"></script>
|
|
||||||
<script src="/common/chunkterrain.js"></script>
|
|
||||||
<script src="/map/renderer.js"></script>
|
|
||||||
<script src="/map/map.js"></script>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
.tile-palette {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
max-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-swatch {
|
|
||||||
position: relative;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-swatch.active {
|
|
||||||
border-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-swatch-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dpad {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 2rem);
|
|
||||||
grid-template-rows: repeat(3, 2rem);
|
|
||||||
grid-template-areas:
|
|
||||||
". n ."
|
|
||||||
"w . e"
|
|
||||||
". s .";
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dpad-n { grid-area: n; }
|
|
||||||
.dpad-w { grid-area: w; }
|
|
||||||
.dpad-e { grid-area: e; }
|
|
||||||
.dpad-s { grid-area: s; }
|
|
||||||
.dpad button { padding: 0; }
|
|
||||||
|
|
||||||
.tile-grid {
|
|
||||||
image-rendering: pixelated;
|
|
||||||
cursor: crosshair;
|
|
||||||
touch-action: none;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#previewCanvas {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
@@ -1,918 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const SHAPES = [
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_NULL, name: "Erase", color: "#20232a" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_GROUND, name: "Ground", color: "#4caf50" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTH, name: "Ramp North", color: "#2196f3" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_EAST, name: "Ramp East", color: "#03a9f4" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTH, name: "Ramp South", color: "#00bcd4" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_WEST, name: "Ramp West", color: "#009688" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST, name: "Ramp NE", color: "#ff9800" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST, name: "Ramp NW", color: "#ff5722" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST, name: "Ramp SE", color: "#9c27b0" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST, name: "Ramp SW", color: "#e91e63" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER, name: "Ramp NE Inner", color: "#795548" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER, name: "Ramp NW Inner", color: "#607d8b" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER, name: "Ramp SE Inner", color: "#8bc34a" },
|
|
||||||
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER, name: "Ramp SW Inner", color: "#ffc107" },
|
|
||||||
];
|
|
||||||
const SHAPE_COLORS = Object.fromEntries(SHAPES.map((s) => [s.type, s.color]));
|
|
||||||
|
|
||||||
// Arrow direction per ramp, in canvas/screen space: north is up (dy < 0),
|
|
||||||
// east is right (dx > 0) - matches renderGrid()'s north-is-up convention.
|
|
||||||
const SHAPE_DIRECTIONS = {
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_NORTH]: { dx: 0, dy: -1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTH]: { dx: 0, dy: 1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_EAST]: { dx: 1, dy: 0 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_WEST]: { dx: -1, dy: 0 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST]: { dx: 1, dy: -1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST]: { dx: -1, dy: -1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST]: { dx: 1, dy: 1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST]: { dx: -1, dy: 1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER]: { dx: 1, dy: -1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER]: { dx: -1, dy: -1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER]: { dx: 1, dy: 1 },
|
|
||||||
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const INNER_CORNER_SHAPES = new Set([
|
|
||||||
ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER,
|
|
||||||
ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER,
|
|
||||||
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER,
|
|
||||||
ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Draws a single-headed arrow from (tailX, tailY) to (tipX, tipY).
|
|
||||||
function drawArrowSegment(ctx, tailX, tailY, tipX, tipY, size) {
|
|
||||||
ctx.strokeStyle = "#ffffff";
|
|
||||||
ctx.fillStyle = "#ffffff";
|
|
||||||
ctx.lineWidth = Math.max(1, size * 0.08);
|
|
||||||
ctx.lineCap = "round";
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(tailX, tailY);
|
|
||||||
ctx.lineTo(tipX, tipY);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
const headLen = size * 0.18;
|
|
||||||
const angle = Math.atan2(tipY - tailY, tipX - tailX);
|
|
||||||
const leftAngle = angle + Math.PI * 0.8;
|
|
||||||
const rightAngle = angle - Math.PI * 0.8;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(tipX, tipY);
|
|
||||||
ctx.lineTo(tipX + Math.cos(leftAngle) * headLen, tipY + Math.sin(leftAngle) * headLen);
|
|
||||||
ctx.lineTo(tipX + Math.cos(rightAngle) * headLen, tipY + Math.sin(rightAngle) * headLen);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws an arrow centered at (cx, cy) pointing toward the ramp's high
|
|
||||||
// side, so ramp direction is visible at a glance on both the palette
|
|
||||||
// swatches and the tile grid. Used for cardinal ramps and outer-corner
|
|
||||||
// ramps (a single raised corner).
|
|
||||||
function drawArrow(ctx, cx, cy, size, dx, dy) {
|
|
||||||
const mag = Math.hypot(dx, dy) || 1;
|
|
||||||
const ux = dx / mag, uy = dy / mag;
|
|
||||||
const len = size * 0.32;
|
|
||||||
drawArrowSegment(ctx, cx - ux * len, cy - uy * len, cx + ux * len, cy + uy * len, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws a plain right-angle bracket - two line segments meeting at 90
|
|
||||||
// degrees at the tile's corner, one running along each of the two edges
|
|
||||||
// adjacent to it - so inner-corner ramps (three corners raised, one
|
|
||||||
// dropped) read as "the adjacent sides meeting in a 90-degree corner",
|
|
||||||
// distinct from the single diagonal arrow used for outer-corner ramps.
|
|
||||||
function drawCornerBracket(ctx, cx, cy, size, dx, dy) {
|
|
||||||
const cornerDist = size * 0.42;
|
|
||||||
const armLen = size * 0.34;
|
|
||||||
const cornerX = cx + dx * cornerDist;
|
|
||||||
const cornerY = cy + dy * cornerDist;
|
|
||||||
|
|
||||||
ctx.strokeStyle = "#ffffff";
|
|
||||||
ctx.lineWidth = Math.max(1, size * 0.08);
|
|
||||||
ctx.lineCap = "round";
|
|
||||||
ctx.lineJoin = "round";
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(cornerX - dx * armLen, cornerY);
|
|
||||||
ctx.lineTo(cornerX, cornerY);
|
|
||||||
ctx.lineTo(cornerX, cornerY - dy * armLen);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws the direction icon for a ramp tile `type` in a `size`x`size` area
|
|
||||||
// centered at (cx, cy): a diagonal arrow for cardinal/outer-corner ramps,
|
|
||||||
// or a right-angle bracket for inner-corner ramps. No-op for shapes with
|
|
||||||
// no direction (ground, erase).
|
|
||||||
function drawShapeIcon(ctx, cx, cy, size, type) {
|
|
||||||
const dir = SHAPE_DIRECTIONS[type];
|
|
||||||
if(!dir) return;
|
|
||||||
if(INNER_CORNER_SHAPES.has(type)) {
|
|
||||||
drawCornerBracket(ctx, cx, cy, size, dir.dx, dir.dy);
|
|
||||||
} else {
|
|
||||||
drawArrow(ctx, cx, cy, size, dir.dx, dir.dy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws a small pencil icon centered in a `size`x`size` canvas.
|
|
||||||
function drawPencilIcon(ctx, size) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(size / 2, size / 2);
|
|
||||||
ctx.rotate(-Math.PI / 4);
|
|
||||||
|
|
||||||
const shaftW = size * 0.18;
|
|
||||||
const shaftH = size * 0.62;
|
|
||||||
|
|
||||||
ctx.fillStyle = "#e0e0e0";
|
|
||||||
ctx.fillRect(-shaftW / 2, shaftH * 0.28, shaftW, shaftH * 0.22);
|
|
||||||
|
|
||||||
ctx.fillStyle = "#f0c419";
|
|
||||||
ctx.fillRect(-shaftW / 2, -shaftH * 0.5, shaftW, shaftH * 0.78);
|
|
||||||
|
|
||||||
ctx.fillStyle = "#8d6e63";
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(-shaftW / 2, -shaftH * 0.5);
|
|
||||||
ctx.lineTo(shaftW / 2, -shaftH * 0.5);
|
|
||||||
ctx.lineTo(0, -shaftH * 0.5 - shaftW);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws a small paint bucket icon centered in a `size`x`size` canvas.
|
|
||||||
function drawBucketIcon(ctx, size) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(size / 2, size / 2);
|
|
||||||
|
|
||||||
const w = size * 0.5, h = size * 0.34;
|
|
||||||
|
|
||||||
ctx.strokeStyle = "#e0e0e0";
|
|
||||||
ctx.lineWidth = Math.max(1, size * 0.06);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, -h * 0.4, w * 0.4, Math.PI, 0);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.fillStyle = "#03a9f4";
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(-w / 2, -h * 0.2);
|
|
||||||
ctx.lineTo(w / 2, -h * 0.2);
|
|
||||||
ctx.lineTo(w * 0.32, h * 0.6);
|
|
||||||
ctx.lineTo(-w * 0.32, h * 0.6);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(-w * 0.55, h * 0.75, size * 0.06, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOOLS = [
|
|
||||||
{ id: "pencil", name: "Pencil", draw: drawPencilIcon },
|
|
||||||
{ id: "bucket", name: "Paint Bucket", draw: drawBucketIcon },
|
|
||||||
];
|
|
||||||
|
|
||||||
let coord = { x: 0, y: 0, z: 0 };
|
|
||||||
let tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
|
|
||||||
let meshes = [];
|
|
||||||
let chunkExists = false;
|
|
||||||
let currentZLevel = 0;
|
|
||||||
let selectedShape = ChunkTerrain.TILE_SHAPE_GROUND;
|
|
||||||
let zoomWorldH = 20;
|
|
||||||
let dirty = false;
|
|
||||||
let hoverTile = null;
|
|
||||||
let activeTool = "pencil";
|
|
||||||
|
|
||||||
let mapRenderer = null;
|
|
||||||
let terrainMesh = null;
|
|
||||||
let gridLinesMesh = null;
|
|
||||||
let terrainTexturePromise = null;
|
|
||||||
let modelIndex = null;
|
|
||||||
let neighborChunks = [];
|
|
||||||
const modelCache = new Map();
|
|
||||||
|
|
||||||
const UNDO_LIMIT = 100;
|
|
||||||
let undoStack = [];
|
|
||||||
|
|
||||||
function markDirty() {
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapshotState() {
|
|
||||||
return {
|
|
||||||
tiles: tiles.slice(),
|
|
||||||
meshes: meshes.map((m) => ({ file: m.file, pos: m.pos.slice() })),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushUndo() {
|
|
||||||
undoStack.push(snapshotState());
|
|
||||||
if(undoStack.length > UNDO_LIMIT) undoStack.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
function undo() {
|
|
||||||
if(!undoStack.length) return;
|
|
||||||
const snap = undoStack.pop();
|
|
||||||
tiles = snap.tiles;
|
|
||||||
meshes = snap.meshes;
|
|
||||||
dirty = true;
|
|
||||||
renderGrid();
|
|
||||||
renderMeshList();
|
|
||||||
renderPreview();
|
|
||||||
updateStatus("Undid last change.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if it's OK to proceed (no unsaved changes, or the user
|
|
||||||
// confirmed discarding them).
|
|
||||||
function confirmDiscardIfDirty() {
|
|
||||||
if(!dirty) return true;
|
|
||||||
return window.confirm("You have unsaved changes to this chunk. Discard them?");
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusLine() {
|
|
||||||
return document.getElementById("statusLine");
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStatus(msg, isError) {
|
|
||||||
const el = statusLine();
|
|
||||||
el.textContent = msg;
|
|
||||||
el.classList.toggle("text-danger", !!isError);
|
|
||||||
el.classList.toggle("text-muted", !isError);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiReadJson(path) {
|
|
||||||
const res = await fetch(`/api/read?path=${encodeURIComponent(path)}`);
|
|
||||||
const data = await res.json();
|
|
||||||
return { ok: res.ok, data };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiWrite(path, content) {
|
|
||||||
return fetch(`/api/write?path=${encodeURIComponent(path)}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ content }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiDelete(path) {
|
|
||||||
return fetch(`/api/delete?path=${encodeURIComponent(path)}`, { method: "POST" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiCompileChunk(x, y, z) {
|
|
||||||
const res = await fetch(`/api/compile-chunk?x=${x}&y=${y}&z=${z}`, { method: "POST" });
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiLs(path) {
|
|
||||||
const res = await fetch(`/api/ls?path=${encodeURIComponent(path)}`);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiFind(path, ext) {
|
|
||||||
const res = await fetch(`/api/find?path=${encodeURIComponent(path)}&ext=${encodeURIComponent(ext)}`);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64ToBuffer(base64) {
|
|
||||||
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
|
||||||
return bytes.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadImageTexture(assetPath) {
|
|
||||||
const { ok, data } = await apiReadJson(assetPath);
|
|
||||||
if(!ok || data.error) return null;
|
|
||||||
const blob = new Blob([base64ToBuffer(data.content)], { type: "image/png" });
|
|
||||||
const bitmap = await createImageBitmap(blob);
|
|
||||||
return mapRenderer.createTextureFromImage(bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readDmfVertices(assetPath) {
|
|
||||||
const { ok, data } = await apiReadJson(assetPath);
|
|
||||||
if(!ok || data.error) throw new Error(`Failed to read ${assetPath}`);
|
|
||||||
return DMF.decode(base64ToBuffer(data.content)).floats;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureModelIndex() {
|
|
||||||
if(modelIndex) return modelIndex;
|
|
||||||
const res = await apiFind("assets/models", ".json");
|
|
||||||
modelIndex = new Map();
|
|
||||||
for(const file of res.files || []) {
|
|
||||||
const base = file.split("/").pop().replace(/\.json$/, "");
|
|
||||||
modelIndex.set(base, file);
|
|
||||||
}
|
|
||||||
return modelIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scales a mesh's stored [x, y, z] offset for preview, matching the
|
|
||||||
// Z scaling mapChunkLoaded() applies at runtime (chunk.meshOffsets[m][2]
|
|
||||||
// * WORLD_LAYER_HEIGHT) - x/y stay 1:1 since only Z is height-scaled.
|
|
||||||
function scaledMeshOffset(pos) {
|
|
||||||
return [pos[0], pos[1], pos[2] * ChunkTerrain.WORLD_LAYER_HEIGHT];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolves a chunk mesh entry's bare filename (e.g. "house_5_3.dmf") to
|
|
||||||
// its model JSON by basename, mirroring find_model() in
|
|
||||||
// tools/asset/chunk/__main__.py - the chunk JSON only ever stores a bare
|
|
||||||
// filename, not a path.
|
|
||||||
async function resolveModelForMeshFile(file) {
|
|
||||||
await ensureModelIndex();
|
|
||||||
const base = file.replace(/\.[^.]+$/, "");
|
|
||||||
const rel = modelIndex.get(base);
|
|
||||||
if(!rel) return null;
|
|
||||||
|
|
||||||
const modelPath = `assets/models/${rel}`;
|
|
||||||
if(modelCache.has(modelPath)) return modelCache.get(modelPath);
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
const { ok, data } = await apiReadJson(modelPath);
|
|
||||||
if(!ok || data.error) throw new Error(`Failed to read model ${modelPath}`);
|
|
||||||
const modelDef = JSON.parse(data.content);
|
|
||||||
const meshFloats = await readDmfVertices(`assets/${modelDef.mesh}`);
|
|
||||||
const mesh = mapRenderer.createMesh(meshFloats);
|
|
||||||
const texture = modelDef.texture ? await loadImageTexture(`assets/${modelDef.texture}`) : null;
|
|
||||||
const colorBytes = modelDef.color || [255, 255, 255, 255];
|
|
||||||
const color = colorBytes.map((c) => c / 255);
|
|
||||||
return { mesh, texture, color };
|
|
||||||
})();
|
|
||||||
modelCache.set(modelPath, promise);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function chunkPaths(x, y, z) {
|
|
||||||
return {
|
|
||||||
raw: `assetsraw/chunks/chunk_${x}_${y}_${z}.json`,
|
|
||||||
dcf: `assets/chunks/${x}_${y}_${z}.dcf`,
|
|
||||||
terrainDmf: `assets/meshes/chunks/chunk_${x}_${y}_${z}_0.dmf`,
|
|
||||||
terrainModel: `assets/models/chunks/chunk_${x}_${y}_${z}_0.json`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads the 8 same-Z neighbors around `coord` (read-only, for context in
|
|
||||||
// the 3D preview) and pre-builds their render data so renderPreview()
|
|
||||||
// doesn't have to rebuild GL buffers on every paint stroke.
|
|
||||||
async function loadNeighborChunks() {
|
|
||||||
const offsets = [];
|
|
||||||
for(let dy = -1; dy <= 1; dy++) {
|
|
||||||
for(let dx = -1; dx <= 1; dx++) {
|
|
||||||
if(dx || dy) offsets.push({ dx, dy });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(offsets.map(async ({ dx, dy }) => {
|
|
||||||
const nx = coord.x + dx, ny = coord.y + dy, nz = coord.z;
|
|
||||||
const { ok, data } = await apiReadJson(chunkPaths(nx, ny, nz).raw);
|
|
||||||
if(!ok || data.error) return null;
|
|
||||||
|
|
||||||
const json = JSON.parse(data.content);
|
|
||||||
const neighborTiles = new Int32Array(ChunkTerrain.TILE_COUNT);
|
|
||||||
for(const t of json.tiles || []) {
|
|
||||||
const [tx, ty, tz] = t.pos;
|
|
||||||
neighborTiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type;
|
|
||||||
}
|
|
||||||
const neighborMeshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] }));
|
|
||||||
|
|
||||||
const floats = ChunkTerrain.buildTerrainVerts(neighborTiles);
|
|
||||||
const terrain = floats.length ? { mesh: mapRenderer.createMesh(floats) } : null;
|
|
||||||
|
|
||||||
const models = [];
|
|
||||||
for(const m of neighborMeshes) {
|
|
||||||
try {
|
|
||||||
const resolved = await resolveModelForMeshFile(m.file);
|
|
||||||
if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
|
|
||||||
} catch(e) {
|
|
||||||
console.warn("Failed to load neighbor mesh preview", m.file, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
offset: [dx * ChunkTerrain.CHUNK_WIDTH, dy * ChunkTerrain.CHUNK_HEIGHT, 0],
|
|
||||||
terrain,
|
|
||||||
models,
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
neighborChunks = results.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadChunk(x, y, z) {
|
|
||||||
coord = { x, y, z };
|
|
||||||
tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
|
|
||||||
meshes = [];
|
|
||||||
chunkExists = false;
|
|
||||||
|
|
||||||
const { ok, data } = await apiReadJson(chunkPaths(x, y, z).raw);
|
|
||||||
if(ok && !data.error) {
|
|
||||||
chunkExists = true;
|
|
||||||
const json = JSON.parse(data.content);
|
|
||||||
for(const t of json.tiles || []) {
|
|
||||||
const [tx, ty, tz] = t.pos;
|
|
||||||
tiles[ChunkTerrain.tileIndex(tx, ty, tz)] = t.type;
|
|
||||||
}
|
|
||||||
meshes = (json.meshes || []).map((m) => ({ file: m.file, pos: m.pos || [0, 0, 0] }));
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadNeighborChunks();
|
|
||||||
dirty = false;
|
|
||||||
undoStack = [];
|
|
||||||
|
|
||||||
renderGrid();
|
|
||||||
renderMeshList();
|
|
||||||
renderPreview();
|
|
||||||
updateStatus(chunkExists
|
|
||||||
? `Loaded chunk_${x}_${y}_${z}.json`
|
|
||||||
: `New (empty) chunk at ${x}, ${y}, ${z} - paint a tile and save to create it.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveChunk() {
|
|
||||||
const tilesArr = [];
|
|
||||||
for(let z = 0; z < ChunkTerrain.CHUNK_DEPTH; z++) {
|
|
||||||
for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) {
|
|
||||||
for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) {
|
|
||||||
const type = tiles[ChunkTerrain.tileIndex(x, y, z)];
|
|
||||||
if(type && type !== ChunkTerrain.TILE_SHAPE_NULL) {
|
|
||||||
tilesArr.push({ pos: [x, y, z], type, tile: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const paths = chunkPaths(coord.x, coord.y, coord.z);
|
|
||||||
|
|
||||||
if(tilesArr.length === 0 && meshes.length === 0) {
|
|
||||||
await Promise.all(Object.values(paths).map((p) => apiDelete(p)));
|
|
||||||
chunkExists = false;
|
|
||||||
dirty = false;
|
|
||||||
updateStatus(`Chunk ${coord.x}, ${coord.y}, ${coord.z} is empty - deleted.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = { tiles: tilesArr, meshes: meshes.map((m) => ({ file: m.file, pos: m.pos })) };
|
|
||||||
await apiWrite(paths.raw, JSON.stringify(json, null, 2));
|
|
||||||
chunkExists = true;
|
|
||||||
dirty = false;
|
|
||||||
updateStatus("Saved. Compiling...");
|
|
||||||
|
|
||||||
const result = await apiCompileChunk(coord.x, coord.y, coord.z);
|
|
||||||
if(result.ok) {
|
|
||||||
updateStatus(`Compiled OK.\n${result.stdout}`);
|
|
||||||
} else {
|
|
||||||
updateStatus(`Compile FAILED (code ${result.returncode}):\n${result.stderr}`, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPalette() {
|
|
||||||
const el = document.getElementById("tilePalette");
|
|
||||||
el.innerHTML = "";
|
|
||||||
for(const s of SHAPES) {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "tile-swatch" + (s.type === selectedShape ? " active" : "");
|
|
||||||
btn.style.background = s.color;
|
|
||||||
btn.title = s.name;
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
selectedShape = s.type;
|
|
||||||
buildPalette();
|
|
||||||
});
|
|
||||||
|
|
||||||
if(SHAPE_DIRECTIONS[s.type]) {
|
|
||||||
const icon = document.createElement("canvas");
|
|
||||||
icon.width = 32;
|
|
||||||
icon.height = 32;
|
|
||||||
icon.className = "tile-swatch-icon";
|
|
||||||
drawShapeIcon(icon.getContext("2d"), 16, 16, 32, s.type);
|
|
||||||
btn.appendChild(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
el.appendChild(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildToolPalette() {
|
|
||||||
const el = document.getElementById("toolPalette");
|
|
||||||
el.innerHTML = "";
|
|
||||||
for(const tool of TOOLS) {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "tile-swatch" + (tool.id === activeTool ? " active" : "");
|
|
||||||
btn.style.background = "#2a2d33";
|
|
||||||
btn.title = tool.name;
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
activeTool = tool.id;
|
|
||||||
buildToolPalette();
|
|
||||||
});
|
|
||||||
|
|
||||||
const icon = document.createElement("canvas");
|
|
||||||
icon.width = 32;
|
|
||||||
icon.height = 32;
|
|
||||||
icon.className = "tile-swatch-icon";
|
|
||||||
tool.draw(icon.getContext("2d"), 32);
|
|
||||||
btn.appendChild(icon);
|
|
||||||
|
|
||||||
el.appendChild(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function canvasToTile(e, canvas) {
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
|
|
||||||
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
|
|
||||||
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
|
|
||||||
const x = Math.floor(px / cell);
|
|
||||||
const yFromTop = Math.floor(py / cell);
|
|
||||||
const y = ChunkTerrain.CHUNK_HEIGHT - 1 - yFromTop;
|
|
||||||
if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) return null;
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGrid() {
|
|
||||||
const canvas = document.getElementById("gridCanvas");
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
for(let y = 0; y < ChunkTerrain.CHUNK_HEIGHT; y++) {
|
|
||||||
for(let x = 0; x < ChunkTerrain.CHUNK_WIDTH; x++) {
|
|
||||||
const type = tiles[ChunkTerrain.tileIndex(x, y, currentZLevel)];
|
|
||||||
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - y) * cell;
|
|
||||||
ctx.fillStyle = SHAPE_COLORS[type] || "#15171b";
|
|
||||||
ctx.fillRect(x * cell, sy, cell, cell);
|
|
||||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.25)";
|
|
||||||
ctx.strokeRect(x * cell, sy, cell, cell);
|
|
||||||
|
|
||||||
drawShapeIcon(ctx, x * cell + cell / 2, sy + cell / 2, cell, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff";
|
|
||||||
for(const m of meshes) {
|
|
||||||
const [mx, my, mz] = m.pos;
|
|
||||||
if(Math.round(mz) !== currentZLevel) continue;
|
|
||||||
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(mx * cell + cell / 2, sy + cell / 2, cell * 0.3, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(hoverTile) {
|
|
||||||
const sy = (ChunkTerrain.CHUNK_HEIGHT - 1 - hoverTile.y) * cell;
|
|
||||||
ctx.strokeStyle = "#ffeb3b";
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.strokeRect(hoverTile.x * cell + 1, sy + 1, cell - 2, cell - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hitTestMesh(e, canvas) {
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
|
|
||||||
const px = (e.clientX - rect.left) * (canvas.width / rect.width);
|
|
||||||
const py = (e.clientY - rect.top) * (canvas.height / rect.height);
|
|
||||||
|
|
||||||
let closest = -1;
|
|
||||||
let closestDist = cell * 0.5;
|
|
||||||
meshes.forEach((m, i) => {
|
|
||||||
const [mx, my, mz] = m.pos;
|
|
||||||
if(Math.round(mz) !== currentZLevel) return;
|
|
||||||
const cx = mx * cell + cell / 2;
|
|
||||||
const cy = (ChunkTerrain.CHUNK_HEIGHT - 1 - my) * cell + cell / 2;
|
|
||||||
const dist = Math.hypot(px - cx, py - cy);
|
|
||||||
if(dist < closestDist) { closestDist = dist; closest = i; }
|
|
||||||
});
|
|
||||||
return closest;
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveMeshTo(e, index) {
|
|
||||||
const canvas = document.getElementById("gridCanvas");
|
|
||||||
const t = canvasToTile(e, canvas);
|
|
||||||
if(!t) return;
|
|
||||||
meshes[index].pos[0] = t.x;
|
|
||||||
meshes[index].pos[1] = t.y;
|
|
||||||
markDirty();
|
|
||||||
renderGrid();
|
|
||||||
renderMeshList();
|
|
||||||
renderPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
function paintAt(e) {
|
|
||||||
const canvas = document.getElementById("gridCanvas");
|
|
||||||
const t = canvasToTile(e, canvas);
|
|
||||||
if(!t) return;
|
|
||||||
tiles[ChunkTerrain.tileIndex(t.x, t.y, currentZLevel)] = selectedShape;
|
|
||||||
markDirty();
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4-connected flood fill of same-type tiles at the current Z-level,
|
|
||||||
// starting from (startX, startY), replacing them with selectedShape.
|
|
||||||
function floodFill(startX, startY) {
|
|
||||||
const targetType = tiles[ChunkTerrain.tileIndex(startX, startY, currentZLevel)];
|
|
||||||
if(targetType === selectedShape) return;
|
|
||||||
|
|
||||||
const stack = [[startX, startY]];
|
|
||||||
const visited = new Set();
|
|
||||||
while(stack.length) {
|
|
||||||
const [x, y] = stack.pop();
|
|
||||||
const key = `${x},${y}`;
|
|
||||||
if(visited.has(key)) continue;
|
|
||||||
if(x < 0 || x >= ChunkTerrain.CHUNK_WIDTH || y < 0 || y >= ChunkTerrain.CHUNK_HEIGHT) continue;
|
|
||||||
visited.add(key);
|
|
||||||
|
|
||||||
const idx = ChunkTerrain.tileIndex(x, y, currentZLevel);
|
|
||||||
if(tiles[idx] !== targetType) continue;
|
|
||||||
tiles[idx] = selectedShape;
|
|
||||||
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillAt(e) {
|
|
||||||
const canvas = document.getElementById("gridCanvas");
|
|
||||||
const t = canvasToTile(e, canvas);
|
|
||||||
if(!t) return;
|
|
||||||
pushUndo();
|
|
||||||
floodFill(t.x, t.y);
|
|
||||||
markDirty();
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildTerrainMesh() {
|
|
||||||
const floats = ChunkTerrain.buildTerrainVerts(tiles);
|
|
||||||
terrainMesh = floats.length ? mapRenderer.createMesh(floats) : null;
|
|
||||||
|
|
||||||
const lineFloats = ChunkTerrain.buildGridLines(tiles);
|
|
||||||
gridLinesMesh = lineFloats.length ? mapRenderer.createMesh(lineFloats) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keeps the preview canvas's backing-store resolution matched to its
|
|
||||||
// actual displayed CSS size (times devicePixelRatio) - the canvas
|
|
||||||
// otherwise stays at its fixed width/height attributes (512x512) while
|
|
||||||
// CSS stretches it to fill the column, which upscales and blurs the
|
|
||||||
// WebGL output on wider or high-DPI screens.
|
|
||||||
function resizePreviewCanvas() {
|
|
||||||
const canvas = document.getElementById("previewCanvas");
|
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const displayWidth = Math.max(1, Math.round(canvas.clientWidth * dpr));
|
|
||||||
const displayHeight = Math.max(1, Math.round(canvas.clientHeight * dpr));
|
|
||||||
if(canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
|
||||||
canvas.width = displayWidth;
|
|
||||||
canvas.height = displayHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderPreview() {
|
|
||||||
if(!mapRenderer) return;
|
|
||||||
resizePreviewCanvas();
|
|
||||||
rebuildTerrainMesh();
|
|
||||||
|
|
||||||
const models = [];
|
|
||||||
for(const m of meshes) {
|
|
||||||
try {
|
|
||||||
const resolved = await resolveModelForMeshFile(m.file);
|
|
||||||
if(resolved) models.push({ ...resolved, offset: scaledMeshOffset(m.pos) });
|
|
||||||
} catch(e) {
|
|
||||||
console.warn("Failed to load mesh preview", m.file, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let terrainTexture = null;
|
|
||||||
if(terrainMesh || neighborChunks.some((n) => n.terrain)) {
|
|
||||||
if(!terrainTexturePromise) terrainTexturePromise = loadImageTexture("assets/tiles.png");
|
|
||||||
terrainTexture = await terrainTexturePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const neighbors = neighborChunks.map((n) => ({
|
|
||||||
offset: n.offset,
|
|
||||||
terrain: n.terrain ? { mesh: n.terrain.mesh, texture: terrainTexture } : null,
|
|
||||||
models: n.models,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const zLevelHeight = currentZLevel * ChunkTerrain.WORLD_LAYER_HEIGHT;
|
|
||||||
mapRenderer.render({
|
|
||||||
target: [ChunkTerrain.CHUNK_WIDTH / 2, ChunkTerrain.CHUNK_HEIGHT / 2, zLevelHeight],
|
|
||||||
worldH: zoomWorldH,
|
|
||||||
terrain: terrainMesh ? { mesh: terrainMesh, texture: terrainTexture } : null,
|
|
||||||
gridLines: gridLinesMesh ? { mesh: gridLinesMesh } : null,
|
|
||||||
models,
|
|
||||||
neighbors,
|
|
||||||
highlight: hoverTile ? { x: hoverTile.x, y: hoverTile.y, z: zLevelHeight } : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMeshList() {
|
|
||||||
const ul = document.getElementById("meshList");
|
|
||||||
ul.innerHTML = "";
|
|
||||||
meshes.forEach((m, i) => {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.className = "list-group-item d-flex align-items-center gap-2 flex-wrap";
|
|
||||||
|
|
||||||
const label = document.createElement("span");
|
|
||||||
label.className = "me-auto";
|
|
||||||
label.textContent = m.file;
|
|
||||||
li.appendChild(label);
|
|
||||||
|
|
||||||
["X", "Y", "Z"].forEach((axis, axisIndex) => {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "number";
|
|
||||||
input.className = "form-control form-control-sm";
|
|
||||||
input.style.width = "5rem";
|
|
||||||
input.title = axis;
|
|
||||||
input.value = m.pos[axisIndex];
|
|
||||||
input.addEventListener("change", () => {
|
|
||||||
pushUndo();
|
|
||||||
m.pos[axisIndex] = parseFloat(input.value) || 0;
|
|
||||||
markDirty();
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
});
|
|
||||||
li.appendChild(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "btn btn-sm btn-outline-danger";
|
|
||||||
btn.textContent = "Remove";
|
|
||||||
btn.addEventListener("click", () => {
|
|
||||||
pushUndo();
|
|
||||||
meshes.splice(i, 1);
|
|
||||||
markDirty();
|
|
||||||
renderMeshList();
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
li.appendChild(btn);
|
|
||||||
ul.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildMeshPicker() {
|
|
||||||
await ensureModelIndex();
|
|
||||||
const select = document.getElementById("meshPicker");
|
|
||||||
select.innerHTML = "";
|
|
||||||
for(const [base, rel] of [...modelIndex.entries()].sort()) {
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = base;
|
|
||||||
opt.textContent = rel;
|
|
||||||
select.appendChild(opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildChunkBrowseList() {
|
|
||||||
const list = document.getElementById("chunkBrowseList");
|
|
||||||
list.innerHTML = "";
|
|
||||||
const res = await apiLs("assetsraw/chunks");
|
|
||||||
for(const entry of res.entries || []) {
|
|
||||||
const m = entry.name.match(/^chunk_(-?\d+)_(-?\d+)_(-?\d+)\.json$/);
|
|
||||||
if(!m) continue;
|
|
||||||
const [, x, y, z] = m;
|
|
||||||
|
|
||||||
const li = document.createElement("li");
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.className = "dropdown-item";
|
|
||||||
a.href = "#";
|
|
||||||
a.textContent = `${x}, ${y}, ${z}`;
|
|
||||||
a.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
goToChunk(parseInt(x, 10), parseInt(y, 10), parseInt(z, 10));
|
|
||||||
});
|
|
||||||
|
|
||||||
li.appendChild(a);
|
|
||||||
list.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCoordInputs(x, y, z) {
|
|
||||||
document.getElementById("coordX").value = x;
|
|
||||||
document.getElementById("coordY").value = y;
|
|
||||||
document.getElementById("coordZ").value = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToChunk(x, y, z) {
|
|
||||||
if(!confirmDiscardIfDirty()) return;
|
|
||||||
setCoordInputs(x, y, z);
|
|
||||||
loadChunk(x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindEvents() {
|
|
||||||
const gridCanvas = document.getElementById("gridCanvas");
|
|
||||||
let painting = false;
|
|
||||||
let draggingMeshIndex = -1;
|
|
||||||
|
|
||||||
gridCanvas.addEventListener("mousedown", (e) => {
|
|
||||||
if(activeTool === "bucket") {
|
|
||||||
fillAt(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hit = hitTestMesh(e, gridCanvas);
|
|
||||||
pushUndo();
|
|
||||||
if(hit !== -1) {
|
|
||||||
draggingMeshIndex = hit;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
painting = true;
|
|
||||||
paintAt(e);
|
|
||||||
});
|
|
||||||
window.addEventListener("mouseup", () => { painting = false; draggingMeshIndex = -1; });
|
|
||||||
gridCanvas.addEventListener("mousemove", (e) => {
|
|
||||||
hoverTile = canvasToTile(e, gridCanvas);
|
|
||||||
if(draggingMeshIndex !== -1) { moveMeshTo(e, draggingMeshIndex); return; }
|
|
||||||
if(painting) { paintAt(e); return; }
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
});
|
|
||||||
gridCanvas.addEventListener("mouseleave", () => {
|
|
||||||
hoverTile = null;
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setZLevel(z) {
|
|
||||||
currentZLevel = Math.max(0, Math.min(ChunkTerrain.CHUNK_DEPTH - 1, z));
|
|
||||||
document.getElementById("zLevel").value = currentZLevel;
|
|
||||||
document.getElementById("zLevelLabel").textContent = currentZLevel;
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("zLevel").addEventListener("input", (e) => {
|
|
||||||
setZLevel(parseInt(e.target.value, 10));
|
|
||||||
});
|
|
||||||
document.getElementById("zLevelDown").addEventListener("click", () => setZLevel(currentZLevel - 1));
|
|
||||||
document.getElementById("zLevelUp").addEventListener("click", () => setZLevel(currentZLevel + 1));
|
|
||||||
|
|
||||||
document.getElementById("btnLoad").addEventListener("click", () => {
|
|
||||||
const x = parseInt(document.getElementById("coordX").value, 10) || 0;
|
|
||||||
const y = parseInt(document.getElementById("coordY").value, 10) || 0;
|
|
||||||
const z = parseInt(document.getElementById("coordZ").value, 10) || 0;
|
|
||||||
goToChunk(x, y, z);
|
|
||||||
});
|
|
||||||
|
|
||||||
// NORTH=+Y, EAST=+X (matches tile-shape convention used by the tile
|
|
||||||
// grid and tools/asset/chunk/__main__.py).
|
|
||||||
document.getElementById("navN").addEventListener("click", () => goToChunk(coord.x, coord.y + 1, coord.z));
|
|
||||||
document.getElementById("navS").addEventListener("click", () => goToChunk(coord.x, coord.y - 1, coord.z));
|
|
||||||
document.getElementById("navE").addEventListener("click", () => goToChunk(coord.x + 1, coord.y, coord.z));
|
|
||||||
document.getElementById("navW").addEventListener("click", () => goToChunk(coord.x - 1, coord.y, coord.z));
|
|
||||||
document.getElementById("navUp").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z + 1));
|
|
||||||
document.getElementById("navDown").addEventListener("click", () => goToChunk(coord.x, coord.y, coord.z - 1));
|
|
||||||
|
|
||||||
function runSave() {
|
|
||||||
saveChunk().catch((e) => updateStatus(`Save failed: ${e.message}`, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("btnSave").addEventListener("click", runSave);
|
|
||||||
|
|
||||||
document.getElementById("btnAddMesh").addEventListener("click", () => {
|
|
||||||
const base = document.getElementById("meshPicker").value;
|
|
||||||
if(!base) return;
|
|
||||||
const pos = [
|
|
||||||
parseFloat(document.getElementById("meshX").value) || 0,
|
|
||||||
parseFloat(document.getElementById("meshY").value) || 0,
|
|
||||||
parseFloat(document.getElementById("meshZ").value) || 0,
|
|
||||||
];
|
|
||||||
pushUndo();
|
|
||||||
meshes.push({ file: `${base}.dmf`, pos });
|
|
||||||
markDirty();
|
|
||||||
renderMeshList();
|
|
||||||
renderGrid();
|
|
||||||
renderPreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("previewCanvas").addEventListener("wheel", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
zoomWorldH = Math.min(60, Math.max(4, zoomWorldH + Math.sign(e.deltaY)));
|
|
||||||
renderPreview();
|
|
||||||
}, { passive: false });
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => renderPreview());
|
|
||||||
|
|
||||||
window.addEventListener("keydown", (e) => {
|
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
|
||||||
const key = e.key.toLowerCase();
|
|
||||||
if(key === "z") {
|
|
||||||
e.preventDefault();
|
|
||||||
undo();
|
|
||||||
} else if(key === "s") {
|
|
||||||
e.preventDefault();
|
|
||||||
runSave();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
|
||||||
buildPalette();
|
|
||||||
buildToolPalette();
|
|
||||||
mapRenderer = MapRenderer.create(document.getElementById("previewCanvas"));
|
|
||||||
bindEvents();
|
|
||||||
await Promise.all([buildChunkBrowseList(), buildMeshPicker()]);
|
|
||||||
await loadChunk(coord.x, coord.y, coord.z);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// MapRenderer - a minimal, dependency-free WebGL renderer for the chunk
|
|
||||||
// preview pane. The camera formula is ported 1:1 from
|
|
||||||
// src/dusk/scene/overworld/sceneoverworld.c (lines 62-81) so the preview
|
|
||||||
// approximates the game's own fixed-angle 3rd-person camera: the eye sits
|
|
||||||
// worldH units "behind" the target along Y and a proportional distance
|
|
||||||
// "above" along Z, always looking at the target, up = (0, 1, 0).
|
|
||||||
//
|
|
||||||
// The material model mirrors SHADER_UNLIT: a texture sampler multiplied by
|
|
||||||
// a flat RGBA tint, with a 1x1 white fallback texture standing in for
|
|
||||||
// untextured models.
|
|
||||||
|
|
||||||
const MapRenderer = (() => {
|
|
||||||
const VERT_SRC = `
|
|
||||||
attribute vec2 aUV;
|
|
||||||
attribute vec3 aPos;
|
|
||||||
uniform mat4 uModel;
|
|
||||||
uniform mat4 uView;
|
|
||||||
uniform mat4 uProjection;
|
|
||||||
varying vec2 vUV;
|
|
||||||
void main() {
|
|
||||||
vUV = aUV;
|
|
||||||
gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FRAG_SRC = `
|
|
||||||
precision mediump float;
|
|
||||||
varying vec2 vUV;
|
|
||||||
uniform sampler2D uTexture;
|
|
||||||
uniform vec4 uColor;
|
|
||||||
void main() {
|
|
||||||
gl_FragColor = texture2D(uTexture, vUV) * uColor;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function mat4Identity(out) {
|
|
||||||
out.fill(0);
|
|
||||||
out[0] = out[5] = out[10] = out[15] = 1;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mat4Translation(out, v) {
|
|
||||||
mat4Identity(out);
|
|
||||||
out[12] = v[0];
|
|
||||||
out[13] = v[1];
|
|
||||||
out[14] = v[2];
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mat4Perspective(out, fovy, aspect, near, far) {
|
|
||||||
const f = 1.0 / Math.tan(fovy / 2);
|
|
||||||
out.fill(0);
|
|
||||||
out[0] = f / aspect;
|
|
||||||
out[5] = f;
|
|
||||||
out[10] = (far + near) / (near - far);
|
|
||||||
out[11] = -1;
|
|
||||||
out[14] = (2 * far * near) / (near - far);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function vec3Sub(a, b) {
|
|
||||||
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
||||||
}
|
|
||||||
|
|
||||||
function vec3Cross(a, b) {
|
|
||||||
return [
|
|
||||||
a[1] * b[2] - a[2] * b[1],
|
|
||||||
a[2] * b[0] - a[0] * b[2],
|
|
||||||
a[0] * b[1] - a[1] * b[0],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function vec3Normalize(a) {
|
|
||||||
const len = Math.hypot(a[0], a[1], a[2]) || 1;
|
|
||||||
return [a[0] / len, a[1] / len, a[2] / len];
|
|
||||||
}
|
|
||||||
|
|
||||||
function mat4LookAt(out, eye, center, up) {
|
|
||||||
const zAxis = vec3Normalize(vec3Sub(eye, center));
|
|
||||||
const xAxis = vec3Normalize(vec3Cross(up, zAxis));
|
|
||||||
const yAxis = vec3Cross(zAxis, xAxis);
|
|
||||||
|
|
||||||
out[0] = xAxis[0]; out[1] = yAxis[0]; out[2] = zAxis[0]; out[3] = 0;
|
|
||||||
out[4] = xAxis[1]; out[5] = yAxis[1]; out[6] = zAxis[1]; out[7] = 0;
|
|
||||||
out[8] = xAxis[2]; out[9] = yAxis[2]; out[10] = zAxis[2]; out[11] = 0;
|
|
||||||
out[12] = -(xAxis[0] * eye[0] + xAxis[1] * eye[1] + xAxis[2] * eye[2]);
|
|
||||||
out[13] = -(yAxis[0] * eye[0] + yAxis[1] * eye[1] + yAxis[2] * eye[2]);
|
|
||||||
out[14] = -(zAxis[0] * eye[0] + zAxis[1] * eye[1] + zAxis[2] * eye[2]);
|
|
||||||
out[15] = 1;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ported from sceneoverworld.c:62-81. Returns { eye, view }.
|
|
||||||
function computeCamera(target, worldH) {
|
|
||||||
const fov = Math.PI / 4; // glm_rad(45.0f)
|
|
||||||
const zDist = (worldH * 0.5) / Math.tan(fov * 0.5);
|
|
||||||
const offset = -worldH;
|
|
||||||
|
|
||||||
const eye = [target[0], target[1] + offset, target[2] + zDist];
|
|
||||||
const view = mat4LookAt(new Float32Array(16), eye, target, [0, 1, 0]);
|
|
||||||
return { eye, view, fov };
|
|
||||||
}
|
|
||||||
|
|
||||||
function compileShader(gl, type, src) {
|
|
||||||
const shader = gl.createShader(type);
|
|
||||||
gl.shaderSource(shader, src);
|
|
||||||
gl.compileShader(shader);
|
|
||||||
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
||||||
const info = gl.getShaderInfoLog(shader);
|
|
||||||
gl.deleteShader(shader);
|
|
||||||
throw new Error(`Shader compile failed: ${info}`);
|
|
||||||
}
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
function create(canvas) {
|
|
||||||
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
||||||
if(!gl) throw new Error("WebGL is not available in this browser");
|
|
||||||
|
|
||||||
const program = gl.createProgram();
|
|
||||||
gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, VERT_SRC));
|
|
||||||
gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC));
|
|
||||||
gl.linkProgram(program);
|
|
||||||
if(!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
||||||
throw new Error(`Program link failed: ${gl.getProgramInfoLog(program)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const attribs = {
|
|
||||||
aUV: gl.getAttribLocation(program, "aUV"),
|
|
||||||
aPos: gl.getAttribLocation(program, "aPos"),
|
|
||||||
};
|
|
||||||
const uniforms = {
|
|
||||||
uModel: gl.getUniformLocation(program, "uModel"),
|
|
||||||
uView: gl.getUniformLocation(program, "uView"),
|
|
||||||
uProjection: gl.getUniformLocation(program, "uProjection"),
|
|
||||||
uTexture: gl.getUniformLocation(program, "uTexture"),
|
|
||||||
uColor: gl.getUniformLocation(program, "uColor"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const whiteTexture = gl.createTexture();
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, whiteTexture);
|
|
||||||
gl.texImage2D(
|
|
||||||
gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
|
|
||||||
new Uint8Array([255, 255, 255, 255]),
|
|
||||||
);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
|
|
||||||
gl.enable(gl.DEPTH_TEST);
|
|
||||||
gl.enable(gl.BLEND);
|
|
||||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
||||||
// Terrain/mesh winding isn't guaranteed consistent relative to this
|
|
||||||
// preview's fixed camera angle, so backface culling stays off - this
|
|
||||||
// is an authoring preview, not a performance-sensitive renderer, and
|
|
||||||
// an invisible mesh is a worse failure mode than a visible backface.
|
|
||||||
|
|
||||||
// Uploads interleaved [u, v, x, y, z] float data (DMF/terrain layout)
|
|
||||||
// into a GL buffer. Returns { buffer, vertexCount }.
|
|
||||||
function createMesh(floats) {
|
|
||||||
const buffer = gl.createBuffer();
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW);
|
|
||||||
return { buffer, vertexCount: floats.length / 5 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTextureFromImage(image) {
|
|
||||||
const texture = gl.createTexture();
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawMesh(mesh, model, texture, color, depthWrite, mode) {
|
|
||||||
gl.depthMask(depthWrite !== false);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer);
|
|
||||||
gl.enableVertexAttribArray(attribs.aUV);
|
|
||||||
gl.vertexAttribPointer(attribs.aUV, 2, gl.FLOAT, false, 20, 0);
|
|
||||||
gl.enableVertexAttribArray(attribs.aPos);
|
|
||||||
gl.vertexAttribPointer(attribs.aPos, 3, gl.FLOAT, false, 20, 8);
|
|
||||||
|
|
||||||
gl.uniformMatrix4fv(uniforms.uModel, false, model);
|
|
||||||
gl.uniform4f(uniforms.uColor, color[0], color[1], color[2], color[3]);
|
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, texture || whiteTexture);
|
|
||||||
gl.uniform1i(uniforms.uTexture, 0);
|
|
||||||
|
|
||||||
gl.drawArrays(mode || gl.TRIANGLES, 0, mesh.vertexCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds a 4-vertex outline of the tile at (x, y, z), lifted slightly
|
|
||||||
// to avoid z-fighting with the terrain surface beneath it.
|
|
||||||
function buildHighlightMesh(x, y, z) {
|
|
||||||
const eps = 0.02;
|
|
||||||
return createMesh(new Float32Array([
|
|
||||||
0, 0, x, y, z + eps,
|
|
||||||
0, 0, x + 1, y, z + eps,
|
|
||||||
0, 0, x + 1, y + 1, z + eps,
|
|
||||||
0, 0, x, y + 1, z + eps,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neighboring chunks are drawn dimmer and semi-transparent, so they
|
|
||||||
// read as context rather than part of the chunk being edited.
|
|
||||||
const NEIGHBOR_ALPHA = 0.35;
|
|
||||||
const NEIGHBOR_DARKEN = 0.5;
|
|
||||||
|
|
||||||
// scene = {
|
|
||||||
// target: [x, y, z], worldH: number,
|
|
||||||
// terrain: { mesh, texture } | null,
|
|
||||||
// gridLines: { mesh } | null,
|
|
||||||
// models: [{ mesh, texture, color: [r,g,b,a in 0..1], offset: [x,y,z] }],
|
|
||||||
// neighbors: [{
|
|
||||||
// offset: [x,y,z],
|
|
||||||
// terrain: { mesh, texture } | null,
|
|
||||||
// models: [{ mesh, texture, color, offset }],
|
|
||||||
// }],
|
|
||||||
// highlight: { x, y, z } | null,
|
|
||||||
// }
|
|
||||||
function render(scene) {
|
|
||||||
const width = canvas.width, height = canvas.height;
|
|
||||||
gl.viewport(0, 0, width, height);
|
|
||||||
gl.clearColor(0.16, 0.18, 0.22, 1);
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
||||||
gl.useProgram(program);
|
|
||||||
gl.depthMask(true);
|
|
||||||
|
|
||||||
const { view, fov } = computeCamera(scene.target, scene.worldH);
|
|
||||||
const projection = mat4Perspective(new Float32Array(16), fov, width / height, 0.1, 1000);
|
|
||||||
gl.uniformMatrix4fv(uniforms.uProjection, false, projection);
|
|
||||||
gl.uniformMatrix4fv(uniforms.uView, false, view);
|
|
||||||
|
|
||||||
const identity = mat4Identity(new Float32Array(16));
|
|
||||||
|
|
||||||
if(scene.terrain) {
|
|
||||||
drawMesh(scene.terrain.mesh, identity, scene.terrain.texture, [1, 1, 1, 1], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(scene.gridLines) {
|
|
||||||
drawMesh(scene.gridLines.mesh, identity, null, [0, 0, 0, 0.35], false, gl.LINES);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const model of scene.models) {
|
|
||||||
const modelMatrix = mat4Translation(new Float32Array(16), model.offset);
|
|
||||||
drawMesh(model.mesh, modelMatrix, model.texture, model.color, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const neighbor of scene.neighbors || []) {
|
|
||||||
if(neighbor.terrain) {
|
|
||||||
const modelMatrix = mat4Translation(new Float32Array(16), neighbor.offset);
|
|
||||||
drawMesh(
|
|
||||||
neighbor.terrain.mesh, modelMatrix, neighbor.terrain.texture,
|
|
||||||
[NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_DARKEN, NEIGHBOR_ALPHA], false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const model of neighbor.models) {
|
|
||||||
const worldOffset = [
|
|
||||||
neighbor.offset[0] + model.offset[0],
|
|
||||||
neighbor.offset[1] + model.offset[1],
|
|
||||||
neighbor.offset[2] + model.offset[2],
|
|
||||||
];
|
|
||||||
const modelMatrix = mat4Translation(new Float32Array(16), worldOffset);
|
|
||||||
const color = [
|
|
||||||
model.color[0] * NEIGHBOR_DARKEN,
|
|
||||||
model.color[1] * NEIGHBOR_DARKEN,
|
|
||||||
model.color[2] * NEIGHBOR_DARKEN,
|
|
||||||
model.color[3] * NEIGHBOR_ALPHA,
|
|
||||||
];
|
|
||||||
drawMesh(model.mesh, modelMatrix, model.texture, color, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(scene.highlight) {
|
|
||||||
const { x, y, z } = scene.highlight;
|
|
||||||
const highlightMesh = buildHighlightMesh(x, y, z);
|
|
||||||
drawMesh(highlightMesh, identity, null, [1, 0.92, 0.23, 1], false, gl.LINE_LOOP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { gl, createMesh, createTextureFromImage, render };
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.freeze({ create, computeCamera });
|
|
||||||
})();
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Dusk Map Editor</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar navbar-expand navbar-dark bg-dark">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="/">Dusk Map Editor</a>
|
|
||||||
<ul class="navbar-nav flex-row">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/map">Map Editor</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div id="editor" class="container-fluid py-3"></div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
"""
|
|
||||||
Map Editor HTTP Server
|
|
||||||
|
|
||||||
Serves editor/client/public/ as static files with smart routing:
|
|
||||||
/ -> index.html
|
|
||||||
/page -> page.html or page/index.html
|
|
||||||
/index -> index.html
|
|
||||||
|
|
||||||
API endpoints (all paths relative to project root):
|
|
||||||
GET /api/ls?path=<dir> directory listing
|
|
||||||
GET /api/read?path=<file> file contents
|
|
||||||
GET /api/find?path=<dir>&ext=<.json> recursive file listing by extension
|
|
||||||
POST /api/write?path=<file> write file (JSON body: {"content":"..."})
|
|
||||||
POST /api/mkdir?path=<dir> create directory
|
|
||||||
POST /api/delete?path=<file> delete a file
|
|
||||||
POST /api/compile-chunk?x=&y=&z= recompile a chunk JSON to .dcf
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import mimetypes
|
|
||||||
import subprocess
|
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
||||||
from urllib.parse import urlparse, parse_qs, unquote
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
CHUNK_COORD_RE = re.compile(r"^-?\d+$")
|
|
||||||
|
|
||||||
PORT = 3000
|
|
||||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
||||||
PUBLIC_DIR = SCRIPT_DIR.parent / "client" / "public"
|
|
||||||
PROJECT_ROOT = SCRIPT_DIR.parent.parent.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
|
||||||
def do_GET(self):
|
|
||||||
parsed = urlparse(self.path)
|
|
||||||
path = unquote(parsed.path)
|
|
||||||
if path.startswith("/api/"):
|
|
||||||
params = parse_qs(parsed.query)
|
|
||||||
if path == "/api/ls":
|
|
||||||
self.api_ls(params)
|
|
||||||
elif path == "/api/read":
|
|
||||||
self.api_read(params)
|
|
||||||
elif path == "/api/find":
|
|
||||||
self.api_find(params)
|
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
else:
|
|
||||||
self.serve_static(path)
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
parsed = urlparse(self.path)
|
|
||||||
params = parse_qs(parsed.query)
|
|
||||||
if parsed.path == "/api/write":
|
|
||||||
self.api_write(params)
|
|
||||||
elif parsed.path == "/api/mkdir":
|
|
||||||
self.api_mkdir(params)
|
|
||||||
elif parsed.path == "/api/delete":
|
|
||||||
self.api_delete(params)
|
|
||||||
elif parsed.path == "/api/compile-chunk":
|
|
||||||
self.api_compile_chunk(params)
|
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Helpers
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def resolve_project_path(self, rel):
|
|
||||||
rel = rel.lstrip("/")
|
|
||||||
target = (PROJECT_ROOT / rel).resolve()
|
|
||||||
if not str(target).startswith(str(PROJECT_ROOT)):
|
|
||||||
return None
|
|
||||||
return target
|
|
||||||
|
|
||||||
def send_json(self, data, code=200):
|
|
||||||
body = json.dumps(data, indent=2).encode("utf-8")
|
|
||||||
self.send_response(code)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Content-Length", len(body))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
|
|
||||||
def read_body(self):
|
|
||||||
length = int(self.headers.get("Content-Length", 0))
|
|
||||||
return self.rfile.read(length)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# API handlers
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def api_ls(self, params):
|
|
||||||
rel = params.get("path", [""])[0]
|
|
||||||
target = self.resolve_project_path(rel)
|
|
||||||
if target is None:
|
|
||||||
self.send_json({"error": "invalid path"}, 400)
|
|
||||||
return
|
|
||||||
if not target.exists():
|
|
||||||
self.send_json({"error": "path not found"}, 404)
|
|
||||||
return
|
|
||||||
if not target.is_dir():
|
|
||||||
self.send_json({"error": "not a directory"}, 400)
|
|
||||||
return
|
|
||||||
|
|
||||||
entries = []
|
|
||||||
for entry in sorted(target.iterdir(), key=lambda e: (e.is_file(), e.name)):
|
|
||||||
entries.append({
|
|
||||||
"name": entry.name,
|
|
||||||
"type": "dir" if entry.is_dir() else "file",
|
|
||||||
"size": entry.stat().st_size if entry.is_file() else None,
|
|
||||||
})
|
|
||||||
self.send_json({
|
|
||||||
"path": str(target.relative_to(PROJECT_ROOT)),
|
|
||||||
"entries": entries,
|
|
||||||
})
|
|
||||||
|
|
||||||
def api_read(self, params):
|
|
||||||
rel = params.get("path", [""])[0]
|
|
||||||
target = self.resolve_project_path(rel)
|
|
||||||
if target is None:
|
|
||||||
self.send_json({"error": "invalid path"}, 400)
|
|
||||||
return
|
|
||||||
if not target.exists():
|
|
||||||
self.send_json({"error": "file not found"}, 404)
|
|
||||||
return
|
|
||||||
if not target.is_file():
|
|
||||||
self.send_json({"error": "not a file"}, 400)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = target.read_text(encoding="utf-8")
|
|
||||||
self.send_json({
|
|
||||||
"path": str(target.relative_to(PROJECT_ROOT)),
|
|
||||||
"content": content,
|
|
||||||
})
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
import base64
|
|
||||||
content = base64.b64encode(target.read_bytes()).decode("ascii")
|
|
||||||
self.send_json({
|
|
||||||
"path": str(target.relative_to(PROJECT_ROOT)),
|
|
||||||
"content": content,
|
|
||||||
"encoding": "base64",
|
|
||||||
})
|
|
||||||
|
|
||||||
def api_write(self, params):
|
|
||||||
rel = params.get("path", [""])[0]
|
|
||||||
target = self.resolve_project_path(rel)
|
|
||||||
if target is None:
|
|
||||||
self.send_json({"error": "invalid path"}, 400)
|
|
||||||
return
|
|
||||||
|
|
||||||
body = self.read_body()
|
|
||||||
try:
|
|
||||||
data = json.loads(body)
|
|
||||||
content = data.get("content", "")
|
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
||||||
content = body.decode("utf-8")
|
|
||||||
|
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
target.write_text(content, encoding="utf-8")
|
|
||||||
self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
|
|
||||||
|
|
||||||
def api_mkdir(self, params):
|
|
||||||
rel = params.get("path", [""])[0]
|
|
||||||
target = self.resolve_project_path(rel)
|
|
||||||
if target is None:
|
|
||||||
self.send_json({"error": "invalid path"}, 400)
|
|
||||||
return
|
|
||||||
target.mkdir(parents=True, exist_ok=True)
|
|
||||||
self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
|
|
||||||
|
|
||||||
def api_delete(self, params):
|
|
||||||
rel = params.get("path", [""])[0]
|
|
||||||
target = self.resolve_project_path(rel)
|
|
||||||
if target is None:
|
|
||||||
self.send_json({"error": "invalid path"}, 400)
|
|
||||||
return
|
|
||||||
if target.is_file():
|
|
||||||
target.unlink()
|
|
||||||
self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))})
|
|
||||||
|
|
||||||
def api_find(self, params):
|
|
||||||
rel = params.get("path", [""])[0]
|
|
||||||
ext = params.get("ext", [""])[0]
|
|
||||||
target = self.resolve_project_path(rel)
|
|
||||||
if target is None:
|
|
||||||
self.send_json({"error": "invalid path"}, 400)
|
|
||||||
return
|
|
||||||
if not target.is_dir():
|
|
||||||
self.send_json({"error": "not a directory"}, 400)
|
|
||||||
return
|
|
||||||
|
|
||||||
pattern = f"*{ext}" if ext else "*"
|
|
||||||
paths = sorted(
|
|
||||||
str(p.relative_to(target)).replace(os.sep, "/")
|
|
||||||
for p in target.rglob(pattern)
|
|
||||||
if p.is_file()
|
|
||||||
)
|
|
||||||
self.send_json({"path": str(target.relative_to(PROJECT_ROOT)), "files": paths})
|
|
||||||
|
|
||||||
def api_compile_chunk(self, params):
|
|
||||||
x = params.get("x", [""])[0]
|
|
||||||
y = params.get("y", [""])[0]
|
|
||||||
z = params.get("z", [""])[0]
|
|
||||||
if not (CHUNK_COORD_RE.match(x) and CHUNK_COORD_RE.match(y) and CHUNK_COORD_RE.match(z)):
|
|
||||||
self.send_json({"error": "invalid chunk coordinates"}, 400)
|
|
||||||
return
|
|
||||||
|
|
||||||
json_path = PROJECT_ROOT / "assetsraw" / "chunks" / f"chunk_{x}_{y}_{z}.json"
|
|
||||||
if not json_path.is_file():
|
|
||||||
self.send_json({"error": "chunk json not found"}, 404)
|
|
||||||
return
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, "-m", "tools.asset.chunk", str(json_path)],
|
|
||||||
cwd=str(PROJECT_ROOT),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
self.send_json({
|
|
||||||
"ok": result.returncode == 0,
|
|
||||||
"returncode": result.returncode,
|
|
||||||
"stdout": result.stdout,
|
|
||||||
"stderr": result.stderr,
|
|
||||||
})
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Static file serving
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def serve_static(self, path):
|
|
||||||
clean = path.lstrip("/")
|
|
||||||
candidates = []
|
|
||||||
|
|
||||||
if clean == "" or clean == "index" or clean == "index.html":
|
|
||||||
candidates = ["index.html"]
|
|
||||||
else:
|
|
||||||
candidates.append(clean)
|
|
||||||
if "." not in Path(clean).name:
|
|
||||||
candidates.append(clean + ".html")
|
|
||||||
candidates.append(clean + "/index.html")
|
|
||||||
|
|
||||||
for candidate in candidates:
|
|
||||||
file_path = PUBLIC_DIR / candidate
|
|
||||||
if file_path.is_file():
|
|
||||||
self.serve_file(file_path)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_error(404)
|
|
||||||
|
|
||||||
def serve_file(self, file_path):
|
|
||||||
mime, _ = mimetypes.guess_type(str(file_path))
|
|
||||||
mime = mime or "application/octet-stream"
|
|
||||||
|
|
||||||
template = PUBLIC_DIR / "template.html"
|
|
||||||
if (
|
|
||||||
mime == "text/html"
|
|
||||||
and file_path.name != "template.html"
|
|
||||||
and template.is_file()
|
|
||||||
):
|
|
||||||
fragment = file_path.read_text(encoding="utf-8")
|
|
||||||
shell = template.read_text(encoding="utf-8")
|
|
||||||
marker = shell.find('id="editor"')
|
|
||||||
if marker == -1:
|
|
||||||
content = shell.encode("utf-8")
|
|
||||||
else:
|
|
||||||
tag_end = shell.index(">", marker) + 1
|
|
||||||
close_idx = shell.index("</div>", tag_end)
|
|
||||||
content = (shell[:tag_end] + fragment + shell[close_idx:]).encode("utf-8")
|
|
||||||
else:
|
|
||||||
content = file_path.read_bytes()
|
|
||||||
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", mime)
|
|
||||||
self.send_header("Content-Length", len(content))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(content)
|
|
||||||
|
|
||||||
def log_message(self, fmt, *args):
|
|
||||||
print(f" {self.address_string()} {fmt % args}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(f"Map Editor")
|
|
||||||
print(f" http://localhost:{PORT}")
|
|
||||||
print(f" public : {PUBLIC_DIR}")
|
|
||||||
print(f" project: {PROJECT_ROOT}")
|
|
||||||
server = HTTPServer(("", PORT), Handler)
|
|
||||||
try:
|
|
||||||
server.serve_forever()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\nStopped.")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
python3 server/server.py
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
docker build -t dusk-dolphin-test -f docker/dolphin-test/Dockerfile .
|
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-dolphin-test /bin/bash -c "./scripts/test-gamecube-dolphin.sh"
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$DEVKITPRO" ]; then
|
|
||||||
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DOLPHIN_BIN="${DOLPHIN_BIN:-dolphin-emu}"
|
|
||||||
if ! command -v "$DOLPHIN_BIN" >/dev/null 2>&1; then
|
|
||||||
echo "$DOLPHIN_BIN not found. Install Dolphin Emulator (e.g. 'apt install dolphin-emu')"
|
|
||||||
echo "or set DOLPHIN_BIN to its path."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! command -v xvfb-run >/dev/null 2>&1; then
|
|
||||||
echo "xvfb-run not found. Install it (e.g. 'apt install xvfb') -- Dolphin's"
|
|
||||||
echo "Qt frontend needs a display even in batch mode."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
./scripts/build-gamecube-iso.sh
|
|
||||||
|
|
||||||
DUSK_TEST_DOLPHIN_SECONDS="${DUSK_TEST_DOLPHIN_SECONDS:-20}"
|
|
||||||
ISO="build-gamecube-iso/Dusk-NTSC-U.iso"
|
|
||||||
|
|
||||||
echo "Booting $ISO in Dolphin for up to ${DUSK_TEST_DOLPHIN_SECONDS}s (batch mode, headless via Xvfb)..."
|
|
||||||
|
|
||||||
# Dolphin's batch mode (-b -e) runs the disc until told to stop -- there's
|
|
||||||
# no "the game exited cleanly" signal to wait for, so we bound it with
|
|
||||||
# timeout and treat "still running when the clock ran out" as success.
|
|
||||||
set +e
|
|
||||||
xvfb-run -a timeout "${DUSK_TEST_DOLPHIN_SECONDS}" "$DOLPHIN_BIN" -b -e "$ISO"
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# timeout returns 124 when it had to kill Dolphin after the duration
|
|
||||||
# elapsed -- that means the disc booted and kept running, the expected
|
|
||||||
# smoke-test-passed outcome, not a failure. Any other non-zero status
|
|
||||||
# means Dolphin crashed or refused to boot the disc.
|
|
||||||
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
|
|
||||||
echo "Dolphin exited with unexpected status $status -- treating as a failure."
|
|
||||||
exit "$status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "GameCube smoke test passed (Dolphin ran $ISO for ${DUSK_TEST_DOLPHIN_SECONDS}s without crashing)."
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
docker build -t dusk-psp-test -f docker/psp-test/Dockerfile .
|
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-psp-test /bin/bash -c "./scripts/test-psp-ppsspp.sh"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# PPSSPP doesn't ship a standard Linux package -- build its "PPSSPPHeadless"
|
|
||||||
# target from source (https://github.com/hrydgard/ppsspp) and either put it
|
|
||||||
# on PATH or point PPSSPP_HEADLESS_BIN at it. See docker/psp-test/Dockerfile
|
|
||||||
# for a from-scratch build.
|
|
||||||
PPSSPP_HEADLESS_BIN="${PPSSPP_HEADLESS_BIN:-PPSSPPHeadless}"
|
|
||||||
if ! command -v "$PPSSPP_HEADLESS_BIN" >/dev/null 2>&1; then
|
|
||||||
echo "$PPSSPP_HEADLESS_BIN not found. Build PPSSPP's 'PPSSPPHeadless' target"
|
|
||||||
echo "from source or set PPSSPP_HEADLESS_BIN to its path."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
./scripts/build-psp.sh
|
|
||||||
|
|
||||||
DUSK_TEST_PPSSPP_SECONDS="${DUSK_TEST_PPSSPP_SECONDS:-20}"
|
|
||||||
EBOOT="build-psp/EBOOT.PBP"
|
|
||||||
|
|
||||||
echo "Booting $EBOOT in PPSSPPHeadless (timeout ${DUSK_TEST_PPSSPP_SECONDS}s)..."
|
|
||||||
|
|
||||||
# PPSSPPHeadless is purpose-built for automated/CI runs -- it renders
|
|
||||||
# without a window and is expected to exit on its own once --timeout
|
|
||||||
# elapses, unlike Dolphin's batch mode which has to be killed externally.
|
|
||||||
# Check `"$PPSSPP_HEADLESS_BIN" --help` if these flags don't match your
|
|
||||||
# PPSSPP checkout -- the headless CLI has changed across versions.
|
|
||||||
set +e
|
|
||||||
"$PPSSPP_HEADLESS_BIN" --timeout="${DUSK_TEST_PPSSPP_SECONDS}" "$EBOOT"
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ "$status" -ne 0 ]; then
|
|
||||||
echo "PPSSPPHeadless exited with status $status"
|
|
||||||
exit "$status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "PSP smoke test passed (PPSSPPHeadless ran EBOOT.PBP for ${DUSK_TEST_PPSSPP_SECONDS}s without crashing)."
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
docker build -t dusk-dolphin-test -f docker/dolphin-test/Dockerfile .
|
|
||||||
docker run --rm -v "$(pwd):/workdir" dusk-dolphin-test /bin/bash -c "./scripts/test-wii-dolphin.sh"
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$DEVKITPRO" ]; then
|
|
||||||
echo "DEVKITPRO environment variable is not set. Please set it to the path of your DEVKITPRO installation."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DOLPHIN_BIN="${DOLPHIN_BIN:-dolphin-emu}"
|
|
||||||
if ! command -v "$DOLPHIN_BIN" >/dev/null 2>&1; then
|
|
||||||
echo "$DOLPHIN_BIN not found. Install Dolphin Emulator (e.g. 'apt install dolphin-emu')"
|
|
||||||
echo "or set DOLPHIN_BIN to its path."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! command -v xvfb-run >/dev/null 2>&1; then
|
|
||||||
echo "xvfb-run not found. Install it (e.g. 'apt install xvfb') -- Dolphin's"
|
|
||||||
echo "Qt frontend needs a display even in batch mode."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
./scripts/build-wii-iso.sh
|
|
||||||
|
|
||||||
DUSK_TEST_DOLPHIN_SECONDS="${DUSK_TEST_DOLPHIN_SECONDS:-20}"
|
|
||||||
ISO="build-wii-iso/Dusk-NTSC-U.iso"
|
|
||||||
|
|
||||||
echo "Booting $ISO in Dolphin for up to ${DUSK_TEST_DOLPHIN_SECONDS}s (batch mode, headless via Xvfb)..."
|
|
||||||
|
|
||||||
# Dolphin's batch mode (-b -e) runs the disc until told to stop -- there's
|
|
||||||
# no "the game exited cleanly" signal to wait for, so we bound it with
|
|
||||||
# timeout and treat "still running when the clock ran out" as success.
|
|
||||||
set +e
|
|
||||||
xvfb-run -a timeout "${DUSK_TEST_DOLPHIN_SECONDS}" "$DOLPHIN_BIN" -b -e "$ISO"
|
|
||||||
status=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# timeout returns 124 when it had to kill Dolphin after the duration
|
|
||||||
# elapsed -- that means the disc booted and kept running, the expected
|
|
||||||
# smoke-test-passed outcome, not a failure. Any other non-zero status
|
|
||||||
# means Dolphin crashed or refused to boot the disc.
|
|
||||||
if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then
|
|
||||||
echo "Dolphin exited with unexpected status $status -- treating as a failure."
|
|
||||||
exit "$status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Wii smoke test passed (Dolphin ran $ISO for ${DUSK_TEST_DOLPHIN_SECONDS}s without crashing)."
|
|
||||||
+5
-4
@@ -4,10 +4,6 @@
|
|||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
add_subdirectory(dusk)
|
add_subdirectory(dusk)
|
||||||
if(DUSK_NETWORKING)
|
|
||||||
add_subdirectory(dusknetwork)
|
|
||||||
endif()
|
|
||||||
add_subdirectory(duskrpg)
|
|
||||||
|
|
||||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||||
add_subdirectory(dusklinux)
|
add_subdirectory(dusklinux)
|
||||||
@@ -19,6 +15,11 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
|||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
add_subdirectory(duskgl)
|
add_subdirectory(duskgl)
|
||||||
|
|
||||||
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
||||||
|
add_subdirectory(duskvita)
|
||||||
|
add_subdirectory(dusksdl2)
|
||||||
|
add_subdirectory(duskgl)
|
||||||
|
|
||||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||||
add_subdirectory(duskdolphin)
|
add_subdirectory(duskdolphin)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
|||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(animation)
|
add_subdirectory(animation)
|
||||||
add_subdirectory(event)
|
|
||||||
add_subdirectory(assert)
|
add_subdirectory(assert)
|
||||||
add_subdirectory(asset)
|
add_subdirectory(asset)
|
||||||
add_subdirectory(console)
|
add_subdirectory(console)
|
||||||
@@ -71,15 +70,14 @@ add_subdirectory(entity)
|
|||||||
add_subdirectory(log)
|
add_subdirectory(log)
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
add_subdirectory(error)
|
add_subdirectory(error)
|
||||||
add_subdirectory(game)
|
|
||||||
add_subdirectory(input)
|
add_subdirectory(input)
|
||||||
add_subdirectory(locale)
|
add_subdirectory(locale)
|
||||||
|
add_subdirectory(physics)
|
||||||
add_subdirectory(scene)
|
add_subdirectory(scene)
|
||||||
|
add_subdirectory(script)
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(physics)
|
add_subdirectory(network)
|
||||||
add_subdirectory(save)
|
|
||||||
add_subdirectory(script)
|
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
add_subdirectory(thread)
|
add_subdirectory(thread)
|
||||||
@@ -6,7 +6,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
easing.c
|
easing.c
|
||||||
keyframe.c
|
|
||||||
keyframeset.c
|
|
||||||
animation.c
|
animation.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,70 +5,48 @@
|
|||||||
|
|
||||||
#include "animation.h"
|
#include "animation.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t **tracks,
|
keyframe_t *keyframes,
|
||||||
uint16_t *trackCounts,
|
uint16_t keyframeCount
|
||||||
uint16_t trackCount
|
|
||||||
) {
|
) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
|
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||||
|
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||||
|
|
||||||
keyframeSetInit(
|
anim->keyframes = keyframes;
|
||||||
&anim->keyframes, tracks, trackCounts, NULL, NULL, trackCount, NULL
|
anim->keyframeCount = keyframeCount;
|
||||||
);
|
|
||||||
anim->time = 0.0f;
|
|
||||||
anim->speed = 1.0f;
|
|
||||||
anim->loop = false;
|
|
||||||
anim->playing = false;
|
|
||||||
anim->eventTime = 0.0f;
|
|
||||||
anim->eventCallback = NULL;
|
|
||||||
anim->eventUser = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void animationUpdate(animation_t *anim) {
|
float_t animationGetValue(animation_t *anim, const float_t time) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
if(!anim->playing) return;
|
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
||||||
|
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
||||||
|
assertTrue(time >= 0, "Time must be non-negative.");
|
||||||
|
|
||||||
float_t duration = keyframeSetGetDuration(&anim->keyframes);
|
keyframe_t *start;
|
||||||
float_t prevTime = anim->time;
|
keyframe_t *end;
|
||||||
anim->time += TIME.delta * anim->speed;
|
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
||||||
|
keyframe_t *current = anim->keyframes;
|
||||||
|
start = current;
|
||||||
|
|
||||||
// Loops that overshoot duration wrap back into [0, duration) -- an event
|
do {
|
||||||
// near the end must still be checked across that wrap, as two segments:
|
if(current->time > time) {
|
||||||
// (prevTime, duration] then [0, newTime].
|
end = current;
|
||||||
bool_t wrapped = anim->loop && duration > 0.0f && anim->time >= duration;
|
break;
|
||||||
|
|
||||||
if(anim->loop) {
|
|
||||||
if(duration > 0.0f) anim->time = mathModFloat(anim->time, duration);
|
|
||||||
} else if(anim->time >= duration) {
|
|
||||||
anim->time = duration;
|
|
||||||
anim->playing = false;
|
|
||||||
}
|
}
|
||||||
|
start = current;
|
||||||
|
current++;
|
||||||
|
|
||||||
if(!anim->eventCallback) return;
|
if(current > last) {
|
||||||
|
end = start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while(true);
|
||||||
|
|
||||||
bool_t crossed = wrapped
|
float_t t = (time - start->time) / (end->time - start->time);
|
||||||
? (prevTime < anim->eventTime || anim->time >= anim->eventTime)
|
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||||
: (prevTime < anim->eventTime && anim->time >= anim->eventTime);
|
|
||||||
if(crossed) anim->eventCallback(anim, anim->eventUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
void animationSetEvent(
|
|
||||||
animation_t *anim,
|
|
||||||
const float_t time,
|
|
||||||
const animationeventcallback_t callback,
|
|
||||||
void *user
|
|
||||||
) {
|
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
|
||||||
anim->eventTime = time;
|
|
||||||
anim->eventCallback = callback;
|
|
||||||
anim->eventUser = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex) {
|
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
|
||||||
return keyframeSetGetValue(&anim->keyframes, trackIndex, anim->time);
|
|
||||||
}
|
}
|
||||||
@@ -4,99 +4,31 @@
|
|||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "keyframeset.h"
|
#include "keyframe.h"
|
||||||
|
|
||||||
typedef struct animation_t animation_t;
|
typedef struct {
|
||||||
|
keyframe_t *keyframes;
|
||||||
/**
|
uint16_t keyframeCount;
|
||||||
* Callback fired once when animationUpdate() advances time past
|
|
||||||
* eventTime (see animationSetEvent()).
|
|
||||||
*
|
|
||||||
* @param anim The animation whose event fired.
|
|
||||||
* @param user The user pointer passed to animationSetEvent().
|
|
||||||
*/
|
|
||||||
typedef void (*animationeventcallback_t)(animation_t *anim, void *user);
|
|
||||||
|
|
||||||
typedef struct animation_t {
|
|
||||||
/** The animation's tracks/channels and their raw keyframe data. */
|
|
||||||
keyframeset_t keyframes;
|
|
||||||
|
|
||||||
/** Current playback position, in seconds. */
|
|
||||||
float_t time;
|
|
||||||
|
|
||||||
/** Playback rate multiplier; 1.0 = normal speed. */
|
|
||||||
float_t speed;
|
|
||||||
|
|
||||||
/** True if animationUpdate() should wrap time back to 0 on reaching the
|
|
||||||
* final keyframe, rather than holding there and clearing playing. */
|
|
||||||
bool_t loop;
|
|
||||||
|
|
||||||
/** True while animationUpdate() should advance time each call. */
|
|
||||||
bool_t playing;
|
|
||||||
|
|
||||||
/** Time (seconds) at which eventCallback fires; see animationSetEvent(). */
|
|
||||||
float_t eventTime;
|
|
||||||
/** Callback fired once when time advances past eventTime, or NULL. */
|
|
||||||
animationeventcallback_t eventCallback;
|
|
||||||
/** User pointer passed to eventCallback unchanged. */
|
|
||||||
void *eventUser;
|
|
||||||
} animation_t;
|
} animation_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an animation: time 0, speed 1.0, not looping, not playing.
|
* Initializes an animation.
|
||||||
* See keyframeSetInit() -- tracks/trackCounts and the keyframe_t arrays
|
|
||||||
* they point to are not copied, and must outlive this animation.
|
|
||||||
*
|
*
|
||||||
* @param anim The animation to initialize.
|
* @param anim The animation to initialize.
|
||||||
* @param tracks Array of trackCount keyframe_t arrays.
|
* @param keyframes The keyframes to use for the animation.
|
||||||
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
* @param keyframeCount The number of keyframes in the animation.
|
||||||
* @param trackCount The number of tracks/channels in this animation.
|
|
||||||
*/
|
*/
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t **tracks,
|
keyframe_t *keyframes,
|
||||||
uint16_t *trackCounts,
|
uint16_t keyframeCount
|
||||||
uint16_t trackCount
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advances an animation's time by TIME.delta * speed. No-op if not
|
* Gets the value of the animation at a given time.
|
||||||
* playing. Duration is the latest of every track's final keyframe time
|
|
||||||
* (see keyframeSetGetDuration()). If loop is set, time wraps back into
|
|
||||||
* [0, duration) on reaching it; otherwise time is clamped there and
|
|
||||||
* playing is cleared.
|
|
||||||
*
|
|
||||||
* If eventCallback is set, it fires once when this call advances time
|
|
||||||
* from at or before eventTime to after it (checked across the loop wrap
|
|
||||||
* point too, so an event near the end of a looping animation still fires
|
|
||||||
* every loop).
|
|
||||||
*
|
|
||||||
* @param anim The animation to update.
|
|
||||||
*/
|
|
||||||
void animationUpdate(animation_t *anim);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets (or clears, with callback NULL) the single time-triggered event
|
|
||||||
* fired by animationUpdate().
|
|
||||||
*
|
|
||||||
* @param anim The animation to set the event on.
|
|
||||||
* @param time The time, in seconds, at which callback fires.
|
|
||||||
* @param callback The callback to invoke, or NULL to clear any event.
|
|
||||||
* @param user Arbitrary pointer forwarded to callback unchanged.
|
|
||||||
*/
|
|
||||||
void animationSetEvent(
|
|
||||||
animation_t *anim,
|
|
||||||
const float_t time,
|
|
||||||
const animationeventcallback_t callback,
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value of one of the animation's tracks at its current
|
|
||||||
* playback time.
|
|
||||||
*
|
*
|
||||||
* @param anim The animation to get the value from.
|
* @param anim The animation to get the value from.
|
||||||
* @param trackIndex The track to evaluate, in [0, trackCount).
|
* @param time The time at which to get the value, in seconds.
|
||||||
* @return The interpolated value of that track at the current time.
|
* @return The value of the animation at the given time.
|
||||||
*/
|
*/
|
||||||
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex);
|
float_t animationGetValue(animation_t *anim, const float_t time);
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include "easing.h"
|
#include "easing.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/math.h"
|
|
||||||
|
|
||||||
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
|
const easingfn_t EASING_FUNCTIONS[EASING_COUNT] = {
|
||||||
easingLinear,
|
easingLinear,
|
||||||
@@ -36,15 +35,15 @@ float_t easingLinear(const float_t t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float_t easingInSine(const float_t t) {
|
float_t easingInSine(const float_t t) {
|
||||||
return 1.0f - cosf(t * MATH_PI * 0.5f);
|
return 1.0f - cosf(t * EASING_PI * 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t easingOutSine(const float_t t) {
|
float_t easingOutSine(const float_t t) {
|
||||||
return sinf(t * MATH_PI * 0.5f);
|
return sinf(t * EASING_PI * 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t easingInOutSine(const float_t t) {
|
float_t easingInOutSine(const float_t t) {
|
||||||
return -(cosf(MATH_PI * t) - 1.0f) * 0.5f;
|
return -(cosf(EASING_PI * t) - 1.0f) * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t easingInQuad(const float_t t) {
|
float_t easingInQuad(const float_t t) {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
#include "keyframe.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/math.h"
|
|
||||||
|
|
||||||
float_t keyframeGetValue(
|
|
||||||
keyframe_t *keyframes,
|
|
||||||
const uint16_t keyframeCount,
|
|
||||||
const float_t time
|
|
||||||
) {
|
|
||||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
|
||||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
|
||||||
assertTrue(time >= 0, "Time must be non-negative.");
|
|
||||||
|
|
||||||
keyframe_t *first = keyframes;
|
|
||||||
keyframe_t *last = keyframes + keyframeCount - 1;
|
|
||||||
|
|
||||||
// Clamp to the boundary keyframes' values directly rather than
|
|
||||||
// interpolating -- start == end at either boundary would otherwise
|
|
||||||
// divide by zero.
|
|
||||||
if(time <= first->time) return first->value;
|
|
||||||
if(time >= last->time) return last->value;
|
|
||||||
|
|
||||||
keyframe_t *start = first;
|
|
||||||
keyframe_t *end;
|
|
||||||
keyframe_t *current = first;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if(current->time > time) {
|
|
||||||
end = current;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
start = current;
|
|
||||||
current++;
|
|
||||||
} while(true);
|
|
||||||
|
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
|
||||||
}
|
|
||||||
@@ -11,17 +11,3 @@ typedef struct {
|
|||||||
float_t value;
|
float_t value;
|
||||||
easingtype_t easing;
|
easingtype_t easing;
|
||||||
} keyframe_t;
|
} keyframe_t;
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the eased, interpolated value of a keyframe array at a given time.
|
|
||||||
*
|
|
||||||
* @param keyframes The keyframes to evaluate, ascending by time.
|
|
||||||
* @param keyframeCount The number of keyframes.
|
|
||||||
* @param time The time at which to get the value, in seconds.
|
|
||||||
* @return The interpolated value at the given time.
|
|
||||||
*/
|
|
||||||
float_t keyframeGetValue(
|
|
||||||
keyframe_t *keyframes,
|
|
||||||
const uint16_t keyframeCount,
|
|
||||||
const float_t time
|
|
||||||
);
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
#include "keyframeset.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
|
|
||||||
void keyframeSetInit(
|
|
||||||
keyframeset_t *set,
|
|
||||||
keyframe_t **tracks,
|
|
||||||
uint16_t *trackCounts,
|
|
||||||
keyframesetcallback_t *callbacks,
|
|
||||||
void **users,
|
|
||||||
uint16_t trackCount,
|
|
||||||
void *user
|
|
||||||
) {
|
|
||||||
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
|
||||||
assertNotNull(tracks, "Tracks pointer cannot be null.");
|
|
||||||
assertNotNull(trackCounts, "Track counts pointer cannot be null.");
|
|
||||||
assertTrue(trackCount > 0, "Track count must be more than 0.");
|
|
||||||
|
|
||||||
set->tracks = tracks;
|
|
||||||
set->trackCounts = trackCounts;
|
|
||||||
set->trackCount = trackCount;
|
|
||||||
set->callbacks = callbacks;
|
|
||||||
set->users = users;
|
|
||||||
set->user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
float_t keyframeSetGetValue(
|
|
||||||
keyframeset_t *set,
|
|
||||||
const uint16_t trackIndex,
|
|
||||||
const float_t time
|
|
||||||
) {
|
|
||||||
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
|
||||||
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
|
|
||||||
|
|
||||||
return keyframeGetValue(
|
|
||||||
set->tracks[trackIndex], set->trackCounts[trackIndex], time
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void keyframeSetGetValues(
|
|
||||||
keyframeset_t *set,
|
|
||||||
const float_t time,
|
|
||||||
float_t *outValues
|
|
||||||
) {
|
|
||||||
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
|
||||||
assertNotNull(outValues, "Output values pointer cannot be null.");
|
|
||||||
|
|
||||||
for(uint16_t i = 0; i < set->trackCount; i++) {
|
|
||||||
outValues[i] = keyframeGetValue(set->tracks[i], set->trackCounts[i], time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float_t keyframeSetGetDuration(keyframeset_t *set) {
|
|
||||||
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
|
||||||
|
|
||||||
float_t duration = 0.0f;
|
|
||||||
for(uint16_t i = 0; i < set->trackCount; i++) {
|
|
||||||
float_t trackDuration = set->tracks[i][set->trackCounts[i] - 1].time;
|
|
||||||
if(trackDuration > duration) duration = trackDuration;
|
|
||||||
}
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex) {
|
|
||||||
assertNotNull(set, "Keyframe set pointer cannot be null.");
|
|
||||||
assertTrue(trackIndex < set->trackCount, "Track index out of bounds.");
|
|
||||||
|
|
||||||
if(!set->callbacks || !set->callbacks[trackIndex]) return;
|
|
||||||
set->callbacks[trackIndex](
|
|
||||||
set, trackIndex, set->users ? set->users[trackIndex] : NULL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
// Copyright (c) 2026 Dominic Masters
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "keyframe.h"
|
|
||||||
|
|
||||||
typedef struct keyframeset_t keyframeset_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback associated with one track of a keyframe set (see
|
|
||||||
* keyframeSetFireCallback()).
|
|
||||||
*
|
|
||||||
* @param set The keyframe set the track belongs to.
|
|
||||||
* @param trackIndex The track this callback is registered for.
|
|
||||||
* @param user The per-track user pointer passed to keyframeSetInit().
|
|
||||||
*/
|
|
||||||
typedef void (*keyframesetcallback_t)(
|
|
||||||
keyframeset_t *set,
|
|
||||||
const uint16_t trackIndex,
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A group of N parallel keyframe tracks (e.g. one per animated channel --
|
|
||||||
* position.x/y/z, bone rotations, etc.) sharing a single timeline. Each
|
|
||||||
* track is independent: its own keyframe array and count, evaluated at
|
|
||||||
* whatever time is asked for.
|
|
||||||
*/
|
|
||||||
typedef struct keyframeset_t {
|
|
||||||
/** Caller-owned array of trackCount keyframe_t arrays. */
|
|
||||||
keyframe_t **tracks;
|
|
||||||
/** Caller-owned array of trackCount counts, one per entry in tracks. */
|
|
||||||
uint16_t *trackCounts;
|
|
||||||
uint16_t trackCount;
|
|
||||||
|
|
||||||
/** Caller-owned array of trackCount callbacks, one per track. Entries
|
|
||||||
* (or the whole array) may be NULL for tracks with no callback. */
|
|
||||||
keyframesetcallback_t *callbacks;
|
|
||||||
/** Caller-owned array of trackCount user pointers, one per track,
|
|
||||||
* passed to that track's callbacks[] entry. May be NULL. */
|
|
||||||
void **users;
|
|
||||||
/** Extra user data for the set as a whole, not tied to any one track. */
|
|
||||||
void *user;
|
|
||||||
} keyframeset_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a keyframe set. None of the passed-in arrays (or the
|
|
||||||
* keyframe_t arrays tracks points to) are copied -- they must outlive
|
|
||||||
* this keyframeset_t.
|
|
||||||
*
|
|
||||||
* @param set The keyframe set to initialize.
|
|
||||||
* @param tracks Array of trackCount keyframe_t arrays.
|
|
||||||
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
|
||||||
* @param callbacks Array of trackCount callbacks, one per track, or NULL
|
|
||||||
* if no track has one.
|
|
||||||
* @param users Array of trackCount user pointers, matching callbacks, or
|
|
||||||
* NULL.
|
|
||||||
* @param trackCount The number of tracks.
|
|
||||||
* @param user Extra user data for the set as a whole; may be NULL.
|
|
||||||
*/
|
|
||||||
void keyframeSetInit(
|
|
||||||
keyframeset_t *set,
|
|
||||||
keyframe_t **tracks,
|
|
||||||
uint16_t *trackCounts,
|
|
||||||
keyframesetcallback_t *callbacks,
|
|
||||||
void **users,
|
|
||||||
uint16_t trackCount,
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value of a single track at a given time.
|
|
||||||
*
|
|
||||||
* @param set The keyframe set to evaluate.
|
|
||||||
* @param trackIndex The track to evaluate, in [0, set->trackCount).
|
|
||||||
* @param time The time at which to get the value, in seconds.
|
|
||||||
* @return The interpolated value of that track at the given time.
|
|
||||||
*/
|
|
||||||
float_t keyframeSetGetValue(
|
|
||||||
keyframeset_t *set,
|
|
||||||
const uint16_t trackIndex,
|
|
||||||
const float_t time
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the value of every track at a given time.
|
|
||||||
*
|
|
||||||
* @param set The keyframe set to evaluate.
|
|
||||||
* @param time The time at which to get the values, in seconds.
|
|
||||||
* @param outValues Destination array of at least set->trackCount floats.
|
|
||||||
*/
|
|
||||||
void keyframeSetGetValues(
|
|
||||||
keyframeset_t *set,
|
|
||||||
const float_t time,
|
|
||||||
float_t *outValues
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the set's duration: the latest final-keyframe time across every
|
|
||||||
* track, i.e. how long it takes for every track to finish.
|
|
||||||
*
|
|
||||||
* @param set The keyframe set to measure.
|
|
||||||
* @return The set's duration, in seconds.
|
|
||||||
*/
|
|
||||||
float_t keyframeSetGetDuration(keyframeset_t *set);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes a track's callback, if it (and its callbacks array) is set.
|
|
||||||
* No-op otherwise.
|
|
||||||
*
|
|
||||||
* @param set The keyframe set the track belongs to.
|
|
||||||
* @param trackIndex The track whose callback to invoke.
|
|
||||||
*/
|
|
||||||
void keyframeSetFireCallback(keyframeset_t *set, const uint16_t trackIndex);
|
|
||||||
@@ -8,13 +8,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
|
||||||
// Release-style CMake configs (Release, RelWithDebInfo, MinSizeRel) define
|
|
||||||
// NDEBUG automatically; fake assertions there rather than requiring every
|
|
||||||
// platform's CMakeLists to set DUSK_ASSERTIONS_FAKED itself.
|
|
||||||
#if defined(NDEBUG) && !defined(DUSK_ASSERTIONS_FAKED)
|
|
||||||
#define DUSK_ASSERTIONS_FAKED
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef DUSK_TEST_ASSERT
|
#ifdef DUSK_TEST_ASSERT
|
||||||
#include <cmocka.h>
|
#include <cmocka.h>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
asset.c
|
asset.c
|
||||||
assetbatch.c
|
|
||||||
assetfile.c
|
assetfile.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -320,9 +320,7 @@ errorret_t assetUpdate(void) {
|
|||||||
"Loader did not set entry state to error on failed load."
|
"Loader did not set entry state to error on failed load."
|
||||||
);
|
);
|
||||||
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
assetentry_t *loadedEntry = loading->entry;
|
|
||||||
loading->entry = NULL;
|
loading->entry = NULL;
|
||||||
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loading++;
|
loading++;
|
||||||
@@ -343,10 +341,8 @@ errorret_t assetUpdate(void) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ASSET_ENTRY_STATE_ERROR: {
|
case ASSET_ENTRY_STATE_ERROR: {
|
||||||
assetentry_t *errEntry = loading->entry;
|
|
||||||
loading->entry = NULL;
|
loading->entry = NULL;
|
||||||
threadMutexUnlock(&loading->mutex);
|
threadMutexUnlock(&loading->mutex);
|
||||||
eventInvoke(&errEntry->onError, errEntry);
|
|
||||||
errorThrow("Failed to load asset asynchronously.");
|
errorThrow("Failed to load asset asynchronously.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#define ASSET_FILE_NAME "dusk.dsk"
|
#define ASSET_FILE_NAME "dusk.dsk"
|
||||||
#define ASSET_HEADER_SIZE 3
|
#define ASSET_HEADER_SIZE 3
|
||||||
|
|
||||||
#define ASSET_LOADING_COUNT_MAX 10
|
#define ASSET_LOADING_COUNT_MAX 16
|
||||||
#define ASSET_ENTRY_COUNT_MAX 64
|
#define ASSET_ENTRY_COUNT_MAX 64
|
||||||
|
|
||||||
typedef struct asset_s {
|
typedef struct asset_s {
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "assetbatch.h"
|
|
||||||
#include "asset.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
void assetBatchInit(
|
|
||||||
assetbatch_t *batch,
|
|
||||||
const uint16_t count,
|
|
||||||
const assetbatchdesc_t *descs
|
|
||||||
) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
assertNotNull(descs, "Descs cannot be NULL.");
|
|
||||||
assertTrue(count > 0, "Count must be greater than 0.");
|
|
||||||
assertTrue(
|
|
||||||
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
|
|
||||||
);
|
|
||||||
|
|
||||||
memoryZero(batch, sizeof(assetbatch_t));
|
|
||||||
batch->count = count;
|
|
||||||
|
|
||||||
eventInit(
|
|
||||||
&batch->onLoaded,
|
|
||||||
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&batch->onEntryLoaded,
|
|
||||||
batch->onEntryLoadedCallbacks,
|
|
||||||
batch->onEntryLoadedUsers,
|
|
||||||
ASSET_BATCH_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&batch->onError,
|
|
||||||
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&batch->onEntryError,
|
|
||||||
batch->onEntryErrorCallbacks,
|
|
||||||
batch->onEntryErrorUsers,
|
|
||||||
ASSET_BATCH_EVENT_MAX
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint16_t i = 0; i < count; i++) {
|
|
||||||
batch->inputs[i] = descs[i].input;
|
|
||||||
batch->entries[i] = assetLock(
|
|
||||||
descs[i].path, descs[i].type, &batch->inputs[i]
|
|
||||||
);
|
|
||||||
|
|
||||||
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
// Already loaded (cached) - count it now, no subscription needed.
|
|
||||||
batch->loadedCount++;
|
|
||||||
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
batch->errorCount++;
|
|
||||||
} else {
|
|
||||||
eventSubscribe(
|
|
||||||
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
|
|
||||||
);
|
|
||||||
eventSubscribe(
|
|
||||||
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void assetBatchLock(assetbatch_t *batch) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
for(uint16_t i = 0; i < batch->count; i++) {
|
|
||||||
assetEntryLock(batch->entries[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void assetBatchUnlock(assetbatch_t *batch) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
for(uint16_t i = 0; i < batch->count; i++) {
|
|
||||||
assetEntryUnlock(batch->entries[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
for(uint16_t i = 0; i < batch->count; i++) {
|
|
||||||
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t assetBatchHasError(const assetbatch_t *batch) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
for(uint16_t i = 0; i < batch->count; i++) {
|
|
||||||
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
|
|
||||||
bool_t allDone;
|
|
||||||
do {
|
|
||||||
allDone = true;
|
|
||||||
for(uint16_t i = 0; i < batch->count; i++) {
|
|
||||||
const assetentrystate_t state = batch->entries[i]->state;
|
|
||||||
if(state == ASSET_ENTRY_STATE_ERROR) {
|
|
||||||
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
|
|
||||||
}
|
|
||||||
if(state != ASSET_ENTRY_STATE_LOADED) {
|
|
||||||
allDone = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!allDone) {
|
|
||||||
usleep(1000);
|
|
||||||
errorChain(assetUpdate());
|
|
||||||
}
|
|
||||||
} while(!allDone);
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void assetBatchDispose(assetbatch_t *batch) {
|
|
||||||
assertNotNull(batch, "Batch cannot be NULL.");
|
|
||||||
for(uint16_t i = 0; i < batch->count; i++) {
|
|
||||||
if(batch->entries[i]) {
|
|
||||||
// Unsubscribe while we still hold a lock so the entry is live.
|
|
||||||
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
|
|
||||||
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
|
|
||||||
assetUnlockEntry(batch->entries[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
memoryZero(batch, sizeof(assetbatch_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
void assetBatchEntryOnLoadedCb(void *params, void *user) {
|
|
||||||
assetentry_t *entry = (assetentry_t *)params;
|
|
||||||
assetbatch_t *batch = (assetbatch_t *)user;
|
|
||||||
|
|
||||||
batch->loadedCount++;
|
|
||||||
eventInvoke(&batch->onEntryLoaded, entry);
|
|
||||||
|
|
||||||
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
|
||||||
if(batch->errorCount == 0) {
|
|
||||||
eventInvoke(&batch->onLoaded, batch);
|
|
||||||
} else {
|
|
||||||
eventInvoke(&batch->onError, batch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void assetBatchEntryOnErrorCb(void *params, void *user) {
|
|
||||||
assetentry_t *entry = (assetentry_t *)params;
|
|
||||||
assetbatch_t *batch = (assetbatch_t *)user;
|
|
||||||
|
|
||||||
batch->errorCount++;
|
|
||||||
eventInvoke(&batch->onEntryError, entry);
|
|
||||||
|
|
||||||
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
|
||||||
eventInvoke(&batch->onError, batch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "asset/loader/assetentry.h"
|
|
||||||
#include "asset/loader/assetloader.h"
|
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
#define ASSET_BATCH_COUNT_MAX 64
|
|
||||||
#define ASSET_BATCH_EVENT_MAX 4
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
const char_t *path;
|
|
||||||
assetloadertype_t type;
|
|
||||||
assetloaderinput_t input;
|
|
||||||
} assetbatchdesc_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
|
|
||||||
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
|
|
||||||
uint16_t count;
|
|
||||||
uint16_t loadedCount;
|
|
||||||
uint16_t errorCount;
|
|
||||||
|
|
||||||
/** Fires once when every entry loaded. params = assetbatch_t * */
|
|
||||||
event_t onLoaded;
|
|
||||||
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
|
||||||
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
|
||||||
|
|
||||||
/** Fires each time a single entry loads. params = assetentry_t * */
|
|
||||||
event_t onEntryLoaded;
|
|
||||||
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
|
||||||
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
|
||||||
|
|
||||||
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
|
|
||||||
event_t onError;
|
|
||||||
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
|
||||||
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
|
|
||||||
|
|
||||||
/** Fires each time a single entry errors. params = assetentry_t * */
|
|
||||||
event_t onEntryError;
|
|
||||||
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
|
||||||
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
|
|
||||||
} assetbatch_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialises the batch from an array of descriptors. Each entry is locked
|
|
||||||
* and queued for loading immediately.
|
|
||||||
*
|
|
||||||
* @param batch Batch to initialise.
|
|
||||||
* @param descs Array of entry descriptors (need not outlive this call).
|
|
||||||
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
|
|
||||||
*/
|
|
||||||
void assetBatchInit(
|
|
||||||
assetbatch_t *batch,
|
|
||||||
uint16_t count,
|
|
||||||
const assetbatchdesc_t *descs
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires one additional lock on every entry in the batch.
|
|
||||||
*
|
|
||||||
* @param batch Batch to lock.
|
|
||||||
*/
|
|
||||||
void assetBatchLock(assetbatch_t *batch);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases one lock from every entry in the batch. When an entry's lock
|
|
||||||
* count reaches zero it will be reaped on the next assetUpdate.
|
|
||||||
*
|
|
||||||
* @param batch Batch to unlock.
|
|
||||||
*/
|
|
||||||
void assetBatchUnlock(assetbatch_t *batch);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if every entry in the batch has finished loading.
|
|
||||||
*
|
|
||||||
* @param batch Batch to query.
|
|
||||||
*/
|
|
||||||
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if any entry in the batch is in an error state.
|
|
||||||
*
|
|
||||||
* @param batch Batch to query.
|
|
||||||
*/
|
|
||||||
bool_t assetBatchHasError(const assetbatch_t *batch);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks until every entry is loaded. Returns an error if any entry fails.
|
|
||||||
*
|
|
||||||
* @param batch Batch to wait on.
|
|
||||||
*/
|
|
||||||
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases the batch's lock on every entry and clears the batch. After this
|
|
||||||
* call the batch struct may be reused with assetBatchInit.
|
|
||||||
*
|
|
||||||
* @param batch Batch to dispose.
|
|
||||||
*/
|
|
||||||
void assetBatchDispose(assetbatch_t *batch);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event trampoline invoked when a batch entry finishes loading.
|
|
||||||
* Increments the loaded counter and fires batch-level events.
|
|
||||||
*
|
|
||||||
* @param params The loaded assetentry_t pointer.
|
|
||||||
* @param user The owning assetbatch_t pointer.
|
|
||||||
*/
|
|
||||||
void assetBatchEntryOnLoadedCb(void *params, void *user);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event trampoline invoked when a batch entry fails to load.
|
|
||||||
* Increments the error counter and fires batch-level events.
|
|
||||||
*
|
|
||||||
* @param params The errored assetentry_t pointer.
|
|
||||||
* @param user The owning assetbatch_t pointer.
|
|
||||||
*/
|
|
||||||
void assetBatchEntryOnErrorCb(void *params, void *user);
|
|
||||||
@@ -16,4 +16,4 @@ add_subdirectory(display)
|
|||||||
add_subdirectory(locale)
|
add_subdirectory(locale)
|
||||||
add_subdirectory(json)
|
add_subdirectory(json)
|
||||||
add_subdirectory(dmf)
|
add_subdirectory(dmf)
|
||||||
add_subdirectory(animation)
|
add_subdirectory(script)
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "assetanimationloader.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/string.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "asset/loader/assetloading.h"
|
|
||||||
#include "asset/loader/assetentry.h"
|
|
||||||
|
|
||||||
errorret_t assetAnimationLoaderAsync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertNotMainThread("Async loader should not be on main thread.");
|
|
||||||
|
|
||||||
if(
|
|
||||||
loading->loading.animation.state != ASSET_ANIMATION_LOADING_STATE_READ_FILE
|
|
||||||
) {
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(loading->loading.animation.buffer, "Buffer already defined?");
|
|
||||||
|
|
||||||
assetfile_t *file = &loading->loading.animation.file;
|
|
||||||
assetLoaderErrorChain(
|
|
||||||
loading, assetFileInit(file, loading->entry->name, NULL, NULL)
|
|
||||||
);
|
|
||||||
|
|
||||||
if(file->size > ASSET_ANIMATION_FILE_SIZE_MAX) {
|
|
||||||
assetLoaderErrorThrow(
|
|
||||||
loading, "Animation JSON exceeds maximum allowed size"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t *buffer;
|
|
||||||
size_t size;
|
|
||||||
assetLoaderErrorChain(loading, assetFileReadEntire(file, &buffer, &size));
|
|
||||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
|
||||||
|
|
||||||
loading->loading.animation.buffer = buffer;
|
|
||||||
loading->loading.animation.size = size;
|
|
||||||
loading->loading.animation.state = ASSET_ANIMATION_LOADING_STATE_PARSE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetAnimationLoaderSync(assetloading_t *loading) {
|
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
|
||||||
assertTrue(loading->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
|
|
||||||
assertIsMainThread("Must be called from the main thread.");
|
|
||||||
|
|
||||||
switch(loading->loading.animation.state) {
|
|
||||||
case ASSET_ANIMATION_LOADING_STATE_INITIAL:
|
|
||||||
loading->loading.animation.state =
|
|
||||||
ASSET_ANIMATION_LOADING_STATE_READ_FILE;
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
|
||||||
errorOk();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ASSET_ANIMATION_LOADING_STATE_PARSE:
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t *buffer = loading->loading.animation.buffer;
|
|
||||||
assertNotNull(buffer, "Animation buffer should have been loaded by now.");
|
|
||||||
|
|
||||||
yyjson_doc *doc = yyjson_read(
|
|
||||||
(char *)buffer,
|
|
||||||
loading->loading.animation.size,
|
|
||||||
YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
|
|
||||||
);
|
|
||||||
memoryFree(buffer);
|
|
||||||
loading->loading.animation.buffer = NULL;
|
|
||||||
|
|
||||||
if(!doc) assetLoaderErrorThrow(loading, "Failed to parse animation JSON");
|
|
||||||
|
|
||||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
|
||||||
yyjson_val *channelsVal = yyjson_obj_get(root, "channels");
|
|
||||||
if(!channelsVal || !yyjson_is_arr(channelsVal)) {
|
|
||||||
yyjson_doc_free(doc);
|
|
||||||
assetLoaderErrorThrow(loading, "Animation JSON missing 'channels' array");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t channelCount = (uint16_t)yyjson_arr_size(channelsVal);
|
|
||||||
if(channelCount == 0) {
|
|
||||||
yyjson_doc_free(doc);
|
|
||||||
assetLoaderErrorThrow(loading, "Animation must have at least one channel");
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframe_t **tracks = memoryAllocate(channelCount * sizeof(keyframe_t *));
|
|
||||||
uint16_t *trackCounts = memoryAllocate(channelCount * sizeof(uint16_t));
|
|
||||||
|
|
||||||
size_t idx, max;
|
|
||||||
yyjson_val *channelJson;
|
|
||||||
uint16_t parsed = 0;
|
|
||||||
yyjson_arr_foreach(channelsVal, idx, max, channelJson) {
|
|
||||||
keyframe_t *keyframes;
|
|
||||||
uint16_t count;
|
|
||||||
errorret_t ret =
|
|
||||||
assetAnimationParseChannel(channelJson, &keyframes, &count);
|
|
||||||
if(errorIsNotOk(ret)) {
|
|
||||||
assetAnimationFreeChannels(tracks, parsed);
|
|
||||||
memoryFree(trackCounts);
|
|
||||||
yyjson_doc_free(doc);
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_ERROR;
|
|
||||||
errorChain(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks[idx] = keyframes;
|
|
||||||
trackCounts[idx] = count;
|
|
||||||
parsed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
animationInit(
|
|
||||||
&loading->entry->data.animation, tracks, trackCounts, channelCount
|
|
||||||
);
|
|
||||||
|
|
||||||
yyjson_val *loopVal = yyjson_obj_get(root, "loop");
|
|
||||||
if(loopVal) loading->entry->data.animation.loop = yyjson_get_bool(loopVal);
|
|
||||||
|
|
||||||
yyjson_val *speedVal = yyjson_obj_get(root, "speed");
|
|
||||||
if(speedVal) {
|
|
||||||
loading->entry->data.animation.speed = (float_t)yyjson_get_num(speedVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
yyjson_doc_free(doc);
|
|
||||||
|
|
||||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetAnimationDispose(assetentry_t *entry) {
|
|
||||||
assertNotNull(entry, "Asset entry cannot be NULL");
|
|
||||||
assertTrue(entry->type == ASSET_LOADER_TYPE_ANIMATION, "Invalid type.");
|
|
||||||
assertIsMainThread("Must be called from the main thread.");
|
|
||||||
|
|
||||||
// A load that failed before animationInit() ran (e.g. a parse error)
|
|
||||||
// never populated these -- nothing to free in that case.
|
|
||||||
keyframeset_t *set = &entry->data.animation.keyframes;
|
|
||||||
if(set->tracks) {
|
|
||||||
assetAnimationFreeChannels(set->tracks, set->trackCount);
|
|
||||||
memoryFree(set->trackCounts);
|
|
||||||
set->tracks = NULL;
|
|
||||||
set->trackCounts = NULL;
|
|
||||||
set->trackCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetAnimationParseEasing(
|
|
||||||
const char_t *name,
|
|
||||||
easingtype_t *outEasing
|
|
||||||
) {
|
|
||||||
if(stringEquals(name, "LINEAR")) *outEasing = EASING_LINEAR;
|
|
||||||
else if(stringEquals(name, "IN_SINE")) *outEasing = EASING_IN_SINE;
|
|
||||||
else if(stringEquals(name, "OUT_SINE")) *outEasing = EASING_OUT_SINE;
|
|
||||||
else if(stringEquals(name, "IN_OUT_SINE")) *outEasing = EASING_IN_OUT_SINE;
|
|
||||||
else if(stringEquals(name, "IN_QUAD")) *outEasing = EASING_IN_QUAD;
|
|
||||||
else if(stringEquals(name, "OUT_QUAD")) *outEasing = EASING_OUT_QUAD;
|
|
||||||
else if(stringEquals(name, "IN_OUT_QUAD")) *outEasing = EASING_IN_OUT_QUAD;
|
|
||||||
else if(stringEquals(name, "IN_CUBIC")) *outEasing = EASING_IN_CUBIC;
|
|
||||||
else if(stringEquals(name, "OUT_CUBIC")) *outEasing = EASING_OUT_CUBIC;
|
|
||||||
else if(stringEquals(name, "IN_OUT_CUBIC")) *outEasing = EASING_IN_OUT_CUBIC;
|
|
||||||
else if(stringEquals(name, "IN_QUART")) *outEasing = EASING_IN_QUART;
|
|
||||||
else if(stringEquals(name, "OUT_QUART")) *outEasing = EASING_OUT_QUART;
|
|
||||||
else if(stringEquals(name, "IN_OUT_QUART")) *outEasing = EASING_IN_OUT_QUART;
|
|
||||||
else if(stringEquals(name, "IN_BACK")) *outEasing = EASING_IN_BACK;
|
|
||||||
else if(stringEquals(name, "OUT_BACK")) *outEasing = EASING_OUT_BACK;
|
|
||||||
else if(stringEquals(name, "IN_OUT_BACK")) *outEasing = EASING_IN_OUT_BACK;
|
|
||||||
else errorThrow("Unknown easing type '%s'", name);
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetAnimationParseChannel(
|
|
||||||
yyjson_val *channelJson,
|
|
||||||
keyframe_t **outKeyframes,
|
|
||||||
uint16_t *outCount
|
|
||||||
) {
|
|
||||||
if(!yyjson_is_arr(channelJson)) {
|
|
||||||
errorThrow("Animation channel must be an array");
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t count = yyjson_arr_size(channelJson);
|
|
||||||
if(count == 0) {
|
|
||||||
errorThrow("Animation channel must have at least one keyframe");
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframe_t *keyframes = memoryAllocate(count * sizeof(keyframe_t));
|
|
||||||
|
|
||||||
size_t idx, max;
|
|
||||||
yyjson_val *keyframeJson;
|
|
||||||
yyjson_arr_foreach(channelJson, idx, max, keyframeJson) {
|
|
||||||
yyjson_val *timeVal = yyjson_obj_get(keyframeJson, "time");
|
|
||||||
yyjson_val *valueVal = yyjson_obj_get(keyframeJson, "value");
|
|
||||||
if(!timeVal || !valueVal) {
|
|
||||||
memoryFree(keyframes);
|
|
||||||
errorThrow("Animation keyframe missing 'time' or 'value'");
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframes[idx].time = (float_t)yyjson_get_num(timeVal);
|
|
||||||
keyframes[idx].value = (float_t)yyjson_get_num(valueVal);
|
|
||||||
|
|
||||||
yyjson_val *easingVal = yyjson_obj_get(keyframeJson, "easing");
|
|
||||||
if(!easingVal) {
|
|
||||||
keyframes[idx].easing = EASING_LINEAR;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t ret = assetAnimationParseEasing(
|
|
||||||
yyjson_get_str(easingVal), &keyframes[idx].easing
|
|
||||||
);
|
|
||||||
if(errorIsNotOk(ret)) {
|
|
||||||
memoryFree(keyframes);
|
|
||||||
errorChain(ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*outKeyframes = keyframes;
|
|
||||||
*outCount = (uint16_t)count;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count) {
|
|
||||||
for(uint16_t i = 0; i < count; i++) memoryFree(tracks[i]);
|
|
||||||
memoryFree(tracks);
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "error/error.h"
|
|
||||||
#include "asset/assetfile.h"
|
|
||||||
#include "animation/animation.h"
|
|
||||||
#include "yyjson.h"
|
|
||||||
|
|
||||||
#define ASSET_ANIMATION_FILE_SIZE_MAX 1024*256
|
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON animation file format:
|
|
||||||
* {
|
|
||||||
* "loop": false,
|
|
||||||
* "speed": 1.0,
|
|
||||||
* "channels": [
|
|
||||||
* [
|
|
||||||
* { "time": 0.0, "value": 0.0, "easing": "LINEAR" },
|
|
||||||
* { "time": 1.0, "value": 10.0 }
|
|
||||||
* ]
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* "loop" and "speed" are optional, defaulting to false and 1.0 (see
|
|
||||||
* animationInit()). "channels" is required and must have at least one
|
|
||||||
* entry; each entry is itself a non-empty array of keyframes, ascending
|
|
||||||
* by "time", sharing one timeline with every other channel (see
|
|
||||||
* keyframeset_t). Each keyframe requires "time" and "value"; "easing" is
|
|
||||||
* optional and defaults to "LINEAR" (see easingtype_t for the full set of
|
|
||||||
* names, e.g. "IN_QUAD", "OUT_BACK", "IN_OUT_CUBIC").
|
|
||||||
*/
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ASSET_ANIMATION_LOADING_STATE_INITIAL,
|
|
||||||
ASSET_ANIMATION_LOADING_STATE_READ_FILE,
|
|
||||||
ASSET_ANIMATION_LOADING_STATE_PARSE,
|
|
||||||
ASSET_ANIMATION_LOADING_STATE_DONE
|
|
||||||
} assetanimationloadingstate_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
assetfile_t file;
|
|
||||||
assetanimationloadingstate_t state;
|
|
||||||
uint8_t *buffer;
|
|
||||||
size_t size;
|
|
||||||
} assetanimationloaderloading_t;
|
|
||||||
|
|
||||||
typedef animation_t assetanimationoutput_t;
|
|
||||||
|
|
||||||
errorret_t assetAnimationLoaderAsync(assetloading_t *loading);
|
|
||||||
errorret_t assetAnimationLoaderSync(assetloading_t *loading);
|
|
||||||
errorret_t assetAnimationDispose(assetentry_t *entry);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Maps a JSON easing name (e.g. "IN_OUT_QUAD") to its
|
|
||||||
* easingtype_t.
|
|
||||||
*
|
|
||||||
* @param name The easing name to parse.
|
|
||||||
* @param outEasing Destination for the parsed easing type.
|
|
||||||
* @return Error state; fails if name doesn't match any easingtype_t.
|
|
||||||
*/
|
|
||||||
errorret_t assetAnimationParseEasing(
|
|
||||||
const char_t *name,
|
|
||||||
easingtype_t *outEasing
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Parses one "channels" array entry into a heap-allocated
|
|
||||||
* keyframe_t array (memoryAllocate) -- freed later by
|
|
||||||
* assetAnimationFreeChannels() (on a parse failure) or
|
|
||||||
* assetAnimationDispose() (once loaded).
|
|
||||||
*
|
|
||||||
* @param channelJson The channel's JSON array of keyframe objects.
|
|
||||||
* @param outKeyframes Destination for the newly allocated keyframe array.
|
|
||||||
* @param outCount Destination for the number of keyframes parsed.
|
|
||||||
* @return Error state.
|
|
||||||
*/
|
|
||||||
errorret_t assetAnimationParseChannel(
|
|
||||||
yyjson_val *channelJson,
|
|
||||||
keyframe_t **outKeyframes,
|
|
||||||
uint16_t *outCount
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Frees the first count entries of tracks (each a
|
|
||||||
* memoryAllocate'd keyframe_t array from assetAnimationParseChannel()),
|
|
||||||
* then frees tracks itself. Used both to unwind a partially-parsed
|
|
||||||
* channel list on failure and to free a fully-loaded animation's
|
|
||||||
* channels in assetAnimationDispose().
|
|
||||||
*
|
|
||||||
* @param tracks The channel array to free.
|
|
||||||
* @param count The number of entries in tracks to free.
|
|
||||||
*/
|
|
||||||
void assetAnimationFreeChannels(keyframe_t **tracks, const uint16_t count);
|
|
||||||
@@ -35,22 +35,6 @@ void assetEntryInit(
|
|||||||
entry->input = NULL;
|
entry->input = NULL;
|
||||||
}
|
}
|
||||||
refInit(&entry->refs, entry, NULL, NULL, NULL);
|
refInit(&entry->refs, entry, NULL, NULL, NULL);
|
||||||
|
|
||||||
eventInit(
|
|
||||||
&entry->onLoaded,
|
|
||||||
entry->onLoadedCallbacks, entry->onLoadedUsers,
|
|
||||||
ASSET_ENTRY_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&entry->onUnloaded,
|
|
||||||
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
|
|
||||||
ASSET_ENTRY_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&entry->onError,
|
|
||||||
entry->onErrorCallbacks, entry->onErrorUsers,
|
|
||||||
ASSET_ENTRY_EVENT_MAX
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void assetEntryLock(assetentry_t *entry) {
|
void assetEntryLock(assetentry_t *entry) {
|
||||||
@@ -97,7 +81,6 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
|
|||||||
"Asset entry still refed at dispose time."
|
"Asset entry still refed at dispose time."
|
||||||
);
|
);
|
||||||
|
|
||||||
eventInvoke(&entry->onUnloaded, entry);
|
|
||||||
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
|
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
|
||||||
memoryZero(entry, sizeof(assetentry_t));
|
memoryZero(entry, sizeof(assetentry_t));
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "asset/loader/assetloading.h"
|
#include "asset/loader/assetloading.h"
|
||||||
#include "event/event.h"
|
|
||||||
#include "util/ref.h"
|
#include "util/ref.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -20,9 +19,6 @@ typedef enum {
|
|||||||
ASSET_ENTRY_STATE_ERROR
|
ASSET_ENTRY_STATE_ERROR
|
||||||
} assetentrystate_t;
|
} assetentrystate_t;
|
||||||
|
|
||||||
/** Maximum number of subscribers for each per-entry event. */
|
|
||||||
#define ASSET_ENTRY_EVENT_MAX 2
|
|
||||||
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
struct assetentry_s {
|
struct assetentry_s {
|
||||||
@@ -33,30 +29,6 @@ struct assetentry_s {
|
|||||||
ref_t refs;
|
ref_t refs;
|
||||||
assetloaderinput_t *input;
|
assetloaderinput_t *input;
|
||||||
assetloaderinput_t inputData;
|
assetloaderinput_t inputData;
|
||||||
/**
|
|
||||||
* Fired once when loading completes successfully (params = assetentry_t *).
|
|
||||||
* Always invoked on the main thread.
|
|
||||||
*/
|
|
||||||
event_t onLoaded;
|
|
||||||
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
|
|
||||||
* The asset data is still accessible when the callback runs.
|
|
||||||
* Always invoked on the main thread.
|
|
||||||
*/
|
|
||||||
event_t onUnloaded;
|
|
||||||
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fired once when loading fails (params = assetentry_t *).
|
|
||||||
* Always invoked on the main thread.
|
|
||||||
*/
|
|
||||||
event_t onError;
|
|
||||||
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +76,6 @@ void assetEntryStartLoading(assetentry_t *entry, assetloading_t *loading);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes an asset entry, freeing any resources it holds.
|
* Disposes an asset entry, freeing any resources it holds.
|
||||||
* Fires the onUnloaded event before releasing asset data.
|
|
||||||
*
|
*
|
||||||
* @param entry The asset entry to dispose.
|
* @param entry The asset entry to dispose.
|
||||||
* @return Any error that occurs during disposal.
|
* @return Any error that occurs during disposal.
|
||||||
|
|||||||
@@ -46,9 +46,8 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
|||||||
.dispose = assetJsonDispose
|
.dispose = assetJsonDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
[ASSET_LOADER_TYPE_ANIMATION] = {
|
[ASSET_LOADER_TYPE_SCRIPT] = {
|
||||||
.loadSync = assetAnimationLoaderSync,
|
.loadSync = assetScriptLoaderSync,
|
||||||
.loadAsync = assetAnimationLoaderAsync,
|
.dispose = assetScriptDispose
|
||||||
.dispose = assetAnimationDispose
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#include "asset/loader/display/assettilesetloader.h"
|
#include "asset/loader/display/assettilesetloader.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "asset/loader/json/assetjsonloader.h"
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "asset/loader/animation/assetanimationloader.h"
|
#include "asset/loader/script/assetscriptloader.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_LOADER_TYPE_NULL,
|
ASSET_LOADER_TYPE_NULL,
|
||||||
@@ -23,7 +23,7 @@ typedef enum {
|
|||||||
ASSET_LOADER_TYPE_TILESET,
|
ASSET_LOADER_TYPE_TILESET,
|
||||||
ASSET_LOADER_TYPE_LOCALE,
|
ASSET_LOADER_TYPE_LOCALE,
|
||||||
ASSET_LOADER_TYPE_JSON,
|
ASSET_LOADER_TYPE_JSON,
|
||||||
ASSET_LOADER_TYPE_ANIMATION,
|
ASSET_LOADER_TYPE_SCRIPT,
|
||||||
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
ASSET_LOADER_TYPE_COUNT
|
||||||
} assetloadertype_t;
|
} assetloadertype_t;
|
||||||
@@ -35,7 +35,6 @@ typedef union {
|
|||||||
assettilesetloaderloading_t tileset;
|
assettilesetloaderloading_t tileset;
|
||||||
assetlocaleloaderloading_t locale;
|
assetlocaleloaderloading_t locale;
|
||||||
assetjsonloaderloading_t json;
|
assetjsonloaderloading_t json;
|
||||||
assetanimationloaderloading_t animation;
|
|
||||||
} assetloaderloading_t;
|
} assetloaderloading_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -45,7 +44,7 @@ typedef union {
|
|||||||
assettilesetoutput_t tileset;
|
assettilesetoutput_t tileset;
|
||||||
assetlocaleoutput_t locale;
|
assetlocaleoutput_t locale;
|
||||||
assetjsonoutput_t json;
|
assetjsonoutput_t json;
|
||||||
assetanimationoutput_t animation;
|
assetscriptoutput_t script;
|
||||||
} assetloaderoutput_t;
|
} assetloaderoutput_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -85,6 +84,25 @@ extern assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT];
|
|||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like @ref assetLoaderErrorChain, but also frees `_ptr` (via memoryFree)
|
||||||
|
* before chaining the error if `_expr` failed. Use this for any loader step
|
||||||
|
* that runs after a buffer has already been allocated, so a later I/O
|
||||||
|
* failure doesn't leak it.
|
||||||
|
*
|
||||||
|
* @param loading The asset loading slot.
|
||||||
|
* @param _ptr A heap pointer to free if `_expr` fails.
|
||||||
|
* @param _expr The error return value to check and chain if it's an error.
|
||||||
|
*/
|
||||||
|
#define assetLoaderErrorChainFree(loading, _ptr, _expr) {\
|
||||||
|
errorret_t _alecf = (_expr); \
|
||||||
|
if(errorIsNotOk(_alecf)) { \
|
||||||
|
memoryFree(_ptr); \
|
||||||
|
(loading)->entry->state = ASSET_ENTRY_STATE_ERROR; \
|
||||||
|
errorChain(_alecf); \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand method to both throw an error (against the loader state) and to
|
* Shorthand method to both throw an error (against the loader state) and to
|
||||||
* set the asset entry state to error.
|
* set the asset entry state to error.
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ errorret_t assetTilesetLoaderAsync(assetloading_t *loading) {
|
|||||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||||
);
|
);
|
||||||
|
|
||||||
uint8_t *data = memoryAllocate(file->size);
|
|
||||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
|
||||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
uint8_t *data = memoryAllocate(file->size);
|
||||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
assetLoaderErrorChainFree(loading, data, assetFileRead(file, data, file->size));
|
||||||
|
assetLoaderErrorChainFree(loading, data, assetFileClose(file));
|
||||||
|
assetLoaderErrorChainFree(loading, data, assetFileDispose(file));
|
||||||
assertTrue(
|
assertTrue(
|
||||||
file->lastRead == file->size,
|
file->lastRead == file->size,
|
||||||
"Failed to read entire tileset file."
|
"Failed to read entire tileset file."
|
||||||
@@ -102,6 +103,11 @@ errorret_t assetTilesetLoaderSync(assetloading_t *loading) {
|
|||||||
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
|
out->uv[0] = endianLittleToHostFloat(*(float *)(data + 16));
|
||||||
out->uv[1] = endianLittleToHostFloat(*(float *)(data + 20));
|
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) {
|
if(out->uv[1] < 0.0f || out->uv[1] > 1.0f) {
|
||||||
memoryFree(data);
|
memoryFree(data);
|
||||||
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
|
assetLoaderErrorThrow(loading, "Invalid v0 value in tileset");
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ errorret_t assetMeshLoaderAsync(assetloading_t *loading) {
|
|||||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||||
);
|
);
|
||||||
|
|
||||||
uint8_t *raw = memoryAllocate(file->size);
|
|
||||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||||
assetLoaderErrorChain(loading, assetFileRead(file, raw, file->size));
|
|
||||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
uint8_t *raw = memoryAllocate(file->size);
|
||||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
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.");
|
assertTrue(file->lastRead == file->size, "Failed to read entire DMF file.");
|
||||||
|
|
||||||
if(raw[0] != 'D' || raw[1] != 'M' || raw[2] != 'F') {
|
if(raw[0] != 'D' || raw[1] != 'M' || raw[2] != 'F') {
|
||||||
@@ -127,6 +128,14 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
|||||||
errorChain(ret);
|
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;
|
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ errorret_t assetJsonLoaderAsync(assetloading_t *loading) {
|
|||||||
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
|
assetLoaderErrorThrow(loading, "JSON exceeds maximum allowed size");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||||
|
|
||||||
size_t fileSize = (size_t)file->size;
|
size_t fileSize = (size_t)file->size;
|
||||||
uint8_t *buffer = memoryAllocate(fileSize);
|
uint8_t *buffer = memoryAllocate(fileSize);
|
||||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
assetLoaderErrorChainFree(loading, buffer, assetFileRead(file, buffer, fileSize));
|
||||||
assetLoaderErrorChain(loading, assetFileRead(file, buffer, fileSize));
|
|
||||||
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
|
assertTrue(file->lastRead == file->size, "Failed to read entire JSON file.");
|
||||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
assetLoaderErrorChainFree(loading, buffer, assetFileClose(file));
|
||||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
assetLoaderErrorChainFree(loading, buffer, assetFileDispose(file));
|
||||||
|
|
||||||
loading->loading.json.buffer = buffer;
|
loading->loading.json.buffer = buffer;
|
||||||
loading->loading.json.size = fileSize;
|
loading->loading.json.size = fileSize;
|
||||||
|
|||||||
@@ -482,6 +482,24 @@ errorret_t assetLocaleGetString(
|
|||||||
assertTrue(pluralCount >= 0, "Plural index cannot be negative.");
|
assertTrue(pluralCount >= 0, "Plural index cannot be negative.");
|
||||||
assertNotNull(stringBuffer, "String buffer cannot be NULL.");
|
assertNotNull(stringBuffer, "String buffer cannot be NULL.");
|
||||||
assertTrue(stringBufferSize > 0, "String buffer size must be > 0");
|
assertTrue(stringBufferSize > 0, "String buffer size must be > 0");
|
||||||
|
|
||||||
|
// Check the cache before rewinding/rescanning the whole file.
|
||||||
|
for(uint8_t i = 0; i < ASSET_LOCALE_STRING_CACHE_SIZE; i++) {
|
||||||
|
assetlocalecacheentry_t *entry = &file->stringCache[i];
|
||||||
|
if(
|
||||||
|
!entry->valid ||
|
||||||
|
entry->pluralCount != pluralCount ||
|
||||||
|
stringCompare(messageId, entry->messageId) != 0
|
||||||
|
) continue;
|
||||||
|
|
||||||
|
size_t valueLen = strlen(entry->value);
|
||||||
|
if(valueLen >= stringBufferSize) {
|
||||||
|
errorThrow("String buffer overflow");
|
||||||
|
}
|
||||||
|
memoryCopy(stringBuffer, entry->value, valueLen + 1);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
assetfilelinereader_t reader;
|
assetfilelinereader_t reader;
|
||||||
|
|
||||||
bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false;
|
bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false;
|
||||||
@@ -625,6 +643,20 @@ errorret_t assetLocaleGetString(
|
|||||||
errorThrow("Failed to find msgstr for message ID: %s", messageId);
|
errorThrow("Failed to find msgstr for message ID: %s", messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache the resolved string for future lookups, if it fits.
|
||||||
|
if(
|
||||||
|
strlen(messageId) < ASSET_LOCALE_CACHE_MESSAGE_ID_MAX &&
|
||||||
|
strlen(stringBuffer) < ASSET_LOCALE_CACHE_VALUE_MAX
|
||||||
|
) {
|
||||||
|
assetlocalecacheentry_t *entry = &file->stringCache[file->stringCacheNext];
|
||||||
|
stringCopy(entry->messageId, messageId, sizeof(entry->messageId) - 1);
|
||||||
|
stringCopy(entry->value, stringBuffer, sizeof(entry->value) - 1);
|
||||||
|
entry->pluralCount = pluralCount;
|
||||||
|
entry->valid = true;
|
||||||
|
file->stringCacheNext =
|
||||||
|
(file->stringCacheNext + 1) % ASSET_LOCALE_STRING_CACHE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,27 @@ typedef struct {
|
|||||||
/** Maximum number of distinct plural forms a locale file may declare. */
|
/** Maximum number of distinct plural forms a locale file may declare. */
|
||||||
#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6
|
#define ASSET_LOCALE_FILE_PLURAL_FORM_COUNT 6
|
||||||
|
|
||||||
|
/** Max distinct resolved strings cached per open locale file. */
|
||||||
|
#define ASSET_LOCALE_STRING_CACHE_SIZE 16
|
||||||
|
|
||||||
|
/** Max message ID length (incl. null terminator) storable in the cache. */
|
||||||
|
#define ASSET_LOCALE_CACHE_MESSAGE_ID_MAX 128
|
||||||
|
|
||||||
|
/** Max resolved string length (incl. null terminator) storable in the cache. */
|
||||||
|
#define ASSET_LOCALE_CACHE_VALUE_MAX 256
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single cached (messageId, pluralCount) -> resolved string lookup, used by
|
||||||
|
* @ref assetLocaleGetString to avoid rescanning the whole PO file for
|
||||||
|
* repeated lookups of the same message.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
bool_t valid;
|
||||||
|
char_t messageId[ASSET_LOCALE_CACHE_MESSAGE_ID_MAX];
|
||||||
|
int32_t pluralCount;
|
||||||
|
char_t value[ASSET_LOCALE_CACHE_VALUE_MAX];
|
||||||
|
} assetlocalecacheentry_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparison operator used in a plural-form expression.
|
* Comparison operator used in a plural-form expression.
|
||||||
*
|
*
|
||||||
@@ -98,6 +119,12 @@ typedef struct {
|
|||||||
|
|
||||||
/** Form index used when no conditional clause matches. */
|
/** Form index used when no conditional clause matches. */
|
||||||
uint8_t pluralDefaultIndex;
|
uint8_t pluralDefaultIndex;
|
||||||
|
|
||||||
|
/** Ring buffer of recently resolved (messageId, pluralCount) lookups. */
|
||||||
|
assetlocalecacheentry_t stringCache[ASSET_LOCALE_STRING_CACHE_SIZE];
|
||||||
|
|
||||||
|
/** Next slot in @ref stringCache to overwrite. */
|
||||||
|
uint8_t stringCacheNext;
|
||||||
} assetlocalefile_t;
|
} assetlocalefile_t;
|
||||||
|
|
||||||
/** Convenience alias - the loaded output type of a locale asset entry. */
|
/** Convenience alias - the loaded output type of a locale asset entry. */
|
||||||
|
|||||||
+1
-1
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
modulerequire.c
|
assetscriptloader.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "assetscriptloader.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "asset/loader/assetloading.h"
|
||||||
|
#include "asset/loader/assetentry.h"
|
||||||
|
|
||||||
|
static void assetScriptSettleWithError(
|
||||||
|
assetscriptoutput_t *output,
|
||||||
|
const char_t *message
|
||||||
|
) {
|
||||||
|
jerry_value_t errVal = jerry_string_sz(message);
|
||||||
|
jerry_value_t rejectResult = jerry_promise_reject(output->promise, errVal);
|
||||||
|
jerry_value_free(rejectResult);
|
||||||
|
jerry_value_free(errVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetScriptLoaderSync(assetloading_t *loading) {
|
||||||
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
|
assertTrue(loading->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
|
||||||
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
|
assetentry_t *entry = loading->entry;
|
||||||
|
assetscriptoutput_t *output = &entry->data.script;
|
||||||
|
assertTrue(
|
||||||
|
output->promise != 0,
|
||||||
|
"Script entry has no promise - was it requested via include()?"
|
||||||
|
);
|
||||||
|
|
||||||
|
assetfile_t file;
|
||||||
|
uint8_t *buffer = NULL;
|
||||||
|
size_t size = 0;
|
||||||
|
|
||||||
|
errorret_t err = assetFileInit(&file, entry->name, NULL, NULL);
|
||||||
|
if(errorIsOk(err)) err = assetFileReadEntire(&file, &buffer, &size);
|
||||||
|
if(errorIsOk(err)) err = assetFileDispose(&file);
|
||||||
|
|
||||||
|
if(errorIsNotOk(err)) {
|
||||||
|
assetScriptSettleWithError(output, err.state->message);
|
||||||
|
entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorChain(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t *src = (char_t *)memoryAllocate(size + 1);
|
||||||
|
memoryCopy(src, buffer, size);
|
||||||
|
src[size] = '\0';
|
||||||
|
memoryFree(buffer);
|
||||||
|
|
||||||
|
// Scripts export their public API by assigning to the global `module`.
|
||||||
|
// Swap it out around the eval so this doesn't clobber a caller's own
|
||||||
|
// in-flight include() of a different file.
|
||||||
|
jerry_value_t global = jerry_current_realm();
|
||||||
|
jerry_value_t moduleKey = jerry_string_sz("module");
|
||||||
|
jerry_value_t prevModule = jerry_object_get(global, moduleKey);
|
||||||
|
jerry_value_t undef = jerry_undefined();
|
||||||
|
jerry_object_set(global, moduleKey, undef);
|
||||||
|
jerry_value_free(undef);
|
||||||
|
|
||||||
|
jerry_value_t evalResult = jerry_eval(
|
||||||
|
(const jerry_char_t *)src, size, JERRY_PARSE_NO_OPTS
|
||||||
|
);
|
||||||
|
memoryFree(src);
|
||||||
|
|
||||||
|
if(jerry_value_is_exception(evalResult)) {
|
||||||
|
jerry_value_t errVal = jerry_exception_value(evalResult, false);
|
||||||
|
jerry_value_t rejectResult = jerry_promise_reject(output->promise, errVal);
|
||||||
|
jerry_value_free(rejectResult);
|
||||||
|
jerry_value_free(errVal);
|
||||||
|
jerry_value_free(evalResult);
|
||||||
|
|
||||||
|
jerry_value_t moduleVal = jerry_object_get(global, moduleKey);
|
||||||
|
jerry_value_free(moduleVal);
|
||||||
|
jerry_object_set(global, moduleKey, prevModule);
|
||||||
|
jerry_value_free(prevModule);
|
||||||
|
jerry_value_free(moduleKey);
|
||||||
|
jerry_value_free(global);
|
||||||
|
|
||||||
|
entry->state = ASSET_ENTRY_STATE_ERROR;
|
||||||
|
errorThrow("Script error in '%s'", entry->name);
|
||||||
|
}
|
||||||
|
jerry_value_free(evalResult);
|
||||||
|
|
||||||
|
jerry_value_t moduleVal = jerry_object_get(global, moduleKey);
|
||||||
|
jerry_object_set(global, moduleKey, prevModule);
|
||||||
|
jerry_value_free(prevModule);
|
||||||
|
jerry_value_free(moduleKey);
|
||||||
|
jerry_value_free(global);
|
||||||
|
|
||||||
|
jerry_value_t resolveResult = jerry_promise_resolve(output->promise, moduleVal);
|
||||||
|
jerry_value_free(resolveResult);
|
||||||
|
jerry_value_free(moduleVal);
|
||||||
|
|
||||||
|
entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t assetScriptDispose(assetentry_t *entry) {
|
||||||
|
assertNotNull(entry, "Asset entry cannot be NULL");
|
||||||
|
assertTrue(entry->type == ASSET_LOADER_TYPE_SCRIPT, "Invalid type.");
|
||||||
|
assertIsMainThread("Must be called from the main thread.");
|
||||||
|
|
||||||
|
assetscriptoutput_t *output = &entry->data.script;
|
||||||
|
if(output->promise != 0) {
|
||||||
|
jerry_value_free(output->promise);
|
||||||
|
output->promise = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "asset/assetfile.h"
|
||||||
|
#include <jerryscript.h>
|
||||||
|
|
||||||
|
typedef struct assetloading_s assetloading_t;
|
||||||
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output data for a script asset entry. The promise is created once, the
|
||||||
|
* first time a script requests this file (see moduleIncludeInclude), and is
|
||||||
|
* owned by the entry for its lifetime - every subsequent request for the
|
||||||
|
* same file is handed a copy of this same promise instead of starting a
|
||||||
|
* second load. It is resolved (with the script's exported `module` value)
|
||||||
|
* or rejected (with the JS exception) exactly once, the moment the entry
|
||||||
|
* transitions to ASSET_ENTRY_STATE_LOADED or ASSET_ENTRY_STATE_ERROR.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
jerry_value_t promise;
|
||||||
|
} assetscriptoutput_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and evaluates a script asset synchronously (reads the whole file
|
||||||
|
* from the archive, then runs it) and resolves/rejects the entry's promise
|
||||||
|
* with the outcome. Scripts are small and read instantly, so there is no
|
||||||
|
* async (background-thread) phase - this is the only loader function.
|
||||||
|
*
|
||||||
|
* @param loading The asset loading slot.
|
||||||
|
* @return An error if reading failed, or errorOk() (a script exception is
|
||||||
|
* reported via promise rejection, not a returned error).
|
||||||
|
*/
|
||||||
|
errorret_t assetScriptLoaderSync(assetloading_t *loading);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the promise reference held by a script asset entry.
|
||||||
|
*
|
||||||
|
* @param entry The asset entry to dispose.
|
||||||
|
*/
|
||||||
|
errorret_t assetScriptDispose(assetentry_t *entry);
|
||||||
@@ -19,7 +19,9 @@ void consoleInit(void) {
|
|||||||
memoryZero(&CONSOLE, sizeof(console_t));
|
memoryZero(&CONSOLE, sizeof(console_t));
|
||||||
CONSOLE.visible = false;
|
CONSOLE.visible = false;
|
||||||
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexInit(&CONSOLE.printMutex);
|
threadMutexInit(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void consolePrint(const char_t *message, ...) {
|
void consolePrint(const char_t *message, ...) {
|
||||||
@@ -30,7 +32,9 @@ void consolePrint(const char_t *message, ...) {
|
|||||||
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexLock(&CONSOLE.printMutex);
|
threadMutexLock(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
|
|
||||||
memoryMove(
|
memoryMove(
|
||||||
CONSOLE.line[0],
|
CONSOLE.line[0],
|
||||||
@@ -38,9 +42,10 @@ void consolePrint(const char_t *message, ...) {
|
|||||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||||
);
|
);
|
||||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||||
CONSOLE.dirty = true;
|
|
||||||
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexUnlock(&CONSOLE.printMutex);
|
threadMutexUnlock(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
|
|
||||||
logDebug("%s\n", buffer);
|
logDebug("%s\n", buffer);
|
||||||
}
|
}
|
||||||
@@ -50,11 +55,13 @@ void consoleUpdate(void) {
|
|||||||
if(TIME.dynamicUpdate) return;
|
if(TIME.dynamicUpdate) return;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(inputPressed(INPUT_BIND_CONSOLE)) {
|
if(inputPressed(INPUT_ACTION_CONSOLE)) {
|
||||||
CONSOLE.visible = !CONSOLE.visible;
|
CONSOLE.visible = !CONSOLE.visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void consoleDispose(void) {
|
void consoleDispose(void) {
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
threadMutexDispose(&CONSOLE.printMutex);
|
threadMutexDispose(&CONSOLE.printMutex);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
@@ -9,18 +9,21 @@
|
|||||||
#include "consoledefs.h"
|
#include "consoledefs.h"
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
#include "thread/thread.h"
|
|
||||||
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
|
#include "thread/thread.h"
|
||||||
|
#include <poll.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#define CONSOLE_POSIX_POLL_RATE 75
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||||
bool_t visible;
|
bool_t visible;
|
||||||
|
|
||||||
// Set whenever the history changes; consumers rendering the console
|
#ifdef DUSK_CONSOLE_POSIX
|
||||||
// (e.g. uiConsoleDraw) check and clear this to know when their own
|
|
||||||
// cached representation of the history needs rebuilding.
|
|
||||||
bool_t dirty;
|
|
||||||
|
|
||||||
threadmutex_t printMutex;
|
threadmutex_t printMutex;
|
||||||
|
#endif
|
||||||
} console_t;
|
} console_t;
|
||||||
|
|
||||||
extern console_t CONSOLE;
|
extern console_t CONSOLE;
|
||||||
|
|||||||
@@ -22,12 +22,6 @@ add_subdirectory(texture)
|
|||||||
dusk_run_python(
|
dusk_run_python(
|
||||||
dusk_color_defs
|
dusk_color_defs
|
||||||
tools.color
|
tools.color
|
||||||
OUTPUT ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
|
|
||||||
DEPENDS
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/color.csv
|
|
||||||
${DUSK_TOOLS_DIR}/color.py
|
|
||||||
${DUSK_TOOLS_DIR}/writeifchanged.py
|
|
||||||
ARGS
|
|
||||||
--csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv
|
--csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv
|
||||||
--output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
|
--output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "display/framebuffer/framebuffer.h"
|
#include "display/framebuffer/framebuffer.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
|
#include "entity/entityrender.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/mesh/quad.h"
|
#include "display/mesh/quad.h"
|
||||||
#include "display/mesh/cube.h"
|
#include "display/mesh/cube.h"
|
||||||
@@ -80,6 +81,7 @@ errorret_t displayUpdate(void) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
errorChain(sceneRender());
|
errorChain(sceneRender());
|
||||||
|
errorChain(entityRenderAll());
|
||||||
|
|
||||||
// Finish up
|
// Finish up
|
||||||
screenUnbind();
|
screenUnbind();
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ mesh_t CUBE_MESH_SIMPLE;
|
|||||||
meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||||
|
|
||||||
errorret_t cubeInit() {
|
errorret_t cubeInit() {
|
||||||
vec3 min = { -0.5f, -0.5f, -0.5f };
|
vec3 min = { 0.0f, 0.0f, 0.0f };
|
||||||
vec3 max = { 0.5f, 0.5f, 0.5f };
|
vec3 max = { 1.0f, 1.0f, 1.0f };
|
||||||
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max);
|
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max);
|
||||||
errorChain(meshInit(
|
errorChain(meshInit(
|
||||||
&CUBE_MESH_SIMPLE,
|
&CUBE_MESH_SIMPLE,
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ extern mesh_t CUBE_MESH_SIMPLE;
|
|||||||
extern meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
extern meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the simple unit cube mesh, centered at (0,0,0), spanning
|
* Initializes the simple unit cube mesh (0,0,0) to (1,1,1).
|
||||||
* (-0.5,-0.5,-0.5) to (0.5,0.5,0.5) -- matching the centered convention
|
|
||||||
* physics shapes and the sphere mesh use (position = center).
|
|
||||||
*
|
*
|
||||||
* @return Error for initialization of the cube mesh.
|
* @return Error for initialization of the cube mesh.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ void planeBuffer(
|
|||||||
|
|
||||||
switch(axis) {
|
switch(axis) {
|
||||||
case PLANE_AXIS_XY: {
|
case PLANE_AXIS_XY: {
|
||||||
// Flat in XY at z = min[2]; spans X and Y.
|
/* Flat in XY at z = min[2]; spans X and Y. */
|
||||||
// +Z normal: CCW when viewed from +Z (matches cube.c's front face).
|
|
||||||
const float_t z = min[2];
|
const float_t z = min[2];
|
||||||
PLANE_VERT(0, min[0], min[1], z, u0, v0)
|
PLANE_VERT(0, min[0], min[1], z, u0, v0)
|
||||||
PLANE_VERT(1, max[0], min[1], z, u1, v0)
|
PLANE_VERT(1, max[0], min[1], z, u1, v0)
|
||||||
@@ -72,24 +71,19 @@ void planeBuffer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case PLANE_AXIS_XZ: {
|
case PLANE_AXIS_XZ: {
|
||||||
// Flat in XZ at y = min[1]; spans X and Z.
|
/* Flat in XZ at y = min[1]; spans X and Z. */
|
||||||
// +Y normal: CCW when viewed from +Y (matches cube.c's top face).
|
|
||||||
// X and Z swap handedness relative to XY/YZ (right-hand rule with Y
|
|
||||||
// up), so the corner order here is deliberately not a straight copy
|
|
||||||
// of the XY/YZ pattern -- copying it as-is would flip this to -Y.
|
|
||||||
const float_t y = min[1];
|
const float_t y = min[1];
|
||||||
PLANE_VERT(0, min[0], y, min[2], u0, v0)
|
PLANE_VERT(0, min[0], y, min[2], u0, v0)
|
||||||
PLANE_VERT(1, max[0], y, max[2], u1, v1)
|
PLANE_VERT(1, max[0], y, min[2], u1, v0)
|
||||||
PLANE_VERT(2, max[0], y, min[2], u1, v0)
|
PLANE_VERT(2, max[0], y, max[2], u1, v1)
|
||||||
PLANE_VERT(3, min[0], y, min[2], u0, v0)
|
PLANE_VERT(3, min[0], y, min[2], u0, v0)
|
||||||
PLANE_VERT(4, min[0], y, max[2], u0, v1)
|
PLANE_VERT(4, max[0], y, max[2], u1, v1)
|
||||||
PLANE_VERT(5, max[0], y, max[2], u1, v1)
|
PLANE_VERT(5, min[0], y, max[2], u0, v1)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case PLANE_AXIS_YZ: {
|
case PLANE_AXIS_YZ: {
|
||||||
// Flat in YZ at x = min[0]; spans Y and Z.
|
/* Flat in YZ at x = min[0]; spans Y and Z. */
|
||||||
// +X normal: CCW when viewed from +X (matches cube.c's right face).
|
|
||||||
const float_t x = min[0];
|
const float_t x = min[0];
|
||||||
PLANE_VERT(0, x, min[1], min[2], u0, v0)
|
PLANE_VERT(0, x, min[1], min[2], u0, v0)
|
||||||
PLANE_VERT(1, x, max[1], min[2], u1, v0)
|
PLANE_VERT(1, x, max[1], min[2], u1, v0)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
|
|||||||
|
|
||||||
errorret_t shaderBind(shader_t *shader) {
|
errorret_t shaderBind(shader_t *shader) {
|
||||||
assertNotNull(shader, "Shader cannot be null");
|
assertNotNull(shader, "Shader cannot be null");
|
||||||
if(bound == shader) errorOk();
|
|
||||||
errorChain(shaderBindPlatform(shader));
|
errorChain(shaderBindPlatform(shader));
|
||||||
bound = shader;
|
bound = shader;
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -39,16 +39,3 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
|||||||
sprite.uvMax[1] = uv[3];
|
sprite.uvMax[1] = uv[3];
|
||||||
return sprite;
|
return sprite;
|
||||||
}
|
}
|
||||||
|
|
||||||
spritebatchsprite_t spriteBatchSpriteTranslate(
|
|
||||||
const spritebatchsprite_t *sprite,
|
|
||||||
const float_t x,
|
|
||||||
const float_t y
|
|
||||||
) {
|
|
||||||
spritebatchsprite_t out = *sprite;
|
|
||||||
out.min[0] += x;
|
|
||||||
out.min[1] += y;
|
|
||||||
out.max[0] += x;
|
|
||||||
out.max[1] += y;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,19 +38,3 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
|||||||
const float_t width,
|
const float_t width,
|
||||||
const float_t height
|
const float_t height
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a copy of sprite translated by (x, y). Used to reposition a
|
|
||||||
* sprite cached relative to origin (0,0) at draw time, instead of
|
|
||||||
* re-deriving its geometry from scratch every frame.
|
|
||||||
*
|
|
||||||
* @param sprite The cached sprite, relative to origin (0,0).
|
|
||||||
* @param x X offset to translate by.
|
|
||||||
* @param y Y offset to translate by.
|
|
||||||
* @returns The translated sprite.
|
|
||||||
*/
|
|
||||||
spritebatchsprite_t spriteBatchSpriteTranslate(
|
|
||||||
const spritebatchsprite_t *sprite,
|
|
||||||
const float_t x,
|
|
||||||
const float_t y
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -6,6 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
font.c
|
|
||||||
text.c
|
text.c
|
||||||
)
|
)
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "font.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/math.h"
|
|
||||||
#include "display/color.h"
|
|
||||||
|
|
||||||
font_t FONT_DEFAULT;
|
|
||||||
static texture_t FONT_DEFAULT_TEXTURE;
|
|
||||||
static tileset_t FONT_DEFAULT_TILESET;
|
|
||||||
|
|
||||||
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
|
||||||
FONT_DEFAULT_TILE_HEIGHT
|
|
||||||
] = {
|
|
||||||
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
|
|
||||||
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
|
|
||||||
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
|
|
||||||
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
|
|
||||||
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
|
|
||||||
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
|
|
||||||
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
|
|
||||||
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
|
|
||||||
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
|
|
||||||
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
|
|
||||||
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
|
|
||||||
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
|
|
||||||
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
|
|
||||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
|
|
||||||
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
|
|
||||||
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
|
|
||||||
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
|
|
||||||
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
|
|
||||||
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
|
|
||||||
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
|
|
||||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
|
|
||||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
|
|
||||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
|
|
||||||
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
|
|
||||||
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
|
|
||||||
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
|
|
||||||
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
|
|
||||||
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
|
|
||||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
|
|
||||||
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
|
|
||||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
|
|
||||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
|
|
||||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
|
|
||||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
|
||||||
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
|
||||||
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
|
|
||||||
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
|
||||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
|
||||||
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
|
|
||||||
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
|
|
||||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
|
|
||||||
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
|
|
||||||
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
|
|
||||||
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
|
|
||||||
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
|
|
||||||
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
|
|
||||||
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
|
|
||||||
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
|
|
||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
|
||||||
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
|
||||||
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
|
|
||||||
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
|
||||||
};
|
|
||||||
|
|
||||||
errorret_t fontInitDefault(void) {
|
|
||||||
const int32_t width = (int32_t)mathNextPowTwo(
|
|
||||||
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
|
||||||
);
|
|
||||||
const int32_t height = (int32_t)mathNextPowTwo(
|
|
||||||
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
|
|
||||||
);
|
|
||||||
|
|
||||||
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
|
|
||||||
memoryZero(pixels, sizeof(color_t) * width * height);
|
|
||||||
|
|
||||||
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
|
|
||||||
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
|
|
||||||
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
|
|
||||||
|
|
||||||
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
|
|
||||||
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
|
|
||||||
|
|
||||||
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
|
|
||||||
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
|
|
||||||
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
|
|
||||||
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
|
|
||||||
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
|
|
||||||
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
|
|
||||||
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
|
|
||||||
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
|
|
||||||
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
|
|
||||||
|
|
||||||
const texturedata_t data = { .rgbaColors = pixels };
|
|
||||||
errorret_t textureResult = textureInit(
|
|
||||||
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
|
|
||||||
);
|
|
||||||
memoryFree(pixels);
|
|
||||||
errorChain(textureResult);
|
|
||||||
|
|
||||||
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
|
|
||||||
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t fontDisposeDefault(void) {
|
|
||||||
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
|
||||||
FONT_DEFAULT.texture = NULL;
|
|
||||||
FONT_DEFAULT.tileset = NULL;
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
#include "display/texture/tileset.h"
|
#include "display/texture/tileset.h"
|
||||||
|
|
||||||
@@ -14,53 +13,3 @@ typedef struct {
|
|||||||
texture_t *texture;
|
texture_t *texture;
|
||||||
tileset_t *tileset;
|
tileset_t *tileset;
|
||||||
} font_t;
|
} font_t;
|
||||||
|
|
||||||
/**
|
|
||||||
* Pixel width/height of a single default-font glyph tile. Matches the
|
|
||||||
* glyph grid baked into the (now retired) ui/minogram.png + .dtf asset
|
|
||||||
* pair this data was extracted from.
|
|
||||||
*/
|
|
||||||
#define FONT_DEFAULT_TILE_WIDTH 6
|
|
||||||
#define FONT_DEFAULT_TILE_HEIGHT 10
|
|
||||||
|
|
||||||
/** Grid layout of the generated default-font texture, in tiles. */
|
|
||||||
#define FONT_DEFAULT_COLUMNS 16
|
|
||||||
#define FONT_DEFAULT_ROWS 6
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
|
||||||
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
|
||||||
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
|
|
||||||
*/
|
|
||||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
|
||||||
|
|
||||||
extern font_t FONT_DEFAULT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hard coded bitmap data for the built-in default font, extracted
|
|
||||||
* pixel-for-pixel from the original ui/minogram.png glyph atlas (alpha
|
|
||||||
* >= 128 counts as set). Indexed [glyph][row], where glyph 0 corresponds
|
|
||||||
* to TEXT_CHAR_START ('!') and glyphs run consecutively through the
|
|
||||||
* printable ASCII range. Each row byte holds FONT_DEFAULT_TILE_WIDTH bit
|
|
||||||
* flags, one per pixel column: bit (FONT_DEFAULT_TILE_WIDTH - 1) is the
|
|
||||||
* leftmost pixel and bit 0 is the rightmost; 1 means the pixel is set,
|
|
||||||
* 0 means it is not.
|
|
||||||
*/
|
|
||||||
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
|
||||||
FONT_DEFAULT_TILE_HEIGHT
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the built-in default font (texture + tileset) directly from
|
|
||||||
* FONT_DEFAULT_GLYPHS, without going through the asset system.
|
|
||||||
*
|
|
||||||
* @return Either an error or success result.
|
|
||||||
*/
|
|
||||||
errorret_t fontInitDefault(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposes of the built-in default font created by fontInitDefault.
|
|
||||||
*
|
|
||||||
* @return Either an error or success result.
|
|
||||||
*/
|
|
||||||
errorret_t fontDisposeDefault(void);
|
|
||||||
|
|||||||
@@ -9,15 +9,34 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/display/assettextureloader.h"
|
||||||
|
#include "asset/loader/display/assettilesetloader.h"
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
|
|
||||||
|
font_t FONT_DEFAULT;
|
||||||
|
|
||||||
errorret_t textInit(void) {
|
errorret_t textInit(void) {
|
||||||
errorChain(fontInitDefault());
|
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
|
||||||
|
assetentry_t *entryTexture = assetLock(
|
||||||
|
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
|
||||||
|
);
|
||||||
|
assetentry_t *entryTileset = assetLock(
|
||||||
|
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
||||||
|
);
|
||||||
|
errorChain(assetRequireLoaded(entryTexture));
|
||||||
|
errorChain(assetRequireLoaded(entryTileset));
|
||||||
|
|
||||||
|
FONT_DEFAULT.texture = &entryTexture->data.texture;
|
||||||
|
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t textDispose(void) {
|
errorret_t textDispose(void) {
|
||||||
errorChain(fontDisposeDefault());
|
FONT_DEFAULT.texture = NULL;
|
||||||
|
FONT_DEFAULT.tileset = NULL;
|
||||||
|
assetUnlock("ui/minogram.png");
|
||||||
|
assetUnlock("ui/minogram.dtf");
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +51,7 @@ spritebatchsprite_t textGetSprite(
|
|||||||
tileIndex = ((int32_t)'@') - TEXT_CHAR_START;
|
tileIndex = ((int32_t)'@') - TEXT_CHAR_START;
|
||||||
}
|
}
|
||||||
assertTrue(
|
assertTrue(
|
||||||
tileIndex >= 0 && tileIndex <= font->tileset->tileCount,
|
tileIndex >= 0 && tileIndex < font->tileset->tileCount,
|
||||||
"Character is out of bounds for font tiles"
|
"Character is out of bounds for font tiles"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -97,89 +116,6 @@ errorret_t textDraw(
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t textBuildSpriteCache(
|
|
||||||
const char_t *text,
|
|
||||||
const font_t *font,
|
|
||||||
spritebatchsprite_t *sprites,
|
|
||||||
const uint32_t spritesMax,
|
|
||||||
int32_t *outWidth,
|
|
||||||
int32_t *outHeight
|
|
||||||
) {
|
|
||||||
assertNotNull(text, "Text cannot be NULL");
|
|
||||||
assertNotNull(font, "Font cannot be NULL");
|
|
||||||
assertNotNull(sprites, "Sprites cannot be NULL");
|
|
||||||
assertNotNull(outWidth, "Output width cannot be NULL");
|
|
||||||
assertNotNull(outHeight, "Output height cannot be NULL");
|
|
||||||
|
|
||||||
uint32_t count = 0;
|
|
||||||
float_t posX = 0.0f;
|
|
||||||
float_t posY = 0.0f;
|
|
||||||
int32_t width = 0;
|
|
||||||
int32_t lineWidth = 0;
|
|
||||||
int32_t height = font->tileset->tileHeight;
|
|
||||||
|
|
||||||
char_t c;
|
|
||||||
int32_t i = 0;
|
|
||||||
while((c = text[i++]) != '\0') {
|
|
||||||
if(c == '\n') {
|
|
||||||
if(lineWidth > width) width = lineWidth;
|
|
||||||
lineWidth = 0;
|
|
||||||
posX = 0.0f;
|
|
||||||
posY += font->tileset->tileHeight;
|
|
||||||
height += font->tileset->tileHeight;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(c == ' ') {
|
|
||||||
posX += font->tileset->tileWidth;
|
|
||||||
lineWidth += font->tileset->tileWidth;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(count < spritesMax, "Text produces too many sprites");
|
|
||||||
sprites[count++] = textGetSprite((vec2){ posX, posY }, c, font);
|
|
||||||
|
|
||||||
posX += font->tileset->tileWidth;
|
|
||||||
lineWidth += font->tileset->tileWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(lineWidth > width) width = lineWidth;
|
|
||||||
*outWidth = width;
|
|
||||||
*outHeight = height;
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t textDrawSpriteCache(
|
|
||||||
const spritebatchsprite_t *sprites,
|
|
||||||
const uint32_t spriteCount,
|
|
||||||
spritebatchsprite_t *scratch,
|
|
||||||
const float_t x,
|
|
||||||
const float_t y,
|
|
||||||
const color_t color,
|
|
||||||
texture_t *texture
|
|
||||||
) {
|
|
||||||
assertNotNull(scratch, "Scratch buffer cannot be NULL");
|
|
||||||
assertNotNull(texture, "Texture cannot be NULL");
|
|
||||||
if(spriteCount == 0) errorOk();
|
|
||||||
|
|
||||||
for(uint32_t i = 0; i < spriteCount; i++) {
|
|
||||||
scratch[i] = sprites[i];
|
|
||||||
scratch[i].min[0] += x;
|
|
||||||
scratch[i].min[1] += y;
|
|
||||||
scratch[i].max[0] += x;
|
|
||||||
scratch[i].max[1] += y;
|
|
||||||
}
|
|
||||||
|
|
||||||
shadermaterial_t material = {
|
|
||||||
.unlit = {
|
|
||||||
.color = color,
|
|
||||||
.texture = texture
|
|
||||||
}
|
|
||||||
};
|
|
||||||
errorChain(spriteBatchBuffer(scratch, spriteCount, &SHADER_UNLIT, material));
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void textMeasure(
|
void textMeasure(
|
||||||
const char_t *text,
|
const char_t *text,
|
||||||
const font_t *font,
|
const font_t *font,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
#define TEXT_CHAR_START '!'
|
#define TEXT_CHAR_START '!'
|
||||||
|
|
||||||
|
extern font_t FONT_DEFAULT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the text system.
|
* Initializes the text system.
|
||||||
*
|
*
|
||||||
@@ -58,54 +60,6 @@ errorret_t textDraw(
|
|||||||
font_t *font
|
font_t *font
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a cache of sprites (glyph geometry + UVs, relative to origin
|
|
||||||
* 0,0) for a string of text. Callers that redraw the same text every
|
|
||||||
* frame (e.g. UI labels/widgets) should build this once and reuse it
|
|
||||||
* via textDrawSpriteCache, instead of re-deriving glyph geometry every
|
|
||||||
* frame the way textDraw does.
|
|
||||||
*
|
|
||||||
* @param text The null-terminated string to build sprites for.
|
|
||||||
* @param font Font to use for tile lookup.
|
|
||||||
* @param sprites Destination array to write sprites into.
|
|
||||||
* @param spritesMax Capacity of the sprites array.
|
|
||||||
* @param outWidth Pointer to store the measured width in pixels.
|
|
||||||
* @param outHeight Pointer to store the measured height in pixels.
|
|
||||||
* @return The number of sprites written.
|
|
||||||
*/
|
|
||||||
uint32_t textBuildSpriteCache(
|
|
||||||
const char_t *text,
|
|
||||||
const font_t *font,
|
|
||||||
spritebatchsprite_t *sprites,
|
|
||||||
const uint32_t spritesMax,
|
|
||||||
int32_t *outWidth,
|
|
||||||
int32_t *outHeight
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a previously-built sprite cache (see textBuildSpriteCache) at the
|
|
||||||
* given position in a single batched draw call.
|
|
||||||
*
|
|
||||||
* @param sprites Cached sprites, relative to origin 0,0.
|
|
||||||
* @param spriteCount Number of sprites in the cache.
|
|
||||||
* @param scratch Caller-owned scratch buffer, at least spriteCount
|
|
||||||
* entries, used to translate the cached sprites into position.
|
|
||||||
* @param x The x-coordinate to draw the text at.
|
|
||||||
* @param y The y-coordinate to draw the text at.
|
|
||||||
* @param color The color to draw the text in.
|
|
||||||
* @param texture The font's texture to sample glyphs from.
|
|
||||||
* @return Either an error or success result.
|
|
||||||
*/
|
|
||||||
errorret_t textDrawSpriteCache(
|
|
||||||
const spritebatchsprite_t *sprites,
|
|
||||||
const uint32_t spriteCount,
|
|
||||||
spritebatchsprite_t *scratch,
|
|
||||||
const float_t x,
|
|
||||||
const float_t y,
|
|
||||||
const color_t color,
|
|
||||||
texture_t *texture
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measures the width and height of the given text string when rendered.
|
* Measures the width and height of the given text string when rendered.
|
||||||
*
|
*
|
||||||
|
|||||||
+43
-46
@@ -13,22 +13,24 @@
|
|||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "script/scriptmanager.h"
|
|
||||||
#include "script/module/scene/modulescene.h"
|
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#ifdef DUSK_NETWORKING
|
#include "entity/entitymanager.h"
|
||||||
#include "network/network.h"
|
#include "physics/physicsmanager.h"
|
||||||
#include "network/socket/client/client.h"
|
#include "script/scriptmanager.h"
|
||||||
#include "network/socket/client/clientroster.h"
|
#include "network/network.h"
|
||||||
#include "network/socket/server/serveronline.h"
|
|
||||||
#endif
|
|
||||||
#include "game/game.h"
|
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
|
||||||
#include "save/savesettings.h"
|
double jerry_port_current_time(void) {
|
||||||
#include "log/log.h"
|
dusktimeepoch_t epoch = timeGetEpoch();
|
||||||
|
return epoch.time * 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t jerry_port_local_tza(double unix_ms) {
|
||||||
|
(void) unix_ms;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -46,17 +48,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(systemInit());
|
errorChain(systemInit());
|
||||||
errorChain(inputInit());
|
errorChain(inputInit());
|
||||||
errorChain(assetInit());
|
errorChain(assetInit());
|
||||||
errorChain(scriptManagerInit());
|
|
||||||
errorChain(saveInit());
|
|
||||||
errorChain(saveSettingsLoad());
|
|
||||||
errorChain(localeManagerInit());
|
errorChain(localeManagerInit());
|
||||||
|
errorChain(scriptManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
#ifdef DUSK_NETWORKING
|
entityManagerInit();
|
||||||
|
physicsManagerInit();
|
||||||
errorChain(networkInit());
|
errorChain(networkInit());
|
||||||
#endif
|
|
||||||
errorChain(sceneInit());
|
errorChain(sceneInit());
|
||||||
errorChain(gameInit());
|
|
||||||
|
errorChain(scriptManagerExecFile("engine.js", NULL));
|
||||||
|
errorChain(scriptManagerCallGlobal("init"));
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
|
|
||||||
@@ -66,40 +68,37 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
consolePrint("Assertions real");
|
consolePrint("Assertions real");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// sceneSet(SCENE_TYPE_OVERWORLD);
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
#ifdef DUSK_NETWORKING
|
|
||||||
errorChain(networkUpdate());
|
errorChain(networkUpdate());
|
||||||
errorChain(clientUpdate());
|
|
||||||
errorChain(serverOnlineUpdate());
|
|
||||||
#endif
|
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
|
|
||||||
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
|
||||||
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
|
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
|
physicsManagerUpdate();
|
||||||
|
|
||||||
|
// Fixed-step logic: runs once per DUSK_TIME_STEP tick. On platforms
|
||||||
|
// without dynamic timing every frame is a fixed step; on platforms with
|
||||||
|
// it, dynamic (interpolation) frames are skipped.
|
||||||
|
#ifdef DUSK_TIME_DYNAMIC
|
||||||
|
if(!TIME.dynamicUpdate) {
|
||||||
|
#endif
|
||||||
|
entityManagerFixedUpdate();
|
||||||
|
errorChain(scriptManagerCallGlobal("fixedUpdate"));
|
||||||
|
#ifdef DUSK_TIME_DYNAMIC
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
errorChain(gameUpdate());
|
|
||||||
errorChain(moduleSceneUpdateCurrent());
|
|
||||||
errorChain(sceneUpdate());
|
errorChain(sceneUpdate());
|
||||||
|
errorChain(scriptManagerCallGlobal("update"));
|
||||||
errorChain(assetUpdate());
|
errorChain(assetUpdate());
|
||||||
errorChain(uiUpdate());
|
errorChain(uiUpdate());
|
||||||
}
|
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
errorChain(displayUpdate());
|
errorChain(displayUpdate());
|
||||||
if(
|
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
||||||
dialogType == SYSTEM_DIALOG_TYPE_NONE &&
|
|
||||||
inputPressed(INPUT_BIND_RAGEQUIT)
|
|
||||||
) {
|
|
||||||
ENGINE.running = false;
|
|
||||||
}
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,23 +107,21 @@ void engineExit(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
errorChain(gameDispose());
|
errorChain(scriptManagerCallGlobal("deinit"));
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
|
|
||||||
#ifdef DUSK_NETWORKING
|
|
||||||
errorChain(serverOnlineStop());
|
|
||||||
client_t *localClient = clientRosterFindLocal();
|
|
||||||
if(localClient) localClientDisconnect(&localClient->local);
|
|
||||||
|
|
||||||
errorChain(networkDispose());
|
errorChain(networkDispose());
|
||||||
#endif
|
entityManagerDispose();
|
||||||
localeManagerDispose();
|
localeManagerDispose();
|
||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
errorChain(saveDispose());
|
|
||||||
errorChain(scriptManagerDispose());
|
// Must run before scriptManagerDispose(): asset entries (e.g. loaded
|
||||||
|
// scripts) hold jerry_value_t references (promises) that need to be
|
||||||
|
// released via their loader's dispose callback before jerry_cleanup()
|
||||||
|
// runs, which fatally asserts if anything is still held.
|
||||||
errorChain(assetDispose());
|
errorChain(assetDispose());
|
||||||
|
errorChain(scriptManagerDispose());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ errorret_t engineUpdate(void);
|
|||||||
* Shuts down the engine.
|
* Shuts down the engine.
|
||||||
*/
|
*/
|
||||||
errorret_t engineDispose(void);
|
errorret_t engineDispose(void);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
entity.c
|
entity.c
|
||||||
entitymanager.c
|
entitymanager.c
|
||||||
component.c
|
component.c
|
||||||
entityprefab.c
|
entityrender.c
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
|
|||||||
+25
-32
@@ -12,13 +12,13 @@
|
|||||||
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
||||||
[COMPONENT_TYPE_NULL] = { 0 },
|
[COMPONENT_TYPE_NULL] = { 0 },
|
||||||
|
|
||||||
#define X(enm, type, field, iMethod, dMethod, rMethod) \
|
#define X(enm, type, field, iMethod, dMethod, fMethod) \
|
||||||
[COMPONENT_TYPE_##enm] = { \
|
[COMPONENT_TYPE_##enm] = { \
|
||||||
.enumName = #enm, \
|
.enumName = #enm, \
|
||||||
.name = #field, \
|
.name = #field, \
|
||||||
.init = iMethod, \
|
.init = iMethod, \
|
||||||
.dispose = dMethod, \
|
.dispose = dMethod, \
|
||||||
.render = rMethod \
|
.fixedUpdate = fMethod \
|
||||||
},
|
},
|
||||||
|
|
||||||
#include "componentlist.h"
|
#include "componentlist.h"
|
||||||
@@ -28,41 +28,37 @@ componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void componentInit(
|
void componentInit(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const componenttype_t type
|
const componenttype_t type
|
||||||
) {
|
) {
|
||||||
assertNotNull(mgr, "Entity manager cannot be null");
|
|
||||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
||||||
|
|
||||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
component_t *cmp = &mgr->components[index];
|
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||||
memoryZero(cmp, sizeof(component_t));
|
memoryZero(cmp, sizeof(component_t));
|
||||||
|
|
||||||
cmp->type = type;
|
cmp->type = type;
|
||||||
if(COMPONENT_DEFINITIONS[type].init) {
|
if(COMPONENT_DEFINITIONS[type].init) {
|
||||||
COMPONENT_DEFINITIONS[type].init(mgr, entityId, componentId);
|
COMPONENT_DEFINITIONS[type].init(entityId, componentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void * componentGetData(
|
void * componentGetData(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const componenttype_t type
|
const componenttype_t type
|
||||||
) {
|
) {
|
||||||
assertNotNull(mgr, "Entity manager cannot be null");
|
|
||||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
||||||
|
|
||||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
component_t *cmp = &mgr->components[index];
|
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||||
assertTrue(cmp->type == type, "Component type mismatch");
|
assertTrue(cmp->type == type, "Component type mismatch");
|
||||||
|
|
||||||
return &cmp->data;
|
return &cmp->data;
|
||||||
@@ -78,12 +74,10 @@ componentindex_t componentGetIndex(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entityid_t componentGetEntitiesWithComponent(
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const componenttype_t type,
|
const componenttype_t type,
|
||||||
entityid_t outEntities[ENTITY_COUNT_MAX],
|
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||||
componentid_t outComponents[ENTITY_COUNT_MAX]
|
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||||
) {
|
) {
|
||||||
assertNotNull(mgr, "Entity manager cannot be null");
|
|
||||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
|
||||||
assertNotNull(outEntities, "Output entities array cannot be null");
|
assertNotNull(outEntities, "Output entities array cannot be null");
|
||||||
@@ -91,16 +85,16 @@ entityid_t componentGetEntitiesWithComponent(
|
|||||||
|
|
||||||
entityid_t written = 0;
|
entityid_t written = 0;
|
||||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
componentid_t used = mgr->entitiesWithComponent[
|
componentid_t used = ENTITY_MANAGER.entitiesWithComponent[
|
||||||
type * ENTITY_COUNT_MAX + i
|
type * ENTITY_COUNT_MAX + i
|
||||||
];
|
];
|
||||||
if(used == COMPONENT_ID_INVALID) continue;
|
if(used == COMPONENT_ID_INVALID) continue;
|
||||||
assertTrue(
|
assertTrue(
|
||||||
mgr->components[componentGetIndex(i, used)].type == type,
|
ENTITY_MANAGER.components[componentGetIndex(i, used)].type == type,
|
||||||
"Component type mismatch in entitiesWithComponent lookup"
|
"Component type mismatch in entitiesWithComponent lookup"
|
||||||
);
|
);
|
||||||
assertTrue(
|
assertTrue(
|
||||||
(mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0,
|
(ENTITY_MANAGER.entities[i].state & ENTITY_STATE_ACTIVE) != 0,
|
||||||
"Inactive entity in entitiesWithComponent lookup"
|
"Inactive entity in entitiesWithComponent lookup"
|
||||||
);
|
);
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@@ -117,36 +111,35 @@ entityid_t componentGetEntitiesWithComponent(
|
|||||||
return written;
|
return written;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t componentRenderAll(entitymanager_t *mgr) {
|
|
||||||
assertNotNull(mgr, "Entity manager cannot be null");
|
|
||||||
for(entityid_t eid = 0; eid < ENTITY_COUNT_MAX; eid++) {
|
|
||||||
if(!(mgr->entities[eid].state & ENTITY_STATE_ACTIVE)) continue;
|
|
||||||
for(componentid_t cid = 0; cid < ENTITY_COMPONENT_COUNT_MAX; cid++) {
|
|
||||||
component_t *cmp = &mgr->components[componentGetIndex(eid, cid)];
|
|
||||||
if(cmp->type == COMPONENT_TYPE_NULL) continue;
|
|
||||||
if(!COMPONENT_DEFINITIONS[cmp->type].render) continue;
|
|
||||||
errorChain(COMPONENT_DEFINITIONS[cmp->type].render(mgr, eid, cid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
void componentDispose(
|
void componentDispose(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
assertNotNull(mgr, "Entity manager cannot be null");
|
|
||||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
|
||||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
component_t *cmp = &mgr->components[index];
|
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||||
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
||||||
|
|
||||||
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
||||||
COMPONENT_DEFINITIONS[cmp->type].dispose(mgr, entityId, componentId);
|
COMPONENT_DEFINITIONS[cmp->type].dispose(entityId, componentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
cmp->type = COMPONENT_TYPE_NULL;
|
cmp->type = COMPONENT_TYPE_NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void componentFixedUpdateAll(void) {
|
||||||
|
for(entityid_t e = 0; e < ENTITY_COUNT_MAX; e++) {
|
||||||
|
if((ENTITY_MANAGER.entities[e].state & ENTITY_STATE_ACTIVE) == 0) continue;
|
||||||
|
|
||||||
|
for(componentid_t c = 0; c < ENTITY_COMPONENT_COUNT_MAX; c++) {
|
||||||
|
componentindex_t index = componentGetIndex(e, c);
|
||||||
|
componenttype_t type = ENTITY_MANAGER.components[index].type;
|
||||||
|
if(type == COMPONENT_TYPE_NULL) continue;
|
||||||
|
if(COMPONENT_DEFINITIONS[type].fixedUpdate) {
|
||||||
|
COMPONENT_DEFINITIONS[type].fixedUpdate(e, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,58 +7,30 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "entitybase.h"
|
#include "entitybase.h"
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
#define X(enumName, type, field, init, dispose, render) \
|
#define X(enumName, type, field, init, dispose, fixedUpdate) \
|
||||||
// do nothing
|
// do nothing
|
||||||
#include "componentlist.h"
|
#include "componentlist.h"
|
||||||
#undef X
|
#undef X
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
#define X(enumName, type, field, init, dispose, render) type field;
|
#define X(enumName, type, field, init, dispose, fixedUpdate) type field;
|
||||||
#include "componentlist.h"
|
#include "componentlist.h"
|
||||||
#undef X
|
#undef X
|
||||||
} componentdata_t;
|
} componentdata_t;
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback signature for a component's init/dispose hooks.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
*/
|
|
||||||
typedef void (*componentcallback_t)(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback signature for a component's render hook.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @return Error state.
|
|
||||||
*/
|
|
||||||
typedef errorret_t (*componentcallbackerror_t)(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char_t *enumName;
|
const char_t *enumName;
|
||||||
const char_t *name;
|
const char_t *name;
|
||||||
componentcallback_t init;
|
void (*init)(const entityid_t, const componentid_t);
|
||||||
componentcallback_t dispose;
|
void (*dispose)(const entityid_t, const componentid_t);
|
||||||
componentcallbackerror_t render;
|
void (*fixedUpdate)(const entityid_t, const componentid_t);
|
||||||
} componentdefinition_t;
|
} componentdefinition_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
COMPONENT_TYPE_NULL,
|
COMPONENT_TYPE_NULL,
|
||||||
|
|
||||||
#define X(enumName, type, field, init, dispose, render) \
|
#define X(enumName, type, field, init, dispose, fixedUpdate) \
|
||||||
COMPONENT_TYPE_##enumName,
|
COMPONENT_TYPE_##enumName,
|
||||||
#include "componentlist.h"
|
#include "componentlist.h"
|
||||||
#undef X
|
#undef X
|
||||||
@@ -76,13 +48,11 @@ extern componentdefinition_t COMPONENT_DEFINITIONS[];
|
|||||||
/**
|
/**
|
||||||
* Initializes a component of the given type for the entity with component ID.
|
* Initializes a component of the given type for the entity with component ID.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
* @param type The type of the component to initialize.
|
* @param type The type of the component to initialize.
|
||||||
*/
|
*/
|
||||||
void componentInit(
|
void componentInit(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const componenttype_t type
|
const componenttype_t type
|
||||||
@@ -91,14 +61,12 @@ void componentInit(
|
|||||||
/**
|
/**
|
||||||
* Gets the pointer to the data of a component for the entity with component ID.
|
* Gets the pointer to the data of a component for the entity with component ID.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
* @param type The type of the component to get, only used for assertion.
|
* @param type The type of the component to get, only used for assertion.
|
||||||
* @return A pointer to the component data.
|
* @return A pointer to the component data.
|
||||||
*/
|
*/
|
||||||
void * componentGetData(
|
void * componentGetData(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const componenttype_t type
|
const componenttype_t type
|
||||||
@@ -119,7 +87,6 @@ componentindex_t componentGetIndex(
|
|||||||
/**
|
/**
|
||||||
* Gets the entity IDs of all entities with a component of the given type.
|
* Gets the entity IDs of all entities with a component of the given type.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager to search.
|
|
||||||
* @param type The type of the component to get entities for.
|
* @param type The type of the component to get entities for.
|
||||||
* @param outEntities An array to write the entity IDs to, must be at least
|
* @param outEntities An array to write the entity IDs to, must be at least
|
||||||
* ENTITY_COUNT_MAX in size.
|
* ENTITY_COUNT_MAX in size.
|
||||||
@@ -127,7 +94,6 @@ componentindex_t componentGetIndex(
|
|||||||
* @return The number of entity IDs written to outEntities.
|
* @return The number of entity IDs written to outEntities.
|
||||||
*/
|
*/
|
||||||
entityid_t componentGetEntitiesWithComponent(
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const componenttype_t type,
|
const componenttype_t type,
|
||||||
entityid_t outEntities[ENTITY_COUNT_MAX],
|
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||||
componentid_t outComponents[ENTITY_COUNT_MAX]
|
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||||
@@ -136,22 +102,16 @@ entityid_t componentGetEntitiesWithComponent(
|
|||||||
/**
|
/**
|
||||||
* Disposes of a component for the entity with component ID.
|
* Disposes of a component for the entity with component ID.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
*/
|
*/
|
||||||
void componentDispose(
|
void componentDispose(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the render callback on every active component that defines one.
|
* Calls fixedUpdate on every active component that defines one. Intended to
|
||||||
* Iterates all active entities and all their component slots. No-op for
|
* be called once per fixed timestep - see entityManagerFixedUpdate().
|
||||||
* components whose definition has render == NULL.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager to render.
|
|
||||||
* @return Error state.
|
|
||||||
*/
|
*/
|
||||||
errorret_t componentRenderAll(entitymanager_t *mgr);
|
void componentFixedUpdateAll(void);
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
# Subdirs
|
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(physics)
|
add_subdirectory(physics)
|
||||||
|
add_subdirectory(script)
|
||||||
add_subdirectory(trigger)
|
add_subdirectory(trigger)
|
||||||
add_subdirectory(animation)
|
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "entityanimation.h"
|
|
||||||
#include "entity/entitymanager.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
|
|
||||||
void entityAnimationInit(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
|
|
||||||
memoryZero(animComp, sizeof(entityanimation_t));
|
|
||||||
|
|
||||||
entityUpdateAdd(mgr, entityId, entityAnimationUpdate, componentId, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
entityanimation_t *entityAnimationGet(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
return componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_ANIMATION
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimationSetKeyframes(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
keyframe_t **channelTracks,
|
|
||||||
uint16_t *channelTrackCounts,
|
|
||||||
const uint16_t channelCount
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
animationInit(
|
|
||||||
&animComp->anim, channelTracks, channelTrackCounts, channelCount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimationPlay(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
animComp->anim.time = 0.0f;
|
|
||||||
animComp->anim.playing = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimationStop(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
animComp->anim.playing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t entityAnimationIsPlaying(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
return animComp->anim.playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimationSetLoop(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const bool_t loop
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
animComp->anim.loop = loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimationSetSpeed(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const float_t speed
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
animComp->anim.speed = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
float_t entityAnimationGetValue(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const uint16_t channelIndex
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
return animationGetValue(&animComp->anim, channelIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityAnimationUpdate(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
void *user
|
|
||||||
) {
|
|
||||||
entityanimation_t *animComp = entityAnimationGet(mgr, entityId, componentId);
|
|
||||||
animationUpdate(&animComp->anim);
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "entity/entitybase.h"
|
|
||||||
#include "animation/animation.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
animation_t anim;
|
|
||||||
} entityanimation_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the animation component: no keyframes set, and registers
|
|
||||||
* entityAnimationUpdate as an update callback. Call
|
|
||||||
* entityAnimationSetKeyframes() before entityAnimationPlay().
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
*/
|
|
||||||
void entityAnimationInit(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the underlying animation structure (temporarily) for the given
|
|
||||||
* entity. Prefer the dedicated getters/setters where possible.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @return The animation component data for the given entity and
|
|
||||||
* component ID.
|
|
||||||
*/
|
|
||||||
entityanimation_t *entityAnimationGet(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the entity's keyframes, stopped, at speed 1.0, non-looping. See
|
|
||||||
* keyframeSetInit() -- channelTracks/channelTrackCounts and the
|
|
||||||
* keyframe_t arrays they point to are not copied, and must outlive this
|
|
||||||
* component (e.g. static/const arrays owned by the caller).
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param channelTracks Array of channelCount keyframe_t arrays -- one per
|
|
||||||
* animated channel (e.g. position.x/y/z), sharing this animation's
|
|
||||||
* timeline.
|
|
||||||
* @param channelTrackCounts Array of channelCount keyframe counts,
|
|
||||||
* matching channelTracks.
|
|
||||||
* @param channelCount The number of channels.
|
|
||||||
*/
|
|
||||||
void entityAnimationSetKeyframes(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
keyframe_t **channelTracks,
|
|
||||||
uint16_t *channelTrackCounts,
|
|
||||||
const uint16_t channelCount
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts (or restarts) playback from time 0.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
*/
|
|
||||||
void entityAnimationPlay(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops playback without resetting the current time.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
*/
|
|
||||||
void entityAnimationStop(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the animation is currently playing.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @return True if playing.
|
|
||||||
*/
|
|
||||||
bool_t entityAnimationIsPlaying(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the animation loops on reaching its final keyframe, rather
|
|
||||||
* than stopping there.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param loop True to loop, false to stop at the end.
|
|
||||||
*/
|
|
||||||
void entityAnimationSetLoop(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const bool_t loop
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the animation's playback rate multiplier.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param speed The new playback rate; 1.0 = normal speed.
|
|
||||||
*/
|
|
||||||
void entityAnimationSetSpeed(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const float_t speed
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluates one of the animation's channels at its current playback
|
|
||||||
* time, regardless of whether it's currently playing.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param channelIndex The channel to evaluate, in [0, channelCount) as
|
|
||||||
* passed to entityAnimationSetKeyframes().
|
|
||||||
* @return That channel's value at the current time.
|
|
||||||
*/
|
|
||||||
float_t entityAnimationGetValue(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const uint16_t channelIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Per-tick update for the animation component: calls animationUpdate()
|
|
||||||
* (a no-op if not playing). Registered automatically as an update
|
|
||||||
* callback by entityAnimationInit.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param user Unused.
|
|
||||||
*/
|
|
||||||
void entityAnimationUpdate(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
@@ -8,30 +8,26 @@
|
|||||||
#include "entity/entitymanager.h"
|
#include "entity/entitymanager.h"
|
||||||
#include "entity/entity.h"
|
#include "entity/entity.h"
|
||||||
#include "entity/component/display/entityposition.h"
|
#include "entity/component/display/entityposition.h"
|
||||||
|
#include "display/framebuffer/framebuffer.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
|
|
||||||
void entityCameraInit(
|
void entityCameraInit(const entityid_t ent, const componentid_t comp) {
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
ent, comp, COMPONENT_TYPE_CAMERA
|
||||||
);
|
);
|
||||||
cam->nearClip = 0.1f;
|
cam->nearClip = 0.1f;
|
||||||
cam->farClip = 5000.0f;
|
cam->farClip = 100.0f;
|
||||||
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||||
cam->perspective.fov = glm_rad(45.0f);
|
cam->perspective.fov = glm_rad(45.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityCameraGetProjection(
|
void entityCameraGetProjection(
|
||||||
entitymanager_t *mgr,
|
const entityid_t ent,
|
||||||
const entityid_t entityId,
|
const componentid_t comp,
|
||||||
const componentid_t componentId,
|
|
||||||
mat4 out
|
mat4 out
|
||||||
) {
|
) {
|
||||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
ent, comp, COMPONENT_TYPE_CAMERA
|
||||||
);
|
);
|
||||||
|
|
||||||
if(
|
if(
|
||||||
@@ -64,77 +60,37 @@ void entityCameraGetProjection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entityid_t entityCameraGetCurrent(entitymanager_t *mgr) {
|
entityid_t entityCameraGetCurrent(void) {
|
||||||
entityid_t camEnts[ENTITY_COUNT_MAX];
|
entityid_t camEnts[ENTITY_COUNT_MAX];
|
||||||
componentid_t camComps[ENTITY_COUNT_MAX];
|
componentid_t camComps[ENTITY_COUNT_MAX];
|
||||||
entityid_t count = componentGetEntitiesWithComponent(
|
entityid_t count = componentGetEntitiesWithComponent(
|
||||||
mgr, COMPONENT_TYPE_CAMERA, camEnts, camComps
|
COMPONENT_TYPE_CAMERA, camEnts, camComps
|
||||||
);
|
);
|
||||||
if(count == 0) return ENTITY_ID_INVALID;
|
if(count == 0) return ENTITY_COUNT_MAX;
|
||||||
return camEnts[0];
|
return camEnts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityCameraGetForward(
|
void entityCameraGetForward(const entityid_t entityId, vec2 out) {
|
||||||
entitymanager_t *mgr,
|
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
|
||||||
const entityid_t entityId,
|
entityposition_t *pos = entityPositionGet(entityId, posComp);
|
||||||
vec2 out
|
// View matrix column layout: M[col][row],
|
||||||
) {
|
// forward = {-M[0][2], -M[1][2], -M[2][2]}
|
||||||
componentid_t posComp = entityGetComponent(
|
float_t fx = -pos->worldTransform[0][2];
|
||||||
mgr, entityId, COMPONENT_TYPE_POSITION
|
float_t fz = -pos->worldTransform[2][2];
|
||||||
);
|
|
||||||
mat4 transform;
|
|
||||||
entityPositionGetTransform(mgr, entityId, posComp, transform);
|
|
||||||
// transform is an object->world matrix; column 2 is the entity's local Z
|
|
||||||
// axis expressed in world space. Cameras look down their local -Z.
|
|
||||||
float_t fx = -transform[2][0];
|
|
||||||
float_t fz = -transform[2][2];
|
|
||||||
float_t len = sqrtf(fx * fx + fz * fz);
|
float_t len = sqrtf(fx * fx + fz * fz);
|
||||||
if(len > 1e-6f) { fx /= len; fz /= len; }
|
if(len > 1e-6f) { fx /= len; fz /= len; }
|
||||||
out[0] = fx;
|
out[0] = fx;
|
||||||
out[1] = fz;
|
out[1] = fz;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityCameraGetRight(
|
void entityCameraGetRight(const entityid_t entityId, vec2 out) {
|
||||||
entitymanager_t *mgr,
|
componentid_t posComp = entityGetComponent(entityId, COMPONENT_TYPE_POSITION);
|
||||||
const entityid_t entityId,
|
entityposition_t *pos = entityPositionGet(entityId, posComp);
|
||||||
vec2 out
|
// View matrix column layout: right = {M[0][0], M[1][0], M[2][0]}
|
||||||
) {
|
float_t rx = pos->worldTransform[0][0];
|
||||||
componentid_t posComp = entityGetComponent(
|
float_t rz = pos->worldTransform[2][0];
|
||||||
mgr, entityId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
mat4 transform;
|
|
||||||
entityPositionGetTransform(mgr, entityId, posComp, transform);
|
|
||||||
// transform is an object->world matrix; column 0 is the entity's local X
|
|
||||||
// (right) axis expressed in world space.
|
|
||||||
float_t rx = transform[0][0];
|
|
||||||
float_t rz = transform[0][2];
|
|
||||||
float_t len = sqrtf(rx * rx + rz * rz);
|
float_t len = sqrtf(rx * rx + rz * rz);
|
||||||
if(len > 1e-6f) { rx /= len; rz /= len; }
|
if(len > 1e-6f) { rx /= len; rz /= len; }
|
||||||
out[0] = rx;
|
out[0] = rx;
|
||||||
out[1] = rz;
|
out[1] = rz;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityCameraLookAtPixelPerfect(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t ent,
|
|
||||||
const componentid_t posComp,
|
|
||||||
const componentid_t camComp,
|
|
||||||
const vec3 point,
|
|
||||||
const vec3 eyeOffset,
|
|
||||||
const float_t scale
|
|
||||||
) {
|
|
||||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
|
||||||
mgr, ent, camComp, COMPONENT_TYPE_CAMERA
|
|
||||||
);
|
|
||||||
float_t dist = (
|
|
||||||
(float_t)SCREEN.height / (2.0f * scale * tanf(cam->perspective.fov * 0.5f))
|
|
||||||
);
|
|
||||||
|
|
||||||
vec3 eye = {
|
|
||||||
point[0] + eyeOffset[0],
|
|
||||||
point[1] + dist + eyeOffset[1],
|
|
||||||
point[2] + eyeOffset[2]
|
|
||||||
};
|
|
||||||
vec3 up = { 0.0f, 0.0f, -1.0f };
|
|
||||||
entityPositionLookAt(mgr, ent, posComp, eye, (float_t *)point, up);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "entity/entitybase.h"
|
#include "entity/entitybase.h"
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
||||||
@@ -37,86 +36,44 @@ typedef struct {
|
|||||||
/**
|
/**
|
||||||
* Initializes an entity camera component.
|
* Initializes an entity camera component.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
* @param ent The entity ID.
|
||||||
* @param entityId The entity ID.
|
* @param comp The component ID.
|
||||||
* @param componentId The component ID.
|
|
||||||
*/
|
*/
|
||||||
void entityCameraInit(
|
void entityCameraInit(const entityid_t ent, const componentid_t comp);
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders out the projection matrix for the given camera.
|
* Renders out the projection matrix for the given camera.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
* @param ent The entity ID.
|
||||||
* @param entityId The entity ID.
|
* @param comp The component ID.
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param out The output projection matrix.
|
* @param out The output projection matrix.
|
||||||
*/
|
*/
|
||||||
void entityCameraGetProjection(
|
void entityCameraGetProjection(
|
||||||
entitymanager_t *mgr,
|
const entityid_t ent,
|
||||||
const entityid_t entityId,
|
const componentid_t comp,
|
||||||
const componentid_t componentId,
|
|
||||||
mat4 out
|
mat4 out
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the entity ID of the first active camera, or ENTITY_ID_INVALID if
|
* Returns the entity ID of the first active camera, or ENTITY_COUNT_MAX if
|
||||||
* none are active.
|
* none are active.
|
||||||
*
|
|
||||||
* @param mgr The entity manager to search.
|
|
||||||
*/
|
*/
|
||||||
entityid_t entityCameraGetCurrent(entitymanager_t *mgr);
|
entityid_t entityCameraGetCurrent(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the camera's horizontal forward direction (XZ plane) from its
|
* Gets the camera's horizontal forward direction (XZ plane) from its position
|
||||||
* position component. Automatically finds the position component on the
|
* component. Automatically finds the position component on the entity.
|
||||||
* entity.
|
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The camera entity ID.
|
* @param entityId The camera entity ID.
|
||||||
* @param out Output vec2: {forwardX, forwardZ} normalized.
|
* @param out Output vec2: {forwardX, forwardZ} normalized.
|
||||||
*/
|
*/
|
||||||
void entityCameraGetForward(
|
void entityCameraGetForward(const entityid_t entityId, vec2 out);
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
vec2 out
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the camera's horizontal right direction (XZ plane) from its position
|
* Gets the camera's horizontal right direction (XZ plane) from its position
|
||||||
* component. Automatically finds the position component on the entity.
|
* component. Automatically finds the position component on the entity.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The camera entity ID.
|
* @param entityId The camera entity ID.
|
||||||
* @param out Output vec2: {rightX, rightZ} normalized.
|
* @param out Output vec2: {rightX, rightZ} normalized.
|
||||||
*/
|
*/
|
||||||
void entityCameraGetRight(
|
void entityCameraGetRight(const entityid_t entityId, vec2 out);
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
vec2 out
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Positions the camera to look at a 3D point at a pixel-perfect distance
|
|
||||||
* derived from the camera's FOV and screen height.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param ent The camera entity ID.
|
|
||||||
* @param posComp The position component ID.
|
|
||||||
* @param camComp The camera component ID.
|
|
||||||
* @param point World position to look at.
|
|
||||||
* @param eyeOffset Offset added to the eye position only (not the target).
|
|
||||||
* @param scale Pixels per world unit. 1.0 = pixel perfect, 2.0 = 2px per unit.
|
|
||||||
*/
|
|
||||||
void entityCameraLookAtPixelPerfect(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t ent,
|
|
||||||
const componentid_t posComp,
|
|
||||||
const componentid_t camComp,
|
|
||||||
const vec3 point,
|
|
||||||
const vec3 eyeOffset,
|
|
||||||
const float_t scale
|
|
||||||
);
|
|
||||||
@@ -6,425 +6,174 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "entity/entitymanager.h"
|
#include "entity/entitymanager.h"
|
||||||
#include "assert/assert.h"
|
|
||||||
|
// Lazily recompute worldTransform from the parent chain.
|
||||||
|
static void entityPositionUpdateWorld(entityposition_t *pos) {
|
||||||
|
if(!pos->dirty) return;
|
||||||
|
|
||||||
|
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
||||||
|
glm_mat4_copy(pos->localTransform, pos->worldTransform);
|
||||||
|
} else {
|
||||||
|
entityposition_t *parent = componentGetData(
|
||||||
|
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionUpdateWorld(parent);
|
||||||
|
glm_mat4_mul(parent->worldTransform, pos->localTransform, pos->worldTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
pos->dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionMarkDirty(entityposition_t *pos) {
|
||||||
|
pos->dirty = true;
|
||||||
|
for(uint8_t i = 0; i < pos->childCount; i++) {
|
||||||
|
entityposition_t *child = componentGetData(
|
||||||
|
pos->childEntityIds[i], pos->childComponentIds[i], COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
entityPositionMarkDirty(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void entityPositionInit(
|
void entityPositionInit(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
|
|
||||||
pos->flags = 0;
|
|
||||||
pos->parentEntityId = ENTITY_ID_INVALID;
|
|
||||||
pos->parentComponentId = COMPONENT_ID_INVALID;
|
|
||||||
pos->childCount = 0;
|
|
||||||
glm_vec3_zero(pos->position);
|
glm_vec3_zero(pos->position);
|
||||||
glm_vec3_zero(pos->rotation);
|
glm_vec3_zero(pos->rotation);
|
||||||
glm_vec3_one(pos->scale);
|
glm_vec3_one(pos->scale);
|
||||||
glm_mat4_identity(pos->localTransform);
|
glm_mat4_identity(pos->localTransform);
|
||||||
glm_mat4_identity(pos->worldTransform);
|
glm_mat4_identity(pos->worldTransform);
|
||||||
|
pos->dirty = false;
|
||||||
|
pos->parentEntityId = ENTITY_ID_INVALID;
|
||||||
|
pos->parentComponentId = COMPONENT_ID_INVALID;
|
||||||
|
pos->childCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionLookAt(
|
void entityPositionLookAt(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 eye,
|
|
||||||
vec3 target,
|
vec3 target,
|
||||||
vec3 up
|
vec3 up,
|
||||||
|
vec3 eye
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
// glm_lookat() produces a view matrix (world -> eye space). Every other
|
glm_lookat(eye, target, up, pos->localTransform);
|
||||||
// setter treats localTransform as this entity's placement in world space
|
entityPositionDecompose(pos);
|
||||||
// (eye -> world), so invert it here to keep that meaning consistent --
|
entityPositionMarkDirty(pos);
|
||||||
// callers that need a view matrix (e.g. scene rendering) invert it back.
|
|
||||||
mat4 view;
|
|
||||||
glm_lookat(eye, target, up, view);
|
|
||||||
glm_mat4_inv(view, pos->localTransform);
|
|
||||||
// localTransform is now authoritative; PRS cache is stale.
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_PRS_DIRTY)
|
|
||||||
& ~(ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
|
||||||
ENTITY_POSITION_FLAG_POSITION_DIRTY);
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetTransform(
|
void entityPositionGetTransform(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
mat4 dest
|
mat4 dest
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
|
||||||
entityPositionEnsureWorld(mgr, pos);
|
|
||||||
glm_mat4_copy(
|
|
||||||
pos->parentEntityId == ENTITY_ID_INVALID
|
|
||||||
? pos->localTransform : pos->worldTransform,
|
|
||||||
dest
|
|
||||||
);
|
);
|
||||||
|
entityPositionUpdateWorld(pos);
|
||||||
|
glm_mat4_copy(pos->worldTransform, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetLocalTransform(
|
void entityPositionGetLocalTransform(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
mat4 dest
|
mat4 dest
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
entityPositionEnsureLocal(pos);
|
|
||||||
glm_mat4_copy(pos->localTransform, dest);
|
glm_mat4_copy(pos->localTransform, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetLocalPosition(
|
void entityPositionGetPosition(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 dest
|
vec3 dest
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
entityPositionEnsurePRS(pos);
|
|
||||||
glm_vec3_copy(pos->position, dest);
|
glm_vec3_copy(pos->position, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetWorldPosition(
|
void entityPositionSetPosition(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 dest
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
|
||||||
entityPositionEnsurePRS(pos);
|
|
||||||
glm_vec3_copy(pos->position, dest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entityPositionEnsureWorld(mgr, pos);
|
|
||||||
dest[0] = pos->worldTransform[3][0];
|
|
||||||
dest[1] = pos->worldTransform[3][1];
|
|
||||||
dest[2] = pos->worldTransform[3][2];
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionSetWorldPosition(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 position
|
vec3 position
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
|
||||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
|
||||||
glm_vec3_copy(position, pos->position);
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entityposition_t *parent = componentGetData(
|
|
||||||
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
entityPositionEnsureWorld(mgr, parent);
|
|
||||||
mat4 invParent;
|
|
||||||
glm_mat4_inv(parent->worldTransform, invParent);
|
|
||||||
vec3 localPos;
|
|
||||||
glm_mat4_mulv3(invParent, position, 1.0f, localPos);
|
|
||||||
glm_vec3_copy(localPos, pos->position);
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionSetLocalPosition(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 position
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
);
|
||||||
glm_vec3_copy(position, pos->position);
|
glm_vec3_copy(position, pos->position);
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
entityPositionRebuild(pos);
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetLocalRotation(
|
void entityPositionGetRotation(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 dest
|
vec3 dest
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
entityPositionEnsurePRS(pos);
|
|
||||||
glm_vec3_copy(pos->rotation, dest);
|
glm_vec3_copy(pos->rotation, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetWorldRotation(
|
void entityPositionSetRotation(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 dest
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
|
||||||
entityPositionEnsurePRS(pos);
|
|
||||||
glm_vec3_copy(pos->rotation, dest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entityPositionEnsureWorld(mgr, pos);
|
|
||||||
const float_t (*wt)[4] = pos->worldTransform;
|
|
||||||
const float_t sx = sqrtf(
|
|
||||||
wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]
|
|
||||||
);
|
|
||||||
const float_t sy = sqrtf(
|
|
||||||
wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]
|
|
||||||
);
|
|
||||||
const float_t sz = sqrtf(
|
|
||||||
wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]
|
|
||||||
);
|
|
||||||
const float_t r00 = sx > 0.0f ? wt[0][0]/sx : 0.0f;
|
|
||||||
const float_t r10 = sy > 0.0f ? wt[1][0]/sy : 0.0f;
|
|
||||||
const float_t r20 = sz > 0.0f ? wt[2][0]/sz : 0.0f;
|
|
||||||
const float_t r01 = sx > 0.0f ? wt[0][1]/sx : 0.0f;
|
|
||||||
const float_t r11 = sy > 0.0f ? wt[1][1]/sy : 0.0f;
|
|
||||||
const float_t r21 = sz > 0.0f ? wt[2][1]/sz : 0.0f;
|
|
||||||
const float_t r22 = sz > 0.0f ? wt[2][2]/sz : 0.0f;
|
|
||||||
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
|
|
||||||
dest[1] = asinf(sinBeta);
|
|
||||||
const float_t cosBeta = cosf(dest[1]);
|
|
||||||
if(fabsf(cosBeta) > 1e-6f) {
|
|
||||||
dest[0] = atan2f(-r21, r22);
|
|
||||||
dest[2] = atan2f(-r10, r00);
|
|
||||||
} else {
|
|
||||||
dest[2] = 0.0f;
|
|
||||||
dest[0] = (sinBeta > 0.0f) ? atan2f(r01, r11) : -atan2f(r01, r11);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionSetLocalRotation(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 rotation
|
vec3 rotation
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
glm_vec3_copy(rotation, pos->rotation);
|
glm_vec3_copy(rotation, pos->rotation);
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
entityPositionRebuild(pos);
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionSetWorldRotation(
|
void entityPositionGetScale(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 rotation
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
|
||||||
glm_vec3_copy(rotation, pos->rotation);
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entityposition_t *parent = componentGetData(
|
|
||||||
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
entityPositionEnsureWorld(mgr, parent);
|
|
||||||
|
|
||||||
// Build target world rotation matrix (unit scale) from XYZ euler.
|
|
||||||
const float_t c0 = cosf(rotation[0]), s0 = sinf(rotation[0]);
|
|
||||||
const float_t c1 = cosf(rotation[1]), s1 = sinf(rotation[1]);
|
|
||||||
const float_t c2 = cosf(rotation[2]), s2 = sinf(rotation[2]);
|
|
||||||
const float_t s0s1 = s0*s1, c0s1 = c0*s1;
|
|
||||||
// Named wr[col_stored][row_stored] matching cglm column-major layout.
|
|
||||||
const float_t wr00 = c1*c2;
|
|
||||||
const float_t wr01 = c0*s2 + s0s1*c2;
|
|
||||||
const float_t wr02 = s0*s2 - c0s1*c2;
|
|
||||||
const float_t wr10 = -c1*s2;
|
|
||||||
const float_t wr11 = c0*c2 - s0s1*s2;
|
|
||||||
const float_t wr12 = s0*c2 + c0s1*s2;
|
|
||||||
const float_t wr20 = s1;
|
|
||||||
const float_t wr21 = -s0*c1;
|
|
||||||
const float_t wr22 = c0*c1;
|
|
||||||
|
|
||||||
// Normalize parent world columns to extract pure rotation.
|
|
||||||
const float_t (*pt)[4] = parent->worldTransform;
|
|
||||||
const float_t psx = sqrtf(
|
|
||||||
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
|
|
||||||
);
|
|
||||||
const float_t psy = sqrtf(
|
|
||||||
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
|
|
||||||
);
|
|
||||||
const float_t psz = sqrtf(
|
|
||||||
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
|
|
||||||
);
|
|
||||||
const float_t pr00 = psx > 0.f ? pt[0][0]/psx : 0.f;
|
|
||||||
const float_t pr01 = psx > 0.f ? pt[0][1]/psx : 0.f;
|
|
||||||
const float_t pr02 = psx > 0.f ? pt[0][2]/psx : 0.f;
|
|
||||||
const float_t pr10 = psy > 0.f ? pt[1][0]/psy : 0.f;
|
|
||||||
const float_t pr11 = psy > 0.f ? pt[1][1]/psy : 0.f;
|
|
||||||
const float_t pr12 = psy > 0.f ? pt[1][2]/psy : 0.f;
|
|
||||||
const float_t pr20 = psz > 0.f ? pt[2][0]/psz : 0.f;
|
|
||||||
const float_t pr21 = psz > 0.f ? pt[2][1]/psz : 0.f;
|
|
||||||
const float_t pr22 = psz > 0.f ? pt[2][2]/psz : 0.f;
|
|
||||||
|
|
||||||
// local_R = parent_R^T * world_R (R^-1 == R^T for orthogonal matrices).
|
|
||||||
// Compute only the 7 entries of the local rotation matrix needed for XYZ
|
|
||||||
// euler extraction (stored column-major: [col][row] = math [row][col]).
|
|
||||||
// sinBeta = stored[2][0] = math[0][2]
|
|
||||||
// r21/r22 = stored[2][1..2] = math[1..2][2]
|
|
||||||
// r10/r00 = stored[1][0], stored[0][0] = math[0][1], math[0][0]
|
|
||||||
// gimbal = stored[0][1], stored[1][1] = math[1][0], math[1][1]
|
|
||||||
const float_t lr00 = pr00*wr00 + pr01*wr10 + pr02*wr20; // math[0][0]
|
|
||||||
const float_t lr10 = pr00*wr01 + pr01*wr11 + pr02*wr21; // math[0][1]
|
|
||||||
const float_t lr20 = pr00*wr02 + pr01*wr12 + pr02*wr22; // [0][2] -> sinBeta
|
|
||||||
const float_t lr01 = pr10*wr00 + pr11*wr10 + pr12*wr20; // math[1][0]
|
|
||||||
const float_t lr11 = pr10*wr01 + pr11*wr11 + pr12*wr21; // math[1][1]
|
|
||||||
const float_t lr21 = pr10*wr02 + pr11*wr12 + pr12*wr22; // [1][2] -> r21
|
|
||||||
const float_t lr22 = pr20*wr02 + pr21*wr12 + pr22*wr22; // [2][2] -> r22
|
|
||||||
|
|
||||||
const float_t sinBeta = glm_clamp(lr20, -1.0f, 1.0f);
|
|
||||||
pos->rotation[1] = asinf(sinBeta);
|
|
||||||
const float_t cosBeta = cosf(pos->rotation[1]);
|
|
||||||
if(fabsf(cosBeta) > 1e-6f) {
|
|
||||||
pos->rotation[0] = atan2f(-lr21, lr22);
|
|
||||||
pos->rotation[2] = atan2f(-lr10, lr00);
|
|
||||||
} else {
|
|
||||||
pos->rotation[2] = 0.0f;
|
|
||||||
pos->rotation[0] = (sinBeta > 0.0f)
|
|
||||||
? atan2f(lr01, lr11) : -atan2f(lr01, lr11);
|
|
||||||
}
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionGetLocalScale(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 dest
|
vec3 dest
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
entityPositionEnsurePRS(pos);
|
|
||||||
glm_vec3_copy(pos->scale, dest);
|
glm_vec3_copy(pos->scale, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionGetWorldScale(
|
void entityPositionSetScale(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 dest
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
|
||||||
entityPositionEnsurePRS(pos);
|
|
||||||
glm_vec3_copy(pos->scale, dest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entityPositionEnsureWorld(mgr, pos);
|
|
||||||
const float_t (*wt)[4] = pos->worldTransform;
|
|
||||||
dest[0] = sqrtf(wt[0][0]*wt[0][0] + wt[0][1]*wt[0][1] + wt[0][2]*wt[0][2]);
|
|
||||||
dest[1] = sqrtf(wt[1][0]*wt[1][0] + wt[1][1]*wt[1][1] + wt[1][2]*wt[1][2]);
|
|
||||||
dest[2] = sqrtf(wt[2][0]*wt[2][0] + wt[2][1]*wt[2][1] + wt[2][2]*wt[2][2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionSetLocalScale(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 scale
|
vec3 scale
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
glm_vec3_copy(scale, pos->scale);
|
glm_vec3_copy(scale, pos->scale);
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
entityPositionRebuild(pos);
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionSetWorldScale(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 scale
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
if(pos->parentEntityId == ENTITY_ID_INVALID) {
|
|
||||||
glm_vec3_copy(scale, pos->scale);
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entityposition_t *parent = componentGetData(
|
|
||||||
mgr, pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
entityPositionEnsureWorld(mgr, parent);
|
|
||||||
const float_t (*pt)[4] = parent->worldTransform;
|
|
||||||
const float_t psx = sqrtf(
|
|
||||||
pt[0][0]*pt[0][0] + pt[0][1]*pt[0][1] + pt[0][2]*pt[0][2]
|
|
||||||
);
|
|
||||||
const float_t psy = sqrtf(
|
|
||||||
pt[1][0]*pt[1][0] + pt[1][1]*pt[1][1] + pt[1][2]*pt[1][2]
|
|
||||||
);
|
|
||||||
const float_t psz = sqrtf(
|
|
||||||
pt[2][0]*pt[2][0] + pt[2][1]*pt[2][1] + pt[2][2]*pt[2][2]
|
|
||||||
);
|
|
||||||
pos->scale[0] = psx > 0.0f ? scale[0] / psx : scale[0];
|
|
||||||
pos->scale[1] = psy > 0.0f ? scale[1] / psy : scale[1];
|
|
||||||
pos->scale[2] = psz > 0.0f ? scale[2] / psz : scale[2];
|
|
||||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
|
||||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionSetParent(
|
void entityPositionSetParent(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const entityid_t parentEntityId,
|
const entityid_t parentEntityId,
|
||||||
const componentid_t parentComponentId
|
const componentid_t parentComponentId
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = componentGetData(
|
entityposition_t *pos = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove from old parent's child list.
|
// Remove from old parent's child list.
|
||||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
entityposition_t *oldParent = componentGetData(
|
entityposition_t *oldParent = componentGetData(
|
||||||
mgr, pos->parentEntityId, pos->parentComponentId,
|
pos->parentEntityId, pos->parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
COMPONENT_TYPE_POSITION
|
|
||||||
);
|
);
|
||||||
for(uint8_t i = 0; i < oldParent->childCount; i++) {
|
for(uint8_t i = 0; i < oldParent->childCount; i++) {
|
||||||
if(
|
if(
|
||||||
@@ -447,7 +196,7 @@ void entityPositionSetParent(
|
|||||||
// Register with new parent.
|
// Register with new parent.
|
||||||
if(parentEntityId != ENTITY_ID_INVALID) {
|
if(parentEntityId != ENTITY_ID_INVALID) {
|
||||||
entityposition_t *parent = componentGetData(
|
entityposition_t *parent = componentGetData(
|
||||||
mgr, parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
|
parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
|
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
|
||||||
parent->childEntityIds[parent->childCount] = entityId;
|
parent->childEntityIds[parent->childCount] = entityId;
|
||||||
@@ -456,86 +205,62 @@ void entityPositionSetParent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
entityPositionMarkDirty(pos);
|
||||||
}
|
|
||||||
|
|
||||||
entityid_t entityPositionGetParent(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
return pos->parentEntityId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entityposition_t *entityPositionGet(
|
entityposition_t *entityPositionGet(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
return componentGetData(
|
return componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos) {
|
void entityPositionRebuild(entityposition_t *pos) {
|
||||||
pos->flags = (
|
glm_mat4_identity(pos->localTransform);
|
||||||
pos->flags |
|
glm_translate(pos->localTransform, pos->position);
|
||||||
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
if(pos->rotation[0] != 0.0f) {
|
||||||
ENTITY_POSITION_FLAG_POSITION_DIRTY
|
glm_rotate_x(pos->localTransform, pos->rotation[0], pos->localTransform);
|
||||||
) & ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
entityPositionMarkDirty(mgr, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos) {
|
|
||||||
if(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY) return;
|
|
||||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
|
||||||
for(uint8_t i = 0; i < pos->childCount; i++) {
|
|
||||||
entityposition_t *child = componentGetData(
|
|
||||||
mgr, pos->childEntityIds[i], pos->childComponentIds[i],
|
|
||||||
COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
entityPositionMarkDirty(mgr, child);
|
|
||||||
}
|
}
|
||||||
|
if(pos->rotation[1] != 0.0f) {
|
||||||
|
glm_rotate_y(pos->localTransform, pos->rotation[1], pos->localTransform);
|
||||||
|
}
|
||||||
|
if(pos->rotation[2] != 0.0f) {
|
||||||
|
glm_rotate_z(pos->localTransform, pos->rotation[2], pos->localTransform);
|
||||||
|
}
|
||||||
|
glm_scale(pos->localTransform, pos->scale);
|
||||||
|
entityPositionMarkDirty(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionDisposeDeep(
|
void entityPositionDisposeDeep(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
entityposition_t *pos = entityPositionGet(mgr, entityId, componentId);
|
entityposition_t *pos = entityPositionGet(entityId, componentId);
|
||||||
|
|
||||||
// Detach from parent so the parent's child list stays consistent.
|
// Detach from parent so the parent's child list stays consistent.
|
||||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||||
entityPositionSetParent(
|
entityPositionSetParent(entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID);
|
||||||
mgr, entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the child list before disposing self (entityDispose invalidates
|
// Copy the child list before disposing self (entityDispose invalidates pos).
|
||||||
// pos).
|
|
||||||
uint8_t childCount = pos->childCount;
|
uint8_t childCount = pos->childCount;
|
||||||
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
for(uint8_t i = 0; i < childCount; i++) {
|
for(uint8_t i = 0; i < childCount; i++) {
|
||||||
childEntityIds[i] = pos->childEntityIds[i];
|
childEntityIds[i] = pos->childEntityIds[i];
|
||||||
childComponentIds[i] = pos->childComponentIds[i];
|
childComponentIds[i] = pos->childComponentIds[i];
|
||||||
// Sever child's parent link so it won't try to modify our disposed
|
// Sever the child's parent link so it won't try to modify our disposed data.
|
||||||
// data.
|
entityposition_t *child = entityPositionGet(childEntityIds[i], childComponentIds[i]);
|
||||||
entityposition_t *child = entityPositionGet(
|
|
||||||
mgr, childEntityIds[i], childComponentIds[i]
|
|
||||||
);
|
|
||||||
child->parentEntityId = ENTITY_ID_INVALID;
|
child->parentEntityId = ENTITY_ID_INVALID;
|
||||||
child->parentComponentId = COMPONENT_ID_INVALID;
|
child->parentComponentId = COMPONENT_ID_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
entityDispose(mgr, entityId);
|
entityDispose(entityId);
|
||||||
|
|
||||||
for(uint8_t i = 0; i < childCount; i++) {
|
for(uint8_t i = 0; i < childCount; i++) {
|
||||||
entityPositionDisposeDeep(mgr, childEntityIds[i], childComponentIds[i]);
|
entityPositionDisposeDeep(childEntityIds[i], childComponentIds[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,108 +287,36 @@ void entityPositionDecompose(entityposition_t *pos) {
|
|||||||
pos->localTransform[2][2] * pos->localTransform[2][2]
|
pos->localTransform[2][2] * pos->localTransform[2][2]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Normalize columns to isolate the rotation matrix (no mat4 needed).
|
// Normalize columns to isolate the rotation matrix.
|
||||||
const float_t invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
|
float invS0 = pos->scale[0] > 0.0f ? 1.0f / pos->scale[0] : 0.0f;
|
||||||
const float_t invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
|
float invS1 = pos->scale[1] > 0.0f ? 1.0f / pos->scale[1] : 0.0f;
|
||||||
const float_t invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
|
float invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
|
||||||
|
|
||||||
const float_t r00 = pos->localTransform[0][0] * invS0;
|
mat4 r;
|
||||||
const float_t r01 = pos->localTransform[0][1] * invS0;
|
glm_mat4_identity(r);
|
||||||
const float_t r02 = pos->localTransform[0][2] * invS0;
|
r[0][0] = pos->localTransform[0][0] * invS0;
|
||||||
const float_t r10 = pos->localTransform[1][0] * invS1;
|
r[0][1] = pos->localTransform[0][1] * invS0;
|
||||||
const float_t r11 = pos->localTransform[1][1] * invS1;
|
r[0][2] = pos->localTransform[0][2] * invS0;
|
||||||
const float_t r20 = pos->localTransform[2][0] * invS2;
|
r[1][0] = pos->localTransform[1][0] * invS1;
|
||||||
const float_t r21 = pos->localTransform[2][1] * invS2;
|
r[1][1] = pos->localTransform[1][1] * invS1;
|
||||||
const float_t r22 = pos->localTransform[2][2] * invS2;
|
r[1][2] = pos->localTransform[1][2] * invS1;
|
||||||
|
r[2][0] = pos->localTransform[2][0] * invS2;
|
||||||
|
r[2][1] = pos->localTransform[2][1] * invS2;
|
||||||
|
r[2][2] = pos->localTransform[2][2] * invS2;
|
||||||
|
|
||||||
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
|
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
|
||||||
const float_t sinBeta = glm_clamp(r20, -1.0f, 1.0f);
|
float sinBeta = glm_clamp(r[2][0], -1.0f, 1.0f);
|
||||||
pos->rotation[1] = asinf(sinBeta);
|
pos->rotation[1] = asinf(sinBeta);
|
||||||
const float_t cosBeta = cosf(pos->rotation[1]);
|
float cosBeta = cosf(pos->rotation[1]);
|
||||||
|
|
||||||
if(fabsf(cosBeta) > 1e-6f) {
|
if(fabsf(cosBeta) > 1e-6f) {
|
||||||
pos->rotation[0] = atan2f(-r21, r22);
|
pos->rotation[0] = atan2f(-r[2][1], r[2][2]);
|
||||||
pos->rotation[2] = atan2f(-r10, r00);
|
pos->rotation[2] = atan2f(-r[1][0], r[0][0]);
|
||||||
} else {
|
} else {
|
||||||
// Gimbal lock: pin Z to 0, recover X.
|
// Gimbal lock: pin Z to 0, recover X.
|
||||||
pos->rotation[2] = 0.0f;
|
pos->rotation[2] = 0.0f;
|
||||||
pos->rotation[0] = (sinBeta > 0.0f)
|
pos->rotation[0] = (sinBeta > 0.0f)
|
||||||
? atan2f(r01, r11)
|
? atan2f(r[0][1], r[1][1])
|
||||||
: -atan2f(r01, r11);
|
: -atan2f(r[0][1], r[1][1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityPositionEnsurePRS(entityposition_t *pos) {
|
|
||||||
if(!(pos->flags & ENTITY_POSITION_FLAG_PRS_DIRTY)) return;
|
|
||||||
entityPositionDecompose(pos);
|
|
||||||
pos->flags &= ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionEnsureLocal(entityposition_t *pos) {
|
|
||||||
const uint8_t dirty = pos->flags & (
|
|
||||||
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
|
|
||||||
);
|
|
||||||
if(!dirty) return;
|
|
||||||
|
|
||||||
if(dirty & ENTITY_POSITION_FLAG_ROTATION_DIRTY) {
|
|
||||||
// Rotation or scale changed: rebuild cols 0-2 analytically (XYZ euler).
|
|
||||||
const float_t c0 = cosf(pos->rotation[0]), s0 = sinf(pos->rotation[0]);
|
|
||||||
const float_t c1 = cosf(pos->rotation[1]), s1 = sinf(pos->rotation[1]);
|
|
||||||
const float_t c2 = cosf(pos->rotation[2]), s2 = sinf(pos->rotation[2]);
|
|
||||||
const float_t s0s1 = s0 * s1;
|
|
||||||
const float_t c0s1 = c0 * s1;
|
|
||||||
|
|
||||||
pos->localTransform[0][0] = c1 * c2 * pos->scale[0];
|
|
||||||
pos->localTransform[0][1] = (c0 * s2 + s0s1 * c2) * pos->scale[0];
|
|
||||||
pos->localTransform[0][2] = (s0 * s2 - c0s1 * c2) * pos->scale[0];
|
|
||||||
pos->localTransform[0][3] = 0.0f;
|
|
||||||
|
|
||||||
pos->localTransform[1][0] = -c1 * s2 * pos->scale[1];
|
|
||||||
pos->localTransform[1][1] = (c0 * c2 - s0s1 * s2) * pos->scale[1];
|
|
||||||
pos->localTransform[1][2] = (s0 * c2 + c0s1 * s2) * pos->scale[1];
|
|
||||||
pos->localTransform[1][3] = 0.0f;
|
|
||||||
|
|
||||||
pos->localTransform[2][0] = s1 * pos->scale[2];
|
|
||||||
pos->localTransform[2][1] = -s0 * c1 * pos->scale[2];
|
|
||||||
pos->localTransform[2][2] = c0 * c1 * pos->scale[2];
|
|
||||||
pos->localTransform[2][3] = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dirty & ENTITY_POSITION_FLAG_POSITION_DIRTY) {
|
|
||||||
// Only position changed: update column 3 only (no trig needed).
|
|
||||||
pos->localTransform[3][0] = pos->position[0];
|
|
||||||
pos->localTransform[3][1] = pos->position[1];
|
|
||||||
pos->localTransform[3][2] = pos->position[2];
|
|
||||||
pos->localTransform[3][3] = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos->flags &= ~(
|
|
||||||
ENTITY_POSITION_FLAG_ROTATION_DIRTY | ENTITY_POSITION_FLAG_POSITION_DIRTY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos) {
|
|
||||||
if(!(pos->flags & ENTITY_POSITION_FLAG_WORLD_DIRTY)) return;
|
|
||||||
entityPositionEnsureLocal(pos);
|
|
||||||
|
|
||||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
|
||||||
// Parented: world = parent.world x local. worldTransform must be
|
|
||||||
// written because children (and this node's getters) read it.
|
|
||||||
entityposition_t *parent = componentGetData(
|
|
||||||
mgr, pos->parentEntityId, pos->parentComponentId,
|
|
||||||
COMPONENT_TYPE_POSITION
|
|
||||||
);
|
|
||||||
entityPositionEnsureWorld(mgr, parent);
|
|
||||||
glm_mat4_mul(
|
|
||||||
parent->worldTransform, pos->localTransform, pos->worldTransform
|
|
||||||
);
|
|
||||||
} else if(pos->childCount > 0) {
|
|
||||||
// Parentless root with children: children need a valid worldTransform
|
|
||||||
// to multiply against, but world == local, so just copy.
|
|
||||||
glm_mat4_copy(pos->localTransform, pos->worldTransform);
|
|
||||||
}
|
|
||||||
// Parentless leaf: world == local. Getters read localTransform directly;
|
|
||||||
// no copy needed.
|
|
||||||
|
|
||||||
pos->flags &= ~ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,124 +7,56 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "entity/entitybase.h"
|
#include "entity/entitybase.h"
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
/** Maximum number of child position components this node can track. */
|
|
||||||
#define ENTITY_POSITION_CHILDREN_MAX 8
|
#define ENTITY_POSITION_CHILDREN_MAX 8
|
||||||
|
|
||||||
/**
|
|
||||||
* PRS cache is stale. localTransform was written directly (e.g. lookAt) and
|
|
||||||
* position/rotation/scale need to be decomposed before they can be read.
|
|
||||||
*/
|
|
||||||
#define ENTITY_POSITION_FLAG_PRS_DIRTY (1 << 0)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Columns 0-2 of localTransform are stale. Rotation or scale changed; the
|
|
||||||
* basis vectors need to be rebuilt analytically before the matrix can be used.
|
|
||||||
* Does not imply column 3 (translation) is stale.
|
|
||||||
*/
|
|
||||||
#define ENTITY_POSITION_FLAG_ROTATION_DIRTY (1 << 1)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Column 3 of localTransform is stale. Position changed; only the
|
|
||||||
* translation column needs to be written. Does not imply columns 0-2 are
|
|
||||||
* stale.
|
|
||||||
*/
|
|
||||||
#define ENTITY_POSITION_FLAG_POSITION_DIRTY (1 << 2)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* worldTransform is stale. Either the local matrix changed or an ancestor
|
|
||||||
* moved; worldTransform must be recomputed before world data can be read.
|
|
||||||
*/
|
|
||||||
#define ENTITY_POSITION_FLAG_WORLD_DIRTY (1 << 3)
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
/*
|
|
||||||
* Hot fields - flag checks, parent/child traversal (markDirty, ensureWorld)
|
|
||||||
* only touch these. Kept at the front so they share the first cache line.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** ENTITY_POSITION_FLAG_* bitmask; describes which caches are stale. */
|
|
||||||
uint8_t flags;
|
|
||||||
/** Entity ID of the parent node, or ENTITY_ID_INVALID if none. */
|
|
||||||
entityid_t parentEntityId;
|
|
||||||
/** Component ID of the parent position, or COMPONENT_ID_INVALID if none. */
|
|
||||||
componentid_t parentComponentId;
|
|
||||||
/** Number of currently registered children. */
|
|
||||||
uint8_t childCount;
|
|
||||||
/** Entity IDs of child nodes. */
|
|
||||||
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
|
||||||
/** Component IDs of child position components. */
|
|
||||||
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Warm fields - read/written by PRS getters/setters.
|
|
||||||
* Accessed more often than the matrices but less often than flags.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Cached local position (XYZ). Stale when PRS_DIRTY is set. */
|
|
||||||
vec3 position;
|
|
||||||
/** Cached local rotation (XYZ euler, radians). Stale when PRS_DIRTY. */
|
|
||||||
vec3 rotation;
|
|
||||||
/** Cached local scale (XYZ). Stale when PRS_DIRTY is set. */
|
|
||||||
vec3 scale;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Cold fields - only touched when actually rebuilding transforms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Local transform matrix, rebuilt lazily from position/rotation/scale. */
|
|
||||||
mat4 localTransform;
|
mat4 localTransform;
|
||||||
/** World transform matrix, recomputed lazily from the parent chain. */
|
|
||||||
mat4 worldTransform;
|
mat4 worldTransform;
|
||||||
|
vec3 position;
|
||||||
|
vec3 rotation;
|
||||||
|
vec3 scale;
|
||||||
|
bool dirty;
|
||||||
|
entityid_t parentEntityId;
|
||||||
|
componentid_t parentComponentId;
|
||||||
|
uint8_t childCount;
|
||||||
|
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
|
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||||
} entityposition_t;
|
} entityposition_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the entity position component, setting identity transforms and
|
* Initialize the entity position component.
|
||||||
* zeroing all parent/child state.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionInit(
|
void entityPositionInit(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Positions and orients the entity at eye, facing target. Stores this as
|
* Transforms the entity's local transform to look at a target point.
|
||||||
* the entity's normal world-space placement (consistent with every other
|
|
||||||
* setter), not as a view matrix -- invert entityPositionGetTransform()'s
|
|
||||||
* result to get a view matrix for rendering.
|
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
* @param eye The eye/camera position.
|
|
||||||
* @param target The target point to look at.
|
* @param target The target point to look at.
|
||||||
* @param up The up vector.
|
* @param up The up vector.
|
||||||
|
* @param eye The eye/camera position.
|
||||||
*/
|
*/
|
||||||
void entityPositionLookAt(
|
void entityPositionLookAt(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 eye,
|
|
||||||
vec3 target,
|
vec3 target,
|
||||||
vec3 up
|
vec3 up,
|
||||||
|
vec3 eye
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the world-space transform matrix, recomputing it lazily if dirty.
|
* Gets the world-space transform matrix, recomputing it lazily if dirty.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
* @param dest Destination matrix.
|
* @param dest Destination matrix.
|
||||||
*/
|
*/
|
||||||
void entityPositionGetTransform(
|
void entityPositionGetTransform(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
mat4 dest
|
mat4 dest
|
||||||
@@ -133,209 +65,65 @@ void entityPositionGetTransform(
|
|||||||
/**
|
/**
|
||||||
* Gets the local transform matrix (does not include parent transforms).
|
* Gets the local transform matrix (does not include parent transforms).
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
* @param dest Destination matrix.
|
* @param dest Destination matrix.
|
||||||
*/
|
*/
|
||||||
void entityPositionGetLocalTransform(
|
void entityPositionGetLocalTransform(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
mat4 dest
|
mat4 dest
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the cached local position (XYZ). Decomposes localTransform into PRS
|
* Gets the cached local position.
|
||||||
* first if ENTITY_POSITION_FLAG_PRS_DIRTY is set; never triggers a matrix
|
|
||||||
* rebuild or world-transform update.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param dest Destination vector.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionGetLocalPosition(
|
void entityPositionGetPosition(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 dest
|
vec3 dest
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the world-space position. For parentless entities this is the same as
|
* Sets the local position and marks the world transform dirty.
|
||||||
* the local position.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param dest Destination vector.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionGetWorldPosition(
|
void entityPositionSetPosition(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 dest
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the world-space position. For parentless entities this is equivalent
|
|
||||||
* to entityPositionSetLocalPosition. For parented entities the position is
|
|
||||||
* converted to local space via the inverted parent world transform.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param position The desired world-space position.
|
|
||||||
*/
|
|
||||||
void entityPositionSetWorldPosition(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 position
|
vec3 position
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the local position, marks localTransform and worldTransform (self +
|
* Gets the cached local euler rotation (XYZ, radians).
|
||||||
* descendants) dirty.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param position The new local position.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionSetLocalPosition(
|
void entityPositionGetRotation(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 position
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the cached local euler rotation (XYZ, radians). Decomposes
|
|
||||||
* localTransform first if ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param dest Destination vector.
|
|
||||||
*/
|
|
||||||
void entityPositionGetLocalRotation(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 dest
|
vec3 dest
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the world-space euler rotation (XYZ, radians) by decomposing the
|
* Sets the local euler rotation (XYZ, radians) and marks the world transform dirty.
|
||||||
* world transform. For parentless entities this is the same as local
|
|
||||||
* rotation.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param dest Destination vector.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionGetWorldRotation(
|
void entityPositionSetRotation(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 dest
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the local euler rotation (XYZ, radians) and marks transforms dirty.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param rotation The new local rotation.
|
|
||||||
*/
|
|
||||||
void entityPositionSetLocalRotation(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 rotation
|
vec3 rotation
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the world-space euler rotation (XYZ, radians). For parentless
|
* Gets the cached local scale.
|
||||||
* entities this is equivalent to entityPositionSetLocalRotation. For
|
|
||||||
* parented entities the rotation is converted to local space by removing
|
|
||||||
* the parent world rotation.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param rotation The desired world-space euler rotation.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionSetWorldRotation(
|
void entityPositionGetScale(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 rotation
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the cached local scale. Decomposes localTransform first if
|
|
||||||
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param dest Destination vector.
|
|
||||||
*/
|
|
||||||
void entityPositionGetLocalScale(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 dest
|
vec3 dest
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the world-space scale by extracting column lengths from the world
|
* Sets the local scale and marks the world transform dirty.
|
||||||
* transform. For parentless entities this is the same as local scale.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param dest Destination vector.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionGetWorldScale(
|
void entityPositionSetScale(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 dest
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the local scale and marks transforms dirty.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param scale The new local scale.
|
|
||||||
*/
|
|
||||||
void entityPositionSetLocalScale(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
vec3 scale
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the world-space scale. For parentless entities this is equivalent to
|
|
||||||
* entityPositionSetLocalScale. For parented entities the scale is converted
|
|
||||||
* to local space by dividing by the parent world scale (assumes no shear).
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @param scale The desired world-space scale.
|
|
||||||
*/
|
|
||||||
void entityPositionSetWorldScale(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
vec3 scale
|
vec3 scale
|
||||||
@@ -345,83 +133,46 @@ void entityPositionSetWorldScale(
|
|||||||
* Sets the parent of this entity's position component.
|
* Sets the parent of this entity's position component.
|
||||||
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
|
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns both entities.
|
|
||||||
* @param entityId The child entity ID.
|
* @param entityId The child entity ID.
|
||||||
* @param componentId The child component ID.
|
* @param componentId The child component ID.
|
||||||
* @param parentEntityId The parent entity ID.
|
* @param parentEntityId The parent entity ID.
|
||||||
* @param parentComponentId The parent component ID.
|
* @param parentComponentId The parent component ID.
|
||||||
*/
|
*/
|
||||||
void entityPositionSetParent(
|
void entityPositionSetParent(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const entityid_t parentEntityId,
|
const entityid_t parentEntityId,
|
||||||
const componentid_t parentComponentId
|
const componentid_t parentComponentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the entity ID of this position component's parent.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @return The parent entity ID, or ENTITY_ID_INVALID if unparented.
|
|
||||||
*/
|
|
||||||
entityid_t entityPositionGetParent(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a direct pointer to the entity position component data.
|
* Returns a direct pointer to the entity position component data.
|
||||||
* After modifying localTransform directly, call entityPositionMarkDirty() to
|
* After modifying localTransform directly, call entityPositionMarkDirty().
|
||||||
* set ENTITY_POSITION_FLAG_WORLD_DIRTY on self and descendants. After
|
|
||||||
* modifying PRS directly, call entityPositionRebuild() instead.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity ID.
|
|
||||||
* @param componentId The component ID.
|
|
||||||
* @return Pointer to the component data.
|
|
||||||
*/
|
*/
|
||||||
entityposition_t *entityPositionGet(
|
entityposition_t *entityPositionGet(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signals that the PRS cache was modified externally. Sets both
|
* Rebuilds the local transform matrix from the cached position/rotation/scale,
|
||||||
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_DIRTY
|
* then marks this node and all descendants dirty.
|
||||||
* so all of localTransform is rebuilt lazily on the next read, clears
|
|
||||||
* ENTITY_POSITION_FLAG_PRS_DIRTY, propagates ENTITY_POSITION_FLAG_WORLD_DIRTY
|
|
||||||
* to self and all descendants.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns pos and its descendants.
|
|
||||||
* @param pos The position component whose PRS was modified.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos);
|
void entityPositionRebuild(entityposition_t *pos);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets ENTITY_POSITION_FLAG_WORLD_DIRTY on this node and all descendants,
|
* Marks this node and all descendants as having a stale world transform.
|
||||||
* indicating that worldTransform must be recomputed before it is read.
|
|
||||||
* Call this after modifying localTransform directly.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns pos and its descendants.
|
|
||||||
* @param pos The position component to mark dirty.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionMarkDirty(entitymanager_t *mgr, entityposition_t *pos);
|
void entityPositionMarkDirty(entityposition_t *pos);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes this entity and all of its position-component descendants
|
* Disposes this entity and all of its position-component descendants
|
||||||
* recursively. Detaches from any parent before destroying.
|
* recursively. Detaches from any parent before destroying.
|
||||||
*
|
*
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The root entity ID.
|
* @param entityId The root entity ID.
|
||||||
* @param componentId The root position component ID.
|
* @param componentId The root position component ID.
|
||||||
*/
|
*/
|
||||||
void entityPositionDisposeDeep(
|
void entityPositionDisposeDeep(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
@@ -429,32 +180,5 @@ void entityPositionDisposeDeep(
|
|||||||
/**
|
/**
|
||||||
* Decomposes the local transform matrix back into the position, rotation
|
* Decomposes the local transform matrix back into the position, rotation
|
||||||
* (XYZ euler, radians), and scale cache fields.
|
* (XYZ euler, radians), and scale cache fields.
|
||||||
*
|
|
||||||
* @param pos The position component to decompose.
|
|
||||||
*/
|
*/
|
||||||
void entityPositionDecompose(entityposition_t *pos);
|
void entityPositionDecompose(entityposition_t *pos);
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Decomposes localTransform into the PRS cache if
|
|
||||||
* ENTITY_POSITION_FLAG_PRS_DIRTY is set.
|
|
||||||
*
|
|
||||||
* @param pos The position component to update.
|
|
||||||
*/
|
|
||||||
void entityPositionEnsurePRS(entityposition_t *pos);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Rebuilds localTransform from the PRS cache, touching only the
|
|
||||||
* columns flagged as stale (ROTATION_DIRTY and/or POSITION_DIRTY).
|
|
||||||
*
|
|
||||||
* @param pos The position component to update.
|
|
||||||
*/
|
|
||||||
void entityPositionEnsureLocal(entityposition_t *pos);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Recomputes worldTransform from the parent chain if
|
|
||||||
* ENTITY_POSITION_FLAG_WORLD_DIRTY is set.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns pos and its ancestors.
|
|
||||||
* @param pos The position component to update.
|
|
||||||
*/
|
|
||||||
void entityPositionEnsureWorld(entitymanager_t *mgr, entityposition_t *pos);
|
|
||||||
|
|||||||
@@ -7,186 +7,143 @@
|
|||||||
|
|
||||||
#include "entityrenderable.h"
|
#include "entityrenderable.h"
|
||||||
#include "entity/entitymanager.h"
|
#include "entity/entitymanager.h"
|
||||||
#include "display/shader/shadermaterial.h"
|
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
#include "display/display.h"
|
|
||||||
#include "display/mesh/cube.h"
|
#include "display/mesh/cube.h"
|
||||||
#include "util/memory.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
|
|
||||||
void entityRenderableInit(
|
void entityRenderableInit(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
entityrenderable_t *r = componentGetData(
|
entityrenderable_t *r = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
);
|
);
|
||||||
memoryZero(r, sizeof(entityrenderable_t));
|
r->type = ENTITY_RENDERABLE_TYPE_MATERIAL;
|
||||||
r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL;
|
r->mesh = &CUBE_MESH_SIMPLE;
|
||||||
r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT;
|
r->shader = &SHADER_UNLIT;
|
||||||
r->data.material.material.unlit.color = COLOR_WHITE;
|
r->material.unlit.color = COLOR_WHITE;
|
||||||
r->data.material.meshes[0] = &CUBE_MESH_SIMPLE;
|
|
||||||
r->data.material.meshOffsets[0] = 0;
|
|
||||||
r->data.material.meshCounts[0] = -1;
|
|
||||||
r->data.material.meshCount = 1;
|
|
||||||
r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRenderableDispose(
|
entityrenderabletype_t entityRenderableGetType(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
return r->type;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRenderableSetType(
|
void entityRenderableSetType(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const entityrenderabletype_t type
|
const entityrenderabletype_t type
|
||||||
) {
|
) {
|
||||||
entityrenderable_t *r = componentGetData(
|
entityrenderable_t *r = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
);
|
);
|
||||||
r->type = type;
|
r->type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRenderableSetPriority(
|
mesh_t * entityRenderableGetMesh(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId
|
||||||
const int8_t priority
|
|
||||||
) {
|
) {
|
||||||
entityrenderable_t *r = componentGetData(
|
entityrenderable_t *r = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
);
|
);
|
||||||
r->priority = priority;
|
return r->mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetMesh(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
mesh_t *mesh
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->mesh = mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
shader_t * entityRenderableGetShader(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
return r->shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityRenderableSetShader(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
shader_t *shader
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
r->shader = shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
shadermaterial_t * entityRenderableGetShaderMaterial(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityrenderable_t *r = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
return &r->material;
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRenderableSetColor(
|
void entityRenderableSetColor(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const color_t color
|
const color_t color
|
||||||
) {
|
) {
|
||||||
entityrenderable_t *r = componentGetData(
|
entityrenderable_t *r = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
);
|
);
|
||||||
assertTrue(
|
r->material.unlit.color = color;
|
||||||
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
|
||||||
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set color"
|
|
||||||
);
|
|
||||||
r->data.material.material.unlit.color = color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRenderableSetMesh(
|
void entityRenderableSpriteBatchAdd(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const uint8_t slot,
|
const spritebatchsprite_t *sprite
|
||||||
mesh_t *mesh
|
|
||||||
) {
|
) {
|
||||||
assertNotNull(mesh, "Mesh cannot be null");
|
|
||||||
assertTrue(slot < ENTITY_RENDERABLE_MESHES_MAX, "Mesh slot out of bounds");
|
|
||||||
|
|
||||||
entityrenderable_t *r = componentGetData(
|
entityrenderable_t *r = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
);
|
);
|
||||||
assertTrue(
|
if(r->spritebatch.spriteCount >= ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX) return;
|
||||||
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
r->spritebatch.sprites[r->spritebatch.spriteCount++] = *sprite;
|
||||||
"Renderable must be ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL to set a mesh"
|
|
||||||
);
|
|
||||||
|
|
||||||
r->data.material.meshes[slot] = mesh;
|
|
||||||
r->data.material.meshOffsets[slot] = 0;
|
|
||||||
r->data.material.meshCounts[slot] = -1;
|
|
||||||
if(slot >= r->data.material.meshCount) {
|
|
||||||
r->data.material.meshCount = slot + 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void entityRenderableSetDraw(
|
void entityRenderableSpriteBatchClear(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
errorret_t (*draw)(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
void *user
|
|
||||||
),
|
|
||||||
void *user
|
|
||||||
) {
|
|
||||||
assertNotNull(draw, "Draw callback cannot be null");
|
|
||||||
entityrenderable_t *r = componentGetData(
|
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
|
||||||
);
|
|
||||||
r->type = ENTITY_RENDERABLE_TYPE_CUSTOM;
|
|
||||||
r->data.custom.draw = draw;
|
|
||||||
r->data.custom.drawUser = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t entityRenderableDraw(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
entityrenderable_t *r = componentGetData(
|
entityrenderable_t *r = componentGetData(
|
||||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
);
|
);
|
||||||
switch(r->type) {
|
r->spritebatch.spriteCount = 0;
|
||||||
case ENTITY_RENDERABLE_TYPE_SPRITEBATCH:
|
|
||||||
return entityRenderableDrawSpritebatch(&r->data.spritebatch);
|
|
||||||
case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL:
|
|
||||||
return entityRenderableDrawMaterial(&r->data.material);
|
|
||||||
case ENTITY_RENDERABLE_TYPE_CUSTOM:
|
|
||||||
return entityRenderableDrawCustom(
|
|
||||||
mgr, entityId, componentId, &r->data.custom
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
assertUnreachable("Invalid renderable type");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t entityRenderableDrawSpritebatch(
|
void entityRenderableDispose(
|
||||||
const entityrenderablespritebatch_t *sb
|
|
||||||
) {
|
|
||||||
if(sb->spriteCount == 0) errorOk();
|
|
||||||
|
|
||||||
errorChain(displaySetState((displaystate_t){
|
|
||||||
.flags = DISPLAY_STATE_FLAG_BLEND
|
|
||||||
}));
|
|
||||||
|
|
||||||
spriteBatchClear();
|
|
||||||
shadermaterial_t mat;
|
|
||||||
memoryZero(&mat, sizeof(shadermaterial_t));
|
|
||||||
mat.unlit.texture = sb->texture;
|
|
||||||
mat.unlit.color = COLOR_WHITE;
|
|
||||||
errorChain(spriteBatchBuffer(
|
|
||||||
sb->sprites, sb->spriteCount,
|
|
||||||
SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat
|
|
||||||
));
|
|
||||||
return spriteBatchFlush();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m) {
|
|
||||||
errorChain(displaySetState(m->state));
|
|
||||||
shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader;
|
|
||||||
assertNotNull(shader, "Shader cannot be null for material type");
|
|
||||||
errorChain(shaderBind(shader));
|
|
||||||
errorChain(shaderSetMaterial(shader, &m->material));
|
|
||||||
for(uint8_t i = 0; i < m->meshCount; i++) {
|
|
||||||
errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i]));
|
|
||||||
}
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t entityRenderableDrawCustom(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId
|
||||||
const entityrenderablecustom_t *custom
|
|
||||||
) {
|
) {
|
||||||
return custom->draw(mgr, entityId, componentId, custom->drawUser);
|
entityrenderable_t *r = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||||
|
);
|
||||||
|
if(
|
||||||
|
r->type == ENTITY_RENDERABLE_TYPE_CALLBACK &&
|
||||||
|
r->userFree &&
|
||||||
|
r->user
|
||||||
|
) {
|
||||||
|
r->userFree(r->user);
|
||||||
|
r->user = NULL;
|
||||||
|
}
|
||||||
|
r->mesh = NULL;
|
||||||
|
r->shader = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,225 +10,148 @@
|
|||||||
#include "display/mesh/mesh.h"
|
#include "display/mesh/mesh.h"
|
||||||
#include "display/shader/shadermaterial.h"
|
#include "display/shader/shadermaterial.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/displaystate.h"
|
|
||||||
#include "error/error.h"
|
|
||||||
|
|
||||||
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
||||||
#define ENTITY_RENDERABLE_MESHES_MAX 8
|
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ENTITY_RENDERABLE_TYPE_CUSTOM = 0,
|
ENTITY_RENDERABLE_TYPE_MATERIAL = 0,
|
||||||
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
|
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
|
||||||
ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
ENTITY_RENDERABLE_TYPE_CALLBACK,
|
||||||
} entityrenderabletype_t;
|
} entityrenderabletype_t;
|
||||||
|
|
||||||
|
typedef errorret_t (*entityrenderablecallback_t)(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const mat4 view,
|
||||||
|
const mat4 proj,
|
||||||
|
const mat4 model,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
|
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
|
||||||
uint32_t spriteCount;
|
uint16_t spriteCount;
|
||||||
texture_t *texture;
|
|
||||||
} entityrenderablespritebatch_t;
|
} entityrenderablespritebatch_t;
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
mesh_t *meshes[ENTITY_RENDERABLE_MESHES_MAX];
|
|
||||||
int32_t meshOffsets[ENTITY_RENDERABLE_MESHES_MAX];
|
|
||||||
int32_t meshCounts[ENTITY_RENDERABLE_MESHES_MAX];
|
|
||||||
uint8_t meshCount;
|
|
||||||
shaderlistshader_t shaderType;
|
|
||||||
shadermaterial_t material;
|
|
||||||
displaystate_t state;
|
|
||||||
} entityrenderablematerial_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
errorret_t (*draw)(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
void *drawUser;
|
|
||||||
} entityrenderablecustom_t;
|
|
||||||
|
|
||||||
typedef union entityrenderabledata_u {
|
|
||||||
entityrenderablespritebatch_t spritebatch;
|
|
||||||
entityrenderablematerial_t material;
|
|
||||||
entityrenderablecustom_t custom;
|
|
||||||
} entityrenderabledata_t;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
entityrenderabletype_t type;
|
entityrenderabletype_t type;
|
||||||
entityrenderabledata_t data;
|
shader_t *shader;
|
||||||
|
union {
|
||||||
/**
|
struct {
|
||||||
* Render priority. 0 = auto (derived from type/flags). Higher values
|
mesh_t *mesh;
|
||||||
* render later (on top of lower values). Range: [-128..127] with 0 auto.
|
shadermaterial_t material;
|
||||||
*/
|
};
|
||||||
int8_t priority;
|
entityrenderablespritebatch_t spritebatch;
|
||||||
|
struct {
|
||||||
|
entityrenderablecallback_t callback;
|
||||||
|
void (*userFree)(void *user);
|
||||||
|
void *user;
|
||||||
|
};
|
||||||
|
};
|
||||||
} entityrenderable_t;
|
} entityrenderable_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the entity renderable component. Defaults to
|
* Initializes the entity renderable component. Defaults to
|
||||||
* ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL with the unlit shader, a white
|
* ENTITY_RENDERABLE_TYPE_MATERIAL, the unlit shader, white color, no mesh.
|
||||||
* cube, and depth-test enabled.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity to initialize the component for.
|
|
||||||
* @param componentId The renderable component of the entity.
|
|
||||||
*/
|
*/
|
||||||
void entityRenderableInit(
|
void entityRenderableInit(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes the entity renderable component.
|
* Disposes the entity renderable component, freeing any callback user data.
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity to dispose the component for.
|
|
||||||
* @param componentId The renderable component of the entity.
|
|
||||||
*/
|
*/
|
||||||
void entityRenderableDispose(
|
void entityRenderableDispose(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the rendering type for the renderable component. Resets
|
* Gets the renderable type.
|
||||||
* type-specific data to zero.
|
*/
|
||||||
*
|
entityrenderabletype_t entityRenderableGetType(
|
||||||
* @param mgr The entity manager that owns the entity.
|
const entityid_t entityId,
|
||||||
* @param entityId The entity to configure.
|
const componentid_t componentId
|
||||||
* @param componentId The renderable component.
|
);
|
||||||
* @param type The rendering type to use.
|
|
||||||
|
/**
|
||||||
|
* Sets the renderable type.
|
||||||
*/
|
*/
|
||||||
void entityRenderableSetType(
|
void entityRenderableSetType(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const entityrenderabletype_t type
|
const entityrenderabletype_t type
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the render priority. 0 = auto (derived from type/flags). Higher
|
* Gets the mesh pointer (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||||
* values render later (on top). Use non-zero to force ordering.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity to configure.
|
|
||||||
* @param componentId The renderable component.
|
|
||||||
* @param priority The priority value, or 0 for auto.
|
|
||||||
*/
|
*/
|
||||||
void entityRenderableSetPriority(
|
mesh_t * entityRenderableGetMesh(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId
|
||||||
const int8_t priority
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the unlit material color. Only meaningful when the renderable is
|
* Sets the mesh pointer (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||||
* (or defaults to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts
|
*/
|
||||||
* otherwise.
|
void entityRenderableSetMesh(
|
||||||
*
|
const entityid_t entityId,
|
||||||
* @param mgr The entity manager that owns the entity.
|
const componentid_t componentId,
|
||||||
* @param entityId The entity to configure.
|
mesh_t *mesh
|
||||||
* @param componentId The renderable component.
|
);
|
||||||
* @param color The color to tint the material with.
|
|
||||||
|
/**
|
||||||
|
* Gets the shader pointer.
|
||||||
|
*/
|
||||||
|
shader_t * entityRenderableGetShader(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the shader pointer.
|
||||||
|
*/
|
||||||
|
void entityRenderableSetShader(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
shader_t *shader
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a pointer to the shader material union
|
||||||
|
* (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||||
|
*/
|
||||||
|
shadermaterial_t * entityRenderableGetShaderMaterial(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the unlit color (ENTITY_RENDERABLE_TYPE_MATERIAL only).
|
||||||
*/
|
*/
|
||||||
void entityRenderableSetColor(
|
void entityRenderableSetColor(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const color_t color
|
const color_t color
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets one of the material renderable's meshes, drawn in full (no offset/
|
* Appends a sprite to the spritebatch renderable
|
||||||
* count override). Only meaningful when the renderable is (or defaults
|
* (ENTITY_RENDERABLE_TYPE_SPRITEBATCH only).
|
||||||
* to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts otherwise.
|
* Does nothing if the sprite buffer is full.
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity to configure.
|
|
||||||
* @param componentId The renderable component.
|
|
||||||
* @param slot Index into the material's meshes array (0 to
|
|
||||||
* ENTITY_RENDERABLE_MESHES_MAX - 1).
|
|
||||||
* @param mesh The mesh to draw in that slot.
|
|
||||||
*/
|
*/
|
||||||
void entityRenderableSetMesh(
|
void entityRenderableSpriteBatchAdd(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId,
|
const componentid_t componentId,
|
||||||
const uint8_t slot,
|
const spritebatchsprite_t *sprite
|
||||||
mesh_t *mesh
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the draw callback, switching the type to
|
* Clears all buffered sprites from the spritebatch renderable
|
||||||
* ENTITY_RENDERABLE_TYPE_CUSTOM.
|
* (ENTITY_RENDERABLE_TYPE_SPRITEBATCH only).
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity to configure.
|
|
||||||
* @param componentId The renderable component of the entity.
|
|
||||||
* @param draw The draw callback to assign.
|
|
||||||
* @param user Userdata passed to the callback.
|
|
||||||
*/
|
*/
|
||||||
void entityRenderableSetDraw(
|
void entityRenderableSpriteBatchClear(
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
errorret_t (*draw)(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
void *user
|
|
||||||
),
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the entity using its renderable component data.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity to draw.
|
|
||||||
* @param componentId The renderable component of the entity.
|
|
||||||
* @return Any error state that happened.
|
|
||||||
*/
|
|
||||||
errorret_t entityRenderableDraw(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Draws a spritebatch-type renderable.
|
|
||||||
*
|
|
||||||
* @param sb The spritebatch data to draw.
|
|
||||||
* @return Error state.
|
|
||||||
*/
|
|
||||||
errorret_t entityRenderableDrawSpritebatch(
|
|
||||||
const entityrenderablespritebatch_t *sb
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Draws a shader-material-type renderable.
|
|
||||||
*
|
|
||||||
* @param m The material data to draw.
|
|
||||||
* @return Error state.
|
|
||||||
*/
|
|
||||||
errorret_t entityRenderableDrawMaterial(const entityrenderablematerial_t *m);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Invokes a custom-type renderable's draw callback.
|
|
||||||
*
|
|
||||||
* @param mgr The entity manager that owns the entity.
|
|
||||||
* @param entityId The entity being drawn.
|
|
||||||
* @param componentId The renderable component of the entity.
|
|
||||||
* @param custom The custom draw data.
|
|
||||||
* @return Error state.
|
|
||||||
*/
|
|
||||||
errorret_t entityRenderableDrawCustom(
|
|
||||||
entitymanager_t *mgr,
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId,
|
|
||||||
const entityrenderablecustom_t *custom
|
|
||||||
);
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user