Compare commits
49 Commits
9aaffff7a8
..
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 |
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
|||||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||||
|
|
||||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||||
|
option(DUSK_NETWORK "Enable network support" ON)
|
||||||
|
|
||||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
DUSK_VERSION="${DUSK_VERSION}"
|
DUSK_VERSION="${DUSK_VERSION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(DUSK_NETWORK)
|
||||||
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
DUSK_NETWORK
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Toolchains
|
# Toolchains
|
||||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -5,11 +5,107 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
"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
|
#: ui/menu.c:10
|
||||||
msgid "ui.title"
|
msgid "ui.title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Welcome"
|
"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
|
#: src/dusk/ui/frame/settings/uisettings.c
|
||||||
msgid "ui.settings.tabs.general"
|
msgid "ui.settings.tabs.general"
|
||||||
msgstr "General"
|
msgstr "General"
|
||||||
@@ -112,6 +208,58 @@ msgstr "Yes"
|
|||||||
msgid "ui.initial.create_save.no"
|
msgid "ui.initial.create_save.no"
|
||||||
msgstr "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"
|
msgid "item.potion.name"
|
||||||
msgstr "Potion"
|
msgstr "Potion"
|
||||||
|
|
||||||
|
|||||||
@@ -1,126 +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/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save"
|
|
||||||
msgstr "Guardar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_success"
|
|
||||||
msgstr "Partida guardada."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_cancelled"
|
|
||||||
msgstr "Guardado cancelado."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_unavailable"
|
|
||||||
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_temporary"
|
|
||||||
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_create_confirm"
|
|
||||||
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_failed_format"
|
|
||||||
msgstr "Error al guardar: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_check_failed_format"
|
|
||||||
msgstr "No se puede guardar: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.message"
|
|
||||||
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.retry"
|
|
||||||
msgstr "Reintentar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.continue"
|
|
||||||
msgstr "Continuar de todos modos"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.message"
|
|
||||||
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.yes"
|
|
||||||
msgstr "Sí"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.no"
|
|
||||||
msgstr "No"
|
|
||||||
|
|
||||||
#: 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,126 +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/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save"
|
|
||||||
msgstr "セーブ"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_success"
|
|
||||||
msgstr "セーブしました。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_cancelled"
|
|
||||||
msgstr "セーブをキャンセルしました。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_unavailable"
|
|
||||||
msgstr "セーブできません - セーブデバイスが見つかりません。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_temporary"
|
|
||||||
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_create_confirm"
|
|
||||||
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_failed_format"
|
|
||||||
msgstr "セーブに失敗しました: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
|
||||||
msgid "ui.game_menu.save_check_failed_format"
|
|
||||||
msgstr "セーブできません: %s"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.message"
|
|
||||||
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.retry"
|
|
||||||
msgstr "再試行"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
|
||||||
msgid "ui.initial.no_card.continue"
|
|
||||||
msgstr "続行する"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.message"
|
|
||||||
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.yes"
|
|
||||||
msgstr "はい"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
|
||||||
msgid "ui.initial.create_save.no"
|
|
||||||
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 "リンゴ"
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -65,6 +65,23 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|||||||
)
|
)
|
||||||
endif()
|
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.
|
# Postbuild, create .pbp file for PSP.
|
||||||
create_pbp_file(
|
create_pbp_file(
|
||||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||||
@@ -74,4 +91,33 @@ create_pbp_file(
|
|||||||
TITLE "${DUSK_BINARY_TARGET_NAME}"
|
TITLE "${DUSK_BINARY_TARGET_NAME}"
|
||||||
PSAR_PATH ${DUSK_ASSETS_ZIP}
|
PSAR_PATH ${DUSK_ASSETS_ZIP}
|
||||||
VERSION 01.00
|
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}")
|
||||||
@@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_WII
|
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
|
# Generate Homebrew Channel meta.xml from project identity variables
|
||||||
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||||
configure_file(
|
configure_file(
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
add_subdirectory(dusk)
|
add_subdirectory(dusk)
|
||||||
|
|
||||||
|
if(DUSK_NETWORK)
|
||||||
|
add_subdirectory(dusknetwork)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||||
add_subdirectory(dusklinux)
|
add_subdirectory(dusklinux)
|
||||||
add_subdirectory(dusksdl2)
|
add_subdirectory(dusksdl2)
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
|||||||
|
|
||||||
# Subdirs
|
# Subdirs
|
||||||
add_subdirectory(animation)
|
add_subdirectory(animation)
|
||||||
add_subdirectory(event)
|
|
||||||
add_subdirectory(assert)
|
add_subdirectory(assert)
|
||||||
add_subdirectory(asset)
|
add_subdirectory(asset)
|
||||||
add_subdirectory(console)
|
add_subdirectory(console)
|
||||||
@@ -68,7 +67,6 @@ add_subdirectory(scene)
|
|||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(network)
|
|
||||||
add_subdirectory(save)
|
add_subdirectory(save)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
add_subdirectory(thread)
|
add_subdirectory(thread)
|
||||||
@@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
easing.c
|
easing.c
|
||||||
animation.c
|
animation.c
|
||||||
|
keyframe.c
|
||||||
)
|
)
|
||||||
|
|||||||
+109
-29
@@ -11,42 +11,122 @@
|
|||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t *keyframes,
|
||||||
uint16_t keyframeCount
|
uint16_t *keyframeCounts,
|
||||||
|
const uint16_t layerCount
|
||||||
) {
|
) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
|
||||||
|
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
|
||||||
|
|
||||||
|
memoryZero(anim, sizeof(animation_t));
|
||||||
anim->keyframes = keyframes;
|
anim->keyframes = keyframes;
|
||||||
anim->keyframeCount = keyframeCount;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t animationGetValue(animation_t *anim, const float_t time) {
|
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
|
||||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
|
||||||
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
|
||||||
assertTrue(time >= 0, "Time must be non-negative.");
|
|
||||||
|
|
||||||
keyframe_t *start;
|
uint16_t keyframeCount = anim->keyframeCounts[layer];
|
||||||
keyframe_t *end;
|
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
|
||||||
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
|
||||||
keyframe_t *current = anim->keyframes;
|
}
|
||||||
start = current;
|
|
||||||
|
void animationUpdate(
|
||||||
do {
|
animation_t *anim,
|
||||||
if(current->time > time) {
|
const float_t deltaTime
|
||||||
end = current;
|
) {
|
||||||
break;
|
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||||
}
|
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
|
||||||
start = current;
|
|
||||||
current++;
|
bool_t justCompleted = false;
|
||||||
|
|
||||||
if(current > last) {
|
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
|
||||||
end = start;
|
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
|
||||||
break;
|
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
|
||||||
}
|
assertFalse(
|
||||||
} while(true);
|
loop && pingpong,
|
||||||
|
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
|
||||||
float_t t = (time - start->time) / (end->time - start->time);
|
);
|
||||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
|
||||||
}
|
}
|
||||||
@@ -6,29 +6,89 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "keyframe.h"
|
#include "keyframe.h"
|
||||||
|
|
||||||
|
#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)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
keyframe_t *keyframes;
|
keyframe_t *keyframes;
|
||||||
uint16_t keyframeCount;
|
uint16_t *keyframeCounts;
|
||||||
|
uint16_t layerCount;
|
||||||
|
float_t time;
|
||||||
|
float_t duration;
|
||||||
|
uint8_t flags;
|
||||||
|
|
||||||
|
void *user;
|
||||||
|
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
|
||||||
|
void (*onComplete)(void *user);
|
||||||
|
void (*onLoop)(void *user);
|
||||||
} animation_t;
|
} animation_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an animation.
|
* Initializes an animation with the given keyframes and layer count.
|
||||||
*
|
*
|
||||||
* @param anim The animation to initialize.
|
* @param anim Pointer to the animation to initialize.
|
||||||
* @param keyframes The keyframes to use for the animation.
|
* @param keyframes Pointer to the array of keyframes for each layer.
|
||||||
* @param keyframeCount The number of keyframes in the animation.
|
* @param keyframeCount Number of keyframes in each layer.
|
||||||
|
* @param layerCount Number of layers in the animation.
|
||||||
*/
|
*/
|
||||||
void animationInit(
|
void animationInit(
|
||||||
animation_t *anim,
|
animation_t *anim,
|
||||||
keyframe_t *keyframes,
|
keyframe_t *keyframes,
|
||||||
uint16_t keyframeCount
|
uint16_t *keyframeCounts,
|
||||||
|
const uint16_t layerCount
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of the animation at a given time.
|
* 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.
|
||||||
*
|
*
|
||||||
* @param anim The animation to get the value from.
|
* @param anim Pointer to the animation to set the time for.
|
||||||
* @param time The time at which to get the value, in seconds.
|
* @param time The new time to set for the animation.
|
||||||
* @return The value of the animation at the given time.
|
|
||||||
*/
|
*/
|
||||||
float_t animationGetValue(animation_t *anim, const float_t time);
|
void animationSetTime(animation_t *anim, const float_t time);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current value of a specific layer in the animation based on the
|
||||||
|
* current animation time.
|
||||||
|
*/
|
||||||
|
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 deltaTime
|
||||||
|
);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
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 *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
||||||
|
if(time >= last->time) return last->value;
|
||||||
|
|
||||||
|
// 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++;
|
||||||
|
}
|
||||||
|
keyframe_t *end = current;
|
||||||
|
|
||||||
|
float_t t = (time - start->time) / (end->time - start->time);
|
||||||
|
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||||
|
}
|
||||||
@@ -11,3 +11,17 @@ typedef struct {
|
|||||||
float_t value;
|
float_t value;
|
||||||
easingtype_t easing;
|
easingtype_t easing;
|
||||||
} keyframe_t;
|
} keyframe_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of a keyframe at a given time.
|
||||||
|
*
|
||||||
|
* @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 value of the keyframe at the given time.
|
||||||
|
*/
|
||||||
|
float_t keyframeGetValue(
|
||||||
|
const keyframe_t *keyframes,
|
||||||
|
const uint32_t keyframeCount,
|
||||||
|
const float_t time
|
||||||
|
);
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef DUSK_ASSERTIONS_FAKED
|
#ifndef DUSK_ASSERTIONS_FAKED
|
||||||
|
#define DUSK_ASSERTIONS 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the assert system. Must be the very first call in engine
|
* Initializes the assert system. Must be the very first call in engine
|
||||||
* startup.
|
* startup.
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
asset.c
|
asset.c
|
||||||
assetbatch.c
|
|
||||||
assetfile.c
|
assetfile.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -322,7 +322,9 @@ errorret_t assetUpdate(void) {
|
|||||||
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||||
assetentry_t *loadedEntry = loading->entry;
|
assetentry_t *loadedEntry = loading->entry;
|
||||||
loading->entry = NULL;
|
loading->entry = NULL;
|
||||||
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
if(loadedEntry->onLoaded) {
|
||||||
|
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loading++;
|
loading++;
|
||||||
@@ -346,8 +348,8 @@ errorret_t assetUpdate(void) {
|
|||||||
assetentry_t *errEntry = loading->entry;
|
assetentry_t *errEntry = loading->entry;
|
||||||
loading->entry = NULL;
|
loading->entry = NULL;
|
||||||
threadMutexUnlock(&loading->mutex);
|
threadMutexUnlock(&loading->mutex);
|
||||||
eventInvoke(&errEntry->onError, errEntry);
|
if(errEntry->onError) errEntry->onError(errEntry, errEntry->onErrorUser);
|
||||||
errorThrow("Failed to load asset asynchronously.");
|
loading++;
|
||||||
break;
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
// I assume zip_fread takes buffer NULL for skipping?
|
// Some zip_fread() implementations (seen on PSP) reject a single call
|
||||||
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
|
// asking for the entire (potentially large) file at once with EINVAL;
|
||||||
if(bytesRead < 0) {
|
// the line reader above only ever asks for up to 1024 bytes per call and
|
||||||
errorThrow("Failed to read from asset file: %s", file->filename);
|
// 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->position += totalRead;
|
||||||
file->lastRead = bytesRead;
|
file->lastRead = totalRead;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
|
|
||||||
#define ASSET_FILE_NAME_MAX 48
|
#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 struct assetfile_s assetfile_t;
|
||||||
|
|
||||||
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
|
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ add_subdirectory(locale)
|
|||||||
add_subdirectory(json)
|
add_subdirectory(json)
|
||||||
add_subdirectory(chunk)
|
add_subdirectory(chunk)
|
||||||
add_subdirectory(dmf)
|
add_subdirectory(dmf)
|
||||||
|
add_subdirectory(cutscene)
|
||||||
@@ -35,22 +35,6 @@ void assetEntryInit(
|
|||||||
entry->input = NULL;
|
entry->input = NULL;
|
||||||
}
|
}
|
||||||
refInit(&entry->refs, entry, NULL, NULL, NULL);
|
refInit(&entry->refs, entry, NULL, NULL, NULL);
|
||||||
|
|
||||||
eventInit(
|
|
||||||
&entry->onLoaded,
|
|
||||||
entry->onLoadedCallbacks, entry->onLoadedUsers,
|
|
||||||
ASSET_ENTRY_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&entry->onUnloaded,
|
|
||||||
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
|
|
||||||
ASSET_ENTRY_EVENT_MAX
|
|
||||||
);
|
|
||||||
eventInit(
|
|
||||||
&entry->onError,
|
|
||||||
entry->onErrorCallbacks, entry->onErrorUsers,
|
|
||||||
ASSET_ENTRY_EVENT_MAX
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void assetEntryLock(assetentry_t *entry) {
|
void assetEntryLock(assetentry_t *entry) {
|
||||||
@@ -97,7 +81,7 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
|
|||||||
"Asset entry still refed at dispose time."
|
"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));
|
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
|
||||||
memoryZero(entry, sizeof(assetentry_t));
|
memoryZero(entry, sizeof(assetentry_t));
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "asset/loader/assetloading.h"
|
#include "asset/loader/assetloading.h"
|
||||||
#include "event/event.h"
|
|
||||||
#include "util/ref.h"
|
#include "util/ref.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -20,11 +19,17 @@ typedef enum {
|
|||||||
ASSET_ENTRY_STATE_ERROR
|
ASSET_ENTRY_STATE_ERROR
|
||||||
} assetentrystate_t;
|
} assetentrystate_t;
|
||||||
|
|
||||||
/** Maximum number of subscribers for each per-entry event. */
|
|
||||||
#define ASSET_ENTRY_EVENT_MAX 2
|
|
||||||
|
|
||||||
typedef struct assetentry_s assetentry_t;
|
typedef struct assetentry_s assetentry_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
struct assetentry_s {
|
||||||
char_t name[ASSET_FILE_NAME_MAX];
|
char_t name[ASSET_FILE_NAME_MAX];
|
||||||
assetloadertype_t type;
|
assetloadertype_t type;
|
||||||
@@ -33,30 +38,27 @@ struct assetentry_s {
|
|||||||
ref_t refs;
|
ref_t refs;
|
||||||
assetloaderinput_t *input;
|
assetloaderinput_t *input;
|
||||||
assetloaderinput_t inputData;
|
assetloaderinput_t inputData;
|
||||||
/**
|
|
||||||
* Fired once when loading completes successfully (params = assetentry_t *).
|
|
||||||
* Always invoked on the main thread.
|
|
||||||
*/
|
|
||||||
event_t onLoaded;
|
|
||||||
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
|
* Fired once when loading completes successfully.
|
||||||
* The asset data is still accessible when the callback runs.
|
|
||||||
* Always invoked on the main thread.
|
* Always invoked on the main thread.
|
||||||
*/
|
*/
|
||||||
event_t onUnloaded;
|
assetentrycallback_t onLoaded;
|
||||||
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
void *onLoadedUser;
|
||||||
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* Always invoked on the main thread.
|
||||||
*/
|
*/
|
||||||
event_t onError;
|
assetentrycallback_t onError;
|
||||||
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
|
void *onErrorUser;
|
||||||
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,4 +51,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
|||||||
.loadAsync = assetChunkLoaderAsync,
|
.loadAsync = assetChunkLoaderAsync,
|
||||||
.dispose = assetChunkDispose
|
.dispose = assetChunkDispose
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ASSET_LOADER_TYPE_CUTSCENE] = {
|
||||||
|
.loadSync = assetCutsceneLoaderSync,
|
||||||
|
.loadAsync = assetCutsceneLoaderAsync,
|
||||||
|
.dispose = assetCutsceneDispose
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "asset/loader/json/assetjsonloader.h"
|
#include "asset/loader/json/assetjsonloader.h"
|
||||||
#include "asset/loader/chunk/assetchunkloader.h"
|
#include "asset/loader/chunk/assetchunkloader.h"
|
||||||
|
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
ASSET_LOADER_TYPE_NULL,
|
ASSET_LOADER_TYPE_NULL,
|
||||||
@@ -24,6 +25,7 @@ typedef enum {
|
|||||||
ASSET_LOADER_TYPE_LOCALE,
|
ASSET_LOADER_TYPE_LOCALE,
|
||||||
ASSET_LOADER_TYPE_JSON,
|
ASSET_LOADER_TYPE_JSON,
|
||||||
ASSET_LOADER_TYPE_CHUNK,
|
ASSET_LOADER_TYPE_CHUNK,
|
||||||
|
ASSET_LOADER_TYPE_CUTSCENE,
|
||||||
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
ASSET_LOADER_TYPE_COUNT
|
||||||
} assetloadertype_t;
|
} assetloadertype_t;
|
||||||
@@ -36,6 +38,7 @@ typedef union {
|
|||||||
assetlocaleloaderloading_t locale;
|
assetlocaleloaderloading_t locale;
|
||||||
assetjsonloaderloading_t json;
|
assetjsonloaderloading_t json;
|
||||||
assetchunkloaderloading_t chunk;
|
assetchunkloaderloading_t chunk;
|
||||||
|
assetcutsceneloaderloading_t cutscene;
|
||||||
} assetloaderloading_t;
|
} assetloaderloading_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -46,6 +49,7 @@ typedef union {
|
|||||||
assetlocaleoutput_t locale;
|
assetlocaleoutput_t locale;
|
||||||
assetjsonoutput_t json;
|
assetjsonoutput_t json;
|
||||||
assetchunkoutput_t chunk;
|
assetchunkoutput_t chunk;
|
||||||
|
assetcutsceneoutput_t cutscene;
|
||||||
} assetloaderoutput_t;
|
} assetloaderoutput_t;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -54,6 +58,7 @@ typedef union {
|
|||||||
assetlocaleloaderinput_t locale;
|
assetlocaleloaderinput_t locale;
|
||||||
assetjsonloaderinput_t json;
|
assetjsonloaderinput_t json;
|
||||||
assetchunkloaderinput_t chunk;
|
assetchunkloaderinput_t chunk;
|
||||||
|
assetcutsceneloaderinput_t cutscene;
|
||||||
} assetloaderinput_t;
|
} assetloaderinput_t;
|
||||||
|
|
||||||
typedef struct assetloading_s assetloading_t;
|
typedef struct assetloading_s assetloading_t;
|
||||||
|
|||||||
+1
-2
@@ -5,6 +5,5 @@
|
|||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
uiinitialnocard.c
|
assetcutsceneloader.c
|
||||||
uiinitialcreatesave.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);
|
||||||
@@ -17,11 +17,9 @@ console_t CONSOLE;
|
|||||||
|
|
||||||
void consoleInit(void) {
|
void consoleInit(void) {
|
||||||
memoryZero(&CONSOLE, sizeof(console_t));
|
memoryZero(&CONSOLE, sizeof(console_t));
|
||||||
CONSOLE.visible = false;
|
// CONSOLE.visible = false;
|
||||||
|
CONSOLE.visible = true;
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
threadMutexInit(&CONSOLE.printMutex);
|
||||||
threadMutexInit(&CONSOLE.printMutex);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void consolePrint(const char_t *message, ...) {
|
void consolePrint(const char_t *message, ...) {
|
||||||
@@ -32,20 +30,14 @@ void consolePrint(const char_t *message, ...) {
|
|||||||
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
threadMutexLock(&CONSOLE.printMutex);
|
||||||
threadMutexLock(&CONSOLE.printMutex);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
memoryMove(
|
memoryMove(
|
||||||
CONSOLE.line[0],
|
CONSOLE.line[0],
|
||||||
CONSOLE.line[1],
|
CONSOLE.line[1],
|
||||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||||
);
|
);
|
||||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||||
|
threadMutexUnlock(&CONSOLE.printMutex);
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
|
||||||
threadMutexUnlock(&CONSOLE.printMutex);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
logDebug("%s\n", buffer);
|
logDebug("%s\n", buffer);
|
||||||
}
|
}
|
||||||
@@ -61,7 +53,5 @@ void consoleUpdate(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void consoleDispose(void) {
|
void consoleDispose(void) {
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
threadMutexDispose(&CONSOLE.printMutex);
|
||||||
threadMutexDispose(&CONSOLE.printMutex);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
@@ -6,24 +6,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "consoledefs.h"
|
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "dusk.h"
|
#include "dusk.h"
|
||||||
|
#include "thread/thread.h"
|
||||||
|
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
#define CONSOLE_LINE_MAX 128
|
||||||
#include "thread/thread.h"
|
#define CONSOLE_HISTORY_MAX 16
|
||||||
#include <poll.h>
|
#define CONSOLE_EXEC_BUFFER_MAX 32
|
||||||
#include <unistd.h>
|
|
||||||
#define CONSOLE_POSIX_POLL_RATE 75
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||||
bool_t visible;
|
bool_t visible;
|
||||||
|
threadmutex_t printMutex;
|
||||||
#ifdef DUSK_CONSOLE_POSIX
|
|
||||||
threadmutex_t printMutex;
|
|
||||||
#endif
|
|
||||||
} console_t;
|
} console_t;
|
||||||
|
|
||||||
extern console_t CONSOLE;
|
extern console_t CONSOLE;
|
||||||
|
|||||||
@@ -33,13 +33,17 @@ errorret_t displayInit(void) {
|
|||||||
#ifdef displayPlatformInit
|
#ifdef displayPlatformInit
|
||||||
errorChain(displayPlatformInit());
|
errorChain(displayPlatformInit());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
||||||
|
|
||||||
|
// Init the fixed textures
|
||||||
errorChain(textureInit(
|
errorChain(textureInit(
|
||||||
&TEXTURE_WHITE, 4, 4,
|
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
||||||
));
|
));
|
||||||
errorChain(textureInit(
|
errorChain(textureInit(
|
||||||
&TEXTURE_TEST, 4, 4,
|
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -51,13 +55,13 @@ errorret_t displayInit(void) {
|
|||||||
errorChain(capsuleInit());
|
errorChain(capsuleInit());
|
||||||
errorChain(triPrismInit());
|
errorChain(triPrismInit());
|
||||||
|
|
||||||
|
// Init the subsystems
|
||||||
errorChain(frameBufferInitBackBuffer());
|
errorChain(frameBufferInitBackBuffer());
|
||||||
errorChain(spriteBatchInit());
|
errorChain(spriteBatchInit());
|
||||||
errorChain(textInit());
|
errorChain(textInit());
|
||||||
errorChain(screenInit());
|
errorChain(screenInit());
|
||||||
|
|
||||||
// Setup initial shader with default values
|
// Setup initial shader with default values
|
||||||
|
|
||||||
errorChain(shaderListInit());
|
errorChain(shaderListInit());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ errorret_t spriteBatchBuffer(
|
|||||||
|
|
||||||
// Buffer to the mesh vertices.
|
// Buffer to the mesh vertices.
|
||||||
spriteBatchBufferToMesh(
|
spriteBatchBufferToMesh(
|
||||||
sprites, batchCount, v, batchCount * QUAD_VERTEX_COUNT
|
sprites + (count - remaining), batchCount, v, batchCount * QUAD_VERTEX_COUNT
|
||||||
);
|
);
|
||||||
SPRITEBATCH.spriteCount += batchCount;
|
SPRITEBATCH.spriteCount += batchCount;
|
||||||
remaining -= batchCount;
|
remaining -= batchCount;
|
||||||
|
|||||||
@@ -7,4 +7,5 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
text.c
|
text.c
|
||||||
|
font.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "font.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
|
||||||
|
font_t FONT_DEFAULT;
|
||||||
|
static texture_t FONT_DEFAULT_TEXTURE;
|
||||||
|
static tileset_t FONT_DEFAULT_TILESET;
|
||||||
|
|
||||||
|
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||||
|
FONT_DEFAULT_TILE_HEIGHT
|
||||||
|
] = {
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
|
||||||
|
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
|
||||||
|
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
|
||||||
|
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
|
||||||
|
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
|
||||||
|
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
|
||||||
|
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
|
||||||
|
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
|
||||||
|
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
|
||||||
|
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
|
||||||
|
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
|
||||||
|
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
|
||||||
|
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
|
||||||
|
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
|
||||||
|
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
|
||||||
|
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
|
||||||
|
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
|
||||||
|
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
|
||||||
|
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
|
||||||
|
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
|
||||||
|
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
|
||||||
|
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
|
||||||
|
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
|
||||||
|
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
|
||||||
|
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
|
||||||
|
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
|
||||||
|
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
|
||||||
|
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
|
||||||
|
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
|
||||||
|
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
|
||||||
|
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
|
||||||
|
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
|
||||||
|
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
||||||
|
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
||||||
|
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
||||||
|
// 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, 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
|
||||||
|
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
|
||||||
|
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
|
||||||
|
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
|
||||||
|
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
|
||||||
|
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
|
||||||
|
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
|
||||||
|
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
|
||||||
|
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
|
||||||
|
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
|
||||||
|
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
|
||||||
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
||||||
|
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
||||||
|
// 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 }, // }
|
||||||
|
// 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 fontDefaultInit(void) {
|
||||||
|
const int32_t width = (int32_t)mathNextPowTwo(
|
||||||
|
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
||||||
|
);
|
||||||
|
const int32_t height = (int32_t)mathNextPowTwo(
|
||||||
|
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
|
||||||
|
);
|
||||||
|
|
||||||
|
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
|
||||||
|
memoryZero(pixels, sizeof(color_t) * width * height);
|
||||||
|
|
||||||
|
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
|
||||||
|
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
|
||||||
|
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
|
||||||
|
|
||||||
|
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
|
||||||
|
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
|
||||||
|
|
||||||
|
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
|
||||||
|
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
|
||||||
|
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
|
||||||
|
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
|
||||||
|
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
|
||||||
|
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
|
||||||
|
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
|
||||||
|
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
|
||||||
|
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
|
||||||
|
|
||||||
|
const texturedata_t data = { .rgbaColors = pixels };
|
||||||
|
errorret_t textureResult = textureInit(
|
||||||
|
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
|
||||||
|
);
|
||||||
|
memoryFree(pixels);
|
||||||
|
errorChain(textureResult);
|
||||||
|
|
||||||
|
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
|
||||||
|
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t fontDefaultDispose(void) {
|
||||||
|
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
||||||
|
FONT_DEFAULT.texture = NULL;
|
||||||
|
FONT_DEFAULT.tileset = NULL;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
#include "display/texture/tileset.h"
|
#include "display/texture/tileset.h"
|
||||||
|
|
||||||
@@ -13,3 +14,86 @@ typedef struct {
|
|||||||
texture_t *texture;
|
texture_t *texture;
|
||||||
tileset_t *tileset;
|
tileset_t *tileset;
|
||||||
} font_t;
|
} font_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pixel width/height of a single default-font glyph tile.
|
||||||
|
*/
|
||||||
|
#define FONT_DEFAULT_TILE_WIDTH 6
|
||||||
|
#define FONT_DEFAULT_TILE_HEIGHT 10
|
||||||
|
|
||||||
|
/** Grid layout of the generated default-font texture, in tiles. */
|
||||||
|
#define FONT_DEFAULT_COLUMNS 16
|
||||||
|
#define FONT_DEFAULT_ROWS 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
||||||
|
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
||||||
|
* TEXT_CHAR_START ('!'), 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. 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 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 fontDefaultInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the default font created by fontDefaultInit().
|
||||||
|
*
|
||||||
|
* @return Either an error or success result.
|
||||||
|
*/
|
||||||
|
errorret_t fontDefaultDispose(void);
|
||||||
|
|||||||
+114
-41
@@ -9,34 +9,15 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "asset/asset.h"
|
|
||||||
#include "asset/loader/display/assettextureloader.h"
|
|
||||||
#include "asset/loader/display/assettilesetloader.h"
|
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
|
|
||||||
font_t FONT_DEFAULT;
|
|
||||||
|
|
||||||
errorret_t textInit(void) {
|
errorret_t textInit(void) {
|
||||||
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
|
errorChain(fontDefaultInit());
|
||||||
assetentry_t *entryTexture = assetLock(
|
|
||||||
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
|
|
||||||
);
|
|
||||||
assetentry_t *entryTileset = assetLock(
|
|
||||||
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
|
||||||
);
|
|
||||||
errorChain(assetRequireLoaded(entryTexture));
|
|
||||||
errorChain(assetRequireLoaded(entryTileset));
|
|
||||||
|
|
||||||
FONT_DEFAULT.texture = &entryTexture->data.texture;
|
|
||||||
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t textDispose(void) {
|
errorret_t textDispose(void) {
|
||||||
FONT_DEFAULT.texture = NULL;
|
errorChain(fontDefaultDispose());
|
||||||
FONT_DEFAULT.tileset = NULL;
|
|
||||||
assetUnlock("ui/minogram.png");
|
|
||||||
assetUnlock("ui/minogram.dtf");
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +54,62 @@ spritebatchsprite_t textGetSprite(
|
|||||||
return sprite;
|
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(
|
errorret_t textDraw(
|
||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y,
|
const float_t y,
|
||||||
@@ -81,10 +118,11 @@ errorret_t textDraw(
|
|||||||
font_t *font
|
font_t *font
|
||||||
) {
|
) {
|
||||||
assertNotNull(text, "Text cannot be NULL");
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
int32_t length = strlen(text);
|
||||||
|
if(length == 0) errorOk();
|
||||||
|
|
||||||
if(font == NULL) font = &FONT_DEFAULT;
|
if(font == NULL) font = &FONT_DEFAULT;
|
||||||
|
|
||||||
spritebatchsprite_t sprite;
|
|
||||||
shadermaterial_t material = {
|
shadermaterial_t material = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
.color = color,
|
.color = color,
|
||||||
@@ -92,31 +130,25 @@ errorret_t textDraw(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
spritebatchsprite_t sprites[32];
|
||||||
float_t posX = x;
|
float_t posX = x;
|
||||||
float_t posY = y;
|
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();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void textMeasure(
|
int32_t textMeasure(
|
||||||
const char_t *text,
|
const char_t *text,
|
||||||
const font_t *font,
|
const font_t *font,
|
||||||
int32_t *outWidth,
|
int32_t *outWidth,
|
||||||
@@ -129,6 +161,7 @@ void textMeasure(
|
|||||||
int32_t width = 0;
|
int32_t width = 0;
|
||||||
int32_t height = font->tileset->tileHeight;
|
int32_t height = font->tileset->tileHeight;
|
||||||
int32_t lineWidth = 0;
|
int32_t lineWidth = 0;
|
||||||
|
int32_t spriteCount = 0;
|
||||||
|
|
||||||
char_t c;
|
char_t c;
|
||||||
int32_t i = 0;
|
int32_t i = 0;
|
||||||
@@ -141,10 +174,50 @@ void textMeasure(
|
|||||||
}
|
}
|
||||||
|
|
||||||
lineWidth += font->tileset->tileWidth;
|
lineWidth += font->tileset->tileWidth;
|
||||||
|
|
||||||
|
if(c != ' ') {
|
||||||
|
spriteCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(lineWidth > width) width = lineWidth;
|
if(lineWidth > width) width = lineWidth;
|
||||||
|
|
||||||
*outWidth = width;
|
*outWidth = width;
|
||||||
*outHeight = height;
|
*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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
|
|
||||||
#define TEXT_CHAR_START '!'
|
#define TEXT_CHAR_START '!'
|
||||||
|
|
||||||
extern font_t FONT_DEFAULT;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the text system.
|
* Initializes the text system.
|
||||||
*
|
*
|
||||||
@@ -42,6 +40,37 @@ spritebatchsprite_t textGetSprite(
|
|||||||
const font_t *font
|
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.
|
* Draws a string of text at the specified position.
|
||||||
*
|
*
|
||||||
@@ -67,10 +96,25 @@ errorret_t textDraw(
|
|||||||
* @param font Font to use for measurement.
|
* @param font Font to use for measurement.
|
||||||
* @param outWidth Pointer to store the measured width in pixels.
|
* @param outWidth Pointer to store the measured width in pixels.
|
||||||
* @param outHeight Pointer to store the measured height in pixels.
|
* @param outHeight Pointer to store the measured height in pixels.
|
||||||
|
* @return The count of sprites that will be rendered for the given text.
|
||||||
*/
|
*/
|
||||||
void textMeasure(
|
int32_t textMeasure(
|
||||||
const char_t *text,
|
const char_t *text,
|
||||||
const font_t *font,
|
const font_t *font,
|
||||||
int32_t *outWidth,
|
int32_t *outWidth,
|
||||||
int32_t *outHeight
|
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"
|
#include "display/display.h"
|
||||||
|
|
||||||
texture_t TEXTURE_WHITE;
|
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,
|
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;
|
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_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||||
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
||||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
#error "textureDisposePlatform should not be defined."
|
#error "textureDisposePlatform should not be defined."
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define TEXTURE_FIXED_WIDTH 4
|
||||||
|
#define TEXTURE_FIXED_HEIGHT 4
|
||||||
|
|
||||||
typedef textureformatplatform_t textureformat_t;
|
typedef textureformatplatform_t textureformat_t;
|
||||||
typedef textureplatform_t texture_t;
|
typedef textureplatform_t texture_t;
|
||||||
|
|
||||||
@@ -29,9 +32,9 @@ typedef union texturedata_u {
|
|||||||
} texturedata_t;
|
} texturedata_t;
|
||||||
|
|
||||||
extern texture_t TEXTURE_WHITE;
|
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 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.
|
* Initializes a texture.
|
||||||
|
|||||||
@@ -16,11 +16,12 @@
|
|||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "network/network.h"
|
#ifdef DUSK_NETWORK
|
||||||
|
#include "network/network.h"
|
||||||
|
#endif
|
||||||
#include "system/system.h"
|
#include "system/system.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"\
|
||||||
#include "save/autosave.h"
|
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
|
|
||||||
@@ -43,7 +44,9 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
errorChain(uiInit());
|
errorChain(uiInit());
|
||||||
errorChain(rpgInit());
|
errorChain(rpgInit());
|
||||||
errorChain(networkInit());
|
#ifdef DUSK_NETWORK
|
||||||
|
errorChain(networkInit());
|
||||||
|
#endif
|
||||||
errorChain(sceneInit());
|
errorChain(sceneInit());
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
@@ -61,9 +64,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
|
|
||||||
errorret_t engineUpdate(void) {
|
errorret_t engineUpdate(void) {
|
||||||
// Order here is important.
|
// Order here is important.
|
||||||
errorChain(networkUpdate());
|
#ifdef DUSK_NETWORK
|
||||||
|
errorChain(networkUpdate());
|
||||||
|
#endif
|
||||||
errorChain(saveUpdate());
|
errorChain(saveUpdate());
|
||||||
autoSaveUpdate();
|
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
inputUpdate();
|
inputUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
@@ -84,7 +88,9 @@ void engineExit(void) {
|
|||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
errorChain(sceneDispose());
|
errorChain(sceneDispose());
|
||||||
errorChain(networkDispose());
|
#ifdef DUSK_NETWORK
|
||||||
|
errorChain(networkDispose());
|
||||||
|
#endif
|
||||||
errorChain(rpgDispose());
|
errorChain(rpgDispose());
|
||||||
localeManagerDispose();
|
localeManagerDispose();
|
||||||
errorChain(uiDispose());
|
errorChain(uiDispose());
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "event.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
|
|
||||||
void eventInit(
|
|
||||||
event_t *event,
|
|
||||||
eventcallback_t *callbacks,
|
|
||||||
void **users,
|
|
||||||
size_t size
|
|
||||||
) {
|
|
||||||
assertNotNull(event, "event must not be NULL");
|
|
||||||
assertNotNull((void *)callbacks, "callbacks must not be NULL");
|
|
||||||
assertTrue(size > 0, "size must be greater than 0");
|
|
||||||
|
|
||||||
event->callbacks = callbacks;
|
|
||||||
event->users = users;
|
|
||||||
event->size = size;
|
|
||||||
event->count = 0;
|
|
||||||
memoryZero(callbacks, sizeof(eventcallback_t) * size);
|
|
||||||
if(users) memoryZero(users, sizeof(void *) * size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void eventSubscribe(event_t *event, eventcallback_t callback, void *user) {
|
|
||||||
assertNotNull(event, "event must not be NULL");
|
|
||||||
assertNotNull(callback, "callback must not be NULL");
|
|
||||||
|
|
||||||
// Ensure callback isn't already susbcribed
|
|
||||||
for(uint32_t i = 0; i < event->count; i++) {
|
|
||||||
if(event->callbacks[i] != callback) continue;
|
|
||||||
assertUnreachable("Callback already registered, cannot subscribe twice.");
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(event->count < event->size, "event subscriber capacity exceeded");
|
|
||||||
|
|
||||||
event->callbacks[event->count] = callback;
|
|
||||||
if(user) {
|
|
||||||
assertNotNull(event->users, "Cannot add user pointer.");
|
|
||||||
event->users[event->count] = user;
|
|
||||||
}
|
|
||||||
event->count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
void eventUnsubscribe(event_t *event, eventcallback_t callback) {
|
|
||||||
assertNotNull(event, "event must not be NULL");
|
|
||||||
assertNotNull(callback, "callback must not be NULL");
|
|
||||||
|
|
||||||
for(uint32_t i = 0; i < event->count; i++) {
|
|
||||||
if(event->callbacks[i] != callback) continue;
|
|
||||||
|
|
||||||
uint32_t last = event->count - 1;
|
|
||||||
if(i != last) {
|
|
||||||
event->callbacks[i] = event->callbacks[last];
|
|
||||||
if(event->users) event->users[i] = event->users[last];
|
|
||||||
}
|
|
||||||
event->callbacks[last] = NULL;
|
|
||||||
if(event->users) event->users[last] = NULL;
|
|
||||||
event->count--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void eventInvoke(const event_t *event, void *params) {
|
|
||||||
assertNotNull(event, "event must not be NULL");
|
|
||||||
|
|
||||||
for(uint32_t i = 0; i < event->count; i++) {
|
|
||||||
void *u = event->users ? event->users[i] : NULL;
|
|
||||||
event->callbacks[i](params, u);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "dusk.h"
|
|
||||||
|
|
||||||
typedef void (*eventcallback_t)(void *params, void *user);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
eventcallback_t *callbacks;
|
|
||||||
void **users;
|
|
||||||
size_t size;
|
|
||||||
uint32_t count;
|
|
||||||
} event_t;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes an event, binding it to the provided backing arrays and clearing
|
|
||||||
* all subscribers. May also be called to reset an event (re-clears subscribers
|
|
||||||
* without changing the backing arrays or size).
|
|
||||||
*
|
|
||||||
* @param event The event to initialize.
|
|
||||||
* @param callbacks Caller-owned array of at least `size` callback slots.
|
|
||||||
* @param users Array of user pointers, matching each callback, or NULL.
|
|
||||||
* @param size Capacity of both arrays, must match.
|
|
||||||
*/
|
|
||||||
void eventInit(
|
|
||||||
event_t *event,
|
|
||||||
eventcallback_t *callbacks,
|
|
||||||
void **users,
|
|
||||||
size_t size
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribes a callback to an event. The callback is invoked with params and
|
|
||||||
* the provided user pointer each time the event fires. The same (callback,
|
|
||||||
* user) pair may only be subscribed once.
|
|
||||||
*
|
|
||||||
* @param event The event to subscribe to.
|
|
||||||
* @param callback The function to call when the event fires.
|
|
||||||
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
|
||||||
*/
|
|
||||||
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a previously subscribed (callback, user) pair. Does nothing if the
|
|
||||||
* pair is not currently subscribed.
|
|
||||||
*
|
|
||||||
* @param event The event to unsubscribe from.
|
|
||||||
* @param callback The callback that was passed to eventSubscribe.
|
|
||||||
*/
|
|
||||||
void eventUnsubscribe(event_t *event, eventcallback_t callback);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invokes all subscribed callbacks, passing params and each subscriber's user
|
|
||||||
* pointer.
|
|
||||||
*
|
|
||||||
* @param event The event to invoke.
|
|
||||||
* @param params Arbitrary pointer forwarded to every callback unchanged.
|
|
||||||
*/
|
|
||||||
void eventInvoke(const event_t *event, void *params);
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
input_t INPUT;
|
input_t INPUT;
|
||||||
|
|
||||||
@@ -22,20 +21,6 @@ errorret_t inputInit(void) {
|
|||||||
INPUT.actions[i].action = (inputaction_t)i;
|
INPUT.actions[i].action = (inputaction_t)i;
|
||||||
INPUT.actions[i].lastValue = 0.0f;
|
INPUT.actions[i].lastValue = 0.0f;
|
||||||
INPUT.actions[i].currentValue = 0.0f;
|
INPUT.actions[i].currentValue = 0.0f;
|
||||||
|
|
||||||
eventInit(
|
|
||||||
&INPUT.actions[i].onPressed,
|
|
||||||
INPUT.actions[i].onPressedCallbacks,
|
|
||||||
INPUT.actions[i].onPressedUsers,
|
|
||||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
|
||||||
);
|
|
||||||
|
|
||||||
eventInit(
|
|
||||||
&INPUT.actions[i].onReleased,
|
|
||||||
INPUT.actions[i].onReleasedCallbacks,
|
|
||||||
INPUT.actions[i].onReleasedUsers,
|
|
||||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef inputInitPlatform
|
#ifdef inputInitPlatform
|
||||||
@@ -106,8 +91,6 @@ void inputUpdate(void) {
|
|||||||
inputactiondata_t *act = &INPUT.actions[i];
|
inputactiondata_t *act = &INPUT.actions[i];
|
||||||
bool_t isDown = act->currentValue > 0.0f;
|
bool_t isDown = act->currentValue > 0.0f;
|
||||||
bool_t wasDown = act->lastValue > 0.0f;
|
bool_t wasDown = act->lastValue > 0.0f;
|
||||||
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
|
|
||||||
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,8 @@
|
|||||||
#include "inputbutton.h"
|
#include "inputbutton.h"
|
||||||
#include "inputaction.h"
|
#include "inputaction.h"
|
||||||
|
|
||||||
#define INPUT_LISTENER_PRESSED_MAX 16
|
|
||||||
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||||
|
|
||||||
inputplatform_t platform;
|
inputplatform_t platform;
|
||||||
} input_t;
|
} input_t;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "input/inputactiondefs.h"
|
#include "input/inputactiondefs.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
||||||
|
|
||||||
@@ -21,13 +20,6 @@ typedef struct {
|
|||||||
float_t lastDynamicValue;
|
float_t lastDynamicValue;
|
||||||
float_t currentDynamicValue;
|
float_t currentDynamicValue;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
|
||||||
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
|
||||||
event_t onPressed;
|
|
||||||
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
|
||||||
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
|
||||||
event_t onReleased;
|
|
||||||
} inputactiondata_t;
|
} inputactiondata_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,17 +13,31 @@ typedef struct {
|
|||||||
const char_t *file;
|
const char_t *file;
|
||||||
} localeinfo_t;
|
} localeinfo_t;
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_EN_US = {
|
static const localeinfo_t LOCALE_INFO_EN_US = {
|
||||||
.name = "en-US",
|
.name = "en-US",
|
||||||
.file = "locale/en_US.po",
|
.file = "locale/en_US.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_JP_JP = {
|
static const localeinfo_t LOCALE_INFO_JP_JP = {
|
||||||
.name = "ja-JP",
|
.name = "ja-JP",
|
||||||
.file = "locale/jp_JP.po",
|
.file = "locale/jp_JP.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const localeinfo_t LOCALE_ES_MX = {
|
static const localeinfo_t LOCALE_INFO_ES_MX = {
|
||||||
.name = "es-MX",
|
.name = "es-MX",
|
||||||
.file = "locale/es_MX.po",
|
.file = "locale/es_MX.po",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
|
||||||
|
&LOCALE_INFO_EN_US,
|
||||||
|
&LOCALE_INFO_JP_JP,
|
||||||
|
&LOCALE_INFO_ES_MX
|
||||||
|
};
|
||||||
|
|
||||||
|
#define LOCALE_INFO_LIST_COUNT ( \
|
||||||
|
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
|
||||||
|
)
|
||||||
|
|
||||||
|
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
|
||||||
|
|
||||||
|
// EOF
|
||||||
|
|||||||
@@ -9,40 +9,23 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "save/save.h"
|
#include "ui/ui.h"
|
||||||
|
#include "system/system.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
|
||||||
localemanager_t LOCALE;
|
localemanager_t LOCALE;
|
||||||
|
|
||||||
const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT] = {
|
|
||||||
&LOCALE_EN_US,
|
|
||||||
&LOCALE_JP_JP,
|
|
||||||
&LOCALE_ES_MX
|
|
||||||
};
|
|
||||||
|
|
||||||
errorret_t localeManagerInit() {
|
errorret_t localeManagerInit() {
|
||||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
memoryZero(&LOCALE, sizeof(localemanager_t));
|
||||||
// saveInit() runs before this (see engine.c) and, on most platforms,
|
|
||||||
// has already loaded a persisted language choice into savemeta_t by
|
// TODO: Set locale based on system locale.
|
||||||
// this point - see localeManagerGetByIndex().
|
const localeinfo_t *locale = systemGetLocale();
|
||||||
errorChain(localeManagerSetLocale(
|
errorChain(localeManagerSetLocale(locale));
|
||||||
localeManagerGetByIndex(saveGetMeta()->language)
|
consolePrint("Locale set to: %s", locale->name);
|
||||||
));
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t localeManagerGetIndex(const localeinfo_t *locale) {
|
|
||||||
assertNotNull(locale, "Locale cannot be NULL");
|
|
||||||
for(uint8_t i = 0; i < LOCALE_LIST_COUNT; i++) {
|
|
||||||
if(stringCompare(LOCALE_LIST[i]->file, locale->file) == 0) return i;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localeinfo_t * localeManagerGetByIndex(const uint8_t index) {
|
|
||||||
if(index >= LOCALE_LIST_COUNT) return LOCALE_LIST[0];
|
|
||||||
return LOCALE_LIST[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||||
assertNotNull(locale, "Locale cannot be NULL");
|
assertNotNull(locale, "Locale cannot be NULL");
|
||||||
|
|
||||||
@@ -56,6 +39,9 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
|||||||
assetEntryLock(LOCALE.entry);
|
assetEntryLock(LOCALE.entry);
|
||||||
errorChain(assetRequireLoaded(LOCALE.entry));
|
errorChain(assetRequireLoaded(LOCALE.entry));
|
||||||
|
|
||||||
|
// TODO : Trigger UI update.
|
||||||
|
errorChain(uiUpdateTranslations());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "locale/localeinfo.h"
|
#include "locale/localeinfo.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
|
||||||
@@ -18,15 +18,6 @@ typedef struct {
|
|||||||
|
|
||||||
extern localemanager_t LOCALE;
|
extern localemanager_t LOCALE;
|
||||||
|
|
||||||
/**
|
|
||||||
* Every locale the game supports, in a fixed, stable order - index into
|
|
||||||
* this array is what gets persisted as savemeta_t.language (see
|
|
||||||
* save/savemeta.h), so the order here must never change once shipped
|
|
||||||
* (only append new locales at the end).
|
|
||||||
*/
|
|
||||||
#define LOCALE_LIST_COUNT 3
|
|
||||||
extern const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the locale system.
|
* Initialize the locale system.
|
||||||
*
|
*
|
||||||
@@ -42,27 +33,6 @@ errorret_t localeManagerInit();
|
|||||||
*/
|
*/
|
||||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the LOCALE_LIST index matching the given locale's file, by
|
|
||||||
* content rather than pointer identity (localeinfo_t instances are
|
|
||||||
* declared `static const` in a header, so the same locale gets a
|
|
||||||
* distinct pointer in every translation unit that references it).
|
|
||||||
*
|
|
||||||
* @param locale The locale to find.
|
|
||||||
* @return The matching index in LOCALE_LIST, or 0 if not found.
|
|
||||||
*/
|
|
||||||
uint8_t localeManagerGetIndex(const localeinfo_t *locale);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the locale at the given LOCALE_LIST index, clamping to index 0
|
|
||||||
* (LOCALE_EN_US) if out of range - e.g. a save file's persisted language
|
|
||||||
* index from a future build with more locales than this one supports.
|
|
||||||
*
|
|
||||||
* @param index The index to look up.
|
|
||||||
* @return The locale at that index, or LOCALE_LIST[0] if out of range.
|
|
||||||
*/
|
|
||||||
const localeinfo_t * localeManagerGetByIndex(const uint8_t index);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a localized string for the given message ID.
|
* Get a localized string for the given message ID.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,3 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
battlefighter.c
|
battlefighter.c
|
||||||
party.c
|
party.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(testbattle)
|
||||||
|
|||||||
+161
-58
@@ -8,6 +8,7 @@
|
|||||||
#include "battle.h"
|
#include "battle.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
|
||||||
battle_t BATTLE;
|
battle_t BATTLE;
|
||||||
|
|
||||||
@@ -51,10 +52,8 @@ void battleStart(
|
|||||||
BATTLE.fleeAvailable = fleeAvailable;
|
BATTLE.fleeAvailable = fleeAvailable;
|
||||||
BATTLE.result = BATTLE_RESULT_NONE;
|
BATTLE.result = BATTLE_RESULT_NONE;
|
||||||
BATTLE.round = 1;
|
BATTLE.round = 1;
|
||||||
BATTLE.turnIndex = 0;
|
|
||||||
battleBuildTurnOrder(true);
|
|
||||||
|
|
||||||
BATTLE.active = true;
|
battleSetState(BATTLE_STATE_OPENING);
|
||||||
}
|
}
|
||||||
|
|
||||||
void battleDispose(void) {
|
void battleDispose(void) {
|
||||||
@@ -62,9 +61,9 @@ void battleDispose(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
battlefighter_t *battleGetCurrentFighter(void) {
|
battlefighter_t *battleGetCurrentFighter(void) {
|
||||||
if(!BATTLE.active) return NULL;
|
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
||||||
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
||||||
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||||
@@ -92,91 +91,105 @@ void battleResolveAttack(
|
|||||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||||
}
|
}
|
||||||
|
|
||||||
void battleNextTurn(void) {
|
|
||||||
BATTLE.turnIndex++;
|
|
||||||
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
|
||||||
|
|
||||||
BATTLE.round++;
|
|
||||||
BATTLE.turnIndex = 0;
|
|
||||||
battleBuildTurnOrder(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
battleresult_t battleCheckResult(void) {
|
battleresult_t battleCheckResult(void) {
|
||||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||||
|
|
||||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||||
BATTLE.result = BATTLE_RESULT_LOSS;
|
battleSetResult(BATTLE_RESULT_LOSS);
|
||||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||||
BATTLE.result = BATTLE_RESULT_WIN;
|
battleSetResult(BATTLE_RESULT_WIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BATTLE.result;
|
return BATTLE.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void battleQueueAction(
|
||||||
|
const uint8_t fighterIndex,
|
||||||
|
const battleactiontype_t type,
|
||||||
|
const uint8_t targetIndex
|
||||||
|
) {
|
||||||
|
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||||
|
action->type = type;
|
||||||
|
action->targetIndex = targetIndex;
|
||||||
|
|
||||||
|
if(BATTLE.onActionDecided != NULL) {
|
||||||
|
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||||
battlefighter_t *attacker = battleGetCurrentFighter();
|
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||||
if(attacker == NULL) return;
|
if(fighter == NULL) return;
|
||||||
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
|
||||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||||
|
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
||||||
|
|
||||||
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
||||||
if(!battleFighterIsAlive(defender)) return;
|
BATTLE.selectionIndex++;
|
||||||
|
battleAdvanceSelection();
|
||||||
battleResolveAttack(attacker, defender);
|
|
||||||
battleCheckResult();
|
|
||||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void battlePlayerFlee(void) {
|
void battlePlayerFlee(void) {
|
||||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||||
if(fighter == NULL) return;
|
if(fighter == NULL) return;
|
||||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
|
||||||
if(!BATTLE.fleeAvailable) return;
|
if(!BATTLE.fleeAvailable) return;
|
||||||
|
|
||||||
BATTLE.result = BATTLE_RESULT_FLED;
|
battleSetResult(BATTLE_RESULT_FLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
void battleUpdate(void) {
|
void battleUpdate(void) {
|
||||||
if(!BATTLE.active) return;
|
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
||||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
||||||
|
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
||||||
|
|
||||||
battlefighter_t *current = battleGetCurrentFighter();
|
switch(BATTLE.state) {
|
||||||
if(current == NULL) return;
|
case BATTLE_STATE_OPENING:
|
||||||
|
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||||
|
break;
|
||||||
|
|
||||||
if(!battleFighterIsAlive(current)) {
|
case BATTLE_STATE_PRE_ROUND:
|
||||||
battleNextTurn();
|
battleUpdatePreRound();
|
||||||
return;
|
break;
|
||||||
|
|
||||||
|
case BATTLE_STATE_AI_SELECTION:
|
||||||
|
battleUpdateAiSelection();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BATTLE_STATE_MOVES_EXECUTING:
|
||||||
|
battleUpdateMovesExecuting();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BATTLE_STATE_POST_ROUND:
|
||||||
|
battleUpdatePostRound();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
||||||
|
// BATTLE_STATE_NONE/ENDED: handled above.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
|
||||||
|
|
||||||
battlefighter_t *target = battleAIChooseTarget(current);
|
|
||||||
if(target != NULL) battleResolveAttack(current, target);
|
|
||||||
|
|
||||||
battleCheckResult();
|
|
||||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
||||||
BATTLE.turnCount = 0;
|
BATTLE.executionCount = 0;
|
||||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||||
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||||
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
||||||
const uint8_t key = BATTLE.turnOrder[i];
|
const uint8_t key = BATTLE.executionOrder[i];
|
||||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||||
|
|
||||||
int8_t j = (int8_t)i - 1;
|
int8_t j = (int8_t)i - 1;
|
||||||
while(
|
while(
|
||||||
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
j >= 0 &&
|
||||||
|
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
||||||
) {
|
) {
|
||||||
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
||||||
j--;
|
j--;
|
||||||
}
|
}
|
||||||
BATTLE.turnOrder[j + 1] = key;
|
BATTLE.executionOrder[j + 1] = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!applyEncounterBias) return;
|
if(!applyEncounterBias) return;
|
||||||
@@ -192,16 +205,16 @@ void battleMoveTeamFirst(const battlefighterteam_t team) {
|
|||||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||||
uint8_t count = 0;
|
uint8_t count = 0;
|
||||||
|
|
||||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
||||||
sorted[count++] = BATTLE.turnOrder[i];
|
sorted[count++] = BATTLE.executionOrder[i];
|
||||||
}
|
}
|
||||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
||||||
sorted[count++] = BATTLE.turnOrder[i];
|
sorted[count++] = BATTLE.executionOrder[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
memoryCopy(BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||||
@@ -221,3 +234,93 @@ battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
|||||||
|
|
||||||
return weakest;
|
return weakest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void battleSetState(const battlestate_t next) {
|
||||||
|
const battlestate_t previous = BATTLE.state;
|
||||||
|
BATTLE.state = next;
|
||||||
|
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleSetResult(const battleresult_t result) {
|
||||||
|
BATTLE.result = result;
|
||||||
|
battleSetState(BATTLE_STATE_ENDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t battleFighterNeedsDecision(
|
||||||
|
const uint8_t fighterIndex,
|
||||||
|
const battlefightercontroller_t controller
|
||||||
|
) {
|
||||||
|
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
||||||
|
&& BATTLE.fighters[fighterIndex].controller == controller
|
||||||
|
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleAdvanceSelection(void) {
|
||||||
|
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
||||||
|
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
||||||
|
if(
|
||||||
|
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BATTLE.selectionIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
battleSetState(BATTLE_STATE_AI_SELECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleUpdatePreRound(void) {
|
||||||
|
// No need to clear BATTLE.actions here: every living fighter's action is
|
||||||
|
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
||||||
|
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
||||||
|
// battleInit(). Clearing it here would also wipe out any action a
|
||||||
|
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
||||||
|
battleBuildExecutionOrder(BATTLE.round == 1);
|
||||||
|
|
||||||
|
BATTLE.selectionIndex = 0;
|
||||||
|
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
||||||
|
battleAdvanceSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleUpdateAiSelection(void) {
|
||||||
|
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||||
|
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
||||||
|
if(
|
||||||
|
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
||||||
|
) continue;
|
||||||
|
|
||||||
|
battlefighter_t *target =
|
||||||
|
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
||||||
|
if(target == NULL) continue;
|
||||||
|
|
||||||
|
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
BATTLE.executionIndex = 0;
|
||||||
|
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleUpdateMovesExecuting(void) {
|
||||||
|
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
||||||
|
battleSetState(BATTLE_STATE_POST_ROUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
||||||
|
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||||
|
if(!battleFighterIsAlive(fighter)) return;
|
||||||
|
|
||||||
|
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||||
|
if(action->type == BATTLE_ACTION_ATTACK) {
|
||||||
|
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
||||||
|
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
||||||
|
}
|
||||||
|
action->type = BATTLE_ACTION_NONE;
|
||||||
|
|
||||||
|
battleCheckResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
void battleUpdatePostRound(void) {
|
||||||
|
BATTLE.round++;
|
||||||
|
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||||
|
}
|
||||||
|
|||||||
+155
-36
@@ -27,19 +27,68 @@ typedef enum {
|
|||||||
BATTLE_RESULT_COUNT
|
BATTLE_RESULT_COUNT
|
||||||
} battleresult_t;
|
} battleresult_t;
|
||||||
|
|
||||||
|
// Where BATTLE currently is within a round. A cutscene can pause progression
|
||||||
|
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
|
||||||
|
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
|
||||||
|
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
|
||||||
|
typedef enum {
|
||||||
|
BATTLE_STATE_NONE, // Battle inactive.
|
||||||
|
|
||||||
|
BATTLE_STATE_OPENING, // Entered once by battleStart().
|
||||||
|
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
|
||||||
|
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
|
||||||
|
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
|
||||||
|
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
|
||||||
|
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
|
||||||
|
|
||||||
|
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
|
||||||
|
|
||||||
|
BATTLE_STATE_COUNT
|
||||||
|
} battlestate_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
||||||
|
BATTLE_ACTION_ATTACK,
|
||||||
|
|
||||||
|
BATTLE_ACTION_COUNT
|
||||||
|
} battleactiontype_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool_t active;
|
battleactiontype_t type;
|
||||||
|
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
||||||
|
} battleaction_t;
|
||||||
|
|
||||||
|
typedef void (*battlestatechangedcallback_t)(
|
||||||
|
const battlestate_t previous,
|
||||||
|
const battlestate_t next
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef void (*battleactiondecidedcallback_t)(
|
||||||
|
const battlefighter_t *fighter,
|
||||||
|
const battleaction_t *action
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
battlestate_t state;
|
||||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||||
|
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
||||||
|
|
||||||
battleencountertype_t encounterType;
|
battleencountertype_t encounterType;
|
||||||
bool_t fleeAvailable;
|
bool_t fleeAvailable;
|
||||||
battleresult_t result;
|
battleresult_t result;
|
||||||
|
|
||||||
// Fighter indices (into fighters[]), sorted for the current round.
|
// Fighter indices (into fighters[]), sorted for the current round.
|
||||||
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||||
uint8_t turnCount;
|
uint8_t executionCount;
|
||||||
uint8_t turnIndex;
|
uint8_t executionIndex;
|
||||||
uint16_t round;
|
uint16_t round;
|
||||||
|
|
||||||
|
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
||||||
|
// the next player-controlled fighter that still needs a decision.
|
||||||
|
uint8_t selectionIndex;
|
||||||
|
|
||||||
|
battlestatechangedcallback_t onStateChanged;
|
||||||
|
battleactiondecidedcallback_t onActionDecided;
|
||||||
} battle_t;
|
} battle_t;
|
||||||
|
|
||||||
extern battle_t BATTLE;
|
extern battle_t BATTLE;
|
||||||
@@ -77,11 +126,10 @@ battlefighter_t *battleAddFighter(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the battle: builds the opening turn order (biased by
|
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
||||||
* encounterType for the first round only) and marks the battle active.
|
* active. Call once every fighter has been added via battleAddFighter.
|
||||||
* Call once every fighter has been added via battleAddFighter.
|
|
||||||
*
|
*
|
||||||
* @param encounterType Determines the opening round's turn order.
|
* @param encounterType Determines the opening round's execution order.
|
||||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||||
*/
|
*/
|
||||||
void battleStart(
|
void battleStart(
|
||||||
@@ -95,10 +143,11 @@ void battleStart(
|
|||||||
void battleDispose(void);
|
void battleDispose(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the fighter whose turn it currently is.
|
* Returns the fighter currently awaiting a player decision.
|
||||||
*
|
*
|
||||||
* @return Pointer to the active fighter, or NULL if the battle isn't
|
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
||||||
* active or has no living fighters left to act.
|
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
||||||
|
* fighter has already decided.
|
||||||
*/
|
*/
|
||||||
battlefighter_t *battleGetCurrentFighter(void);
|
battlefighter_t *battleGetCurrentFighter(void);
|
||||||
|
|
||||||
@@ -125,61 +174,68 @@ void battleResolveAttack(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ends the current fighter's turn and advances to the next fighter in
|
* Checks whether the battle has been won or lost, transitioning to
|
||||||
* the turn order, starting a new round (rebuilding turn order purely by
|
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
||||||
* speed) once every fighter in the current round has acted.
|
* result has already been set (e.g. by a successful flee).
|
||||||
*/
|
|
||||||
void battleNextTurn(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the battle has been won or lost, updating and
|
|
||||||
* returning BATTLE.result. Does nothing if a result has already been
|
|
||||||
* set (e.g. by a successful flee).
|
|
||||||
*
|
*
|
||||||
* @return The battle's current result.
|
* @return The battle's current result.
|
||||||
*/
|
*/
|
||||||
battleresult_t battleCheckResult(void);
|
battleresult_t battleCheckResult(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits the current fighter's attack against a target, if it is
|
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
||||||
* currently a player-controlled fighter's turn. Resolves the attack,
|
* reaches them this round, overwriting any action already queued for that
|
||||||
* checks for a battle result, and advances the turn.
|
* fighter. Fires BATTLE.onActionDecided.
|
||||||
|
*
|
||||||
|
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||||
|
* @param type The type of action to perform.
|
||||||
|
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
||||||
|
* for BATTLE_ACTION_ATTACK.
|
||||||
|
*/
|
||||||
|
void battleQueueAction(
|
||||||
|
const uint8_t fighterIndex,
|
||||||
|
const battleactiontype_t type,
|
||||||
|
const uint8_t targetIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the currently-selecting fighter's attack against a target, if the
|
||||||
|
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
||||||
|
* Queues the action and advances the selection cursor.
|
||||||
*
|
*
|
||||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||||
*/
|
*/
|
||||||
void battlePlayerAttack(const uint8_t targetIndex);
|
void battlePlayerAttack(const uint8_t targetIndex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submits a flee attempt for the current fighter's turn, if it is
|
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
||||||
* currently a player-controlled fighter's turn and fleeing is
|
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
||||||
* available for this battle. Always succeeds, ending the battle with
|
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
||||||
* BATTLE_RESULT_FLED.
|
|
||||||
*/
|
*/
|
||||||
void battlePlayerFlee(void);
|
void battlePlayerFlee(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the battle simulation for one frame: resolves the current
|
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
||||||
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
* No-op if the battle isn't active, has already ended, or
|
||||||
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
* CUTSCENE_PAUSE_BATTLE is set.
|
||||||
* battle isn't active or already has a result.
|
|
||||||
*/
|
*/
|
||||||
void battleUpdate(void);
|
void battleUpdate(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
|
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
||||||
* fighter, sorted by speed descending.
|
* fighter, sorted by speed descending.
|
||||||
*
|
*
|
||||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||||
* for the opening round).
|
* for the opening round).
|
||||||
*/
|
*/
|
||||||
void battleBuildTurnOrder(const bool_t applyEncounterBias);
|
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stably partitions BATTLE.turnOrder so every fighter on the given team
|
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
||||||
* comes first, preserving each side's relative (speed-sorted) order.
|
* comes first, preserving each side's relative (speed-sorted) order.
|
||||||
*
|
*
|
||||||
* @param team The team to move to the front of the turn order.
|
* @param team The team to move to the front of the execution order.
|
||||||
*/
|
*/
|
||||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||||
|
|
||||||
@@ -192,3 +248,66 @@ void battleMoveTeamFirst(const battlefighterteam_t team);
|
|||||||
* living fighters.
|
* living fighters.
|
||||||
*/
|
*/
|
||||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
||||||
|
* new state.
|
||||||
|
*
|
||||||
|
* @param next The state to transition to.
|
||||||
|
*/
|
||||||
|
void battleSetState(const battlestate_t next);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
||||||
|
*
|
||||||
|
* @param result The result to end the battle with.
|
||||||
|
*/
|
||||||
|
void battleSetResult(const battleresult_t result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a fighter is a live, undecided candidate for the given
|
||||||
|
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
||||||
|
* be deciding an action for it this round.
|
||||||
|
*
|
||||||
|
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||||
|
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
||||||
|
* currently deciding for.
|
||||||
|
* @return True if the fighter is alive, matches controller, and has no
|
||||||
|
* action queued yet.
|
||||||
|
*/
|
||||||
|
bool_t battleFighterNeedsDecision(
|
||||||
|
const uint8_t fighterIndex,
|
||||||
|
const battlefightercontroller_t controller
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
||||||
|
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
||||||
|
* none remain.
|
||||||
|
*/
|
||||||
|
void battleAdvanceSelection(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
||||||
|
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
||||||
|
*/
|
||||||
|
void battleUpdatePreRound(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
||||||
|
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
||||||
|
*/
|
||||||
|
void battleUpdateAiSelection(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
||||||
|
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
||||||
|
* once every fighter this round has been processed.
|
||||||
|
*/
|
||||||
|
void battleUpdateMovesExecuting(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
||||||
|
* back to BATTLE_STATE_PRE_ROUND.
|
||||||
|
*/
|
||||||
|
void battleUpdatePostRound(void);
|
||||||
|
|||||||
@@ -6,6 +6,5 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
savevita.c
|
testbattle.c
|
||||||
savestreamvita.c
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "testbattle.h"
|
||||||
|
#include "rpg/battle/battle.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
|
||||||
|
void testBattleStart(void) {
|
||||||
|
battleInit();
|
||||||
|
|
||||||
|
const battlefighterstats_t allyOneStats =
|
||||||
|
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
|
||||||
|
const battlefighterstats_t allyTwoStats =
|
||||||
|
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
|
||||||
|
const battlefighterstats_t enemyOneStats =
|
||||||
|
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
|
||||||
|
const battlefighterstats_t enemyTwoStats =
|
||||||
|
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
|
||||||
|
|
||||||
|
battleAddFighter(
|
||||||
|
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||||
|
allyOneStats, 30, 10
|
||||||
|
);
|
||||||
|
battleAddFighter(
|
||||||
|
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||||
|
allyTwoStats, 25, 10
|
||||||
|
);
|
||||||
|
battleAddFighter(
|
||||||
|
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||||
|
enemyOneStats, 20, 5
|
||||||
|
);
|
||||||
|
battleAddFighter(
|
||||||
|
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||||
|
enemyTwoStats, 20, 5
|
||||||
|
);
|
||||||
|
|
||||||
|
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||||
|
sceneSet(SCENE_TYPE_BATTLE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
|
||||||
|
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
|
||||||
|
* played without a real encounter trigger yet.
|
||||||
|
*/
|
||||||
|
void testBattleStart(void);
|
||||||
@@ -53,6 +53,25 @@ typedef struct cutscene_s {
|
|||||||
#define CUTSCENE_WAIT(WAIT) \
|
#define CUTSCENE_WAIT(WAIT) \
|
||||||
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
|
{ .type = CUTSCENE_ITEM_TYPE_WAIT, .wait = WAIT }
|
||||||
|
|
||||||
|
// A named, otherwise no-op position in the item list that cutsceneGoTo
|
||||||
|
// can jump execution straight to. NAME is matched with stringEquals,
|
||||||
|
// not pointer identity, so it's safe to use separate string literals
|
||||||
|
// with the same contents at the marker and at each call site.
|
||||||
|
#define CUTSCENE_MARKER(NAME) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_MARKER, .marker = { .name = NAME } }
|
||||||
|
|
||||||
|
// Restarts the currently running cutscene from its first item,
|
||||||
|
// preserving whatever interact/interacted entities triggered it.
|
||||||
|
#define CUTSCENE_RESTART() \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_RESTART }
|
||||||
|
|
||||||
|
// Requests a switch to a different SCENE_TYPE via sceneSet, then
|
||||||
|
// immediately continues on to whatever follows this item - the switch
|
||||||
|
// itself doesn't happen until the next sceneUpdate() tick, so it does
|
||||||
|
// not take effect this frame.
|
||||||
|
#define CUTSCENE_SCENE(TYPE) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_SCENE, .sceneChange = { .type = TYPE } }
|
||||||
|
|
||||||
#define CUTSCENE_CUTSCENE(CUTSCENE) \
|
#define CUTSCENE_CUTSCENE(CUTSCENE) \
|
||||||
{ \
|
{ \
|
||||||
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, \
|
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, \
|
||||||
@@ -62,6 +81,124 @@ typedef struct cutscene_s {
|
|||||||
#define CUTSCENE_CALLBACK(CALLBACK) \
|
#define CUTSCENE_CALLBACK(CALLBACK) \
|
||||||
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK }
|
{ .type = CUTSCENE_ITEM_TYPE_CALLBACK, .callback = CALLBACK }
|
||||||
|
|
||||||
|
#define CUTSCENE_PRINT(TEXT) \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_PRINT, .print = { .text = TEXT } }
|
||||||
|
|
||||||
|
// Shows a message-only modal (no option buttons) and immediately
|
||||||
|
// continues on to whatever follows this item - it does not wait for
|
||||||
|
// the dialog to be dismissed. Script the rest of the interaction (e.g.
|
||||||
|
// CUTSCENE_CALLBACK to kick off work, CUTSCENE_WAIT, then
|
||||||
|
// CUTSCENE_MODAL_CLOSE) as later items in the same cutscene.
|
||||||
|
// TITLE and MESSAGE are each displayed as-is unless they match a
|
||||||
|
// locale message ID, in which case the translated string is shown
|
||||||
|
// instead - see uiModalLocalize.
|
||||||
|
#define CUTSCENE_MODAL(TITLE, MESSAGE) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||||
|
.modal = { .title = TITLE, .message = MESSAGE } \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows a modal with option buttons and immediately continues on, same
|
||||||
|
// as CUTSCENE_MODAL - it does not block waiting for a selection.
|
||||||
|
// Back/cancel input is disabled while it's open (see
|
||||||
|
// uiMenuSetDisableBack), so it must be dismissed by picking one; CALLBACK
|
||||||
|
// then fires with the selected option index once the dialog closes.
|
||||||
|
// Option labels are passed as trailing arguments, e.g.
|
||||||
|
// CUTSCENE_MODAL_OPTIONS(title, message, callback, "Retry", "Cancel") -
|
||||||
|
// their strings are not copied by this item, so they must stay valid
|
||||||
|
// until the modal opens (string literals are fine). Like TITLE and
|
||||||
|
// MESSAGE, each option is translated if it matches a locale message ID.
|
||||||
|
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||||
|
.modal = { \
|
||||||
|
.title = TITLE, .message = MESSAGE, \
|
||||||
|
.options = (const char_t *[]){ __VA_ARGS__ }, \
|
||||||
|
.optionCount = (uint8_t)( \
|
||||||
|
sizeof((const char_t *[]){ __VA_ARGS__ }) / sizeof(const char_t *) \
|
||||||
|
), \
|
||||||
|
.callback = CALLBACK \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly one option and
|
||||||
|
// MARKER1 to jump straight to via cutsceneGoTo once it's selected - no
|
||||||
|
// callback function to write. Does not fall through to whatever follows
|
||||||
|
// this item, so MARKER1 must be scripted elsewhere in the same cutscene.
|
||||||
|
#define CUTSCENE_MODAL_OPTIONS_ONE(TITLE, MESSAGE, OPTION1, MARKER1) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
|
||||||
|
.modalOptionsMarkers = { \
|
||||||
|
.title = TITLE, .message = MESSAGE, \
|
||||||
|
.options = { OPTION1 }, \
|
||||||
|
.markers = { MARKER1 }, \
|
||||||
|
.optionCount = 1 \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as CUTSCENE_MODAL_OPTIONS, but fixed to exactly two options,
|
||||||
|
// jumping straight to OPTION1_MARKER or OPTION2_MARKER via cutsceneGoTo
|
||||||
|
// once the corresponding option is selected - no callback function to
|
||||||
|
// write. Does not fall through to whatever follows this item, so both
|
||||||
|
// markers must be scripted elsewhere in the same cutscene.
|
||||||
|
#define CUTSCENE_MODAL_OPTIONS_TWO( \
|
||||||
|
TITLE, MESSAGE, OPTION1, OPTION1_MARKER, OPTION2, OPTION2_MARKER \
|
||||||
|
) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS, \
|
||||||
|
.modalOptionsMarkers = { \
|
||||||
|
.title = TITLE, .message = MESSAGE, \
|
||||||
|
.options = { OPTION1, OPTION2 }, \
|
||||||
|
.markers = { OPTION1_MARKER, OPTION2_MARKER }, \
|
||||||
|
.optionCount = 2 \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the currently open modal (if any). Useful when a modal was
|
||||||
|
// opened outside of a blocking CUTSCENE_MODAL item (e.g. directly via
|
||||||
|
// uiModalOpen) and this cutscene just needs to dismiss it and continue
|
||||||
|
// on to whatever follows this item in the sequence.
|
||||||
|
#define CUTSCENE_MODAL_CLOSE() \
|
||||||
|
{ .type = CUTSCENE_ITEM_TYPE_MODAL_CLOSE }
|
||||||
|
|
||||||
|
// Opens the on-screen keyboard and blocks the cutscene until it closes,
|
||||||
|
// then caches whatever was typed - see cutsceneSystemGetTextCache to
|
||||||
|
// read it back afterwards. __VA_ARGS__ are uikeyboardopen_t designated
|
||||||
|
// initializers, e.g. CUTSCENE_KEYBOARD(.cancel = true, .maxLength = 8).
|
||||||
|
#define CUTSCENE_KEYBOARD(...) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_KEYBOARD, \
|
||||||
|
.keyboard = { .open = { __VA_ARGS__ } } \
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Re)requests an available save device and jumps straight to
|
||||||
|
// SUCCESS_MARKER or FAILURE_MARKER once it resolves, same as a
|
||||||
|
// CUTSCENE_MODAL_OPTIONS callback - it does not fall through to
|
||||||
|
// whatever follows this item, so both markers must be scripted
|
||||||
|
// elsewhere in the same cutscene.
|
||||||
|
#define CUTSCENE_SAVE_DEVICE_CHECK(SUCCESS_MARKER, FAILURE_MARKER) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, \
|
||||||
|
.saveDeviceCheck = { \
|
||||||
|
.successMarker = SUCCESS_MARKER, \
|
||||||
|
.failureMarker = FAILURE_MARKER \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Re)loads every save slot via saveLoadAllSlots() and jumps straight to
|
||||||
|
// SUCCESS_MARKER or FAILURE_MARKER once it resolves, same shape as
|
||||||
|
// CUTSCENE_SAVE_DEVICE_CHECK - it does not fall through to whatever
|
||||||
|
// follows this item, so both markers must be scripted elsewhere in the
|
||||||
|
// same cutscene.
|
||||||
|
#define CUTSCENE_SAVE_LOAD_ALL_SLOTS(SUCCESS_MARKER, FAILURE_MARKER) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, \
|
||||||
|
.saveLoadAllSlots = { \
|
||||||
|
.successMarker = SUCCESS_MARKER, \
|
||||||
|
.failureMarker = FAILURE_MARKER \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \
|
#define CUTSCENE_ENTITY_WALK_TO(ENTITY_INDEX, X, Y, Z) \
|
||||||
{ \
|
{ \
|
||||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
||||||
@@ -162,6 +299,28 @@ typedef struct cutscene_s {
|
|||||||
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Waits until BATTLE.state reaches STATE. Put this BEFORE
|
||||||
|
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
|
||||||
|
// freezes BATTLE.state wherever it already is, so it would never reach
|
||||||
|
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
|
||||||
|
// moment it's satisfied, catches the battle right at STATE before it can
|
||||||
|
// advance further.
|
||||||
|
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
|
||||||
|
.battleWaitState = { .state = STATE } \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
|
||||||
|
// bypassing normal player/AI selection for that fighter this round.
|
||||||
|
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
|
||||||
|
{ \
|
||||||
|
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
|
||||||
|
.battleForceAction = { \
|
||||||
|
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ typedef uint8_t cutscenepause_t;
|
|||||||
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
||||||
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
||||||
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
||||||
|
#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3))
|
||||||
|
|
||||||
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
||||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
||||||
))
|
))
|
||||||
|
|
||||||
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
||||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
|
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \
|
||||||
|
CUTSCENE_PAUSE_BATTLE \
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "cutscenesystem.h"
|
#include "cutscenesystem.h"
|
||||||
#include "rpg/entity/entity.h"
|
#include "rpg/entity/entity.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|
||||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||||
@@ -16,11 +17,7 @@ void cutsceneSystemInit() {
|
|||||||
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
|
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
|
||||||
}
|
}
|
||||||
|
|
||||||
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
|
void cutsceneSystemPrepare(
|
||||||
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void cutsceneSystemStartCutsceneWith(
|
|
||||||
const cutscene_t *cutscene,
|
const cutscene_t *cutscene,
|
||||||
entity_t *interact,
|
entity_t *interact,
|
||||||
entity_t *interacted
|
entity_t *interacted
|
||||||
@@ -38,10 +35,49 @@ void cutsceneSystemStartCutsceneWith(
|
|||||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
CUTSCENE_SYSTEM.currentItem = 0xFF;// Set to 0xFF so Next wraps to 0.
|
||||||
|
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
|
||||||
|
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemStartCutsceneWith(
|
||||||
|
const cutscene_t *cutscene,
|
||||||
|
entity_t *interact,
|
||||||
|
entity_t *interacted
|
||||||
|
) {
|
||||||
|
cutsceneSystemPrepare(cutscene, interact, interacted);
|
||||||
cutsceneSystemNext();
|
cutsceneSystemNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||||
|
const cutscene_t *cutscene,
|
||||||
|
const char_t *marker
|
||||||
|
) {
|
||||||
|
cutsceneSystemPrepare(cutscene, NULL, NULL);
|
||||||
|
cutsceneGoTo(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneRestart(void) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.scene, "cutsceneRestart called with no cutscene running"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A restart is the same logical run trying again (e.g. retrying a failed
|
||||||
|
// save-device check), not a fresh unrelated start, so it should not
|
||||||
|
// silently drop a completion callback the caller already armed.
|
||||||
|
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
||||||
|
cutsceneSystemStartCutsceneWith(
|
||||||
|
CUTSCENE_SYSTEM.scene,
|
||||||
|
CUTSCENE_SYSTEM.entityInteract,
|
||||||
|
CUTSCENE_SYSTEM.entityInteracted
|
||||||
|
);
|
||||||
|
CUTSCENE_SYSTEM.onComplete = onComplete;
|
||||||
|
}
|
||||||
|
|
||||||
void cutsceneSystemUpdate() {
|
void cutsceneSystemUpdate() {
|
||||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
||||||
|
|
||||||
@@ -58,6 +94,13 @@ void cutsceneSystemNext() {
|
|||||||
if(
|
if(
|
||||||
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
|
CUTSCENE_SYSTEM.currentItem >= CUTSCENE_SYSTEM.scene->itemCount
|
||||||
) {
|
) {
|
||||||
|
// Saved and cleared before firing so a callback that immediately
|
||||||
|
// starts another cutscene (or sets its own onComplete) isn't clobbered
|
||||||
|
// by this function's own cleanup running after it - same reentrancy
|
||||||
|
// hazard as uiFocusPop, see src/dusk/ui/focus/uifocus.c.
|
||||||
|
cutscenecallback_t onComplete = CUTSCENE_SYSTEM.onComplete;
|
||||||
|
void *userData = CUTSCENE_SYSTEM.userData;
|
||||||
|
|
||||||
CUTSCENE_SYSTEM.scene = NULL;
|
CUTSCENE_SYSTEM.scene = NULL;
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||||
@@ -66,7 +109,11 @@ void cutsceneSystemNext() {
|
|||||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||||
|
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||||
|
|
||||||
|
if(onComplete != NULL) onComplete(userData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +123,35 @@ void cutsceneSystemNext() {
|
|||||||
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.scene,
|
||||||
|
"cutsceneSystemSetOnComplete called with no cutscene running"
|
||||||
|
);
|
||||||
|
CUTSCENE_SYSTEM.onComplete = onComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneGoTo(const char_t *name) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.scene, "cutsceneGoTo called with no cutscene running"
|
||||||
|
);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < CUTSCENE_SYSTEM.scene->itemCount; i++) {
|
||||||
|
const cutsceneitem_t *item = &CUTSCENE_SYSTEM.scene->items[i];
|
||||||
|
if(
|
||||||
|
item->type == CUTSCENE_ITEM_TYPE_MARKER &&
|
||||||
|
stringEquals(item->marker.name, name)
|
||||||
|
) {
|
||||||
|
CUTSCENE_SYSTEM.currentItem = i;
|
||||||
|
memoryZero(&CUTSCENE_SYSTEM.data, sizeof(CUTSCENE_SYSTEM.data));
|
||||||
|
cutsceneItemStart(item, &CUTSCENE_SYSTEM.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(false, "cutsceneGoTo: no marker found with that name");
|
||||||
|
}
|
||||||
|
|
||||||
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
const cutsceneitem_t * cutsceneSystemGetCurrentItem() {
|
||||||
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
||||||
|
|
||||||
@@ -144,6 +220,16 @@ uint8_t cutsceneSystemGetTextMiniId(const uint8_t index) {
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char_t * cutsceneSystemGetTextCache(void) {
|
||||||
|
return CUTSCENE_SYSTEM.textCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemSetTextCache(const char_t *text) {
|
||||||
|
stringCopy(
|
||||||
|
CUTSCENE_SYSTEM.textCache, text, CUTSCENE_TEXT_CACHE_MAX - 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void cutsceneSystemDispose() {
|
void cutsceneSystemDispose() {
|
||||||
CUTSCENE_SYSTEM.scene = NULL;
|
CUTSCENE_SYSTEM.scene = NULL;
|
||||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||||
@@ -154,4 +240,6 @@ void cutsceneSystemDispose() {
|
|||||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_LAST_CREATED;
|
||||||
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
CUTSCENE_SYSTEM.textMiniLastCreated = CUTSCENE_TEXT_MINI_LAST_CREATED;
|
||||||
|
CUTSCENE_SYSTEM.textCache[0] = '\0';
|
||||||
|
CUTSCENE_SYSTEM.onComplete = NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ typedef struct entity_s entity_t;
|
|||||||
// cutscene_t.dataSize.
|
// cutscene_t.dataSize.
|
||||||
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
|
#define CUTSCENE_SYSTEM_SIZE_MAX 8192
|
||||||
|
|
||||||
|
// Size of CUTSCENE_SYSTEM.textCache - keep >= UI_KEYBOARD_TEXT_MAX (see
|
||||||
|
// ui/dialog/keyboard/uikeyboard.h) so text entered via a
|
||||||
|
// CUTSCENE_ITEM_TYPE_KEYBOARD item is never truncated caching it here.
|
||||||
|
#define CUTSCENE_TEXT_CACHE_MAX 64
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const cutscene_t *scene;
|
const cutscene_t *scene;
|
||||||
uint8_t currentItem;
|
uint8_t currentItem;
|
||||||
@@ -32,12 +37,21 @@ typedef struct {
|
|||||||
uint8_t areaLastCreated;
|
uint8_t areaLastCreated;
|
||||||
uint8_t textMiniLastCreated;
|
uint8_t textMiniLastCreated;
|
||||||
|
|
||||||
|
// Free-form text cache, e.g. holding whatever was last typed via a
|
||||||
|
// CUTSCENE_ITEM_TYPE_KEYBOARD item - see cutsceneSystemGetTextCache/
|
||||||
|
// cutsceneSystemSetTextCache. Not tied to any one item type; any item
|
||||||
|
// may read or write it.
|
||||||
|
char_t textCache[CUTSCENE_TEXT_CACHE_MAX];
|
||||||
|
|
||||||
// Data (used by the current item).
|
// Data (used by the current item).
|
||||||
cutsceneitemdata_t data;
|
cutsceneitemdata_t data;
|
||||||
|
|
||||||
// Custom user data for the running cutscene, sized per-scene by
|
// Custom user data for the running cutscene, sized per-scene by
|
||||||
// cutscene_t.dataSize.
|
// cutscene_t.dataSize.
|
||||||
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
|
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
|
||||||
|
|
||||||
|
// See cutsceneSystemSetOnComplete.
|
||||||
|
cutscenecallback_t onComplete;
|
||||||
} cutscenesystem_t;
|
} cutscenesystem_t;
|
||||||
|
|
||||||
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||||
@@ -67,6 +81,47 @@ void cutsceneSystemStartCutsceneWith(
|
|||||||
entity_t *interacted
|
entity_t *interacted
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a cutscene with no bound entities, jumping straight to the
|
||||||
|
* CUTSCENE_MARKER item with the given name instead of running from the
|
||||||
|
* first item - as if cutsceneGoTo(marker) had been called immediately
|
||||||
|
* after cutsceneSystemStartCutscene. Asserts if no marker with that
|
||||||
|
* name exists in the cutscene.
|
||||||
|
*
|
||||||
|
* @param cutscene Pointer to the cutscene to start.
|
||||||
|
* @param marker Marker name to jump to, matched with stringEquals.
|
||||||
|
*/
|
||||||
|
void cutsceneSystemStartCutsceneAndGoToMarker(
|
||||||
|
const cutscene_t *cutscene,
|
||||||
|
const char_t *marker
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restarts the currently running cutscene from its first item,
|
||||||
|
* preserving whatever interact/interacted entities triggered it.
|
||||||
|
* Asserts if no cutscene is running.
|
||||||
|
*/
|
||||||
|
void cutsceneRestart(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a native callback to fire once when the currently running cutscene
|
||||||
|
* finishes by running off the end of its item list. A fresh
|
||||||
|
* cutsceneSystemStartCutscene* call clears any previously set callback, so
|
||||||
|
* call this again after starting a new cutscene to arm it - but
|
||||||
|
* cutsceneRestart() preserves whatever was armed, since a restart (e.g.
|
||||||
|
* retrying a failed check) is the same logical run trying again, not a new
|
||||||
|
* one. Invoked with CUTSCENE_SYSTEM.userData, same as CUTSCENE_CALLBACK.
|
||||||
|
*
|
||||||
|
* Exists so a runtime-loaded cutscene file (which can't store a native
|
||||||
|
* function pointer) can still hand off to native code once it's done,
|
||||||
|
* without needing a whole name->function registry: the file just ends
|
||||||
|
* normally, and whoever started it supplies what happens next.
|
||||||
|
*
|
||||||
|
* @param onComplete Callback to fire on natural completion. May be NULL
|
||||||
|
* to clear a previously set one.
|
||||||
|
*/
|
||||||
|
void cutsceneSystemSetOnComplete(cutscenecallback_t onComplete);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a raw entity index (or sentinel) to an entity pointer.
|
* Resolves a raw entity index (or sentinel) to an entity pointer.
|
||||||
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
|
* Handles CUTSCENE_ENTITY_INTERACT, CUTSCENE_ENTITY_INTERACTED,
|
||||||
@@ -97,11 +152,44 @@ uint8_t cutsceneSystemGetAreaId(const uint8_t areaId);
|
|||||||
*/
|
*/
|
||||||
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
|
uint8_t cutsceneSystemGetTextMiniId(const uint8_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns CUTSCENE_SYSTEM.textCache - whatever was last written there via
|
||||||
|
* cutsceneSystemSetTextCache (e.g. by a CUTSCENE_ITEM_TYPE_KEYBOARD item
|
||||||
|
* once its keyboard closes). Empty ("") if nothing has been cached yet
|
||||||
|
* for the running cutscene.
|
||||||
|
*
|
||||||
|
* @returns The cached text.
|
||||||
|
*/
|
||||||
|
const char_t * cutsceneSystemGetTextCache(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites CUTSCENE_SYSTEM.textCache with a copy of text. Any item may
|
||||||
|
* call this - it isn't tied to any one item type - so later items can
|
||||||
|
* read back whatever the caller wants to pass along, up to
|
||||||
|
* CUTSCENE_TEXT_CACHE_MAX - 1 characters.
|
||||||
|
*
|
||||||
|
* @param text The text to cache; copied internally, safe to be
|
||||||
|
* transient. Must not exceed CUTSCENE_TEXT_CACHE_MAX - 1 characters.
|
||||||
|
*/
|
||||||
|
void cutsceneSystemSetTextCache(const char_t *text);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advance to the next item in the cutscene.
|
* Advance to the next item in the cutscene.
|
||||||
*/
|
*/
|
||||||
void cutsceneSystemNext();
|
void cutsceneSystemNext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jumps the running cutscene directly to the CUTSCENE_MARKER item with
|
||||||
|
* the given name and starts it immediately, as if cutsceneSystemNext()
|
||||||
|
* had advanced straight to it. Intended to be called from within
|
||||||
|
* another item's start/update (e.g. a CUTSCENE_CALLBACK) to implement
|
||||||
|
* flow control. Asserts if no cutscene is running or no marker with
|
||||||
|
* that name exists in it.
|
||||||
|
*
|
||||||
|
* @param name Marker name to search for, matched with stringEquals.
|
||||||
|
*/
|
||||||
|
void cutsceneGoTo(const char_t *name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the cutscene system for one frame.
|
* Update the cutscene system for one frame.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
cutsceneitem.c
|
cutsceneitem.c
|
||||||
cutscenecallback.c
|
cutscenecallback.c
|
||||||
|
cutsceneprint.c
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(control)
|
add_subdirectory(control)
|
||||||
@@ -15,3 +16,4 @@ add_subdirectory(item)
|
|||||||
add_subdirectory(maparea)
|
add_subdirectory(maparea)
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
add_subdirectory(battle)
|
add_subdirectory(battle)
|
||||||
|
add_subdirectory(save)
|
||||||
|
|||||||
@@ -6,4 +6,6 @@
|
|||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
cutscenestartbattle.c
|
cutscenestartbattle.c
|
||||||
|
cutscenebattlewaitstate.c
|
||||||
|
cutscenebattleforceaction.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
|
||||||
|
void cutsceneBattleForceActionStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
battleQueueAction(
|
||||||
|
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
|
||||||
|
item->battleForceAction.targetIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneBattleForceActionUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/battle/battle.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t fighterIndex;
|
||||||
|
uint8_t targetIndex;
|
||||||
|
} cutscenebattleforceaction_t;
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a battle force-action step: immediately queues an attack for the
|
||||||
|
* given fighter against the given target, bypassing normal player/AI
|
||||||
|
* selection for that fighter this round.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneBattleForceActionStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a battle force-action step (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneBattleForceActionUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
|
||||||
|
bool_t cutsceneBattleWaitStateUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return BATTLE.state == item->battleWaitState.state;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/battle/battle.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
battlestate_t state;
|
||||||
|
} cutscenebattlewaitstate_t;
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a battle wait-state step, completing once BATTLE.state reaches
|
||||||
|
* the watched state. Has no Start callback -- there's nothing to do until
|
||||||
|
* the state is actually reached.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once BATTLE.state equals the watched state.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneBattleWaitStateUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
cutscenewait.c
|
cutscenewait.c
|
||||||
cutscenesetpause.c
|
cutscenesetpause.c
|
||||||
cutsceneconcurrent.c
|
cutsceneconcurrent.c
|
||||||
|
cutscenemarker.c
|
||||||
|
cutscenerestart.c
|
||||||
|
cutscenescene.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
|
||||||
|
void cutsceneMarkerStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneMarkerUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char_t *name;
|
||||||
|
} cutscenemarker_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a marker item. A marker does nothing on its own - it exists
|
||||||
|
* purely as a named position for cutsceneGoTo to jump to.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneMarkerStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a marker item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneMarkerUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
|
||||||
|
void cutsceneRestartStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
cutsceneRestart();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneRestartUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a restart item (restarts the currently running cutscene from
|
||||||
|
* its first item via cutsceneRestart).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneRestartStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a restart item. By the time this would run, the cutscene has
|
||||||
|
* already restarted from its first item, so this always reports
|
||||||
|
* incomplete (mirrors cutsceneCutsceneUpdate).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns false always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneRestartUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
|
||||||
|
void cutsceneSceneStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
sceneSet(item->sceneChange.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneSceneUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
#include "scene/scenetype.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
scenetype_t type;
|
||||||
|
} cutscenescene_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a scene item (requests a switch to the given scene via
|
||||||
|
* sceneSet). The switch itself doesn't happen until the next
|
||||||
|
* sceneUpdate() tick, so the rest of the current frame - including
|
||||||
|
* whatever else this cutscene does after this item - still runs
|
||||||
|
* against the scene that's being left.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneSceneStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a scene item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneSceneUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -118,6 +118,65 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
|||||||
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||||
.init = cutsceneShakeStart,
|
.init = cutsceneShakeStart,
|
||||||
.update = cutsceneShakeUpdate
|
.update = cutsceneShakeUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = {
|
||||||
|
.update = cutsceneBattleWaitStateUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
||||||
|
.init = cutsceneBattleForceActionStart,
|
||||||
|
.update = cutsceneBattleForceActionUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MODAL] = {
|
||||||
|
.init = cutsceneModalStart,
|
||||||
|
.update = cutsceneModalUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
|
||||||
|
.init = cutsceneModalOptionsMarkersStart,
|
||||||
|
.update = cutsceneModalOptionsMarkersUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
|
||||||
|
.init = cutsceneModalCloseStart,
|
||||||
|
.update = cutsceneModalCloseUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_PRINT] = {
|
||||||
|
.init = cutscenePrintStart,
|
||||||
|
.update = cutscenePrintUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_MARKER] = {
|
||||||
|
.init = cutsceneMarkerStart,
|
||||||
|
.update = cutsceneMarkerUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_RESTART] = {
|
||||||
|
.init = cutsceneRestartStart,
|
||||||
|
.update = cutsceneRestartUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_SCENE] = {
|
||||||
|
.init = cutsceneSceneStart,
|
||||||
|
.update = cutsceneSceneUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
|
||||||
|
.init = cutsceneSaveDeviceCheckStart,
|
||||||
|
.update = cutsceneSaveDeviceCheckUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS] = {
|
||||||
|
.init = cutsceneSaveLoadAllSlotsStart,
|
||||||
|
.update = cutsceneSaveLoadAllSlotsUpdate
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_KEYBOARD] = {
|
||||||
|
.init = cutsceneKeyboardStart,
|
||||||
|
.update = cutsceneKeyboardUpdate
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,13 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "cutscenecallback.h"
|
#include "cutscenecallback.h"
|
||||||
|
#include "cutsceneprint.h"
|
||||||
#include "control/cutscenewait.h"
|
#include "control/cutscenewait.h"
|
||||||
#include "control/cutscenesetpause.h"
|
#include "control/cutscenesetpause.h"
|
||||||
#include "control/cutsceneconcurrent.h"
|
#include "control/cutsceneconcurrent.h"
|
||||||
|
#include "control/cutscenemarker.h"
|
||||||
|
#include "control/cutscenerestart.h"
|
||||||
|
#include "control/cutscenescene.h"
|
||||||
#include "entity/cutsceneentityteleport.h"
|
#include "entity/cutsceneentityteleport.h"
|
||||||
#include "entity/cutsceneentitywalkto.h"
|
#include "entity/cutsceneentitywalkto.h"
|
||||||
#include "entity/cutsceneentityremove.h"
|
#include "entity/cutsceneentityremove.h"
|
||||||
@@ -22,11 +26,18 @@
|
|||||||
#include "ui/cutscenefade.h"
|
#include "ui/cutscenefade.h"
|
||||||
#include "ui/cutsceneemoji.h"
|
#include "ui/cutsceneemoji.h"
|
||||||
#include "ui/cutsceneshake.h"
|
#include "ui/cutsceneshake.h"
|
||||||
|
#include "ui/cutscenemodal.h"
|
||||||
|
#include "ui/cutscenemodaloptionsmarkers.h"
|
||||||
|
#include "ui/cutscenekeyboard.h"
|
||||||
#include "item/cutsceneitemgive.h"
|
#include "item/cutsceneitemgive.h"
|
||||||
#include "maparea/cutscenemapareaadd.h"
|
#include "maparea/cutscenemapareaadd.h"
|
||||||
#include "maparea/cutscenemaparearemove.h"
|
#include "maparea/cutscenemaparearemove.h"
|
||||||
#include "maparea/cutscenemapareawait.h"
|
#include "maparea/cutscenemapareawait.h"
|
||||||
#include "battle/cutscenestartbattle.h"
|
#include "battle/cutscenestartbattle.h"
|
||||||
|
#include "battle/cutscenebattlewaitstate.h"
|
||||||
|
#include "battle/cutscenebattleforceaction.h"
|
||||||
|
#include "save/cutscenesavedevicecheck.h"
|
||||||
|
#include "save/cutscenesaveloadallslots.h"
|
||||||
|
|
||||||
typedef struct cutscene_s cutscene_t;
|
typedef struct cutscene_s cutscene_t;
|
||||||
|
|
||||||
@@ -55,6 +66,18 @@ typedef enum {
|
|||||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||||
|
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
||||||
|
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
||||||
|
CUTSCENE_ITEM_TYPE_MODAL,
|
||||||
|
CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS,
|
||||||
|
CUTSCENE_ITEM_TYPE_MODAL_CLOSE,
|
||||||
|
CUTSCENE_ITEM_TYPE_PRINT,
|
||||||
|
CUTSCENE_ITEM_TYPE_MARKER,
|
||||||
|
CUTSCENE_ITEM_TYPE_RESTART,
|
||||||
|
CUTSCENE_ITEM_TYPE_SCENE,
|
||||||
|
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
|
||||||
|
CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS,
|
||||||
|
CUTSCENE_ITEM_TYPE_KEYBOARD,
|
||||||
|
|
||||||
CUTSCENE_ITEM_TYPE_COUNT
|
CUTSCENE_ITEM_TYPE_COUNT
|
||||||
} cutsceneitemtype_t;
|
} cutsceneitemtype_t;
|
||||||
@@ -85,6 +108,16 @@ struct cutsceneitem_s {
|
|||||||
cutscenestartbattle_t startBattle;
|
cutscenestartbattle_t startBattle;
|
||||||
cutsceneemoji_t emoji;
|
cutsceneemoji_t emoji;
|
||||||
cutsceneshake_t shake;
|
cutsceneshake_t shake;
|
||||||
|
cutscenebattlewaitstate_t battleWaitState;
|
||||||
|
cutscenebattleforceaction_t battleForceAction;
|
||||||
|
cutscenemodal_t modal;
|
||||||
|
cutscenemodaloptionsmarkers_t modalOptionsMarkers;
|
||||||
|
cutsceneprint_t print;
|
||||||
|
cutscenemarker_t marker;
|
||||||
|
cutscenescene_t sceneChange;
|
||||||
|
cutscenesavedevicecheck_t saveDeviceCheck;
|
||||||
|
cutscenesaveloadallslots_t saveLoadAllSlots;
|
||||||
|
cutscenekeyboard_t keyboard;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "cutsceneitem.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
|
||||||
|
void cutscenePrintStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
consolePrint("%s", item->print.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutscenePrintUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
#define CUTSCENE_PRINT_MAX_CHARS 128
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t text[CUTSCENE_PRINT_MAX_CHARS];
|
||||||
|
} cutsceneprint_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a print item (prints the item's text to the console).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutscenePrintStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a print item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutscenePrintUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
cutscenesavedevicecheck.c
|
||||||
|
cutscenesaveloadallslots.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
|
void cutsceneSaveDeviceCheckCallback(savedevice_t *device, void *user) {
|
||||||
|
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
||||||
|
cutsceneGoTo(
|
||||||
|
device != NULL ?
|
||||||
|
item->saveDeviceCheck.successMarker :
|
||||||
|
item->saveDeviceCheck.failureMarker
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneSaveDeviceCheckStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
saveFindAvailableDevice(cutsceneSaveDeviceCheckCallback, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneSaveDeviceCheckUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char_t *successMarker;
|
||||||
|
const char_t *failureMarker;
|
||||||
|
} cutscenesavedevicecheck_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a save-device-check item: (re)requests an available save
|
||||||
|
* device via saveFindAvailableDevice. Never completes on its own - see
|
||||||
|
* cutsceneSaveDeviceCheckUpdate.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneSaveDeviceCheckStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a save-device-check item. Like a CUTSCENE_MODAL_OPTIONS item,
|
||||||
|
* this never completes on its own - once saveFindAvailableDevice's
|
||||||
|
* callback fires, it jumps straight to the item's successMarker or
|
||||||
|
* failureMarker via cutsceneGoTo.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns false always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneSaveDeviceCheckUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
|
void cutsceneSaveLoadAllSlotsStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
errorret_t result = saveLoadAllSlots();
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
errorCatch(errorPrint(result));
|
||||||
|
cutsceneGoTo(item->saveLoadAllSlots.failureMarker);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cutsceneGoTo(item->saveLoadAllSlots.successMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneSaveLoadAllSlotsUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char_t *successMarker;
|
||||||
|
const char_t *failureMarker;
|
||||||
|
} cutscenesaveloadallslots_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a save-load-all-slots item: calls saveLoadAllSlots() and jumps
|
||||||
|
* straight to the item's successMarker or failureMarker via
|
||||||
|
* cutsceneGoTo, depending on the result.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneSaveLoadAllSlotsStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a save-load-all-slots item. Never completes on its own -
|
||||||
|
* Start already jumps to whichever marker applies before this would
|
||||||
|
* ever run.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns false always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneSaveLoadAllSlotsUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -11,4 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
cutscenefade.c
|
cutscenefade.c
|
||||||
cutsceneemoji.c
|
cutsceneemoji.c
|
||||||
cutsceneshake.c
|
cutsceneshake.c
|
||||||
|
cutscenemodal.c
|
||||||
|
cutscenemodaloptionsmarkers.c
|
||||||
|
cutscenekeyboard.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "ui/dialog/keyboard/uikeyboard.h"
|
||||||
|
|
||||||
|
void cutsceneKeyboardStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
uiKeyboardOpen(&item->keyboard.open);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneKeyboardUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
if(uiKeyboardIsOpen()) return false;
|
||||||
|
|
||||||
|
cutsceneSystemSetTextCache(UI_KEYBOARD.text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
#include "ui/dialog/keyboard/uikeyboard.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// Passed straight through to uiKeyboardOpen - see uikeyboardopen_t.
|
||||||
|
uikeyboardopen_t open;
|
||||||
|
} cutscenekeyboard_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a keyboard item (opens the on-screen keyboard with the item's
|
||||||
|
* open parameters, unmodified - see uiKeyboardOpen).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneKeyboardStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a keyboard item. Blocks the cutscene while the keyboard is
|
||||||
|
* open; once it closes (confirmed or cancelled), caches its resulting
|
||||||
|
* text via cutsceneSystemSetTextCache (see cutsceneSystemGetTextCache)
|
||||||
|
* before completing, regardless of whether confirm or cancel was
|
||||||
|
* picked - check UI_KEYBOARD.confirmed from the item's own onInput
|
||||||
|
* callback if that distinction matters.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once the keyboard has closed; false while still open.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneKeyboardUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "ui/widget/uimodal.h"
|
||||||
|
|
||||||
|
void cutsceneModalStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
const cutscenemodal_t *modal = &item->modal;
|
||||||
|
assertTrue(
|
||||||
|
modal->optionCount <= CUTSCENE_MODAL_OPTIONS_MAX,
|
||||||
|
"Too many options for cutscene modal"
|
||||||
|
);
|
||||||
|
|
||||||
|
uiModalOpen(
|
||||||
|
modal->title,
|
||||||
|
modal->message,
|
||||||
|
modal->options,
|
||||||
|
modal->optionCount,
|
||||||
|
modal->callback,
|
||||||
|
NULL,
|
||||||
|
CUTSCENE_SYSTEM.userData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneModalUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return item->modal.optionCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneModalCloseStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
uiModalClose(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneModalCloseUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
#define CUTSCENE_MODAL_TITLE_MAX_CHARS 64
|
||||||
|
#define CUTSCENE_MODAL_MESSAGE_MAX_CHARS 256
|
||||||
|
#define CUTSCENE_MODAL_OPTIONS_MAX 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked with the selected option once a cutscene modal
|
||||||
|
* item's dialog closes. Back/cancel input is disabled while it's open
|
||||||
|
* (see uiMenuSetDisableBack), so it must be dismissed by picking one.
|
||||||
|
*
|
||||||
|
* @param optionIndex Index into the options array that was selected.
|
||||||
|
* @param userData CUTSCENE_SYSTEM.userData for the running cutscene.
|
||||||
|
*/
|
||||||
|
typedef void (*cutscenemodaloptioncallback_t)(
|
||||||
|
const uint8_t optionIndex, void *userData
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// Display text, or a locale message ID to translate - see
|
||||||
|
// uiModalLocalize.
|
||||||
|
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
||||||
|
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
||||||
|
|
||||||
|
// NOT copied by this struct - these pointers are only read once,
|
||||||
|
// while uiModalOpen is opening the modal in cutsceneModalStart, so
|
||||||
|
// they only need to stay valid until then (e.g. string literals, as
|
||||||
|
// CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a
|
||||||
|
// locale message ID, same as title/message.
|
||||||
|
const char_t **options;
|
||||||
|
uint8_t optionCount;
|
||||||
|
|
||||||
|
cutscenemodaloptioncallback_t callback;
|
||||||
|
} cutscenemodal_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a modal item (shows the modal dialog with the item's title,
|
||||||
|
* message, and options, wiring callback to fire with the selected
|
||||||
|
* option once it closes). optionCount may be 0 for a message-only
|
||||||
|
* dialog with no option buttons.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneModalStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a modal item. A message-only modal (optionCount == 0, e.g.
|
||||||
|
* from CUTSCENE_MODAL) always completes immediately - the cutscene
|
||||||
|
* continues on to whatever follows this item right away, it does not
|
||||||
|
* wait for the dialog to be dismissed. Script what should happen while
|
||||||
|
* it's up, and how it gets closed, as later items in the same cutscene
|
||||||
|
* (e.g. CUTSCENE_CALLBACK, CUTSCENE_WAIT, CUTSCENE_MODAL_CLOSE).
|
||||||
|
*
|
||||||
|
* A modal with options (optionCount > 0, e.g. from
|
||||||
|
* CUTSCENE_MODAL_OPTIONS) never completes on its own - the cutscene
|
||||||
|
* blocks here indefinitely. The option callback fires once the user
|
||||||
|
* picks something, and it alone is responsible for moving the cutscene
|
||||||
|
* on from there (typically via cutsceneGoTo or cutsceneRestart).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true once immediately for a message-only modal; false
|
||||||
|
* always for a modal with options.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneModalUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a modal-close item (closes the currently open modal, if any).
|
||||||
|
* Whatever should happen next belongs in the cutscene's own item
|
||||||
|
* sequence (e.g. a CUTSCENE_CALLBACK or CUTSCENE_PRINT placed right
|
||||||
|
* after this item), not on this item itself.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneModalCloseStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a modal-close item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneModalCloseUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "ui/widget/uimodal.h"
|
||||||
|
|
||||||
|
void cutsceneModalOptionsMarkersCallback(
|
||||||
|
const uint8_t optionIndex,
|
||||||
|
void *userData
|
||||||
|
) {
|
||||||
|
const cutscenemodaloptionsmarkers_t *options =
|
||||||
|
&cutsceneSystemGetCurrentItem()->modalOptionsMarkers;
|
||||||
|
|
||||||
|
// uiModalOpen disables back/cancel for a dialog with options, so
|
||||||
|
// UI_MODAL_RESULT_NONE can't reach here via input - this fallback to
|
||||||
|
// the last option's marker only matters if something ever force-closes
|
||||||
|
// the modal directly (uiModalClose) before an option is picked.
|
||||||
|
const uint8_t index =
|
||||||
|
optionIndex < options->optionCount ? optionIndex : options->optionCount - 1;
|
||||||
|
cutsceneGoTo(options->markers[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cutsceneModalOptionsMarkersStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
const cutscenemodaloptionsmarkers_t *options = &item->modalOptionsMarkers;
|
||||||
|
|
||||||
|
uiModalOpen(
|
||||||
|
options->title,
|
||||||
|
options->message,
|
||||||
|
options->options,
|
||||||
|
options->optionCount,
|
||||||
|
cutsceneModalOptionsMarkersCallback,
|
||||||
|
NULL,
|
||||||
|
CUTSCENE_SYSTEM.userData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutsceneModalOptionsMarkersUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "dusk.h"
|
||||||
|
|
||||||
|
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||||
|
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||||
|
|
||||||
|
// Backs CUTSCENE_MODAL_OPTIONS_ONE and CUTSCENE_MODAL_OPTIONS_TWO, so two
|
||||||
|
// is the most either macro ever needs.
|
||||||
|
#define CUTSCENE_MODAL_OPTIONS_MARKERS_MAX 2
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
// Display text, or a locale message ID to translate - see
|
||||||
|
// uiModalLocalize.
|
||||||
|
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
||||||
|
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
||||||
|
|
||||||
|
// Each option's display text/locale message ID, and the marker to
|
||||||
|
// cutsceneGoTo when that option is selected.
|
||||||
|
const char_t *options[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
|
||||||
|
const char_t *markers[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
|
||||||
|
uint8_t optionCount;
|
||||||
|
} cutscenemodaloptionsmarkers_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a modal-options-markers item (shows the modal dialog with the
|
||||||
|
* item's title, message, and options).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutsceneModalOptionsMarkersStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a modal-options-markers item. Never completes on its own -
|
||||||
|
* the cutscene blocks here indefinitely. Back/cancel input is disabled
|
||||||
|
* while it's open (see uiMenuSetDisableBack), so it must be dismissed
|
||||||
|
* by picking an option, which jumps straight to that option's marker
|
||||||
|
* via cutsceneGoTo.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns false always.
|
||||||
|
*/
|
||||||
|
bool_t cutsceneModalOptionsMarkersUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -6,5 +6,4 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
globalitemstore.c
|
|
||||||
)
|
)
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "globalitemstore.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
|
|
||||||
bool_t globalItemStoreIsCollected(
|
|
||||||
const saveslot_t *file, const entityglobalid_t id
|
|
||||||
) {
|
|
||||||
assertNotNull(file, "Save slot cannot be NULL");
|
|
||||||
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
|
||||||
return file->globalItemCollected[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
void globalItemStoreSetCollected(
|
|
||||||
saveslot_t *file, const entityglobalid_t id, const bool_t collected
|
|
||||||
) {
|
|
||||||
assertNotNull(file, "Save slot cannot be NULL");
|
|
||||||
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
|
||||||
file->globalItemCollected[id] = collected;
|
|
||||||
}
|
|
||||||
@@ -1,40 +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 "save/saveslot.h"
|
|
||||||
#include "rpg/entity/entity.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the global entity with the given ID has already been
|
|
||||||
* marked collected in the given save slot's data - e.g. so a global item
|
|
||||||
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
|
|
||||||
* skip spawning itself if the player already picked it up in a prior
|
|
||||||
* session, without needing to keep the entity itself alive to remember
|
|
||||||
* that (which would need render/collision special-casing - this doesn't).
|
|
||||||
*
|
|
||||||
* @param file The save slot to check.
|
|
||||||
* @param id The global entity ID to check.
|
|
||||||
* @return True if already marked collected.
|
|
||||||
*/
|
|
||||||
bool_t globalItemStoreIsCollected(
|
|
||||||
const saveslot_t *file, const entityglobalid_t id
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the global entity with the given ID as collected (or not) in the
|
|
||||||
* given save slot's data. Does not itself write the save to disk - call
|
|
||||||
* saveWriteSlot() separately once ready to persist it.
|
|
||||||
*
|
|
||||||
* @param file The save slot to write into.
|
|
||||||
* @param id The global entity ID to mark.
|
|
||||||
* @param collected The new collected state.
|
|
||||||
*/
|
|
||||||
void globalItemStoreSetCollected(
|
|
||||||
saveslot_t *file, const entityglobalid_t id, const bool_t collected
|
|
||||||
);
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "ui/focus/uifocus.h"
|
#include "ui/focus/uifocus.h"
|
||||||
#include "ui/frame/game/uigamemenu.h"
|
#include "ui/screen/game/uigamemenu.h"
|
||||||
#include "rpg/cutscene/cutscenesystem.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
|
||||||
void playerInit(entity_t *entity) {
|
void playerInit(entity_t *entity) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "event/event.h"
|
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include "rpg/entity/global/entityglobal.h"
|
#include "rpg/entity/global/entityglobal.h"
|
||||||
#include "rpg/entity/item/entityitem.h"
|
#include "rpg/entity/item/entityitem.h"
|
||||||
@@ -137,8 +136,10 @@ void mapChunkUnload(chunk_t *chunk) {
|
|||||||
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
||||||
|
|
||||||
if(chunk->dcfEntry != NULL) {
|
if(chunk->dcfEntry != NULL) {
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
chunk->dcfEntry->onLoaded = NULL;
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
chunk->dcfEntry->onLoadedUser = NULL;
|
||||||
|
chunk->dcfEntry->onError = NULL;
|
||||||
|
chunk->dcfEntry->onErrorUser = NULL;
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
assetUnlockEntry(chunk->dcfEntry);
|
||||||
chunk->dcfEntry = NULL;
|
chunk->dcfEntry = NULL;
|
||||||
}
|
}
|
||||||
@@ -159,8 +160,10 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
|||||||
mapChunkLoadingSlotClear(chunk);
|
mapChunkLoadingSlotClear(chunk);
|
||||||
|
|
||||||
if(chunk->dcfEntry != NULL) {
|
if(chunk->dcfEntry != NULL) {
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
chunk->dcfEntry->onLoaded = NULL;
|
||||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
chunk->dcfEntry->onLoadedUser = NULL;
|
||||||
|
chunk->dcfEntry->onError = NULL;
|
||||||
|
chunk->dcfEntry->onErrorUser = NULL;
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
assetUnlockEntry(chunk->dcfEntry);
|
||||||
chunk->dcfEntry = NULL;
|
chunk->dcfEntry = NULL;
|
||||||
}
|
}
|
||||||
@@ -244,8 +247,12 @@ void mapChunkLoadNext() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
assertNull(entry->onLoaded, "Entry already has an onLoaded subscriber.");
|
||||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
assertNull(entry->onError, "Entry already has an onError subscriber.");
|
||||||
|
entry->onLoaded = mapChunkLoaded;
|
||||||
|
entry->onLoadedUser = chunk;
|
||||||
|
entry->onError = mapChunkLoadError;
|
||||||
|
entry->onErrorUser = chunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,10 +392,9 @@ void mapRebuildChunkOrder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void mapChunkLoadError(void *params, void *user) {
|
void mapChunkLoadError(assetentry_t *entry, void *user) {
|
||||||
assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
|
assertNotNull(entry, "mapChunkLoadError: entry cannot be NULL");
|
||||||
assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
|
assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
|
||||||
assetentry_t *entry = (assetentry_t *)params;
|
|
||||||
chunk_t *chunk = (chunk_t *)user;
|
chunk_t *chunk = (chunk_t *)user;
|
||||||
if(chunk->dcfEntry != entry) return;
|
if(chunk->dcfEntry != entry) return;
|
||||||
consolePrint(
|
consolePrint(
|
||||||
@@ -397,8 +403,10 @@ void mapChunkLoadError(void *params, void *user) {
|
|||||||
(int32_t)chunk->position.y,
|
(int32_t)chunk->position.y,
|
||||||
(int32_t)chunk->position.z
|
(int32_t)chunk->position.z
|
||||||
);
|
);
|
||||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
entry->onLoaded = NULL;
|
||||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
entry->onLoadedUser = NULL;
|
||||||
|
entry->onError = NULL;
|
||||||
|
entry->onErrorUser = NULL;
|
||||||
assetUnlockEntry(chunk->dcfEntry);
|
assetUnlockEntry(chunk->dcfEntry);
|
||||||
chunk->dcfEntry = NULL;
|
chunk->dcfEntry = NULL;
|
||||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||||
@@ -407,10 +415,9 @@ void mapChunkLoadError(void *params, void *user) {
|
|||||||
mapChunkLoadNext();
|
mapChunkLoadNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void mapChunkLoaded(void *params, void *user) {
|
void mapChunkLoaded(assetentry_t *entry, void *user) {
|
||||||
assertNotNull(params, "mapChunkLoaded: params cannot be NULL");
|
assertNotNull(entry, "mapChunkLoaded: entry cannot be NULL");
|
||||||
assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
|
assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
|
||||||
assetentry_t *entry = (assetentry_t *)params;
|
|
||||||
chunk_t *chunk = (chunk_t *)user;
|
chunk_t *chunk = (chunk_t *)user;
|
||||||
if(chunk->dcfEntry != entry) return;
|
if(chunk->dcfEntry != entry) return;
|
||||||
// consolePrint(
|
// consolePrint(
|
||||||
@@ -457,8 +464,10 @@ void mapChunkLoaded(void *params, void *user) {
|
|||||||
// modelEntries must still be intact for that next reuse to copy from.
|
// modelEntries must still be intact for that next reuse to copy from.
|
||||||
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
||||||
}
|
}
|
||||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
entry->onLoaded = NULL;
|
||||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
entry->onLoadedUser = NULL;
|
||||||
|
entry->onError = NULL;
|
||||||
|
entry->onErrorUser = NULL;
|
||||||
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
|
// Deliberately keep chunk->dcfEntry locked and set - it is what keeps the
|
||||||
// chunk asset entry (and therefore its model locks) alive for as long as
|
// chunk asset entry (and therefore its model locks) alive for as long as
|
||||||
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
// this chunk_t is displaying it. Released in mapChunkUnload instead.
|
||||||
|
|||||||
@@ -101,19 +101,19 @@ void mapChunkLoadQueueRemove(chunk_t *chunk);
|
|||||||
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
|
* chunk tiles with TILE_SHAPE_GROUND as a fallback.
|
||||||
* Always invoked on the main thread.
|
* Always invoked on the main thread.
|
||||||
*
|
*
|
||||||
* @param params The failed assetentry_t.
|
* @param entry The failed assetentry_t.
|
||||||
* @param user The chunk_t that owns the entry.
|
* @param user The chunk_t that owns the entry.
|
||||||
*/
|
*/
|
||||||
void mapChunkLoadError(void *params, void *user);
|
void mapChunkLoadError(assetentry_t *entry, void *user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback invoked when a chunk DCF asset finishes loading.
|
* Callback invoked when a chunk DCF asset finishes loading.
|
||||||
* Always invoked on the main thread.
|
* Always invoked on the main thread.
|
||||||
*
|
*
|
||||||
* @param params The loaded assetentry_t.
|
* @param entry The loaded assetentry_t.
|
||||||
* @param user The chunk_t that owns the entry.
|
* @param user The chunk_t that owns the entry.
|
||||||
*/
|
*/
|
||||||
void mapChunkLoaded(void *params, void *user);
|
void mapChunkLoaded(assetentry_t *entry, void *user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuilds chunkOrder from the loaded chunks that fall within the
|
* Rebuilds chunkOrder from the loaded chunks that fall within the
|
||||||
|
|||||||
+4
-25
@@ -17,11 +17,8 @@
|
|||||||
#include "rpgcamera.h"
|
#include "rpgcamera.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "save/save.h"
|
|
||||||
#include "save/autosave.h"
|
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
|
|
||||||
#include "ui/rpg/uiemoji.h"
|
#include "ui/rpg/uiemoji.h"
|
||||||
#include "rpg/story/storyflag.h"
|
#include "rpg/story/storyflag.h"
|
||||||
|
|
||||||
@@ -29,27 +26,14 @@ errorret_t rpgInit(void) {
|
|||||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||||
|
|
||||||
saveslot_t *saveSlot = saveGetSlot(SAVE_ACTIVE_SLOT);
|
|
||||||
// saveInit() eagerly loads every slot from disk, but there's no
|
|
||||||
// continue-game flow yet - every boot is a fresh game regardless of
|
|
||||||
// what was found on disk, so force this false rather than let a stale
|
|
||||||
// "exists" from a real save skip storyFlagInitDefaults() below while
|
|
||||||
// everything else here still hardcodes new-game state.
|
|
||||||
saveSlot->exists = false;
|
|
||||||
|
|
||||||
// Must run before any code reads a story flag - stamps CSV-defined
|
|
||||||
// defaults onto the active save slot since it's now forced to look
|
|
||||||
// unloaded.
|
|
||||||
storyFlagInitDefaults(saveSlot);
|
|
||||||
|
|
||||||
backpackInit();
|
backpackInit();
|
||||||
partyInit();
|
partyInit();
|
||||||
cutsceneSystemInit();
|
cutsceneSystemInit();
|
||||||
|
|
||||||
errorChain(mapInit());
|
errorChain(mapInit());
|
||||||
|
|
||||||
rpgCameraInit();
|
rpgCameraInit();
|
||||||
// Init world
|
|
||||||
|
|
||||||
|
// Init test world
|
||||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||||
|
|
||||||
// The player is the one entity that isn't sourced from map/chunk data -
|
// The player is the one entity that isn't sourced from map/chunk data -
|
||||||
@@ -68,6 +52,7 @@ errorret_t rpgInit(void) {
|
|||||||
backpackAdd(ITEM_ID_POTATO, 3);
|
backpackAdd(ITEM_ID_POTATO, 3);
|
||||||
backpackAdd(ITEM_ID_APPLE, 8);
|
backpackAdd(ITEM_ID_APPLE, 8);
|
||||||
|
|
||||||
|
|
||||||
// All Good!
|
// All Good!
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -79,12 +64,6 @@ errorret_t rpgUpdate(void) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// A failed autosave forces a no-save-device prompt (see autosave.h) -
|
|
||||||
// freeze the world entirely until the player resolves it, rather than
|
|
||||||
// letting NPCs/cutscenes/camera keep running behind a modal that's
|
|
||||||
// supposed to be blocking.
|
|
||||||
if(autoSaveIsBlocking()) errorOk();
|
|
||||||
|
|
||||||
// TODO: Do not update if the scene is not the map scene?
|
// TODO: Do not update if the scene is not the map scene?
|
||||||
errorChain(mapUpdate());
|
errorChain(mapUpdate());
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user