Compare commits
63 Commits
06bc4fcd55
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a858cc424 | |||
| c0292842a5 | |||
| 717902462b | |||
| 1d73b9d224 | |||
| 51f262efa0 | |||
| a6e4e3f71f | |||
| 4387d223b9 | |||
| 00bfaf6360 | |||
| 709bd7be52 | |||
| 80f4348e21 | |||
| e630827b34 | |||
| 106d9b0fc0 | |||
| 4a26f79945 | |||
| 1a199f6ce3 | |||
| ec59e77867 | |||
| cb28d2b611 | |||
| 27b6ddf5cb | |||
| 475c865e33 | |||
| 0b4ba062bb | |||
| 28f5e66662 | |||
| e225a076f0 | |||
| 774c8ad0f8 | |||
| 52d1e7414d | |||
| af4cb53e5f | |||
| c019271e12 | |||
| 82ae2bce9d | |||
| 092e259a06 | |||
| 08b4bbfe91 | |||
| 560c51cf27 | |||
| 674f86b18a | |||
| e3f10e0926 | |||
| d7223d7387 | |||
| 45331c2a60 | |||
| 33f50a2c69 | |||
| 6c8e4d5cbd | |||
| 3b7215876a | |||
| c74f5890bd | |||
| fbd3c71ba7 | |||
| 128f9ab9d4 | |||
| 36fb359aa2 | |||
| 1bd73d69fe | |||
| fb48285143 | |||
| 3de50b8370 | |||
| e61cbe25b7 | |||
| 8395830be6 | |||
| 6e4ec2b9d8 | |||
| f501bb8e28 | |||
| d07cd3397d | |||
| e008fb108a | |||
| 9aaffff7a8 | |||
| 7357b4a5df | |||
| 1ddc298a74 | |||
| e2a9442aa6 | |||
| aa0180571e | |||
| 7f7be39230 | |||
| 4d95415232 | |||
| 2cbd80a004 | |||
| 9abf8101da | |||
| 24badd06a5 | |||
| 7a03ef8eaf | |||
| f3ea507313 | |||
| 4b0388a0e1 | |||
| a84137b5ff |
@@ -33,74 +33,3 @@ jobs:
|
||||
libssl-dev
|
||||
- name: Run tests
|
||||
run: ./scripts/test-linux.sh
|
||||
|
||||
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
|
||||
# confirm the emulator doesn't crash. Not yet verified against a real
|
||||
# runner (no CI run has exercised these) -- continue-on-error so a
|
||||
# flaky/broken emulator step doesn't block the required Linux test job.
|
||||
run-tests-gamecube-dolphin:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ghcr.io/extremscorner/libogc2:latest
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
run: apt-get update && apt-get install -y nodejs
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install additional dependencies
|
||||
run: |
|
||||
apt-get install -y \
|
||||
python3-pip python3-polib python3-pil \
|
||||
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||
dolphin-emu xvfb
|
||||
dkp-pacman -Syu --noconfirm
|
||||
dkp-pacman -S --needed --noconfirm \
|
||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||
- name: Build GameCube ISO and boot it in Dolphin
|
||||
run: ./scripts/test-gamecube-dolphin.sh
|
||||
|
||||
run-tests-wii-dolphin:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ghcr.io/extremscorner/libogc2:latest
|
||||
steps:
|
||||
- name: Install Node.js
|
||||
run: apt-get update && apt-get install -y nodejs
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install additional dependencies
|
||||
run: |
|
||||
apt-get install -y \
|
||||
python3-pip python3-polib python3-pil \
|
||||
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||
dolphin-emu xvfb
|
||||
dkp-pacman -Syu --noconfirm
|
||||
dkp-pacman -S --needed --noconfirm \
|
||||
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||
- name: Build Wii ISO and boot it in Dolphin
|
||||
run: ./scripts/test-wii-dolphin.sh
|
||||
|
||||
run-tests-psp-ppsspp:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pspdev
|
||||
uses: ./.github/actions/setup-pspdev
|
||||
- name: Install PPSSPPHeadless build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
|
||||
- name: Build PPSSPPHeadless
|
||||
run: |
|
||||
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
|
||||
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
|
||||
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
|
||||
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
|
||||
run: ./scripts/test-psp-ppsspp.sh
|
||||
|
||||
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||
|
||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||
option(DUSK_NETWORK "Enable network support" ON)
|
||||
|
||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
||||
DUSK_VERSION="${DUSK_VERSION}"
|
||||
)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_NETWORK
|
||||
)
|
||||
endif()
|
||||
|
||||
# Toolchains
|
||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,11 +5,107 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||
|
||||
# Initial Scene
|
||||
msgid "initial.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "initial.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "initial.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "initial.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "initial.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "initial.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
|
||||
# Main Menu Scene
|
||||
msgid "main_menu.start_game"
|
||||
msgstr "Start Game"
|
||||
|
||||
msgid "main_menu.options"
|
||||
msgstr "Options"
|
||||
|
||||
msgid "main_menu.quit"
|
||||
msgstr "Quit Game"
|
||||
|
||||
msgid "main_menu.quit_confirm"
|
||||
msgstr "Are you sure you want to quit?"
|
||||
|
||||
msgid "main_menu.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "main_menu.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "main_menu.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "main_menu.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "main_menu.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "main_menu.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
msgid "main_menu.save_load_error.title"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "main_menu.save_load_error.message"
|
||||
msgstr "Failed to load save data. Please try again."
|
||||
|
||||
msgid "main_menu.save_load_error.retry"
|
||||
msgstr "Try Again"
|
||||
|
||||
|
||||
# Select Save Screen
|
||||
msgid "ui.select_save.title"
|
||||
msgstr "Select Save"
|
||||
|
||||
msgid "ui.select_save.empty"
|
||||
msgstr "Empty Slot"
|
||||
|
||||
msgid "ui.select_save.slot_format"
|
||||
msgstr "%s Lv.%d %s"
|
||||
|
||||
msgid "ui.select_save.delete_mode"
|
||||
msgstr "Delete a Save"
|
||||
|
||||
msgid "ui.select_save.delete_confirm"
|
||||
msgstr "Are you sure you want to delete this save?"
|
||||
|
||||
msgid "ui.select_save.name_title"
|
||||
msgstr "Enter game save file name"
|
||||
|
||||
msgid "ui.save_slot.number_format"
|
||||
msgstr "Slot %d"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Welcome"
|
||||
|
||||
msgid "save.linux.mkdirp_failed"
|
||||
msgstr "Failed to create save directory, check the disk is not full or write-protected."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
@@ -56,6 +152,114 @@ msgstr "Items"
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save"
|
||||
msgstr "Save"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_success"
|
||||
msgstr "Game saved."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_cancelled"
|
||||
msgstr "Save cancelled."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_unavailable"
|
||||
msgstr "Can't save - no save device found."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_temporary"
|
||||
msgstr "This session is temporary - no save device was found, so saving is disabled."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_create_confirm"
|
||||
msgstr "No save data found. Create a new save?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_failed_format"
|
||||
msgstr "Save failed: %s"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_check_failed_format"
|
||||
msgstr "Can't save: %s"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.message"
|
||||
msgstr "No save device found. You can continue, but\nprogress will not be saved."
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.retry"
|
||||
msgstr "Retry"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.continue"
|
||||
msgstr "Continue Anyway"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.message"
|
||||
msgstr "No save data found. Create a new save?"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.yes"
|
||||
msgstr "Yes"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.no"
|
||||
msgstr "No"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.confirm"
|
||||
msgstr "Confirm"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.attack"
|
||||
msgstr "Attack"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.flee"
|
||||
msgstr "Flee"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.target_format"
|
||||
msgstr "Enemy %u (%u/%u HP)"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.hp_format"
|
||||
msgstr "HP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.mp_format"
|
||||
msgstr "MP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
||||
msgid "ui.settings.audio.placeholder"
|
||||
msgstr "No audio settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
||||
msgid "ui.settings.display.placeholder"
|
||||
msgstr "No display settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
||||
msgid "ui.settings.input.placeholder"
|
||||
msgstr "No input settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
||||
msgid "ui.backpack.category_format"
|
||||
msgstr "Category %u"
|
||||
|
||||
#: src/dusk/ui/overlay/uiloading.c
|
||||
msgid "ui.loading.text"
|
||||
msgstr "loading"
|
||||
|
||||
#: src/dusk/ui/overlay/uiautosave.c
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
|
||||
@@ -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,26 +0,0 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Player: dynamic capsule body, moved relative to the camera by PLAYER's
|
||||
// own update callback (see entityplayer.c).
|
||||
class Player {
|
||||
constructor() {
|
||||
this.entity = new Entity();
|
||||
|
||||
this.position = this.entity.add(POSITION);
|
||||
this.position.setLocalPosition(0.0, 2.0, 0.0);
|
||||
|
||||
this.physics = this.entity.add(PHYSICS);
|
||||
this.physics.setShape(PHYSICS_SHAPE_CAPSULE, 0.5, 0.5);
|
||||
|
||||
this.renderable = this.entity.add(RENDERABLE);
|
||||
this.renderable.setMesh(0, MESH_CAPSULE);
|
||||
this.renderable.setColor(0, 0, 255, 255);
|
||||
|
||||
this.entity.add(PLAYER);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Player;
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Entry point run once at startup (see game.c). Swap which scene module
|
||||
// gets passed to Scene.set() here to change what the game boots into.
|
||||
var overworldScene = require('./overworldscene.js');
|
||||
Scene.set(overworldScene);
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
// Radius must stay well outside the floor's footprint (a 20x20 plane has a
|
||||
// corner-to-center distance of 10*sqrt(2) =~ 14.1) -- orbiting inside that
|
||||
// puts parts of the floor's own geometry near/behind the camera's view
|
||||
// direction, which the PSP's legacy GU pipeline can't clip properly and
|
||||
// drops the whole triangle instead of clipping it.
|
||||
var CAMERA_ORBIT_RADIUS = 18.0;
|
||||
var CAMERA_ORBIT_HEIGHT = 10.0;
|
||||
var CAMERA_ORBIT_SPEED = 0.5;
|
||||
|
||||
var Player = require('./Player.js');
|
||||
|
||||
var cameraOrbitAngle = 0.0;
|
||||
var cameraPosition = null;
|
||||
|
||||
// Orbits the camera around the world origin at a fixed radius/height/
|
||||
// speed, always looking back at the origin. Called once up front (so the
|
||||
// very first rendered frame is already positioned correctly) and then
|
||||
// once per frame via update() below.
|
||||
function updateCameraOrbit() {
|
||||
cameraOrbitAngle += Time.delta * CAMERA_ORBIT_SPEED;
|
||||
|
||||
var eyeX = Math.cos(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
|
||||
var eyeY = CAMERA_ORBIT_HEIGHT;
|
||||
var eyeZ = Math.sin(cameraOrbitAngle) * CAMERA_ORBIT_RADIUS;
|
||||
|
||||
cameraPosition.lookAt(eyeX, eyeY, eyeZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// Called once by Scene.set(), right after it creates and activates the
|
||||
// scene this module owns.
|
||||
init: function() {
|
||||
// Camera, orbiting the origin (see updateCameraOrbit above).
|
||||
var camera = new Entity();
|
||||
cameraPosition = camera.add(POSITION);
|
||||
camera.add(CAMERA);
|
||||
updateCameraOrbit();
|
||||
|
||||
// Static ground plane. Physics ignores the entity's position
|
||||
// component -- the shape's own normal/distance fully define the
|
||||
// plane in world space.
|
||||
var plane = new Entity();
|
||||
var planePosition = plane.add(POSITION);
|
||||
planePosition.setLocalPosition(-10.0, 0.0, -10.0);
|
||||
planePosition.setLocalScale(20.0, 1.0, 20.0);
|
||||
|
||||
var planePhysics = plane.add(PHYSICS);
|
||||
planePhysics.setBodyType(PHYSICS_BODY_STATIC);
|
||||
planePhysics.setShape(PHYSICS_SHAPE_PLANE, 0.0, 1.0, 0.0, 0.0);
|
||||
|
||||
var planeRenderable = plane.add(RENDERABLE);
|
||||
planeRenderable.setMesh(0, MESH_PLANE);
|
||||
planeRenderable.setColor(128, 128, 128, 255);
|
||||
|
||||
new Player();
|
||||
},
|
||||
|
||||
// Called once per engine frame while this module is the active scene
|
||||
// (see Scene.set(), engineUpdate() -> moduleSceneUpdateCurrent()).
|
||||
update: function() {
|
||||
if(cameraPosition) updateCameraOrbit();
|
||||
},
|
||||
|
||||
// Called once by Scene.set() when this module is replaced by another,
|
||||
// right before the scene it owns is destroyed.
|
||||
dispose: function() {
|
||||
cameraPosition = null;
|
||||
}
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -2179,5 +2179,27 @@
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
"type": "global",
|
||||
"globalId": 3,
|
||||
"pos": [8, 8, 1]
|
||||
},
|
||||
{
|
||||
"type": "item",
|
||||
"itemId": 1,
|
||||
"quantity": 1,
|
||||
"pos": [12, 2, 0]
|
||||
}
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"min": [11, 3, 0],
|
||||
"max": [16, 9, 10],
|
||||
"callbackId": 1,
|
||||
"notify": 3,
|
||||
"trigger": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"items": [
|
||||
// Boot check: make sure a save device is available before handing off
|
||||
// to the main menu.
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "initial.checking_save.title",
|
||||
"message": "initial.checking_save.message"
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 0.2
|
||||
},
|
||||
{
|
||||
"type": "SAVE_DEVICE_CHECK",
|
||||
"successMarker": "CONTINUE",
|
||||
"failureMarker": "NO_DEVICE"
|
||||
},
|
||||
|
||||
// No save device found - offer to retry or continue without saving.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "NO_DEVICE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_OPTIONS_MARKERS",
|
||||
"title": "initial.no_device.title",
|
||||
"message": "initial.no_device.message",
|
||||
"options": [
|
||||
{
|
||||
"text": "initial.no_device.retry",
|
||||
"marker": "RETRY"
|
||||
},
|
||||
{
|
||||
"text": "initial.no_device.continue",
|
||||
"marker": "CONTINUE"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "RETRY"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "RESTART"
|
||||
},
|
||||
|
||||
// Save device found (or continuing without one) - hand off to the
|
||||
// main menu scene.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "CONTINUE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "SCENE",
|
||||
"sceneType": "MAIN_MENU"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "main_menu.checking_save.title",
|
||||
"message": "main_menu.checking_save.message"
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 0.2
|
||||
},
|
||||
{
|
||||
"type": "SAVE_DEVICE_CHECK",
|
||||
"successMarker": "CONTINUE",
|
||||
"failureMarker": "NO_DEVICE"
|
||||
},
|
||||
|
||||
// No save device found - offer to retry or continue without saving.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "NO_DEVICE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_OPTIONS_MARKERS",
|
||||
"title": "main_menu.no_device.title",
|
||||
"message": "main_menu.no_device.message",
|
||||
"options": [
|
||||
{
|
||||
"text": "main_menu.no_device.retry",
|
||||
"marker": "RETRY"
|
||||
},
|
||||
{
|
||||
"text": "main_menu.no_device.continue",
|
||||
"marker": "CONTINUE"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Retry
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "RETRY"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "RESTART"
|
||||
},
|
||||
|
||||
// Save device found - attempt to load all save slots.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "CONTINUE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "SAVE_LOAD_ALL_SLOTS",
|
||||
"successMarker": "LOADED",
|
||||
"failureMarker": "LOAD_ERROR"
|
||||
},
|
||||
|
||||
// Save data failed to load (e.g. corrupt/unreadable) - only option is
|
||||
// to retry, no "continue without saving" here since we already know a
|
||||
// device is present.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "LOAD_ERROR"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_OPTIONS_MARKERS",
|
||||
"title": "main_menu.save_load_error.title",
|
||||
"message": "main_menu.save_load_error.message",
|
||||
"options": [
|
||||
{
|
||||
"text": "main_menu.save_load_error.retry",
|
||||
"marker": "RETRY"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "LOADED"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Turn things off we don't need
|
||||
set(JERRY_CMDLINE OFF CACHE BOOL "" FORCE)
|
||||
set(JERRY_EXT ON CACHE BOOL "" FORCE)
|
||||
set(JERRY_DEBUGGER OFF CACHE BOOL "" FORCE)
|
||||
set(JERRY_BUILTIN_DATE OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_LTO OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Fetch Jerry
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
jerryscript
|
||||
GIT_REPOSITORY https://git.wish.moe/YourWishes/jerryscript
|
||||
GIT_TAG float32-fix
|
||||
)
|
||||
FetchContent_MakeAvailable(jerryscript)
|
||||
|
||||
# Mark found
|
||||
set(jerryscript_FOUND ON)
|
||||
|
||||
# Define targets
|
||||
if(TARGET jerryscript-core)
|
||||
set(JERRY_CORE_TARGET jerryscript-core)
|
||||
elseif(TARGET jerry-core)
|
||||
set(JERRY_CORE_TARGET jerry-core)
|
||||
endif()
|
||||
|
||||
if(TARGET jerryscript-ext)
|
||||
set(JERRY_EXT_TARGET jerryscript-ext)
|
||||
elseif(TARGET jerry-ext)
|
||||
set(JERRY_EXT_TARGET jerry-ext)
|
||||
endif()
|
||||
|
||||
if(TARGET jerryscript-port-default)
|
||||
set(JERRY_PORT_TARGET jerryscript-port-default)
|
||||
elseif(TARGET jerry-port-default)
|
||||
set(JERRY_PORT_TARGET jerry-port-default)
|
||||
elseif(TARGET jerryscript-port)
|
||||
set(JERRY_PORT_TARGET jerryscript-port)
|
||||
elseif(TARGET jerry-port)
|
||||
set(JERRY_PORT_TARGET jerry-port)
|
||||
endif()
|
||||
|
||||
if(NOT JERRY_CORE_TARGET)
|
||||
message(FATAL_ERROR "JerryScript core target not found")
|
||||
endif()
|
||||
|
||||
if(NOT JERRY_EXT_TARGET)
|
||||
message(FATAL_ERROR "JerryScript ext target not found")
|
||||
endif()
|
||||
|
||||
if(NOT JERRY_PORT_TARGET)
|
||||
message(FATAL_ERROR "JerryScript port target not found")
|
||||
endif()
|
||||
|
||||
foreach(tgt IN ITEMS
|
||||
${JERRY_CORE_TARGET}
|
||||
${JERRY_EXT_TARGET}
|
||||
${JERRY_PORT_TARGET}
|
||||
)
|
||||
if(TARGET ${tgt})
|
||||
set_property(TARGET ${tgt} PROPERTY INTERPROCEDURAL_OPTIMIZATION OFF)
|
||||
target_compile_definitions(${JERRY_CORE_TARGET} PRIVATE
|
||||
JERRY_NUMBER_TYPE_FLOAT64=0
|
||||
JERRY_BUILTIN_DATE=0
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Export include dirs through the targets
|
||||
target_include_directories(${JERRY_CORE_TARGET} INTERFACE
|
||||
${jerryscript_SOURCE_DIR}/jerry-core/include
|
||||
)
|
||||
|
||||
target_include_directories(${JERRY_EXT_TARGET} INTERFACE
|
||||
${jerryscript_SOURCE_DIR}/jerry-ext/include
|
||||
)
|
||||
|
||||
target_include_directories(${JERRY_PORT_TARGET} INTERFACE
|
||||
${jerryscript_SOURCE_DIR}/jerry-port/default/include
|
||||
)
|
||||
|
||||
# Suppress JerryScript-only warning
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(${JERRY_CORE_TARGET} PRIVATE
|
||||
-Wno-error
|
||||
)
|
||||
endif()
|
||||
|
||||
add_library(jerryscript::core ALIAS ${JERRY_CORE_TARGET})
|
||||
add_library(jerryscript::ext ALIAS ${JERRY_EXT_TARGET})
|
||||
add_library(jerryscript::port ALIAS ${JERRY_PORT_TARGET})
|
||||
@@ -6,7 +6,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PRIVATE
|
||||
bba
|
||||
# bba
|
||||
)
|
||||
|
||||
# ISO post-build: produce NTSC-J, NTSC-U and PAL disc images
|
||||
|
||||
+53
-4
@@ -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_RANLIB "$ENV{PSPDEV}/bin/psp-ranlib" CACHE FILEPATH "" FORCE)
|
||||
set(CMAKE_C_COMPILER_AR "$ENV{PSPDEV}/bin/psp-ar" CACHE FILEPATH "" FORCE)
|
||||
@@ -59,9 +55,33 @@ target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_DISPLAY_WIDTH=480
|
||||
DUSK_DISPLAY_HEIGHT=272
|
||||
DUSK_THREAD_PTHREAD
|
||||
DUSK_TIME_DYNAMIC
|
||||
DUSK_DISPLAY_OVERSCAN=6
|
||||
)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
DUSK_ASSERTIONS_FAKED
|
||||
)
|
||||
endif()
|
||||
|
||||
# Generate PARAM.SFO as a normal tracked build output (instead of letting
|
||||
# create_pbp_file() auto-generate + delete it) so it can be reused below by
|
||||
# a properly dependency-tracked EBOOT.PBP repack step.
|
||||
set(DUSK_PSP_PARAM_SFO "${DUSK_BUILD_DIR}/PARAM.SFO")
|
||||
add_custom_command(
|
||||
OUTPUT "${DUSK_PSP_PARAM_SFO}"
|
||||
COMMAND "$ENV{PSPDEV}/bin/mksfoex" "-d" "MEMSIZE=1" "-s" "APP_VER=01.00"
|
||||
"${DUSK_BINARY_TARGET_NAME}" "${DUSK_PSP_PARAM_SFO}"
|
||||
COMMENT "Generating PARAM.SFO for ${DUSK_BINARY_TARGET_NAME}"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(DuskPspParamSfo DEPENDS "${DUSK_PSP_PARAM_SFO}")
|
||||
|
||||
# create_pbp_file()'s own POST_BUILD chain (below) also consumes
|
||||
# DUSK_PSP_PARAM_SFO, so make sure it exists before that chain runs.
|
||||
add_dependencies(${DUSK_BINARY_TARGET_NAME} DuskPspParamSfo)
|
||||
|
||||
# Postbuild, create .pbp file for PSP.
|
||||
create_pbp_file(
|
||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||
@@ -71,4 +91,33 @@ create_pbp_file(
|
||||
TITLE "${DUSK_BINARY_TARGET_NAME}"
|
||||
PSAR_PATH ${DUSK_ASSETS_ZIP}
|
||||
VERSION 01.00
|
||||
SFO_PATH "${DUSK_PSP_PARAM_SFO}"
|
||||
OUTPUT_DIR "${DUSK_BUILD_DIR}"
|
||||
)
|
||||
|
||||
# CreatePBP.cmake's pack-pbp step is a POST_BUILD command tied to the
|
||||
# executable target, so it only reruns when the ELF itself relinks. That
|
||||
# means regenerating dusk.dsk (assets) alone, without touching any C
|
||||
# source, silently leaves EBOOT.PBP embedding a stale asset pak. Repack it
|
||||
# here as a normal file-tracked custom command depending on both the
|
||||
# executable and the asset zip, so EBOOT.PBP always reflects the current
|
||||
# assets even when nothing else about the build changed.
|
||||
set(DUSK_PSP_EBOOT "${DUSK_BUILD_DIR}/EBOOT.PBP")
|
||||
if(BUILD_PRX)
|
||||
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>.prx")
|
||||
else()
|
||||
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>")
|
||||
endif()
|
||||
add_custom_command(
|
||||
OUTPUT "${DUSK_PSP_EBOOT}"
|
||||
COMMAND "$ENV{PSPDEV}/bin/pack-pbp" "${DUSK_PSP_EBOOT}" "${DUSK_PSP_PARAM_SFO}"
|
||||
"NULL" "NULL" "NULL" "NULL" "NULL"
|
||||
"${DUSK_PSP_EXECUTABLE}" "${DUSK_ASSETS_ZIP}"
|
||||
DEPENDS
|
||||
"$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>"
|
||||
"${DUSK_PSP_PARAM_SFO}"
|
||||
"${DUSK_ASSETS_ZIP}"
|
||||
COMMENT "Repacking EBOOT.PBP (tracks executable + asset pak freshness)"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(DuskPspEbootRepack ALL DEPENDS "${DUSK_PSP_EBOOT}")
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
@@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_WII
|
||||
)
|
||||
|
||||
# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h.
|
||||
set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING
|
||||
"Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\
|
||||
compatible memory card emulation), or SD (SD card via libfat)"
|
||||
)
|
||||
set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD")
|
||||
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD}
|
||||
)
|
||||
|
||||
# Generate Homebrew Channel meta.xml from project identity variables
|
||||
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||
configure_file(
|
||||
|
||||
@@ -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"]
|
||||
@@ -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)."
|
||||
+9
-1
@@ -4,7 +4,10 @@
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
add_subdirectory(dusk)
|
||||
add_subdirectory(duskrpg)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
add_subdirectory(dusknetwork)
|
||||
endif()
|
||||
|
||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||
add_subdirectory(dusklinux)
|
||||
@@ -16,6 +19,11 @@ elseif(DUSK_TARGET_SYSTEM STREQUAL "psp")
|
||||
add_subdirectory(dusksdl2)
|
||||
add_subdirectory(duskgl)
|
||||
|
||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "vita")
|
||||
add_subdirectory(duskvita)
|
||||
add_subdirectory(dusksdl2)
|
||||
add_subdirectory(duskgl)
|
||||
|
||||
elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" OR DUSK_TARGET_SYSTEM STREQUAL "gamecube")
|
||||
add_subdirectory(duskdolphin)
|
||||
|
||||
|
||||
+1
-15
@@ -32,15 +32,6 @@ if(NOT yyjson_FOUND)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT jerryscript_FOUND)
|
||||
find_package(jerryscript REQUIRED)
|
||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
jerryscript::core
|
||||
jerryscript::ext
|
||||
jerryscript::port
|
||||
)
|
||||
endif()
|
||||
|
||||
if(DUSK_BACKTRACE)
|
||||
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
|
||||
target_compile_definitions(${DUSK_BINARY_TARGET_NAME} PUBLIC
|
||||
@@ -62,25 +53,20 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(event)
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(asset)
|
||||
add_subdirectory(console)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(entity)
|
||||
add_subdirectory(log)
|
||||
add_subdirectory(engine)
|
||||
add_subdirectory(error)
|
||||
add_subdirectory(game)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(rpg)
|
||||
add_subdirectory(scene)
|
||||
add_subdirectory(system)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(network)
|
||||
add_subdirectory(physics)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(script)
|
||||
add_subdirectory(util)
|
||||
add_subdirectory(thread)
|
||||
@@ -6,7 +6,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
easing.c
|
||||
keyframe.c
|
||||
keyframeset.c
|
||||
animation.c
|
||||
keyframe.c
|
||||
)
|
||||
|
||||
+108
-50
@@ -5,70 +5,128 @@
|
||||
|
||||
#include "animation.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include "time/time.h"
|
||||
|
||||
void animationInit(
|
||||
animation_t *anim,
|
||||
keyframe_t **tracks,
|
||||
uint16_t *trackCounts,
|
||||
uint16_t trackCount
|
||||
keyframe_t *keyframes,
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
|
||||
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
|
||||
|
||||
keyframeSetInit(
|
||||
&anim->keyframes, tracks, trackCounts, NULL, NULL, trackCount, NULL
|
||||
);
|
||||
anim->time = 0.0f;
|
||||
anim->speed = 1.0f;
|
||||
anim->loop = false;
|
||||
anim->playing = false;
|
||||
anim->eventTime = 0.0f;
|
||||
anim->eventCallback = NULL;
|
||||
anim->eventUser = NULL;
|
||||
memoryZero(anim, sizeof(animation_t));
|
||||
anim->keyframes = keyframes;
|
||||
anim->keyframeCounts = keyframeCounts;
|
||||
anim->layerCount = layerCount;
|
||||
|
||||
// Determine duration
|
||||
float_t duration = 0.0f;
|
||||
for(uint16_t layer = 0; layer < layerCount; layer++) {
|
||||
uint16_t keyframeCount = keyframeCounts[layer];
|
||||
assertTrue(keyframeCount > 0, "Keyframe count invalid.");
|
||||
keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
|
||||
|
||||
#ifdef DUSK_ASSERTIONS
|
||||
// Check that the keyframes are sorted by time.
|
||||
for(uint16_t i = 1; i < keyframeCount; i++) {
|
||||
assertTrue(
|
||||
layerKeyframes[i].time >= layerKeyframes[i - 1].time,
|
||||
"Keyframes must be sorted by time."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
|
||||
duration = mathMax(duration, lastKeyframe->time);
|
||||
}
|
||||
assertTrue(duration > 0, "Animation duration must be greater than 0.");
|
||||
anim->duration = duration;
|
||||
}
|
||||
|
||||
void animationUpdate(animation_t *anim) {
|
||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
if(!anim->playing) return;
|
||||
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
|
||||
|
||||
float_t duration = keyframeSetGetDuration(&anim->keyframes);
|
||||
float_t prevTime = anim->time;
|
||||
anim->time += TIME.delta * anim->speed;
|
||||
uint16_t keyframeCount = anim->keyframeCounts[layer];
|
||||
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
|
||||
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
|
||||
}
|
||||
|
||||
// Loops that overshoot duration wrap back into [0, duration) -- an event
|
||||
// near the end must still be checked across that wrap, as two segments:
|
||||
// (prevTime, duration] then [0, newTime].
|
||||
bool_t wrapped = anim->loop && duration > 0.0f && anim->time >= duration;
|
||||
void animationUpdate(
|
||||
animation_t *anim,
|
||||
const float_t deltaTime
|
||||
) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
|
||||
|
||||
if(anim->loop) {
|
||||
if(duration > 0.0f) anim->time = mathModFloat(anim->time, duration);
|
||||
} else if(anim->time >= duration) {
|
||||
anim->time = duration;
|
||||
anim->playing = false;
|
||||
bool_t justCompleted = false;
|
||||
|
||||
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
|
||||
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
|
||||
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
|
||||
assertFalse(
|
||||
loop && pingpong,
|
||||
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
|
||||
);
|
||||
|
||||
bool_t backward;
|
||||
if(pingpong) {
|
||||
backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
|
||||
} else {
|
||||
backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
|
||||
}
|
||||
|
||||
// Resolve boundary crossings one at a time, so a single large deltaTime
|
||||
// can correctly loop/pingpong across multiple boundaries in one call.
|
||||
float_t remaining = deltaTime;
|
||||
while(remaining > 0.0f) {
|
||||
float_t toBoundary = (
|
||||
backward ? anim->time : (anim->duration - anim->time)
|
||||
);
|
||||
if(remaining < toBoundary) {
|
||||
anim->time += backward ? -remaining : remaining;
|
||||
break;
|
||||
}
|
||||
|
||||
remaining -= toBoundary;
|
||||
anim->time = backward ? 0.0f : anim->duration;
|
||||
|
||||
bool_t stopHere;
|
||||
if(backward) {
|
||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
|
||||
} else {
|
||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
|
||||
}
|
||||
|
||||
if(stopHere) {
|
||||
justCompleted = true;
|
||||
break;
|
||||
} else if(pingpong) {
|
||||
backward = !backward;
|
||||
if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
} else if(loop) {
|
||||
anim->time = backward ? anim->duration : 0.0f;
|
||||
if(anim->onLoop) anim->onLoop(anim->user);
|
||||
} else {
|
||||
justCompleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
|
||||
}
|
||||
|
||||
if(!anim->eventCallback) return;
|
||||
// Call onUpdate for each layer.
|
||||
for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
|
||||
float_t value = animationGetLayerValue(anim, layer);
|
||||
if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
|
||||
}
|
||||
|
||||
bool_t crossed = wrapped
|
||||
? (prevTime < anim->eventTime || anim->time >= anim->eventTime)
|
||||
: (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);
|
||||
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
|
||||
}
|
||||
@@ -4,99 +4,91 @@
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
#pragma once
|
||||
#include "keyframeset.h"
|
||||
#include "keyframe.h"
|
||||
|
||||
typedef struct animation_t animation_t;
|
||||
#define ANIMATION_FLAG_LOOP (1 << 0)
|
||||
#define ANIMATION_FLAG_REVERSE (1 << 1)
|
||||
#define ANIMATION_FLAG_PINGPONG (1 << 2)
|
||||
#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
|
||||
#define ANIMATION_FLAG_STOP_END (1 << 4)
|
||||
|
||||
/**
|
||||
* 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);
|
||||
// Internal - tracks which direction a pingponging animation is currently
|
||||
// travelling. Do not set this manually, it is managed by animationUpdate().
|
||||
#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
|
||||
|
||||
typedef struct animation_t {
|
||||
/** The animation's tracks/channels and their raw keyframe data. */
|
||||
keyframeset_t keyframes;
|
||||
// Internal - set once the animation has stopped advancing (see
|
||||
// animationUpdate()). Do not set this manually. There is currently no way to
|
||||
// restart a completed animation short of clearing this bit and resetting
|
||||
// anim->time by hand.
|
||||
#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
|
||||
|
||||
/** Current playback position, in seconds. */
|
||||
typedef struct {
|
||||
keyframe_t *keyframes;
|
||||
uint16_t *keyframeCounts;
|
||||
uint16_t layerCount;
|
||||
float_t time;
|
||||
float_t duration;
|
||||
uint8_t flags;
|
||||
|
||||
/** 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;
|
||||
void *user;
|
||||
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
|
||||
void (*onComplete)(void *user);
|
||||
void (*onLoop)(void *user);
|
||||
} animation_t;
|
||||
|
||||
/**
|
||||
* Initializes an animation: time 0, speed 1.0, not looping, not playing.
|
||||
* See keyframeSetInit() -- tracks/trackCounts and the keyframe_t arrays
|
||||
* they point to are not copied, and must outlive this animation.
|
||||
* Initializes an animation with the given keyframes and layer count.
|
||||
*
|
||||
* @param anim The animation to initialize.
|
||||
* @param tracks Array of trackCount keyframe_t arrays.
|
||||
* @param trackCounts Array of trackCount keyframe counts, matching tracks.
|
||||
* @param trackCount The number of tracks/channels in this animation.
|
||||
* @param anim Pointer to the animation to initialize.
|
||||
* @param keyframes Pointer to the array of keyframes for each layer.
|
||||
* @param keyframeCount Number of keyframes in each layer.
|
||||
* @param layerCount Number of layers in the animation.
|
||||
*/
|
||||
void animationInit(
|
||||
animation_t *anim,
|
||||
keyframe_t **tracks,
|
||||
uint16_t *trackCounts,
|
||||
uint16_t trackCount
|
||||
keyframe_t *keyframes,
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances an animation's time by TIME.delta * speed. No-op if not
|
||||
* 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.
|
||||
* Sets the current time of the animation, clamping it to the valid range.
|
||||
* This will call the onUpdate callback but none of the other callbacks.
|
||||
*
|
||||
* 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.
|
||||
* @param anim Pointer to the animation to set the time for.
|
||||
* @param time The new time to set for the animation.
|
||||
*/
|
||||
void animationUpdate(animation_t *anim);
|
||||
void animationSetTime(animation_t *anim, const float_t time);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Gets the current value of a specific layer in the animation based on the
|
||||
* current animation time.
|
||||
*/
|
||||
void animationSetEvent(
|
||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
|
||||
|
||||
/**
|
||||
* Updates the animation state based on the elapsed time. Advances anim->time
|
||||
* by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
|
||||
* resolves whatever happens when it reaches the 0 or duration boundary:
|
||||
*
|
||||
* - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
|
||||
* in the opposite direction, forever, unless stopped (see below).
|
||||
* - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
|
||||
* playing in the same direction, forever, unless stopped (see below).
|
||||
* - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
|
||||
* animation reaches that specific boundary, it clamps there and stops
|
||||
* (firing onComplete) instead of looping/pingponging past it.
|
||||
* - If none of the above apply at a boundary, the animation clamps there and
|
||||
* stops, firing onComplete.
|
||||
*
|
||||
* onUpdate is called for every layer on every call. onLoop is called each
|
||||
* time a loop wraps around. onComplete is called at most once, the moment
|
||||
* the animation stops advancing.
|
||||
*
|
||||
* @param anim Pointer to the animation to update.
|
||||
* @param deltaTime Time elapsed since the last update (in seconds).
|
||||
*/
|
||||
void animationUpdate(
|
||||
animation_t *anim,
|
||||
const float_t time,
|
||||
const animationeventcallback_t callback,
|
||||
void *user
|
||||
const float_t deltaTime
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 trackIndex The track to evaluate, in [0, trackCount).
|
||||
* @return The interpolated value of that track at the current time.
|
||||
*/
|
||||
float_t animationGetValue(animation_t *anim, const uint16_t trackIndex);
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
/**
|
||||
* 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 keyframe_t *keyframes,
|
||||
const uint32_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.");
|
||||
#ifdef DUSK_ASSERTIONS
|
||||
// Checks that the keyframes are sorted by time.
|
||||
for(uint32_t i = 1; i < keyframeCount; i++) {
|
||||
assertTrue(
|
||||
keyframes[i].time >= keyframes[i - 1].time,
|
||||
"Keyframes must be sorted by time."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
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;
|
||||
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
||||
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;
|
||||
}
|
||||
// Since time < last->time (checked above), current is guaranteed to stop
|
||||
// at or before reaching last, so no separate end-of-array check is needed.
|
||||
keyframe_t *current = (keyframe_t *)keyframes;
|
||||
keyframe_t *start = current;
|
||||
while(current->time <= time) {
|
||||
start = current;
|
||||
current++;
|
||||
} while(true);
|
||||
}
|
||||
keyframe_t *end = current;
|
||||
|
||||
float_t t = (time - start->time) / (end->time - start->time);
|
||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||
|
||||
@@ -13,15 +13,15 @@ typedef struct {
|
||||
} keyframe_t;
|
||||
|
||||
/**
|
||||
* Gets the eased, interpolated value of a keyframe array at a given time.
|
||||
* Gets the value of a keyframe at a given time.
|
||||
*
|
||||
* @param keyframes The keyframes to evaluate, ascending by time.
|
||||
* @param keyframeCount The number of keyframes.
|
||||
* @param keyframes The keyframes to get the value from.
|
||||
* @param keyframeCount The number of keyframes in the array.
|
||||
* @param time The time at which to get the value, in seconds.
|
||||
* @return The interpolated value at the given time.
|
||||
* @return The value of the keyframe at the given time.
|
||||
*/
|
||||
float_t keyframeGetValue(
|
||||
keyframe_t *keyframes,
|
||||
const uint16_t keyframeCount,
|
||||
const keyframe_t *keyframes,
|
||||
const uint32_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
|
||||
#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
|
||||
#include <cmocka.h>
|
||||
|
||||
@@ -29,6 +22,8 @@
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_ASSERTIONS_FAKED
|
||||
#define DUSK_ASSERTIONS 1
|
||||
|
||||
/**
|
||||
* Initializes the assert system. Must be the very first call in engine
|
||||
* startup.
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
asset.c
|
||||
assetbatch.c
|
||||
assetfile.c
|
||||
)
|
||||
|
||||
|
||||
@@ -322,7 +322,9 @@ errorret_t assetUpdate(void) {
|
||||
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
assetentry_t *loadedEntry = loading->entry;
|
||||
loading->entry = NULL;
|
||||
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
||||
if(loadedEntry->onLoaded) {
|
||||
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
|
||||
}
|
||||
}
|
||||
|
||||
loading++;
|
||||
@@ -346,8 +348,8 @@ errorret_t assetUpdate(void) {
|
||||
assetentry_t *errEntry = loading->entry;
|
||||
loading->entry = NULL;
|
||||
threadMutexUnlock(&loading->mutex);
|
||||
eventInvoke(&errEntry->onError, errEntry);
|
||||
errorThrow("Failed to load asset asynchronously.");
|
||||
if(errEntry->onError) errEntry->onError(errEntry, errEntry->onErrorUser);
|
||||
loading++;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -87,13 +87,30 @@ errorret_t assetFileRead(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// I assume zip_fread takes buffer NULL for skipping?
|
||||
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
|
||||
if(bytesRead < 0) {
|
||||
errorThrow("Failed to read from asset file: %s", file->filename);
|
||||
// Some zip_fread() implementations (seen on PSP) reject a single call
|
||||
// asking for the entire (potentially large) file at once with EINVAL;
|
||||
// the line reader above only ever asks for up to 1024 bytes per call and
|
||||
// works fine, so read in bounded chunks here too.
|
||||
size_t totalRead = 0;
|
||||
uint8_t *dest = (uint8_t *)buffer;
|
||||
while(totalRead < bufferSize) {
|
||||
size_t chunkSize = mathMin(
|
||||
bufferSize - totalRead, ASSET_FILE_READ_CHUNK_MAX
|
||||
);
|
||||
zip_int64_t bytesRead = zip_fread(
|
||||
file->zipFile, dest + totalRead, chunkSize
|
||||
);
|
||||
if(bytesRead < 0) {
|
||||
errorThrow(
|
||||
"Failed to read from asset file: %s (%s)",
|
||||
file->filename, zip_file_strerror(file->zipFile)
|
||||
);
|
||||
}
|
||||
if(bytesRead == 0) break;
|
||||
totalRead += (size_t)bytesRead;
|
||||
}
|
||||
file->position += bytesRead;
|
||||
file->lastRead = bytesRead;
|
||||
file->position += totalRead;
|
||||
file->lastRead = totalRead;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
#define ASSET_FILE_NAME_MAX 48
|
||||
|
||||
// Max bytes requested per zip_fread() call in assetFileRead(). Some
|
||||
// zip_fread() implementations (seen on PSP) reject a single call asking
|
||||
// for very large amounts of data at once; the locale line reader has
|
||||
// always used 1024-byte reads successfully, so that's the proven-safe cap.
|
||||
#define ASSET_FILE_READ_CHUNK_MAX 1024
|
||||
|
||||
typedef struct assetfile_s assetfile_t;
|
||||
|
||||
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
|
||||
|
||||
@@ -15,5 +15,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(cutscene)
|
||||
@@ -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;
|
||||
}
|
||||
refInit(&entry->refs, entry, NULL, NULL, NULL);
|
||||
|
||||
eventInit(
|
||||
&entry->onLoaded,
|
||||
entry->onLoadedCallbacks, entry->onLoadedUsers,
|
||||
ASSET_ENTRY_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&entry->onUnloaded,
|
||||
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
|
||||
ASSET_ENTRY_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&entry->onError,
|
||||
entry->onErrorCallbacks, entry->onErrorUsers,
|
||||
ASSET_ENTRY_EVENT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
void assetEntryLock(assetentry_t *entry) {
|
||||
@@ -97,7 +81,7 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
|
||||
"Asset entry still refed at dispose time."
|
||||
);
|
||||
|
||||
eventInvoke(&entry->onUnloaded, entry);
|
||||
if(entry->onUnloaded) entry->onUnloaded(entry, entry->onUnloadedUser);
|
||||
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
|
||||
memoryZero(entry, sizeof(assetentry_t));
|
||||
errorOk();
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "event/event.h"
|
||||
#include "util/ref.h"
|
||||
|
||||
typedef enum {
|
||||
@@ -20,11 +19,17 @@ typedef enum {
|
||||
ASSET_ENTRY_STATE_ERROR
|
||||
} assetentrystate_t;
|
||||
|
||||
/** Maximum number of subscribers for each per-entry event. */
|
||||
#define ASSET_ENTRY_EVENT_MAX 2
|
||||
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
/**
|
||||
* A single asset entry callback. Each entry supports at most one subscriber
|
||||
* per event - a second assignment without clearing the first is a bug.
|
||||
*
|
||||
* @param entry The assetentry_t the event fired on.
|
||||
* @param user The user pointer passed alongside the callback.
|
||||
*/
|
||||
typedef void (*assetentrycallback_t)(assetentry_t *entry, void *user);
|
||||
|
||||
struct assetentry_s {
|
||||
char_t name[ASSET_FILE_NAME_MAX];
|
||||
assetloadertype_t type;
|
||||
@@ -33,30 +38,27 @@ struct assetentry_s {
|
||||
ref_t refs;
|
||||
assetloaderinput_t *input;
|
||||
assetloaderinput_t inputData;
|
||||
/**
|
||||
* Fired once when loading completes successfully (params = assetentry_t *).
|
||||
* Always invoked on the main thread.
|
||||
*/
|
||||
event_t onLoaded;
|
||||
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
||||
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
|
||||
|
||||
/**
|
||||
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
|
||||
* The asset data is still accessible when the callback runs.
|
||||
* Fired once when loading completes successfully.
|
||||
* Always invoked on the main thread.
|
||||
*/
|
||||
event_t onUnloaded;
|
||||
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
||||
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
|
||||
assetentrycallback_t onLoaded;
|
||||
void *onLoadedUser;
|
||||
|
||||
/**
|
||||
* Fired once when loading fails (params = assetentry_t *).
|
||||
* Fired once when the entry is disposed/reaped. The asset data is still
|
||||
* accessible when the callback runs. Always invoked on the main thread.
|
||||
*/
|
||||
assetentrycallback_t onUnloaded;
|
||||
void *onUnloadedUser;
|
||||
|
||||
/**
|
||||
* Fired once when loading fails.
|
||||
* Always invoked on the main thread.
|
||||
*/
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
|
||||
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
|
||||
assetentrycallback_t onError;
|
||||
void *onErrorUser;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,9 +46,15 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.dispose = assetJsonDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_ANIMATION] = {
|
||||
.loadSync = assetAnimationLoaderSync,
|
||||
.loadAsync = assetAnimationLoaderAsync,
|
||||
.dispose = assetAnimationDispose
|
||||
[ASSET_LOADER_TYPE_CHUNK] = {
|
||||
.loadSync = assetChunkLoaderSync,
|
||||
.loadAsync = assetChunkLoaderAsync,
|
||||
.dispose = assetChunkDispose
|
||||
},
|
||||
|
||||
[ASSET_LOADER_TYPE_CUTSCENE] = {
|
||||
.loadSync = assetCutsceneLoaderSync,
|
||||
.loadAsync = assetCutsceneLoaderAsync,
|
||||
.dispose = assetCutsceneDispose
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
#include "asset/loader/display/assettilesetloader.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "asset/loader/json/assetjsonloader.h"
|
||||
#include "asset/loader/animation/assetanimationloader.h"
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
@@ -23,7 +24,8 @@ typedef enum {
|
||||
ASSET_LOADER_TYPE_TILESET,
|
||||
ASSET_LOADER_TYPE_LOCALE,
|
||||
ASSET_LOADER_TYPE_JSON,
|
||||
ASSET_LOADER_TYPE_ANIMATION,
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_CUTSCENE,
|
||||
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
@@ -35,7 +37,8 @@ typedef union {
|
||||
assettilesetloaderloading_t tileset;
|
||||
assetlocaleloaderloading_t locale;
|
||||
assetjsonloaderloading_t json;
|
||||
assetanimationloaderloading_t animation;
|
||||
assetchunkloaderloading_t chunk;
|
||||
assetcutsceneloaderloading_t cutscene;
|
||||
} assetloaderloading_t;
|
||||
|
||||
typedef union {
|
||||
@@ -45,7 +48,8 @@ typedef union {
|
||||
assettilesetoutput_t tileset;
|
||||
assetlocaleoutput_t locale;
|
||||
assetjsonoutput_t json;
|
||||
assetanimationoutput_t animation;
|
||||
assetchunkoutput_t chunk;
|
||||
assetcutsceneoutput_t cutscene;
|
||||
} assetloaderoutput_t;
|
||||
|
||||
typedef union {
|
||||
@@ -53,6 +57,8 @@ typedef union {
|
||||
assettilesetloaderinput_t tileset;
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
assetchunkloaderinput_t chunk;
|
||||
assetcutsceneloaderinput_t cutscene;
|
||||
} assetloaderinput_t;
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
assetchunkloader.c
|
||||
)
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetchunkloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/endian.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
// Reads a little-endian int16 from a potentially-unaligned offset into a
|
||||
// worldunit_t, advancing *offset past it.
|
||||
static worldunit_t assetChunkReadWorldUnit(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
int16_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(int16_t));
|
||||
*offset += sizeof(int16_t);
|
||||
return (worldunit_t)endianLittleToHost16((uint16_t)value);
|
||||
}
|
||||
|
||||
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
|
||||
worldpos_t pos;
|
||||
pos.x = assetChunkReadWorldUnit(data, offset);
|
||||
pos.y = assetChunkReadWorldUnit(data, offset);
|
||||
pos.z = assetChunkReadWorldUnit(data, offset);
|
||||
return pos;
|
||||
}
|
||||
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.chunk.state != ASSET_CHUNK_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.chunk.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.chunk.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire chunk file."
|
||||
);
|
||||
|
||||
loading->loading.chunk.data = data;
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetchunkoutput_t *out = &loading->entry->data.chunk;
|
||||
|
||||
switch(loading->loading.chunk.state) {
|
||||
case ASSET_CHUNK_LOADING_STATE_INITIAL:
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_PARSE:
|
||||
break;
|
||||
|
||||
case ASSET_CHUNK_LOADING_STATE_LOAD_MODELS:
|
||||
while(loading->loading.chunk.modelIndex < out->meshCount) {
|
||||
uint8_t m = loading->loading.chunk.modelIndex;
|
||||
if(out->modelEntries[m] == NULL) {
|
||||
out->modelEntries[m] = assetLock(
|
||||
out->modelNames[m], ASSET_LOADER_TYPE_MODEL, NULL
|
||||
);
|
||||
assertNotNull(
|
||||
out->modelEntries[m], "Failed to lock model."
|
||||
);
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
if(out->modelEntries[m]->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Model failed to load: %s", out->modelNames[m]
|
||||
);
|
||||
}
|
||||
if(out->modelEntries[m]->state != ASSET_ENTRY_STATE_LOADED) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
loading->loading.chunk.modelIndex++;
|
||||
}
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
|
||||
default:
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t *data = loading->loading.chunk.data;
|
||||
assertNotNull(data, "Chunk data should have been loaded by now.");
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'F') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid chunk file header");
|
||||
}
|
||||
|
||||
uint32_t version = endianLittleToHost32(*(uint32_t *)(data + 4));
|
||||
if(version != ASSET_CHUNK_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported chunk file version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
size_t offset = 8;
|
||||
|
||||
size_t tileSize = CHUNK_TILE_COUNT * sizeof(tile_t);
|
||||
out->tiles = memoryAllocate(tileSize);
|
||||
memoryCopy(out->tiles, data + offset, tileSize);
|
||||
offset += tileSize;
|
||||
|
||||
for(size_t t = 0; t < CHUNK_TILE_COUNT; t++) {
|
||||
uint32_t *shape = (uint32_t *)&out->tiles[t].shape;
|
||||
*shape = endianLittleToHost32(*shape);
|
||||
}
|
||||
|
||||
out->meshCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->meshCount <= CHUNK_MESH_COUNT_MAX,
|
||||
"Chunk mesh count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
uint8_t nameLen = 0;
|
||||
while(
|
||||
data[offset + nameLen] != '\0' &&
|
||||
nameLen < CHUNK_MESH_NAME_MAX - 1
|
||||
) {
|
||||
nameLen++;
|
||||
}
|
||||
memoryCopy(out->modelNames[m], data + offset, nameLen);
|
||||
out->modelNames[m][nameLen] = '\0';
|
||||
offset += nameLen + 1;
|
||||
|
||||
memoryCopy(out->meshOffsets[m], data + offset, sizeof(vec3));
|
||||
offset += sizeof(vec3);
|
||||
out->meshOffsets[m][0] = endianLittleToHostFloat(out->meshOffsets[m][0]);
|
||||
out->meshOffsets[m][1] = endianLittleToHostFloat(out->meshOffsets[m][1]);
|
||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||
}
|
||||
|
||||
out->entitySpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
||||
"Chunk entity spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
||||
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
uint16_t a;
|
||||
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
||||
a = endianLittleToHost16(a);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
uint8_t b = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = a;
|
||||
spawn->itemQuantity = b;
|
||||
} else {
|
||||
spawn->globalId = a;
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
}
|
||||
|
||||
spawn->position = assetChunkReadWorldPos(data, &offset);
|
||||
}
|
||||
|
||||
out->areaSpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
||||
"Chunk area spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||
area->min = assetChunkReadWorldPos(data, &offset);
|
||||
area->max = assetChunkReadWorldPos(data, &offset);
|
||||
|
||||
uint16_t callbackId;
|
||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||
area->callbackId = endianLittleToHost16(callbackId);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
area->notify = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
area->trigger = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
if(out->meshCount == 0) {
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
loading->loading.chunk.state = ASSET_CHUNK_LOADING_STATE_LOAD_MODELS;
|
||||
loading->loading.chunk.modelIndex = 0;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetChunkDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CHUNK, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetchunkoutput_t *out = &entry->data.chunk;
|
||||
|
||||
if(out->tiles != NULL) {
|
||||
memoryFree(out->tiles);
|
||||
out->tiles = NULL;
|
||||
}
|
||||
|
||||
for(uint8_t m = 0; m < out->meshCount; m++) {
|
||||
if(out->modelEntries[m] == NULL) continue;
|
||||
assetUnlockEntry(out->modelEntries[m]);
|
||||
out->modelEntries[m] = NULL;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 5
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetchunkloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_CHUNK_LOADING_STATE_INITIAL,
|
||||
ASSET_CHUNK_LOADING_STATE_READ_FILE,
|
||||
ASSET_CHUNK_LOADING_STATE_PARSE,
|
||||
ASSET_CHUNK_LOADING_STATE_LOAD_MODELS,
|
||||
ASSET_CHUNK_LOADING_STATE_DONE
|
||||
} assetchunkloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetchunkloadingstate_t state;
|
||||
uint8_t *data;
|
||||
uint8_t modelIndex;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
typedef enum {
|
||||
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
|
||||
CHUNK_ENTITY_SPAWN_KIND_ITEM
|
||||
} chunkentityspawnkind_t;
|
||||
|
||||
typedef struct {
|
||||
chunkentityspawnkind_t kind;
|
||||
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
|
||||
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||
worldpos_t position;
|
||||
} chunkentityspawn_t;
|
||||
|
||||
typedef struct {
|
||||
worldpos_t min;
|
||||
worldpos_t max;
|
||||
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
|
||||
uint8_t notify;
|
||||
uint8_t trigger;
|
||||
} chunkareaspawn_t;
|
||||
|
||||
typedef struct {
|
||||
tile_t *tiles;
|
||||
uint8_t meshCount;
|
||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
uint8_t entitySpawnCount;
|
||||
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
||||
|
||||
uint8_t areaSpawnCount;
|
||||
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for chunk assets. Reads the raw DCF file bytes into
|
||||
* the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for chunk assets. Validates the DCF binary previously
|
||||
* read by the async phase and populates the output assetchunkoutput_t with
|
||||
* tile data and model paths.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetChunkLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for chunk assets.
|
||||
*
|
||||
* @param entry Asset entry containing the chunk data to dispose.
|
||||
* @return Error code indicating success or failure of the dispose operation.
|
||||
*/
|
||||
errorret_t assetChunkDispose(assetentry_t *entry);
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
assetcutsceneloader.c
|
||||
)
|
||||
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetcutsceneloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/endian.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
// DCTS header: magic "DCTS" (4), version u32 LE (4), pauseType u8 (1),
|
||||
// itemCount u8 (1), poolSize u16 LE (2) = 12 bytes.
|
||||
#define ASSET_CUTSCENE_HEADER_SIZE 12
|
||||
|
||||
static uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset) {
|
||||
uint8_t value = data[*offset];
|
||||
*offset += sizeof(uint8_t);
|
||||
return value;
|
||||
}
|
||||
|
||||
static uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset) {
|
||||
uint16_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(uint16_t));
|
||||
*offset += sizeof(uint16_t);
|
||||
return endianLittleToHost16(value);
|
||||
}
|
||||
|
||||
static uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset) {
|
||||
uint32_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(uint32_t));
|
||||
*offset += sizeof(uint32_t);
|
||||
return endianLittleToHost32(value);
|
||||
}
|
||||
|
||||
static float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset) {
|
||||
float_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(float_t));
|
||||
*offset += sizeof(float_t);
|
||||
return endianLittleToHostFloat(value);
|
||||
}
|
||||
|
||||
static worldunit_t assetCutsceneReadWorldUnit(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
uint16_t value = assetCutsceneReadU16(data, offset);
|
||||
return (worldunit_t)value;
|
||||
}
|
||||
|
||||
static worldpos_t assetCutsceneReadWorldPos(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
worldpos_t pos;
|
||||
pos.x = assetCutsceneReadWorldUnit(data, offset);
|
||||
pos.y = assetCutsceneReadWorldUnit(data, offset);
|
||||
pos.z = assetCutsceneReadWorldUnit(data, offset);
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Copies a length-prefixed string directly into an item's own embedded
|
||||
// char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these
|
||||
// are never pool references, see the item-field inventory in the runtime
|
||||
// cutscene file design.
|
||||
static void assetCutsceneReadEmbeddedString(
|
||||
const uint8_t *data,
|
||||
size_t *offset,
|
||||
char_t *dest,
|
||||
const size_t destCapacity
|
||||
) {
|
||||
uint8_t len = assetCutsceneReadU8(data, offset);
|
||||
assertTrue(len < destCapacity, "Cutscene string exceeds field capacity");
|
||||
memoryCopy(dest, data + *offset, len);
|
||||
dest[len] = '\0';
|
||||
*offset += len;
|
||||
}
|
||||
|
||||
// Resolves a u16 pool offset (read from the item stream) to a real pointer
|
||||
// into the entry's own persistent pool allocation.
|
||||
static const char_t * assetCutsceneReadPoolString(
|
||||
const uint8_t *data,
|
||||
size_t *offset,
|
||||
const char_t *pool
|
||||
) {
|
||||
uint16_t poolOffset = assetCutsceneReadU16(data, offset);
|
||||
return pool + poolOffset;
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.cutscene.state != ASSET_CUTSCENE_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.cutscene.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.cutscene.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire cutscene file."
|
||||
);
|
||||
// Saved before assetFileDispose zeroes the whole assetfile_t struct
|
||||
// (including .size) - the sync phase needs the file's total length to
|
||||
// locate the pool region, which starts poolSize bytes before the end.
|
||||
loading->loading.cutscene.dataSize = (size_t)file->size;
|
||||
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
|
||||
loading->loading.cutscene.data = data;
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
|
||||
uint8_t *data = loading->loading.cutscene.data;
|
||||
assertNotNull(data, "Cutscene data should have been loaded by now.");
|
||||
|
||||
size_t fileSize = loading->loading.cutscene.dataSize;
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'T' || data[3] != 'S') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid cutscene file header");
|
||||
}
|
||||
|
||||
size_t offset = 4;
|
||||
uint32_t version = assetCutsceneReadU32(data, &offset);
|
||||
if(version != ASSET_CUTSCENE_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported cutscene file version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
cutscenepause_t pauseType = (cutscenepause_t)assetCutsceneReadU8(data, &offset);
|
||||
uint8_t itemCount = assetCutsceneReadU8(data, &offset);
|
||||
uint16_t poolSize = assetCutsceneReadU16(data, &offset);
|
||||
assertTrue(offset == ASSET_CUTSCENE_HEADER_SIZE, "Cutscene header size mismatch");
|
||||
|
||||
out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL;
|
||||
if(poolSize > 0) {
|
||||
size_t poolStart = fileSize - (size_t)poolSize;
|
||||
memoryCopy(out->pool, data + poolStart, poolSize);
|
||||
}
|
||||
const char_t *pool = out->pool;
|
||||
|
||||
out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t));
|
||||
memoryZero(out->items, itemCount * sizeof(cutsceneitem_t));
|
||||
|
||||
for(uint8_t i = 0; i < itemCount; i++) {
|
||||
cutsceneitem_t *item = &out->items[i];
|
||||
item->type = (cutsceneitemtype_t)assetCutsceneReadU8(data, &offset);
|
||||
|
||||
switch(item->type) {
|
||||
case CUTSCENE_ITEM_TYPE_TEXT:
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->text.text, CUTSCENE_TEXT_MAX_CHARS
|
||||
);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI:
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->textMini.text, CUTSCENE_TEXT_MINI_MAX_CHARS
|
||||
);
|
||||
item->textMini.position[0] = assetCutsceneReadFloat(data, &offset);
|
||||
item->textMini.position[1] = assetCutsceneReadFloat(data, &offset);
|
||||
item->textMini.position[2] = assetCutsceneReadFloat(data, &offset);
|
||||
item->textMini.duration = assetCutsceneReadFloat(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE:
|
||||
item->textMiniHide.index = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_WAIT:
|
||||
item->wait = assetCutsceneReadFloat(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
|
||||
item->entityTeleport.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->entityTeleport.target = assetCutsceneReadWorldPos(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: {
|
||||
item->entityWalkTo.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->entityWalkTo.walkAround = assetCutsceneReadU8(data, &offset) != 0;
|
||||
uint8_t count = assetCutsceneReadU8(data, &offset);
|
||||
uint16_t poolOffset = assetCutsceneReadU16(data, &offset);
|
||||
item->entityWalkTo.count = count;
|
||||
item->entityWalkTo.positions = (const worldpos_t *)(pool + poolOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_FADE:
|
||||
item->fade.from.r = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.from.g = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.from.b = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.from.a = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.r = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.g = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.b = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.a = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.duration = assetCutsceneReadFloat(data, &offset);
|
||||
item->fade.easing = (easingtype_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
|
||||
item->setPause = (cutscenepause_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
|
||||
item->itemGive.item = (itemid_t)assetCutsceneReadU16(data, &offset);
|
||||
item->itemGive.quantity = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
|
||||
item->entityRemove.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
|
||||
item->entityAdd.entityType = assetCutsceneReadU8(data, &offset);
|
||||
item->entityAdd.position = assetCutsceneReadWorldPos(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
|
||||
item->entityTurn.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->entityTurn.direction =
|
||||
(entitydir_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
|
||||
item->entityWalkToEntity.entityIndex =
|
||||
assetCutsceneReadU8(data, &offset);
|
||||
item->entityWalkToEntity.targetEntityIndex =
|
||||
assetCutsceneReadU8(data, &offset);
|
||||
item->entityWalkToEntity.offsetX =
|
||||
assetCutsceneReadWorldUnit(data, &offset);
|
||||
item->entityWalkToEntity.offsetY =
|
||||
assetCutsceneReadWorldUnit(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
|
||||
item->mapAreaRemove.areaId = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: {
|
||||
uint8_t count = assetCutsceneReadU8(data, &offset);
|
||||
uint16_t poolOffset = assetCutsceneReadU16(data, &offset);
|
||||
assertTrue(
|
||||
count <= CUTSCENE_MAP_AREA_WAIT_MAX,
|
||||
"Cutscene map area wait count exceeds maximum"
|
||||
);
|
||||
item->mapAreaWait.count = count;
|
||||
item->mapAreaWait.areaIds = (const uint8_t *)(pool + poolOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_START_BATTLE: {
|
||||
item->startBattle.encounterType =
|
||||
(battleencountertype_t)assetCutsceneReadU8(data, &offset);
|
||||
item->startBattle.fleeAvailable =
|
||||
assetCutsceneReadU8(data, &offset) != 0;
|
||||
uint8_t enemyCount = assetCutsceneReadU8(data, &offset);
|
||||
assertTrue(
|
||||
enemyCount <= CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX,
|
||||
"Cutscene battle enemy count exceeds maximum"
|
||||
);
|
||||
item->startBattle.enemyCount = enemyCount;
|
||||
for(uint8_t e = 0; e < enemyCount; e++) {
|
||||
cutscenestartbattleenemy_t *enemy = &item->startBattle.enemies[e];
|
||||
enemy->stats.attack = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.defense = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.magic = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.speed = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.luck = assetCutsceneReadU16(data, &offset);
|
||||
enemy->healthMax = assetCutsceneReadU16(data, &offset);
|
||||
enemy->mpMax = assetCutsceneReadU16(data, &offset);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_EMOJI:
|
||||
item->emoji.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->emoji.duration = assetCutsceneReadFloat(data, &offset);
|
||||
item->emoji.emojiType = (uiemojitype_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SHAKE:
|
||||
item->shake.amount = assetCutsceneReadU8(data, &offset);
|
||||
item->shake.duration = assetCutsceneReadFloat(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE:
|
||||
item->battleWaitState.state =
|
||||
(battlestate_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION:
|
||||
item->battleForceAction.fighterIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->battleForceAction.targetIndex = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MODAL: {
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS
|
||||
);
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modal.message, CUTSCENE_MODAL_MESSAGE_MAX_CHARS
|
||||
);
|
||||
// v1 only supports the message-only form: MODAL and MODAL_OPTIONS
|
||||
// share this same tag with no separate discriminator, and there is
|
||||
// no native-callback registry yet to resolve an options callback.
|
||||
// Read into a local first (not inline in the check below) - on a
|
||||
// release build with DUSK_ASSERTIONS_FAKED, an assert's condition
|
||||
// is never evaluated at all, so a byte-consuming call inside one
|
||||
// would silently desync every item after it. This is untrusted
|
||||
// file content anyway, so it gets a real error, not an assert.
|
||||
uint8_t optionCount = assetCutsceneReadU8(data, &offset);
|
||||
if(optionCount != 0) {
|
||||
memoryFree(data);
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene MODAL item with options is not supported in "
|
||||
"file-based cutscenes yet - use MODAL_OPTIONS_MARKERS instead"
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS: {
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modalOptionsMarkers.title,
|
||||
CUTSCENE_MODAL_TITLE_MAX_CHARS
|
||||
);
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modalOptionsMarkers.message,
|
||||
CUTSCENE_MODAL_MESSAGE_MAX_CHARS
|
||||
);
|
||||
uint8_t optionCount = assetCutsceneReadU8(data, &offset);
|
||||
assertTrue(
|
||||
optionCount <= CUTSCENE_MODAL_OPTIONS_MARKERS_MAX,
|
||||
"Cutscene modal option count exceeds maximum"
|
||||
);
|
||||
item->modalOptionsMarkers.optionCount = optionCount;
|
||||
for(uint8_t o = 0; o < optionCount; o++) {
|
||||
item->modalOptionsMarkers.options[o] =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
item->modalOptionsMarkers.markers[o] =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MODAL_CLOSE:
|
||||
case CUTSCENE_ITEM_TYPE_RESTART:
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_PRINT:
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->print.text, CUTSCENE_PRINT_MAX_CHARS
|
||||
);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MARKER:
|
||||
item->marker.name = assetCutsceneReadPoolString(data, &offset, pool);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SCENE:
|
||||
item->sceneChange.type = (scenetype_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK:
|
||||
item->saveDeviceCheck.successMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
item->saveDeviceCheck.failureMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS:
|
||||
item->saveLoadAllSlots.successMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
item->saveLoadAllSlots.failureMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
break;
|
||||
|
||||
default:
|
||||
memoryFree(data);
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene item type %u is not supported in file-based cutscenes "
|
||||
"(item %u/%u, offset %u/%u, poolSize %u)",
|
||||
(uint32_t)item->type, (uint32_t)i, (uint32_t)itemCount,
|
||||
(uint32_t)offset, (uint32_t)fileSize, (uint32_t)poolSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.cutscene.data = NULL;
|
||||
|
||||
out->cutscene.items = out->items;
|
||||
out->cutscene.itemCount = itemCount;
|
||||
out->cutscene.pause = pauseType;
|
||||
out->cutscene.dataSize = 0;
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetcutsceneoutput_t *out = &entry->data.cutscene;
|
||||
|
||||
if(out->items != NULL) {
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
}
|
||||
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
#define ASSET_CUTSCENE_FILE_VERSION 1
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetcutsceneloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_CUTSCENE_LOADING_STATE_INITIAL,
|
||||
ASSET_CUTSCENE_LOADING_STATE_READ_FILE,
|
||||
ASSET_CUTSCENE_LOADING_STATE_PARSE
|
||||
} assetcutsceneloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetcutsceneloadingstate_t state;
|
||||
uint8_t *data;
|
||||
size_t dataSize;// Saved before assetFileDispose zeroes file.size.
|
||||
} assetcutsceneloaderloading_t;
|
||||
|
||||
// Runtime-loaded cutscene: items/pool are heap-allocated to the file's
|
||||
// actual declared sizes (not fixed-capacity), so an entry that never holds
|
||||
// a cutscene costs nothing extra in the shared assetloaderoutput_t union -
|
||||
// see assetchunkoutput_t.tiles for the same pattern.
|
||||
typedef struct {
|
||||
cutscene_t cutscene; // .items points at the items array below
|
||||
cutsceneitem_t *items;
|
||||
char_t *pool;
|
||||
} assetcutsceneoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for cutscene assets. Reads the raw DCTS file bytes
|
||||
* into the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for cutscene assets. Validates the DCTS binary
|
||||
* previously read by the async phase and decodes it into a heap-allocated
|
||||
* cutsceneitem_t array + string/data pool.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for cutscene assets.
|
||||
*
|
||||
* @param entry Asset entry containing the cutscene data to dispose.
|
||||
* @return Error code indicating success or failure of the dispose operation.
|
||||
*/
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry);
|
||||
@@ -127,6 +127,14 @@ errorret_t assetMeshLoaderSync(assetloading_t *loading) {
|
||||
errorChain(ret);
|
||||
}
|
||||
|
||||
#if defined(DUSK_OPENGL) && !defined(DUSK_OPENGL_LEGACY)
|
||||
// VBO owns the data now; CPU copy is no longer needed. The platform
|
||||
// mesh object itself still needs meshDispose later - tracked via
|
||||
// out->meshInitialized, independent of the CPU buffer's lifetime.
|
||||
memoryFree(out->vertices);
|
||||
out->vertices = NULL;
|
||||
#endif
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -17,11 +17,9 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = false;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
#endif
|
||||
// CONSOLE.visible = false;
|
||||
CONSOLE.visible = true;
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
}
|
||||
|
||||
void consolePrint(const char_t *message, ...) {
|
||||
@@ -32,21 +30,14 @@ void consolePrint(const char_t *message, ...) {
|
||||
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
||||
va_end(args);
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexLock(&CONSOLE.printMutex);
|
||||
#endif
|
||||
|
||||
threadMutexLock(&CONSOLE.printMutex);
|
||||
memoryMove(
|
||||
CONSOLE.line[0],
|
||||
CONSOLE.line[1],
|
||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||
);
|
||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||
CONSOLE.dirty = true;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexUnlock(&CONSOLE.printMutex);
|
||||
#endif
|
||||
threadMutexUnlock(&CONSOLE.printMutex);
|
||||
|
||||
logDebug("%s\n", buffer);
|
||||
}
|
||||
@@ -56,13 +47,11 @@ void consoleUpdate(void) {
|
||||
if(TIME.dynamicUpdate) return;
|
||||
#endif
|
||||
|
||||
if(inputPressed(INPUT_BIND_CONSOLE)) {
|
||||
if(inputPressed(INPUT_ACTION_CONSOLE)) {
|
||||
CONSOLE.visible = !CONSOLE.visible;
|
||||
}
|
||||
}
|
||||
|
||||
void consoleDispose(void) {
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexDispose(&CONSOLE.printMutex);
|
||||
#endif
|
||||
threadMutexDispose(&CONSOLE.printMutex);
|
||||
}
|
||||
@@ -6,29 +6,18 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "consoledefs.h"
|
||||
#include "error/error.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
|
||||
#define CONSOLE_LINE_MAX 128
|
||||
#define CONSOLE_HISTORY_MAX 16
|
||||
#define CONSOLE_EXEC_BUFFER_MAX 32
|
||||
|
||||
typedef struct {
|
||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||
bool_t visible;
|
||||
|
||||
// Set whenever the history changes; consumers rendering the console
|
||||
// (e.g. uiConsoleDraw) check and clear this to know when their own
|
||||
// cached representation of the history needs rebuilding.
|
||||
bool_t dirty;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadmutex_t printMutex;
|
||||
#endif
|
||||
threadmutex_t printMutex;
|
||||
} console_t;
|
||||
|
||||
extern console_t CONSOLE;
|
||||
|
||||
@@ -33,13 +33,17 @@ errorret_t displayInit(void) {
|
||||
#ifdef displayPlatformInit
|
||||
errorChain(displayPlatformInit());
|
||||
#endif
|
||||
|
||||
// Set initial state
|
||||
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
||||
|
||||
// Init the fixed textures
|
||||
errorChain(textureInit(
|
||||
&TEXTURE_WHITE, 4, 4,
|
||||
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
||||
));
|
||||
errorChain(textureInit(
|
||||
&TEXTURE_TEST, 4, 4,
|
||||
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
||||
));
|
||||
|
||||
@@ -51,13 +55,13 @@ errorret_t displayInit(void) {
|
||||
errorChain(capsuleInit());
|
||||
errorChain(triPrismInit());
|
||||
|
||||
// Init the subsystems
|
||||
errorChain(frameBufferInitBackBuffer());
|
||||
errorChain(spriteBatchInit());
|
||||
errorChain(textInit());
|
||||
errorChain(screenInit());
|
||||
|
||||
// Setup initial shader with default values
|
||||
|
||||
errorChain(shaderListInit());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -12,8 +12,8 @@ mesh_t CUBE_MESH_SIMPLE;
|
||||
meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||
|
||||
errorret_t cubeInit() {
|
||||
vec3 min = { -0.5f, -0.5f, -0.5f };
|
||||
vec3 max = { 0.5f, 0.5f, 0.5f };
|
||||
vec3 min = { 0.0f, 0.0f, 0.0f };
|
||||
vec3 max = { 1.0f, 1.0f, 1.0f };
|
||||
cubeBuffer(CUBE_MESH_SIMPLE_VERTICES, min, max);
|
||||
errorChain(meshInit(
|
||||
&CUBE_MESH_SIMPLE,
|
||||
|
||||
@@ -17,9 +17,7 @@ extern mesh_t CUBE_MESH_SIMPLE;
|
||||
extern meshvertex_t CUBE_MESH_SIMPLE_VERTICES[CUBE_VERTEX_COUNT];
|
||||
|
||||
/**
|
||||
* Initializes the simple unit cube mesh, centered at (0,0,0), spanning
|
||||
* (-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).
|
||||
* Initializes the simple unit cube mesh (0,0,0) to (1,1,1).
|
||||
*
|
||||
* @return Error for initialization of the cube mesh.
|
||||
*/
|
||||
|
||||
@@ -59,8 +59,7 @@ void planeBuffer(
|
||||
|
||||
switch(axis) {
|
||||
case PLANE_AXIS_XY: {
|
||||
// Flat in XY at z = min[2]; spans X and Y.
|
||||
// +Z normal: CCW when viewed from +Z (matches cube.c's front face).
|
||||
/* Flat in XY at z = min[2]; spans X and Y. */
|
||||
const float_t z = min[2];
|
||||
PLANE_VERT(0, min[0], min[1], z, u0, v0)
|
||||
PLANE_VERT(1, max[0], min[1], z, u1, v0)
|
||||
@@ -72,24 +71,19 @@ void planeBuffer(
|
||||
}
|
||||
|
||||
case PLANE_AXIS_XZ: {
|
||||
// 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.
|
||||
/* Flat in XZ at y = min[1]; spans X and Z. */
|
||||
const float_t y = min[1];
|
||||
PLANE_VERT(0, min[0], y, min[2], u0, v0)
|
||||
PLANE_VERT(1, max[0], y, max[2], u1, v1)
|
||||
PLANE_VERT(2, max[0], y, min[2], u1, v0)
|
||||
PLANE_VERT(1, 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(4, min[0], y, max[2], u0, v1)
|
||||
PLANE_VERT(5, max[0], y, max[2], u1, v1)
|
||||
PLANE_VERT(4, max[0], y, max[2], u1, v1)
|
||||
PLANE_VERT(5, min[0], y, max[2], u0, v1)
|
||||
break;
|
||||
}
|
||||
|
||||
case PLANE_AXIS_YZ: {
|
||||
// Flat in YZ at x = min[0]; spans Y and Z.
|
||||
// +X normal: CCW when viewed from +X (matches cube.c's right face).
|
||||
/* Flat in YZ at x = min[0]; spans Y and Z. */
|
||||
const float_t x = min[0];
|
||||
PLANE_VERT(0, x, min[1], min[2], u0, 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) {
|
||||
assertNotNull(shader, "Shader cannot be null");
|
||||
if(bound == shader) errorOk();
|
||||
errorChain(shaderBindPlatform(shader));
|
||||
bound = shader;
|
||||
errorOk();
|
||||
|
||||
@@ -76,7 +76,7 @@ errorret_t spriteBatchBuffer(
|
||||
|
||||
// Buffer to the mesh vertices.
|
||||
spriteBatchBufferToMesh(
|
||||
sprites, batchCount, v, batchCount * QUAD_VERTEX_COUNT
|
||||
sprites + (count - remaining), batchCount, v, batchCount * QUAD_VERTEX_COUNT
|
||||
);
|
||||
SPRITEBATCH.spriteCount += batchCount;
|
||||
remaining -= batchCount;
|
||||
|
||||
@@ -39,16 +39,3 @@ spritebatchsprite_t spriteBatchSpriteTilesetPosition(
|
||||
sprite.uvMax[1] = uv[3];
|
||||
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 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,6 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
font.c
|
||||
text.c
|
||||
font.c
|
||||
)
|
||||
@@ -76,11 +76,23 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
{ 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)
|
||||
// Custom icon glyph, not a real backslash - a backspace symbol: Caps
|
||||
// Lock's up arrow (see FONT_ICON_CAPSLOCK) rotated 270 degrees to
|
||||
// point left instead, adapted to this font's fixed 6x10 tile (each of
|
||||
// Caps Lock's row widths becomes a column height here, centered
|
||||
// vertically). Backslash was never drawn anyway, and isn't a key this
|
||||
// virtual keyboard can type - see FONT_ICON_BACKSPACE.
|
||||
{ 0x00, 0x00, 0x08, 0x1F, 0x3F, 0x3F, 0x1F, 0x08, 0x00, 0x00 }, // FONT_ICON_BACKSPACE
|
||||
{ 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E }, // _
|
||||
// Custom icon glyph, not a real backtick - an up arrow for Shift, same
|
||||
// head as Caps Lock's (see FONT_ICON_CAPSLOCK) but with a shaft 2px
|
||||
// thinner, plus one empty row near the bottom of it, so it reads as a
|
||||
// lighter/broken version of Caps Lock's thicker uninterrupted one.
|
||||
// Backtick was never drawn anyway, and isn't a key this virtual
|
||||
// keyboard can type - see FONT_ICON_SHIFT.
|
||||
{ 0x0C, 0x1E, 0x3F, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x0C, 0x00 }, // FONT_ICON_SHIFT
|
||||
{ 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
|
||||
@@ -108,14 +120,29 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
{ 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)
|
||||
// Custom icon glyph, not a real pipe - a "return" arrow for the
|
||||
// keyboard's newline key: a vertical riser down the right side that
|
||||
// hooks left into a leftward-pointing arrowhead, i.e. "<-|" rotated
|
||||
// into an L. Pipe was never drawn anyway, and isn't a key this
|
||||
// virtual keyboard can type - see FONT_ICON_NEWLINE.
|
||||
{ 0x02, 0x02, 0x02, 0x02, 0x0E, 0x1E, 0x08, 0x00, 0x00, 0x00 }, // FONT_ICON_NEWLINE
|
||||
{ 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
|
||||
// Custom icon glyph, not a real tilde - a spacebar symbol for the
|
||||
// keyboard's space key: an underscore with a tick at each end, like
|
||||
// "|___|". Tilde was never drawn anyway, and was explicitly dropped
|
||||
// from this virtual keyboard's own key set - see FONT_ICON_SPACE.
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x3F }, // FONT_ICON_SPACE
|
||||
// Custom icon glyph, not a real ASCII character - a thick, solid,
|
||||
// uninterrupted up arrow for Caps Lock (contrast FONT_ICON_SHIFT's
|
||||
// same head but a thinner, gapped shaft). Assigned to char code 127
|
||||
// (DEL) since that codepoint is never legitimately typed text and
|
||||
// (unlike 128+) is still a positive value regardless of whether this
|
||||
// platform's plain `char` is signed - see FONT_ICON_CAPSLOCK.
|
||||
{ 0x0C, 0x1E, 0x3F, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x00 }, // FONT_ICON_CAPSLOCK
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||
};
|
||||
|
||||
errorret_t fontInitDefault(void) {
|
||||
errorret_t fontDefaultInit(void) {
|
||||
const int32_t width = (int32_t)mathNextPowTwo(
|
||||
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
||||
);
|
||||
@@ -160,7 +187,7 @@ errorret_t fontInitDefault(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t fontDisposeDefault(void) {
|
||||
errorret_t fontDefaultDispose(void) {
|
||||
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
||||
FONT_DEFAULT.texture = NULL;
|
||||
FONT_DEFAULT.tileset = NULL;
|
||||
|
||||
@@ -16,9 +16,7 @@ typedef struct {
|
||||
} 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.
|
||||
* Pixel width/height of a single default-font glyph tile.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_WIDTH 6
|
||||
#define FONT_DEFAULT_TILE_HEIGHT 10
|
||||
@@ -30,37 +28,72 @@ typedef struct {
|
||||
/**
|
||||
* 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.
|
||||
* TEXT_CHAR_START ('!'), one custom icon glyph in a trailing unused tile
|
||||
* (see FONT_ICON_CAPSLOCK), plus one more genuinely unused trailing tile.
|
||||
* FONT_ICON_SHIFT/FONT_ICON_NEWLINE reuse existing-but-blank slots within
|
||||
* the printable range instead of more trailing tiles - see their own
|
||||
* comments.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
||||
|
||||
/**
|
||||
* Custom (non-ASCII-meaning) icon glyphs baked into FONT_DEFAULT_GLYPHS
|
||||
* at otherwise-unused codepoints within the range this font covers -
|
||||
* safe to use anywhere a char_t string is expected, e.g. a uibutton_t
|
||||
* label. Kept below 128: char_t is a plain `char`, whose signedness
|
||||
* varies by platform, so a codepoint of 128 or above isn't safely
|
||||
* representable everywhere this engine targets.
|
||||
*/
|
||||
// Thick, solid, uninterrupted up arrow - Caps Lock. A trailing tile
|
||||
// (char code 127/DEL) that was never a real character to begin with.
|
||||
#define FONT_ICON_CAPSLOCK "\x7F"
|
||||
// Same up arrow, but with a thinner and gapped shaft - Shift. Reuses
|
||||
// the backtick's glyph slot: backtick was never drawn by this font
|
||||
// anyway, and isn't a key this engine's virtual keyboard can type.
|
||||
#define FONT_ICON_SHIFT "`"
|
||||
// A "return" arrow (down then left, with a leftward arrowhead) - the
|
||||
// keyboard's newline key. Reuses the pipe's glyph slot, for the same
|
||||
// reason as FONT_ICON_SHIFT.
|
||||
#define FONT_ICON_NEWLINE "|"
|
||||
// An underscore with a tick at each end ("|___|") - the keyboard's
|
||||
// space key. Reuses the tilde's glyph slot: tilde was never drawn by
|
||||
// this font, and was explicitly dropped from this engine's virtual
|
||||
// keyboard's own key set.
|
||||
#define FONT_ICON_SPACE "~"
|
||||
// Caps Lock's arrow rotated to point left - the keyboard's backspace
|
||||
// key. Reuses the backslash's glyph slot, for the same reason as
|
||||
// FONT_ICON_SHIFT/FONT_ICON_NEWLINE/FONT_ICON_SPACE. Last free reused
|
||||
// slot below 128 - one more custom icon after this needs
|
||||
// FONT_DEFAULT_COLUMNS/ROWS grown to make room.
|
||||
#define FONT_ICON_BACKSPACE "\\"
|
||||
|
||||
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.
|
||||
* Hard coded bitmap data for the built-in default font. 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.
|
||||
* Builds the default font's texture + tileset directly from
|
||||
* FONT_DEFAULT_GLYPHS, without going through the asset system - so the
|
||||
* engine always has a usable font to render with regardless of whether
|
||||
* asset loading (e.g. the packed .dsk archive) succeeds.
|
||||
*
|
||||
* @return Either an error or success result.
|
||||
*/
|
||||
errorret_t fontInitDefault(void);
|
||||
errorret_t fontDefaultInit(void);
|
||||
|
||||
/**
|
||||
* Disposes of the built-in default font created by fontInitDefault.
|
||||
* Disposes of the default font created by fontDefaultInit().
|
||||
*
|
||||
* @return Either an error or success result.
|
||||
*/
|
||||
errorret_t fontDisposeDefault(void);
|
||||
errorret_t fontDefaultDispose(void);
|
||||
|
||||
+114
-105
@@ -12,12 +12,12 @@
|
||||
#include "display/shader/shaderunlit.h"
|
||||
|
||||
errorret_t textInit(void) {
|
||||
errorChain(fontInitDefault());
|
||||
errorChain(fontDefaultInit());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t textDispose(void) {
|
||||
errorChain(fontDisposeDefault());
|
||||
errorChain(fontDefaultDispose());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,62 @@ spritebatchsprite_t textGetSprite(
|
||||
return sprite;
|
||||
}
|
||||
|
||||
int32_t textBuffer(
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const char_t *text,
|
||||
font_t *font,
|
||||
spritebatchsprite_t *outSprites,
|
||||
const int32_t maxSprites,
|
||||
int32_t *charIndex,
|
||||
float_t *posX,
|
||||
float_t *posY
|
||||
) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
|
||||
if(outSprites == NULL) {
|
||||
int32_t count = 0;
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
while((c = text[i++]) != '\0') {
|
||||
if(c != ' ' && c != '\n') count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
assertNotNull(font, "Font cannot be NULL");
|
||||
assertTrue(maxSprites > 0, "Max sprites must be greater than zero");
|
||||
assertNotNull(posX, "Output posX cannot be NULL");
|
||||
assertNotNull(posY, "Output posY cannot be NULL");
|
||||
assertNotNull(charIndex, "Output charIndex cannot be NULL");
|
||||
|
||||
int32_t spriteIndex = 0;
|
||||
char_t c;
|
||||
for(;;) {
|
||||
c = text[*charIndex];
|
||||
if(c == '\0') break;
|
||||
(*charIndex)++;
|
||||
|
||||
if(c == '\n') {
|
||||
*posX = x;
|
||||
*posY += font->tileset->tileHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(c == ' ') {
|
||||
*posX += font->tileset->tileWidth;
|
||||
continue;
|
||||
}
|
||||
|
||||
outSprites[spriteIndex++] = textGetSprite((vec2){*posX, *posY}, c, font);
|
||||
*posX += font->tileset->tileWidth;
|
||||
|
||||
if(spriteIndex >= maxSprites) break;
|
||||
}
|
||||
|
||||
return spriteIndex;
|
||||
}
|
||||
|
||||
errorret_t textDraw(
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
@@ -62,10 +118,11 @@ errorret_t textDraw(
|
||||
font_t *font
|
||||
) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
int32_t length = strlen(text);
|
||||
if(length == 0) errorOk();
|
||||
|
||||
if(font == NULL) font = &FONT_DEFAULT;
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = color,
|
||||
@@ -73,114 +130,25 @@ errorret_t textDraw(
|
||||
}
|
||||
};
|
||||
|
||||
spritebatchsprite_t sprites[32];
|
||||
float_t posX = x;
|
||||
float_t posY = y;
|
||||
int32_t buffered = 0;
|
||||
int32_t charIndex = 0;
|
||||
do {
|
||||
buffered = textBuffer(
|
||||
x, y, text, font,
|
||||
sprites,
|
||||
sizeof(sprites) / sizeof(spritebatchsprite_t),
|
||||
&charIndex, &posX, &posY
|
||||
);
|
||||
errorChain(spriteBatchBuffer(sprites, buffered, &SHADER_UNLIT, material));
|
||||
} while(charIndex < length);
|
||||
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
while((c = text[i++]) != '\0') {
|
||||
if(c == '\n') {
|
||||
posX = x;
|
||||
posY += font->tileset->tileHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(c == ' ') {
|
||||
posX += font->tileset->tileWidth;
|
||||
continue;
|
||||
}
|
||||
|
||||
sprite = textGetSprite((vec2){posX, posY}, c, font);
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
posX += font->tileset->tileWidth;
|
||||
}
|
||||
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(
|
||||
int32_t textMeasure(
|
||||
const char_t *text,
|
||||
const font_t *font,
|
||||
int32_t *outWidth,
|
||||
@@ -193,6 +161,7 @@ void textMeasure(
|
||||
int32_t width = 0;
|
||||
int32_t height = font->tileset->tileHeight;
|
||||
int32_t lineWidth = 0;
|
||||
int32_t spriteCount = 0;
|
||||
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
@@ -205,10 +174,50 @@ void textMeasure(
|
||||
}
|
||||
|
||||
lineWidth += font->tileset->tileWidth;
|
||||
|
||||
if(c != ' ') {
|
||||
spriteCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(lineWidth > width) width = lineWidth;
|
||||
|
||||
*outWidth = width;
|
||||
*outHeight = height;
|
||||
|
||||
return spriteCount;
|
||||
}
|
||||
|
||||
void textWrap(char_t *text, const font_t *font, const float_t maxWidth) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
assertNotNull(font, "Font cannot be NULL");
|
||||
|
||||
float_t fontWidth = (float_t)font->tileset->tileWidth;
|
||||
if(fontWidth <= 0.0f) return;
|
||||
|
||||
int32_t charsPerLine = (int32_t)(maxWidth / fontWidth);
|
||||
if(charsPerLine <= 0) return;
|
||||
|
||||
int32_t lineWidth = 0;
|
||||
int32_t lastSpace = -1;
|
||||
|
||||
for(int32_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '\n') {
|
||||
lineWidth = 0;
|
||||
lastSpace = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(text[i] == ' ') {
|
||||
lastSpace = i;
|
||||
}
|
||||
|
||||
lineWidth++;
|
||||
|
||||
if(lineWidth > charsPerLine && lastSpace != -1) {
|
||||
text[lastSpace] = '\n';
|
||||
lineWidth = i - lastSpace;
|
||||
lastSpace = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,37 @@ spritebatchsprite_t textGetSprite(
|
||||
const font_t *font
|
||||
);
|
||||
|
||||
/**
|
||||
* Buffers a string into sprites for rendering. If outSprites is NULL then
|
||||
* the function will only return the count of sprites necessary for the buffer.
|
||||
*
|
||||
* posX and posY are updated whilst buffering characters, if you need to do
|
||||
* buffering in sets then these will be reusable between buffer commands. Start
|
||||
* by setting these to x and y initially.
|
||||
*
|
||||
* @param x The x-coordinate to start buffering the text at.
|
||||
* @param y The y-coordinate to start buffering the text at.
|
||||
* @param text The null-terminated string of text to buffer.
|
||||
* @param font Font to use for rendering.
|
||||
* @param outSprites Pointer to an array of spritebatchsprite_t.
|
||||
* @param maxSprites The maximum number of sprites in outSprites.
|
||||
* @param charIndex Pointer to an int32_t to store the character indexed.
|
||||
* @param posX Pointer to a float_t to store the final x position.
|
||||
* @param posY Pointer to a float_t to store the final y position.
|
||||
* @return The count of sprites buffered.
|
||||
*/
|
||||
int32_t textBuffer(
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const char_t *text,
|
||||
font_t *font,
|
||||
spritebatchsprite_t *outSprites,
|
||||
const int32_t maxSprites,
|
||||
int32_t *charIndex,
|
||||
float_t *posX,
|
||||
float_t *posY
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws a string of text at the specified position.
|
||||
*
|
||||
@@ -58,54 +89,6 @@ errorret_t textDraw(
|
||||
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.
|
||||
*
|
||||
@@ -113,10 +96,25 @@ errorret_t textDrawSpriteCache(
|
||||
* @param font Font to use for measurement.
|
||||
* @param outWidth Pointer to store the measured width in pixels.
|
||||
* @param outHeight Pointer to store the measured height in pixels.
|
||||
* @return The count of sprites that will be rendered for the given text.
|
||||
*/
|
||||
void textMeasure(
|
||||
int32_t textMeasure(
|
||||
const char_t *text,
|
||||
const font_t *font,
|
||||
int32_t *outWidth,
|
||||
int32_t *outHeight
|
||||
);
|
||||
|
||||
/**
|
||||
* Word-wraps text in place for display at up to maxWidth pixels wide,
|
||||
* by replacing the space nearest each overflow point with a newline.
|
||||
* Length is unchanged - this only ever swaps existing spaces for
|
||||
* newlines, never inserts characters - so it's always safe to call on a
|
||||
* fixed-size buffer. A single word wider than maxWidth on its own is
|
||||
* left unbroken.
|
||||
*
|
||||
* @param text Null-terminated, caller-owned buffer to wrap in place.
|
||||
* @param font Font to measure character width with.
|
||||
* @param maxWidth Maximum line width, in pixels.
|
||||
*/
|
||||
void textWrap(char_t *text, const font_t *font, const float_t maxWidth);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "display/display.h"
|
||||
|
||||
texture_t TEXTURE_WHITE;
|
||||
color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||
color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
@@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||
};
|
||||
|
||||
texture_t TEXTURE_TEST;
|
||||
color_t TEXTURE_TEST_PIXELS[4*4] = {
|
||||
color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#error "textureDisposePlatform should not be defined."
|
||||
#endif
|
||||
|
||||
#define TEXTURE_FIXED_WIDTH 4
|
||||
#define TEXTURE_FIXED_HEIGHT 4
|
||||
|
||||
typedef textureformatplatform_t textureformat_t;
|
||||
typedef textureplatform_t texture_t;
|
||||
|
||||
@@ -29,9 +32,9 @@ typedef union texturedata_u {
|
||||
} texturedata_t;
|
||||
|
||||
extern texture_t TEXTURE_WHITE;
|
||||
extern color_t TEXTURE_WHITE_PIXELS[4*4];
|
||||
extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
||||
extern texture_t TEXTURE_TEST;
|
||||
extern color_t TEXTURE_TEST_PIXELS[4*4];
|
||||
extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
||||
|
||||
/**
|
||||
* Initializes a texture.
|
||||
|
||||
+25
-44
@@ -10,23 +10,18 @@
|
||||
#include "time/time.h"
|
||||
#include "input/input.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "rpg/rpg.h"
|
||||
#include "display/display.h"
|
||||
#include "scene/scene.h"
|
||||
#include "asset/asset.h"
|
||||
#include "script/scriptmanager.h"
|
||||
#include "script/module/scene/modulescene.h"
|
||||
#include "ui/ui.h"
|
||||
#include "assert/assert.h"
|
||||
#include "network/network.h"
|
||||
#include "network/socket/client/client.h"
|
||||
#include "network/socket/client/clientroster.h"
|
||||
#include "network/socket/server/serveronline.h"
|
||||
#include "game/game.h"
|
||||
#ifdef DUSK_NETWORK
|
||||
#include "network/network.h"
|
||||
#endif
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
#include "save/save.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "log/log.h"
|
||||
#include "save/save.h"\
|
||||
|
||||
engine_t ENGINE;
|
||||
|
||||
@@ -44,15 +39,15 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
errorChain(scriptManagerInit());
|
||||
errorChain(saveInit());
|
||||
errorChain(saveSettingsLoad());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(networkInit());
|
||||
errorChain(rpgInit());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkInit());
|
||||
#endif
|
||||
errorChain(sceneInit());
|
||||
errorChain(gameInit());
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
|
||||
@@ -62,38 +57,28 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
consolePrint("Assertions real");
|
||||
#endif
|
||||
|
||||
// sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
sceneSet(SCENE_TYPE_INITIAL);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
errorChain(networkUpdate());
|
||||
errorChain(clientUpdate());
|
||||
errorChain(serverOnlineUpdate());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkUpdate());
|
||||
#endif
|
||||
errorChain(saveUpdate());
|
||||
timeUpdate();
|
||||
|
||||
const systemdialogtype_t dialogType = systemGetActiveDialogType();
|
||||
if(dialogType == SYSTEM_DIALOG_TYPE_NONE) {
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
|
||||
errorChain(gameUpdate());
|
||||
errorChain(moduleSceneUpdateCurrent());
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
}
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
errorChain(rpgUpdate());
|
||||
errorChain(sceneUpdate());
|
||||
errorChain(assetUpdate());
|
||||
errorChain(uiUpdate());
|
||||
|
||||
// Render
|
||||
errorChain(displayUpdate());
|
||||
if(
|
||||
dialogType == SYSTEM_DIALOG_TYPE_NONE &&
|
||||
inputPressed(INPUT_BIND_RAGEQUIT)
|
||||
) {
|
||||
ENGINE.running = false;
|
||||
}
|
||||
if(inputPressed(INPUT_ACTION_RAGEQUIT)) ENGINE.running = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -102,20 +87,16 @@ void engineExit(void) {
|
||||
}
|
||||
|
||||
errorret_t engineDispose(void) {
|
||||
errorChain(gameDispose());
|
||||
errorChain(sceneDispose());
|
||||
|
||||
errorChain(serverOnlineStop());
|
||||
client_t *localClient = clientRosterFindLocal();
|
||||
if(localClient) localClientDisconnect(&localClient->local);
|
||||
|
||||
errorChain(networkDispose());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkDispose());
|
||||
#endif
|
||||
errorChain(rpgDispose());
|
||||
localeManagerDispose();
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
errorChain(saveDispose());
|
||||
errorChain(scriptManagerDispose());
|
||||
errorChain(assetDispose());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -37,3 +37,4 @@ errorret_t engineUpdate(void);
|
||||
* Shuts down the engine.
|
||||
*/
|
||||
errorret_t engineDispose(void);
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
entity.c
|
||||
entitymanager.c
|
||||
component.c
|
||||
entityprefab.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(component)
|
||||
@@ -1,152 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitymanager.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
componentdefinition_t COMPONENT_DEFINITIONS[] = {
|
||||
[COMPONENT_TYPE_NULL] = { 0 },
|
||||
|
||||
#define X(enm, type, field, iMethod, dMethod, rMethod) \
|
||||
[COMPONENT_TYPE_##enm] = { \
|
||||
.enumName = #enm, \
|
||||
.name = #field, \
|
||||
.init = iMethod, \
|
||||
.dispose = dMethod, \
|
||||
.render = rMethod \
|
||||
},
|
||||
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
[COMPONENT_TYPE_COUNT] = { 0 }
|
||||
};
|
||||
|
||||
void componentInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
||||
|
||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||
component_t *cmp = &mgr->components[index];
|
||||
memoryZero(cmp, sizeof(component_t));
|
||||
|
||||
cmp->type = type;
|
||||
if(COMPONENT_DEFINITIONS[type].init) {
|
||||
COMPONENT_DEFINITIONS[type].init(mgr, entityId, componentId);
|
||||
}
|
||||
}
|
||||
|
||||
void * componentGetData(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
||||
|
||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||
component_t *cmp = &mgr->components[index];
|
||||
assertTrue(cmp->type == type, "Component type mismatch");
|
||||
|
||||
return &cmp->data;
|
||||
}
|
||||
|
||||
componentindex_t componentGetIndex(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId;
|
||||
}
|
||||
|
||||
entityid_t componentGetEntitiesWithComponent(
|
||||
entitymanager_t *mgr,
|
||||
const componenttype_t type,
|
||||
entityid_t outEntities[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_NULL, "Cannot check NULL type");
|
||||
assertNotNull(outEntities, "Output entities array cannot be null");
|
||||
assertNotNull(outComponents, "Output components array cannot be null");
|
||||
|
||||
entityid_t written = 0;
|
||||
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||
componentid_t used = mgr->entitiesWithComponent[
|
||||
type * ENTITY_COUNT_MAX + i
|
||||
];
|
||||
if(used == COMPONENT_ID_INVALID) continue;
|
||||
assertTrue(
|
||||
mgr->components[componentGetIndex(i, used)].type == type,
|
||||
"Component type mismatch in entitiesWithComponent lookup"
|
||||
);
|
||||
assertTrue(
|
||||
(mgr->entities[i].state & ENTITY_STATE_ACTIVE) != 0,
|
||||
"Inactive entity in entitiesWithComponent lookup"
|
||||
);
|
||||
assertTrue(
|
||||
used < ENTITY_COMPONENT_COUNT_MAX,
|
||||
"Component ID OOB in entitiesWithComponent lookup"
|
||||
);
|
||||
assertTrue(
|
||||
componentGetIndex(i,used) < ENTITY_COUNT_MAX*ENTITY_COMPONENT_COUNT_MAX,
|
||||
"Component index OOB in entitiesWithComponent lookup"
|
||||
);
|
||||
outComponents[written] = used;
|
||||
outEntities[written++] = i;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
assertNotNull(mgr, "Entity manager cannot be null");
|
||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||
|
||||
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||
component_t *cmp = &mgr->components[index];
|
||||
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
||||
|
||||
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
||||
COMPONENT_DEFINITIONS[cmp->type].dispose(mgr, entityId, componentId);
|
||||
}
|
||||
|
||||
cmp->type = COMPONENT_TYPE_NULL;
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "entitybase.h"
|
||||
#include "error/error.h"
|
||||
|
||||
#define X(enumName, type, field, init, dispose, render) \
|
||||
// do nothing
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
typedef union {
|
||||
#define X(enumName, type, field, init, dispose, render) type field;
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
} 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 {
|
||||
const char_t *enumName;
|
||||
const char_t *name;
|
||||
componentcallback_t init;
|
||||
componentcallback_t dispose;
|
||||
componentcallbackerror_t render;
|
||||
} componentdefinition_t;
|
||||
|
||||
typedef enum {
|
||||
COMPONENT_TYPE_NULL,
|
||||
|
||||
#define X(enumName, type, field, init, dispose, render) \
|
||||
COMPONENT_TYPE_##enumName,
|
||||
#include "componentlist.h"
|
||||
#undef X
|
||||
|
||||
COMPONENT_TYPE_COUNT
|
||||
} componenttype_t;
|
||||
|
||||
typedef struct {
|
||||
componenttype_t type;
|
||||
componentdata_t data;
|
||||
} component_t;
|
||||
|
||||
extern componentdefinition_t COMPONENT_DEFINITIONS[];
|
||||
|
||||
/**
|
||||
* Initializes a component of the given type for the entity with component ID.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The type of the component to initialize.
|
||||
*/
|
||||
void componentInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the pointer to the data of a component for the entity with component ID.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The type of the component to get, only used for assertion.
|
||||
* @return A pointer to the component data.
|
||||
*/
|
||||
void * componentGetData(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const componenttype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the index of a component for the entity with component ID.
|
||||
*
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The index of the component in the component array.
|
||||
*/
|
||||
componentindex_t componentGetIndex(
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the entity IDs of all entities with a component of the given type.
|
||||
*
|
||||
* @param mgr The entity manager to search.
|
||||
* @param type The type of the component to get entities for.
|
||||
* @param outEntities An array to write the entity IDs to, must be at least
|
||||
* ENTITY_COUNT_MAX in size.
|
||||
* @param outComponents An array to write the component IDs to.
|
||||
* @return The number of entity IDs written to outEntities.
|
||||
*/
|
||||
entityid_t componentGetEntitiesWithComponent(
|
||||
entitymanager_t *mgr,
|
||||
const componenttype_t type,
|
||||
entityid_t outEntities[ENTITY_COUNT_MAX],
|
||||
componentid_t outComponents[ENTITY_COUNT_MAX]
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of a component for the entity with component ID.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
*/
|
||||
void componentDispose(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Calls the render callback on every active component that defines one.
|
||||
* Iterates all active entities and all their component slots. No-op for
|
||||
* components whose definition has render == NULL.
|
||||
*
|
||||
* @param mgr The entity manager to render.
|
||||
* @return Error state.
|
||||
*/
|
||||
errorret_t componentRenderAll(entitymanager_t *mgr);
|
||||
@@ -1,10 +0,0 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(physics)
|
||||
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
|
||||
);
|
||||
@@ -1,140 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entity/entitymanager.h"
|
||||
#include "entity/entity.h"
|
||||
#include "entity/component/display/entityposition.h"
|
||||
#include "display/screen/screen.h"
|
||||
|
||||
void entityCameraInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||
);
|
||||
cam->nearClip = 0.1f;
|
||||
cam->farClip = 5000.0f;
|
||||
cam->projType = ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||
cam->perspective.fov = glm_rad(45.0f);
|
||||
}
|
||||
|
||||
void entityCameraGetProjection(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 out
|
||||
) {
|
||||
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_CAMERA
|
||||
);
|
||||
|
||||
if(
|
||||
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
|
||||
cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
|
||||
) {
|
||||
glm_mat4_identity(out);
|
||||
glm_perspective(
|
||||
cam->perspective.fov,
|
||||
SCREEN.aspect,
|
||||
cam->nearClip,
|
||||
cam->farClip,
|
||||
out
|
||||
);
|
||||
|
||||
if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
|
||||
out[1][1] *= -1.0f;
|
||||
}
|
||||
} else if(cam->projType == ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||
glm_mat4_identity(out);
|
||||
glm_ortho(
|
||||
cam->orthographic.left,
|
||||
cam->orthographic.right,
|
||||
cam->orthographic.top,
|
||||
cam->orthographic.bottom,
|
||||
cam->nearClip,
|
||||
cam->farClip,
|
||||
out
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
entityid_t entityCameraGetCurrent(entitymanager_t *mgr) {
|
||||
entityid_t camEnts[ENTITY_COUNT_MAX];
|
||||
componentid_t camComps[ENTITY_COUNT_MAX];
|
||||
entityid_t count = componentGetEntitiesWithComponent(
|
||||
mgr, COMPONENT_TYPE_CAMERA, camEnts, camComps
|
||||
);
|
||||
if(count == 0) return ENTITY_ID_INVALID;
|
||||
return camEnts[0];
|
||||
}
|
||||
|
||||
void entityCameraGetForward(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
vec2 out
|
||||
) {
|
||||
componentid_t posComp = entityGetComponent(
|
||||
mgr, entityId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
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);
|
||||
if(len > 1e-6f) { fx /= len; fz /= len; }
|
||||
out[0] = fx;
|
||||
out[1] = fz;
|
||||
}
|
||||
|
||||
void entityCameraGetRight(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
vec2 out
|
||||
) {
|
||||
componentid_t posComp = entityGetComponent(
|
||||
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);
|
||||
if(len > 1e-6f) { rx /= len; rz /= len; }
|
||||
out[0] = rx;
|
||||
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);
|
||||
}
|
||||
@@ -1,122 +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 "error/error.h"
|
||||
|
||||
typedef enum {
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE,
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED,
|
||||
ENTITY_CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC
|
||||
} entitycameraprojectiontype_t;
|
||||
|
||||
typedef struct {
|
||||
union {
|
||||
struct {
|
||||
float_t fov;
|
||||
} perspective;
|
||||
|
||||
struct {
|
||||
float_t left;
|
||||
float_t right;
|
||||
float_t top;
|
||||
float_t bottom;
|
||||
} orthographic;
|
||||
};
|
||||
|
||||
float_t nearClip;
|
||||
float_t farClip;
|
||||
entitycameraprojectiontype_t projType;
|
||||
} entitycamera_t;
|
||||
|
||||
/**
|
||||
* Initializes an entity camera component.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
*/
|
||||
void entityCameraInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders out the projection matrix for the given camera.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param out The output projection matrix.
|
||||
*/
|
||||
void entityCameraGetProjection(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 out
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the entity ID of the first active camera, or ENTITY_ID_INVALID if
|
||||
* none are active.
|
||||
*
|
||||
* @param mgr The entity manager to search.
|
||||
*/
|
||||
entityid_t entityCameraGetCurrent(entitymanager_t *mgr);
|
||||
|
||||
/**
|
||||
* Gets the camera's horizontal forward direction (XZ plane) from its
|
||||
* position 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 out Output vec2: {forwardX, forwardZ} normalized.
|
||||
*/
|
||||
void entityCameraGetForward(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
vec2 out
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the camera's horizontal right direction (XZ plane) from its position
|
||||
* component. Automatically finds the position component on the entity.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The camera entity ID.
|
||||
* @param out Output vec2: {rightX, rightZ} normalized.
|
||||
*/
|
||||
void entityCameraGetRight(
|
||||
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
|
||||
);
|
||||
@@ -1,669 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entity/entitymanager.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityPositionInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, 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->rotation);
|
||||
glm_vec3_one(pos->scale);
|
||||
glm_mat4_identity(pos->localTransform);
|
||||
glm_mat4_identity(pos->worldTransform);
|
||||
}
|
||||
|
||||
void entityPositionLookAt(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 eye,
|
||||
vec3 target,
|
||||
vec3 up
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
// glm_lookat() produces a view matrix (world -> eye space). Every other
|
||||
// setter treats localTransform as this entity's placement in world space
|
||||
// (eye -> world), so invert it here to keep that meaning consistent --
|
||||
// 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionEnsureWorld(mgr, pos);
|
||||
glm_mat4_copy(
|
||||
pos->parentEntityId == ENTITY_ID_INVALID
|
||||
? pos->localTransform : pos->worldTransform,
|
||||
dest
|
||||
);
|
||||
}
|
||||
|
||||
void entityPositionGetLocalTransform(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionEnsureLocal(pos);
|
||||
glm_mat4_copy(pos->localTransform, dest);
|
||||
}
|
||||
|
||||
void entityPositionGetLocalPosition(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionEnsurePRS(pos);
|
||||
glm_vec3_copy(pos->position, dest);
|
||||
}
|
||||
|
||||
void entityPositionGetWorldPosition(
|
||||
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 componentid_t componentId,
|
||||
vec3 position
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, 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);
|
||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_POSITION_DIRTY)
|
||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||
entityPositionMarkDirty(mgr, pos);
|
||||
}
|
||||
|
||||
void entityPositionGetLocalRotation(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionEnsurePRS(pos);
|
||||
glm_vec3_copy(pos->rotation, dest);
|
||||
}
|
||||
|
||||
void entityPositionGetWorldRotation(
|
||||
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 componentid_t componentId,
|
||||
vec3 rotation
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(rotation, pos->rotation);
|
||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||
& ~ENTITY_POSITION_FLAG_PRS_DIRTY;
|
||||
entityPositionMarkDirty(mgr, pos);
|
||||
}
|
||||
|
||||
void entityPositionSetWorldRotation(
|
||||
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 componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
entityPositionEnsurePRS(pos);
|
||||
glm_vec3_copy(pos->scale, dest);
|
||||
}
|
||||
|
||||
void entityPositionGetWorldScale(
|
||||
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 componentid_t componentId,
|
||||
vec3 scale
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
glm_vec3_copy(scale, pos->scale);
|
||||
pos->flags = (pos->flags | ENTITY_POSITION_FLAG_ROTATION_DIRTY)
|
||||
& ~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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityid_t parentEntityId,
|
||||
const componentid_t parentComponentId
|
||||
) {
|
||||
entityposition_t *pos = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
|
||||
// Remove from old parent's child list.
|
||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||
entityposition_t *oldParent = componentGetData(
|
||||
mgr, pos->parentEntityId, pos->parentComponentId,
|
||||
COMPONENT_TYPE_POSITION
|
||||
);
|
||||
for(uint8_t i = 0; i < oldParent->childCount; i++) {
|
||||
if(
|
||||
oldParent->childEntityIds[i] == entityId &&
|
||||
oldParent->childComponentIds[i] == componentId
|
||||
) {
|
||||
oldParent->childCount--;
|
||||
for(uint8_t j = i; j < oldParent->childCount; j++) {
|
||||
oldParent->childEntityIds[j] = oldParent->childEntityIds[j + 1];
|
||||
oldParent->childComponentIds[j] = oldParent->childComponentIds[j + 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos->parentEntityId = parentEntityId;
|
||||
pos->parentComponentId = parentComponentId;
|
||||
|
||||
// Register with new parent.
|
||||
if(parentEntityId != ENTITY_ID_INVALID) {
|
||||
entityposition_t *parent = componentGetData(
|
||||
mgr, parentEntityId, parentComponentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
if(parent->childCount < ENTITY_POSITION_CHILDREN_MAX) {
|
||||
parent->childEntityIds[parent->childCount] = entityId;
|
||||
parent->childComponentIds[parent->childCount] = componentId;
|
||||
parent->childCount++;
|
||||
}
|
||||
}
|
||||
|
||||
entityPositionMarkDirty(mgr, 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
return componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_POSITION
|
||||
);
|
||||
}
|
||||
|
||||
void entityPositionRebuild(entitymanager_t *mgr, entityposition_t *pos) {
|
||||
pos->flags = (
|
||||
pos->flags |
|
||||
ENTITY_POSITION_FLAG_ROTATION_DIRTY |
|
||||
ENTITY_POSITION_FLAG_POSITION_DIRTY
|
||||
) & ~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);
|
||||
}
|
||||
}
|
||||
|
||||
void entityPositionDisposeDeep(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityposition_t *pos = entityPositionGet(mgr, entityId, componentId);
|
||||
|
||||
// Detach from parent so the parent's child list stays consistent.
|
||||
if(pos->parentEntityId != ENTITY_ID_INVALID) {
|
||||
entityPositionSetParent(
|
||||
mgr, entityId, componentId, ENTITY_ID_INVALID, COMPONENT_ID_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
// Copy the child list before disposing self (entityDispose invalidates
|
||||
// pos).
|
||||
uint8_t childCount = pos->childCount;
|
||||
entityid_t childEntityIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||
componentid_t childComponentIds[ENTITY_POSITION_CHILDREN_MAX];
|
||||
for(uint8_t i = 0; i < childCount; i++) {
|
||||
childEntityIds[i] = pos->childEntityIds[i];
|
||||
childComponentIds[i] = pos->childComponentIds[i];
|
||||
// Sever child's parent link so it won't try to modify our disposed
|
||||
// data.
|
||||
entityposition_t *child = entityPositionGet(
|
||||
mgr, childEntityIds[i], childComponentIds[i]
|
||||
);
|
||||
child->parentEntityId = ENTITY_ID_INVALID;
|
||||
child->parentComponentId = COMPONENT_ID_INVALID;
|
||||
}
|
||||
|
||||
entityDispose(mgr, entityId);
|
||||
|
||||
for(uint8_t i = 0; i < childCount; i++) {
|
||||
entityPositionDisposeDeep(mgr, childEntityIds[i], childComponentIds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void entityPositionDecompose(entityposition_t *pos) {
|
||||
// Translation: column 3
|
||||
pos->position[0] = pos->localTransform[3][0];
|
||||
pos->position[1] = pos->localTransform[3][1];
|
||||
pos->position[2] = pos->localTransform[3][2];
|
||||
|
||||
// Scale: length of each basis column (xyz only)
|
||||
pos->scale[0] = sqrtf(
|
||||
pos->localTransform[0][0] * pos->localTransform[0][0] +
|
||||
pos->localTransform[0][1] * pos->localTransform[0][1] +
|
||||
pos->localTransform[0][2] * pos->localTransform[0][2]
|
||||
);
|
||||
pos->scale[1] = sqrtf(
|
||||
pos->localTransform[1][0] * pos->localTransform[1][0] +
|
||||
pos->localTransform[1][1] * pos->localTransform[1][1] +
|
||||
pos->localTransform[1][2] * pos->localTransform[1][2]
|
||||
);
|
||||
pos->scale[2] = sqrtf(
|
||||
pos->localTransform[2][0] * pos->localTransform[2][0] +
|
||||
pos->localTransform[2][1] * pos->localTransform[2][1] +
|
||||
pos->localTransform[2][2] * pos->localTransform[2][2]
|
||||
);
|
||||
|
||||
// Normalize columns to isolate the rotation matrix (no mat4 needed).
|
||||
const float_t 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;
|
||||
const float_t invS2 = pos->scale[2] > 0.0f ? 1.0f / pos->scale[2] : 0.0f;
|
||||
|
||||
const float_t r00 = pos->localTransform[0][0] * invS0;
|
||||
const float_t r01 = pos->localTransform[0][1] * invS0;
|
||||
const float_t r02 = pos->localTransform[0][2] * invS0;
|
||||
const float_t r10 = pos->localTransform[1][0] * invS1;
|
||||
const float_t r11 = pos->localTransform[1][1] * invS1;
|
||||
const float_t r20 = pos->localTransform[2][0] * invS2;
|
||||
const float_t r21 = pos->localTransform[2][1] * invS2;
|
||||
const float_t r22 = pos->localTransform[2][2] * invS2;
|
||||
|
||||
// Extract XYZ euler angles (R = Rx * Ry * Rz, column-major)
|
||||
const float_t sinBeta = glm_clamp(r20, -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(-r21, r22);
|
||||
pos->rotation[2] = atan2f(-r10, r00);
|
||||
} else {
|
||||
// Gimbal lock: pin Z to 0, recover X.
|
||||
pos->rotation[2] = 0.0f;
|
||||
pos->rotation[0] = (sinBeta > 0.0f)
|
||||
? atan2f(r01, r11)
|
||||
: -atan2f(r01, r11);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,460 +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 "error/error.h"
|
||||
|
||||
/** Maximum number of child position components this node can track. */
|
||||
#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 {
|
||||
/*
|
||||
* 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;
|
||||
/** World transform matrix, recomputed lazily from the parent chain. */
|
||||
mat4 worldTransform;
|
||||
} entityposition_t;
|
||||
|
||||
/**
|
||||
* Initializes the entity position component, setting identity transforms and
|
||||
* 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Positions and orients the entity at eye, facing target. Stores this as
|
||||
* 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 componentId The component ID.
|
||||
* @param eye The eye/camera position.
|
||||
* @param target The target point to look at.
|
||||
* @param up The up vector.
|
||||
*/
|
||||
void entityPositionLookAt(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 eye,
|
||||
vec3 target,
|
||||
vec3 up
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 componentId The component ID.
|
||||
* @param dest Destination matrix.
|
||||
*/
|
||||
void entityPositionGetTransform(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 componentId The component ID.
|
||||
* @param dest Destination matrix.
|
||||
*/
|
||||
void entityPositionGetLocalTransform(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
mat4 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the cached local position (XYZ). Decomposes localTransform into PRS
|
||||
* 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the world-space position. For parentless entities this is the same as
|
||||
* 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(
|
||||
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 componentid_t componentId,
|
||||
vec3 position
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the local position, marks localTransform and worldTransform (self +
|
||||
* 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(
|
||||
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 componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the world-space euler rotation (XYZ, radians) by decomposing the
|
||||
* 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(
|
||||
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 componentid_t componentId,
|
||||
vec3 rotation
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the world-space euler rotation (XYZ, radians). For parentless
|
||||
* 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(
|
||||
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 componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the world-space scale by extracting column lengths from the world
|
||||
* 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(
|
||||
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 componentid_t componentId,
|
||||
vec3 scale
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the parent of this entity's position component.
|
||||
* Pass ENTITY_ID_INVALID / COMPONENT_ID_INVALID to detach from any parent.
|
||||
*
|
||||
* @param mgr The entity manager that owns both entities.
|
||||
* @param entityId The child entity ID.
|
||||
* @param componentId The child component ID.
|
||||
* @param parentEntityId The parent entity ID.
|
||||
* @param parentComponentId The parent component ID.
|
||||
*/
|
||||
void entityPositionSetParent(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityid_t parentEntityId,
|
||||
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.
|
||||
* After modifying localTransform directly, call entityPositionMarkDirty() to
|
||||
* 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Signals that the PRS cache was modified externally. Sets both
|
||||
* ENTITY_POSITION_FLAG_ROTATION_DIRTY and ENTITY_POSITION_FLAG_POSITION_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);
|
||||
|
||||
/**
|
||||
* Sets ENTITY_POSITION_FLAG_WORLD_DIRTY on this node and all descendants,
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Disposes this entity and all of its position-component descendants
|
||||
* recursively. Detaches from any parent before destroying.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The root entity ID.
|
||||
* @param componentId The root position component ID.
|
||||
*/
|
||||
void entityPositionDisposeDeep(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Decomposes the local transform matrix back into the position, rotation
|
||||
* (XYZ euler, radians), and scale cache fields.
|
||||
*
|
||||
* @param pos The position component to decompose.
|
||||
*/
|
||||
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);
|
||||
@@ -1,192 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entityrenderable.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "display/shader/shadermaterial.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "display/display.h"
|
||||
#include "display/mesh/cube.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityRenderableInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
memoryZero(r, sizeof(entityrenderable_t));
|
||||
r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL;
|
||||
r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT;
|
||||
r->data.material.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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
}
|
||||
|
||||
void entityRenderableSetType(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityrenderabletype_t type
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->type = type;
|
||||
}
|
||||
|
||||
void entityRenderableSetPriority(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const int8_t priority
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
r->priority = priority;
|
||||
}
|
||||
|
||||
void entityRenderableSetColor(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const color_t color
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
assertTrue(
|
||||
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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint8_t slot,
|
||||
mesh_t *mesh
|
||||
) {
|
||||
assertNotNull(mesh, "Mesh cannot be null");
|
||||
assertTrue(slot < ENTITY_RENDERABLE_MESHES_MAX, "Mesh slot out of bounds");
|
||||
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
assertTrue(
|
||||
r->type == ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||
"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(
|
||||
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 componentid_t componentId
|
||||
) {
|
||||
entityrenderable_t *r = componentGetData(
|
||||
mgr, entityId, componentId, COMPONENT_TYPE_RENDERABLE
|
||||
);
|
||||
switch(r->type) {
|
||||
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(
|
||||
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 componentid_t componentId,
|
||||
const entityrenderablecustom_t *custom
|
||||
) {
|
||||
return custom->draw(mgr, entityId, componentId, custom->drawUser);
|
||||
}
|
||||
@@ -1,234 +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 "display/mesh/mesh.h"
|
||||
#include "display/shader/shadermaterial.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/displaystate.h"
|
||||
#include "error/error.h"
|
||||
|
||||
#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64
|
||||
#define ENTITY_RENDERABLE_MESHES_MAX 8
|
||||
|
||||
typedef enum {
|
||||
ENTITY_RENDERABLE_TYPE_CUSTOM = 0,
|
||||
ENTITY_RENDERABLE_TYPE_SPRITEBATCH,
|
||||
ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL,
|
||||
} entityrenderabletype_t;
|
||||
|
||||
typedef struct {
|
||||
spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX];
|
||||
uint32_t spriteCount;
|
||||
texture_t *texture;
|
||||
} 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 {
|
||||
entityrenderabletype_t type;
|
||||
entityrenderabledata_t data;
|
||||
|
||||
/**
|
||||
* Render priority. 0 = auto (derived from type/flags). Higher values
|
||||
* render later (on top of lower values). Range: [-128..127] with 0 auto.
|
||||
*/
|
||||
int8_t priority;
|
||||
} entityrenderable_t;
|
||||
|
||||
/**
|
||||
* Initializes the entity renderable component. Defaults to
|
||||
* ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL with the unlit shader, a white
|
||||
* 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes the entity renderable component.
|
||||
*
|
||||
* @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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the rendering type for the renderable component. Resets
|
||||
* type-specific data to zero.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity to configure.
|
||||
* @param componentId The renderable component.
|
||||
* @param type The rendering type to use.
|
||||
*/
|
||||
void entityRenderableSetType(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityrenderabletype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the render priority. 0 = auto (derived from type/flags). Higher
|
||||
* 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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const int8_t priority
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the unlit material color. Only meaningful when the renderable is
|
||||
* (or defaults to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts
|
||||
* otherwise.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity to configure.
|
||||
* @param componentId The renderable component.
|
||||
* @param color The color to tint the material with.
|
||||
*/
|
||||
void entityRenderableSetColor(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const color_t color
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets one of the material renderable's meshes, drawn in full (no offset/
|
||||
* count override). Only meaningful when the renderable is (or defaults
|
||||
* to) ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL -- asserts otherwise.
|
||||
*
|
||||
* @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(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint8_t slot,
|
||||
mesh_t *mesh
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the draw callback, switching the type to
|
||||
* ENTITY_RENDERABLE_TYPE_CUSTOM.
|
||||
*
|
||||
* @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(
|
||||
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 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
|
||||
);
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entityphysics.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void entityPhysicsInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
|
||||
memoryZero(phys, sizeof(entityphysics_t));
|
||||
|
||||
// Default to cube
|
||||
phys->type = PHYSICS_BODY_DYNAMIC;
|
||||
phys->shape.type = PHYSICS_SHAPE_CUBE;
|
||||
phys->shape.data.cube.halfExtents[0] = 0.5f;
|
||||
phys->shape.data.cube.halfExtents[1] = 0.5f;
|
||||
phys->shape.data.cube.halfExtents[2] = 0.5f;
|
||||
phys->gravityScale = 1.0f;
|
||||
phys->onGround = false;
|
||||
phys->collideMask = 0x1;
|
||||
}
|
||||
|
||||
entityphysics_t *entityPhysicsGet(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_PHYSICS);
|
||||
}
|
||||
|
||||
void entityPhysicsSetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsshape_t shape
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
phys->shape = shape;
|
||||
}
|
||||
|
||||
physicsshape_t entityPhysicsGetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
return phys->shape;
|
||||
}
|
||||
|
||||
void entityPhysicsGetVelocity(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
glm_vec3_copy(phys->velocity, dest);
|
||||
}
|
||||
|
||||
void entityPhysicsSetVelocity(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 velocity
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
glm_vec3_copy(velocity, phys->velocity);
|
||||
}
|
||||
|
||||
void entityPhysicsApplyImpulse(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 impulse
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
if(phys->type == PHYSICS_BODY_STATIC) return;
|
||||
glm_vec3_add(phys->velocity, impulse, phys->velocity);
|
||||
}
|
||||
|
||||
bool_t entityPhysicsIsOnGround(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
return phys->onGround;
|
||||
}
|
||||
|
||||
void entityPhysicsSetBodyType(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsbodytype_t type
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
phys->type = type;
|
||||
}
|
||||
|
||||
physicsbodytype_t entityPhysicsGetBodyType(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
return phys->type;
|
||||
}
|
||||
|
||||
void entityPhysicsSetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint32_t mask
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
phys->collideMask = mask;
|
||||
}
|
||||
|
||||
uint32_t entityPhysicsGetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entityphysics_t *phys = entityPhysicsGet(mgr, entityId, componentId);
|
||||
return phys->collideMask;
|
||||
}
|
||||
@@ -1,211 +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 "physics/physicsshape.h"
|
||||
#include "physics/physicsbodytype.h"
|
||||
#include "error/error.h"
|
||||
|
||||
typedef struct {
|
||||
physicsbodytype_t type;
|
||||
physicsshape_t shape;
|
||||
vec3 velocity;
|
||||
float_t gravityScale;
|
||||
bool_t onGround;
|
||||
|
||||
/**
|
||||
* Bitmask of collision layers/categories this body belongs to. Two
|
||||
* bodies only collide with each other if (a.collideMask & b.collideMask)
|
||||
* is non-zero -- i.e. they share at least one set bit. Defaults to 0x1
|
||||
* (only bit 0 set).
|
||||
*/
|
||||
uint32_t collideMask;
|
||||
} entityphysics_t;
|
||||
|
||||
/**
|
||||
* Initializes the physics component: defaults to a dynamic 1x1x1 cube
|
||||
* body, zero velocity, unit gravity scale, and onGround false.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
*/
|
||||
void entityPhysicsInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the underlying physics structure (temporarily) for the given entity.
|
||||
* This is really just intended for doing operations faster than using the
|
||||
* getters and setters, but it is preferred that you use those.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The physics component data for the given entity and component ID.
|
||||
*/
|
||||
entityphysics_t *entityPhysicsGet(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the shape of the entity's physics body. This will not reset the body
|
||||
* state, so if you change from a cube to a sphere, it will keep the same
|
||||
* velocity and onGround state.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param shape The new shape to set on the physics body.
|
||||
*/
|
||||
void entityPhysicsSetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsshape_t shape
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the shape of the entity's physics body.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The shape of the physics body.
|
||||
*/
|
||||
physicsshape_t entityPhysicsGetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the velocity of the entity's physics body.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param dest The destination vec3 to write the velocity to.
|
||||
*/
|
||||
void entityPhysicsGetVelocity(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 dest
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the velocity of the entity's physics body. This is not an impulse, so
|
||||
* it will be affected by mass and drag.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param velocity The new velocity to set on the physics body.
|
||||
*/
|
||||
void entityPhysicsSetVelocity(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 velocity
|
||||
);
|
||||
|
||||
/**
|
||||
* Applies an impulse to the entity's physics body. This is an immediate
|
||||
* velocity change that is not affected by mass or drag. No-op on STATIC
|
||||
* bodies.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param impulse The impulse to apply to the physics body.
|
||||
*/
|
||||
void entityPhysicsApplyImpulse(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
vec3 impulse
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the entity's physics body rested on a surface during the
|
||||
* last physicsWorldStep.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return True if the body is on the ground, false otherwise.
|
||||
*/
|
||||
bool_t entityPhysicsIsOnGround(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the body type of the entity's physics body.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param type The body type to set.
|
||||
*/
|
||||
void entityPhysicsSetBodyType(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsbodytype_t type
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the body type of the entity's physics body.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The body type of the physics body.
|
||||
*/
|
||||
physicsbodytype_t entityPhysicsGetBodyType(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the collision mask of the entity's physics body. Two bodies only
|
||||
* collide if their masks share at least one set bit.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param mask The new collision mask to set on the physics body.
|
||||
*/
|
||||
void entityPhysicsSetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint32_t mask
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the collision mask of the entity's physics body.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The collision mask of the physics body.
|
||||
*/
|
||||
uint32_t entityPhysicsGetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitytrigger.h"
|
||||
#include "entity/entitymanager.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void entityTriggerInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
|
||||
memoryZero(trig, sizeof(entitytrigger_t));
|
||||
|
||||
trig->shape.type = PHYSICS_SHAPE_CUBE;
|
||||
trig->shape.data.cube.halfExtents[0] = 0.5f;
|
||||
trig->shape.data.cube.halfExtents[1] = 0.5f;
|
||||
trig->shape.data.cube.halfExtents[2] = 0.5f;
|
||||
trig->collideMask = 0x1;
|
||||
}
|
||||
|
||||
entitytrigger_t *entityTriggerGet(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
return componentGetData(mgr, entityId, componentId, COMPONENT_TYPE_TRIGGER);
|
||||
}
|
||||
|
||||
void entityTriggerSetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsshape_t shape
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
trig->shape = shape;
|
||||
}
|
||||
|
||||
physicsshape_t entityTriggerGetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
return trig->shape;
|
||||
}
|
||||
|
||||
void entityTriggerSetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint32_t mask
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
trig->collideMask = mask;
|
||||
}
|
||||
|
||||
uint32_t entityTriggerGetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
return trig->collideMask;
|
||||
}
|
||||
|
||||
uint8_t entityTriggerGetOccupantCount(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
return trig->occupantCount;
|
||||
}
|
||||
|
||||
entityid_t entityTriggerGetOccupantEntityId(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint8_t index
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
if(index >= trig->occupantCount) return ENTITY_ID_INVALID;
|
||||
return trig->occupants[index].entityId;
|
||||
}
|
||||
|
||||
bool_t entityTriggerIsOccupyingEntity(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityid_t otherEntityId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
for(uint8_t i = 0; i < trig->occupantCount; i++) {
|
||||
if(trig->occupants[i].entityId == otherEntityId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,226 +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 "physics/physicsshape.h"
|
||||
#include "error/error.h"
|
||||
|
||||
/** Maximum number of entities a single trigger can track at once. */
|
||||
#define ENTITY_TRIGGER_OCCUPANTS_MAX 8
|
||||
|
||||
/** Maximum number of subscribers per trigger event (onEnter/onActive/...). */
|
||||
#define ENTITY_TRIGGER_CALLBACK_COUNT_MAX 4
|
||||
|
||||
/**
|
||||
* Callback invoked for a trigger's onEnter/onActive/onMove/onLeave events.
|
||||
* See entityTriggerOnEnterAdd() and its onActive/onMove/onLeave siblings.
|
||||
*
|
||||
* @param mgr The entity manager that owns both entities.
|
||||
* @param triggerEntityId The entity ID of the trigger volume.
|
||||
* @param triggerComponentId The TRIGGER component ID.
|
||||
* @param otherEntityId The entity ID that entered/is inside/left the
|
||||
* trigger volume.
|
||||
* @param otherComponentId The other entity's POSITION component ID.
|
||||
* @param user The user pointer passed to the matching *Add() call.
|
||||
*/
|
||||
typedef void (*triggercallback_t)(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId,
|
||||
void *user
|
||||
);
|
||||
|
||||
/** Tracks one entity currently overlapping a trigger volume. */
|
||||
typedef struct {
|
||||
/** The occupying entity's ID. */
|
||||
entityid_t entityId;
|
||||
/** The occupying entity's POSITION component ID. */
|
||||
componentid_t componentId;
|
||||
/** The occupying entity's local position as of the last step, used to
|
||||
* detect movement (see entityTriggerOnMoveAdd()). */
|
||||
vec3 lastPosition;
|
||||
} entitytriggeroccupant_t;
|
||||
|
||||
typedef struct {
|
||||
/** The trigger volume's shape, in the owning entity's local space. */
|
||||
physicsshape_t shape;
|
||||
|
||||
/**
|
||||
* Bitmask of collision layers/categories this trigger detects. An
|
||||
* entity's PHYSICS body is only tested against this trigger if
|
||||
* (trigger.collideMask & body.collideMask) is non-zero -- i.e. they
|
||||
* share at least one set bit. Defaults to 0x1 (only bit 0 set). See
|
||||
* entityPhysicsSetCollideMask().
|
||||
*/
|
||||
uint32_t collideMask;
|
||||
|
||||
/** Number of entities currently tracked in occupants. */
|
||||
uint8_t occupantCount;
|
||||
/** Entities currently overlapping this trigger. */
|
||||
entitytriggeroccupant_t occupants[ENTITY_TRIGGER_OCCUPANTS_MAX];
|
||||
|
||||
/** Fired the step an entity begins overlapping this trigger. */
|
||||
uint8_t onEnterCount;
|
||||
triggercallback_t onEnter[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
void *onEnterUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
|
||||
/** Fired every step an entity continues overlapping this trigger,
|
||||
* starting the step after onEnter fired. */
|
||||
uint8_t onActiveCount;
|
||||
triggercallback_t onActive[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
void *onActiveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
|
||||
/** Fired alongside onActive on any step where the entity's position
|
||||
* changed since the previous step. */
|
||||
uint8_t onMoveCount;
|
||||
triggercallback_t onMove[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
void *onMoveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
|
||||
/** Fired the step an entity stops overlapping this trigger. */
|
||||
uint8_t onLeaveCount;
|
||||
triggercallback_t onLeave[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
void *onLeaveUser[ENTITY_TRIGGER_CALLBACK_COUNT_MAX];
|
||||
} entitytrigger_t;
|
||||
|
||||
/**
|
||||
* Initializes the trigger component: defaults to a 1x1x1 cube volume, no
|
||||
* tracked occupants, and no event subscribers.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
*/
|
||||
void entityTriggerInit(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the underlying trigger 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 trigger component data for the given entity and component ID.
|
||||
*/
|
||||
entitytrigger_t *entityTriggerGet(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the shape of the trigger volume.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param shape The new shape to set on the trigger volume.
|
||||
*/
|
||||
void entityTriggerSetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const physicsshape_t shape
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the shape of the trigger volume.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The shape of the trigger volume.
|
||||
*/
|
||||
physicsshape_t entityTriggerGetShape(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the collision mask of the trigger volume. An entity's PHYSICS body
|
||||
* is only detected if their masks share at least one set bit.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param mask The new collision mask to set on the trigger volume.
|
||||
*/
|
||||
void entityTriggerSetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint32_t mask
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the collision mask of the trigger volume.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The collision mask of the trigger volume.
|
||||
*/
|
||||
uint32_t entityTriggerGetCollideMask(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the number of entities currently overlapping this trigger.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @return The number of currently tracked occupants.
|
||||
*/
|
||||
uint8_t entityTriggerGetOccupantCount(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the entity ID of the occupant at the given index, in [0,
|
||||
* entityTriggerGetOccupantCount()). Order is not stable across steps.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param index The occupant index.
|
||||
* @return The occupying entity's ID, or ENTITY_ID_INVALID if index is out
|
||||
* of range.
|
||||
*/
|
||||
entityid_t entityTriggerGetOccupantEntityId(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const uint8_t index
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks whether a specific entity is currently overlapping this trigger.
|
||||
*
|
||||
* @param mgr The entity manager that owns the entity.
|
||||
* @param entityId The entity ID.
|
||||
* @param componentId The component ID.
|
||||
* @param otherEntityId The entity ID to check for.
|
||||
* @return True if otherEntityId is a currently tracked occupant.
|
||||
*/
|
||||
bool_t entityTriggerIsOccupyingEntity(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const entityid_t otherEntityId
|
||||
);
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "entitytriggerevents.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void entityTriggerOnEnterAdd(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
assertTrue(
|
||||
trig->onEnterCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||
"Trigger onEnter callback slots full"
|
||||
);
|
||||
trig->onEnter[trig->onEnterCount] = callback;
|
||||
trig->onEnterUser[trig->onEnterCount] = user;
|
||||
trig->onEnterCount++;
|
||||
}
|
||||
|
||||
void entityTriggerOnEnterRemove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
|
||||
if(trig->onEnter[i] != callback) continue;
|
||||
trig->onEnterCount--;
|
||||
for(uint8_t j = i; j < trig->onEnterCount; j++) {
|
||||
trig->onEnter[j] = trig->onEnter[j + 1];
|
||||
trig->onEnterUser[j] = trig->onEnterUser[j + 1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerOnActiveAdd(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
assertTrue(
|
||||
trig->onActiveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||
"Trigger onActive callback slots full"
|
||||
);
|
||||
trig->onActive[trig->onActiveCount] = callback;
|
||||
trig->onActiveUser[trig->onActiveCount] = user;
|
||||
trig->onActiveCount++;
|
||||
}
|
||||
|
||||
void entityTriggerOnActiveRemove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
|
||||
if(trig->onActive[i] != callback) continue;
|
||||
trig->onActiveCount--;
|
||||
for(uint8_t j = i; j < trig->onActiveCount; j++) {
|
||||
trig->onActive[j] = trig->onActive[j + 1];
|
||||
trig->onActiveUser[j] = trig->onActiveUser[j + 1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerOnMoveAdd(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
assertTrue(
|
||||
trig->onMoveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||
"Trigger onMove callback slots full"
|
||||
);
|
||||
trig->onMove[trig->onMoveCount] = callback;
|
||||
trig->onMoveUser[trig->onMoveCount] = user;
|
||||
trig->onMoveCount++;
|
||||
}
|
||||
|
||||
void entityTriggerOnMoveRemove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
|
||||
if(trig->onMove[i] != callback) continue;
|
||||
trig->onMoveCount--;
|
||||
for(uint8_t j = i; j < trig->onMoveCount; j++) {
|
||||
trig->onMove[j] = trig->onMove[j + 1];
|
||||
trig->onMoveUser[j] = trig->onMoveUser[j + 1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerOnLeaveAdd(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
assertTrue(
|
||||
trig->onLeaveCount < ENTITY_TRIGGER_CALLBACK_COUNT_MAX,
|
||||
"Trigger onLeave callback slots full"
|
||||
);
|
||||
trig->onLeave[trig->onLeaveCount] = callback;
|
||||
trig->onLeaveUser[trig->onLeaveCount] = user;
|
||||
trig->onLeaveCount++;
|
||||
}
|
||||
|
||||
void entityTriggerOnLeaveRemove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t entityId,
|
||||
const componentid_t componentId,
|
||||
const triggercallback_t callback
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(mgr, entityId, componentId);
|
||||
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
|
||||
if(trig->onLeave[i] != callback) continue;
|
||||
trig->onLeaveCount--;
|
||||
for(uint8_t j = i; j < trig->onLeaveCount; j++) {
|
||||
trig->onLeave[j] = trig->onLeave[j + 1];
|
||||
trig->onLeaveUser[j] = trig->onLeaveUser[j + 1];
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerFireEnter(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(
|
||||
mgr, triggerEntityId, triggerComponentId
|
||||
);
|
||||
for(uint8_t i = 0; i < trig->onEnterCount; i++) {
|
||||
trig->onEnter[i](
|
||||
mgr, triggerEntityId, triggerComponentId,
|
||||
otherEntityId, otherComponentId, trig->onEnterUser[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerFireActive(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(
|
||||
mgr, triggerEntityId, triggerComponentId
|
||||
);
|
||||
for(uint8_t i = 0; i < trig->onActiveCount; i++) {
|
||||
trig->onActive[i](
|
||||
mgr, triggerEntityId, triggerComponentId,
|
||||
otherEntityId, otherComponentId, trig->onActiveUser[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerFireMove(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(
|
||||
mgr, triggerEntityId, triggerComponentId
|
||||
);
|
||||
for(uint8_t i = 0; i < trig->onMoveCount; i++) {
|
||||
trig->onMove[i](
|
||||
mgr, triggerEntityId, triggerComponentId,
|
||||
otherEntityId, otherComponentId, trig->onMoveUser[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void entityTriggerFireLeave(
|
||||
entitymanager_t *mgr,
|
||||
const entityid_t triggerEntityId,
|
||||
const componentid_t triggerComponentId,
|
||||
const entityid_t otherEntityId,
|
||||
const componentid_t otherComponentId
|
||||
) {
|
||||
entitytrigger_t *trig = entityTriggerGet(
|
||||
mgr, triggerEntityId, triggerComponentId
|
||||
);
|
||||
for(uint8_t i = 0; i < trig->onLeaveCount; i++) {
|
||||
trig->onLeave[i](
|
||||
mgr, triggerEntityId, triggerComponentId,
|
||||
otherEntityId, otherComponentId, trig->onLeaveUser[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user