Compare commits
21 Commits
774c8ad0f8
..
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 |
Binary file not shown.
Binary file not shown.
+93
-16
@@ -5,6 +5,99 @@ 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 ""
|
||||||
@@ -115,22 +208,6 @@ msgstr "Yes"
|
|||||||
msgid "ui.initial.create_save.no"
|
msgid "ui.initial.create_save.no"
|
||||||
msgstr "No"
|
msgstr "No"
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.new_game"
|
|
||||||
msgstr "New Game"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.load_game"
|
|
||||||
msgstr "Load Game"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.options"
|
|
||||||
msgstr "Options"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.quit"
|
|
||||||
msgstr "Quit Game"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
#: src/dusk/ui/frame/uiconfirm.c
|
||||||
msgid "ui.confirm.confirm"
|
msgid "ui.confirm.confirm"
|
||||||
msgstr "Confirm"
|
msgstr "Confirm"
|
||||||
|
|||||||
@@ -1,194 +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/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.new_game"
|
|
||||||
msgstr "Nueva Partida"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.load_game"
|
|
||||||
msgstr "Cargar Partida"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.options"
|
|
||||||
msgstr "Opciones"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.quit"
|
|
||||||
msgstr "Salir del Juego"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.confirm"
|
|
||||||
msgstr "Confirmar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.cancel"
|
|
||||||
msgstr "Cancelar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.attack"
|
|
||||||
msgstr "Atacar"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.flee"
|
|
||||||
msgstr "Huir"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.target_format"
|
|
||||||
msgstr "Enemigo %u (%u/%u PS)"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.hp_format"
|
|
||||||
msgstr "PS %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
|
||||||
msgid "ui.battle.hud.mp_format"
|
|
||||||
msgstr "PM %u/%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
|
||||||
msgid "ui.settings.audio.placeholder"
|
|
||||||
msgstr "Aún no hay opciones de audio"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
|
||||||
msgid "ui.settings.display.placeholder"
|
|
||||||
msgstr "Aún no hay opciones de pantalla"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
|
||||||
msgid "ui.settings.input.placeholder"
|
|
||||||
msgstr "Aún no hay opciones de entrada"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
|
||||||
msgid "ui.backpack.category_format"
|
|
||||||
msgstr "Categoría %u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiloading.c
|
|
||||||
msgid "ui.loading.text"
|
|
||||||
msgstr "cargando"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiautosave.c
|
|
||||||
msgid "ui.autosave.saving"
|
|
||||||
msgstr "GUARDANDO"
|
|
||||||
|
|
||||||
#: 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,194 +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/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.new_game"
|
|
||||||
msgstr "ニューゲーム"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.load_game"
|
|
||||||
msgstr "ロードゲーム"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.options"
|
|
||||||
msgstr "オプション"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/mainmenu/uimainmenu.c
|
|
||||||
msgid "ui.main_menu.quit"
|
|
||||||
msgstr "ゲームを終了"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.confirm"
|
|
||||||
msgstr "確認"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/uiconfirm.c
|
|
||||||
msgid "ui.confirm.cancel"
|
|
||||||
msgstr "キャンセル"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.attack"
|
|
||||||
msgstr "攻撃"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.flee"
|
|
||||||
msgstr "逃げる"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
|
||||||
msgid "ui.battle.menu.target_format"
|
|
||||||
msgstr "敵%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 "オーディオ設定はまだありません"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
|
||||||
msgid "ui.settings.display.placeholder"
|
|
||||||
msgstr "表示設定はまだありません"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
|
||||||
msgid "ui.settings.input.placeholder"
|
|
||||||
msgstr "入力設定はまだありません"
|
|
||||||
|
|
||||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
|
||||||
msgid "ui.backpack.category_format"
|
|
||||||
msgstr "カテゴリー%u"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiloading.c
|
|
||||||
msgid "ui.loading.text"
|
|
||||||
msgstr "読み込み中"
|
|
||||||
|
|
||||||
#: src/dusk/ui/overlay/uiautosave.c
|
|
||||||
msgid "ui.autosave.saving"
|
|
||||||
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 "リンゴ"
|
|
||||||
@@ -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}")
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
// the line reader above only ever asks for up to 1024 bytes per call and
|
||||||
|
// works fine, so read in bounded chunks here too.
|
||||||
|
size_t totalRead = 0;
|
||||||
|
uint8_t *dest = (uint8_t *)buffer;
|
||||||
|
while(totalRead < bufferSize) {
|
||||||
|
size_t chunkSize = mathMin(
|
||||||
|
bufferSize - totalRead, ASSET_FILE_READ_CHUNK_MAX
|
||||||
|
);
|
||||||
|
zip_int64_t bytesRead = zip_fread(
|
||||||
|
file->zipFile, dest + totalRead, chunkSize
|
||||||
|
);
|
||||||
if(bytesRead < 0) {
|
if(bytesRead < 0) {
|
||||||
errorThrow("Failed to read from asset file: %s", file->filename);
|
errorThrow(
|
||||||
|
"Failed to read from asset file: %s (%s)",
|
||||||
|
file->filename, zip_file_strerror(file->zipFile)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
file->position += bytesRead;
|
if(bytesRead == 0) break;
|
||||||
file->lastRead = bytesRead;
|
totalRead += (size_t)bytesRead;
|
||||||
|
}
|
||||||
|
file->position += totalRead;
|
||||||
|
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;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
include(dusktest)
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
# Tests
|
assetcutsceneloader.c
|
||||||
dusktest(test_event.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);
|
||||||
@@ -76,11 +76,23 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
|||||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
||||||
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
||||||
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
|
// Custom icon glyph, not a real backslash - a backspace symbol: Caps
|
||||||
|
// Lock's up arrow (see FONT_ICON_CAPSLOCK) rotated 270 degrees to
|
||||||
|
// point left instead, adapted to this font's fixed 6x10 tile (each of
|
||||||
|
// Caps Lock's row widths becomes a column height here, centered
|
||||||
|
// vertically). Backslash was never drawn anyway, and isn't a key this
|
||||||
|
// virtual keyboard can type - see FONT_ICON_BACKSPACE.
|
||||||
|
{ 0x00, 0x00, 0x08, 0x1F, 0x3F, 0x3F, 0x1F, 0x08, 0x00, 0x00 }, // FONT_ICON_BACKSPACE
|
||||||
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E }, // _
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
|
// 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, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
||||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
||||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
||||||
@@ -108,10 +120,25 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
|||||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
||||||
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
||||||
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
|
// Custom icon glyph, not a real pipe - a "return" arrow for the
|
||||||
|
// keyboard's newline key: a vertical riser down the right side that
|
||||||
|
// hooks left into a leftward-pointing arrowhead, i.e. "<-|" rotated
|
||||||
|
// into an L. Pipe was never drawn anyway, and isn't a key this
|
||||||
|
// virtual keyboard can type - see FONT_ICON_NEWLINE.
|
||||||
|
{ 0x02, 0x02, 0x02, 0x02, 0x0E, 0x1E, 0x08, 0x00, 0x00, 0x00 }, // FONT_ICON_NEWLINE
|
||||||
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
|
// Custom icon glyph, not a real tilde - a spacebar symbol for the
|
||||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
// 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
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,45 @@ typedef struct {
|
|||||||
/**
|
/**
|
||||||
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
||||||
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
||||||
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
|
* TEXT_CHAR_START ('!'), one custom icon glyph in a trailing unused tile
|
||||||
|
* (see FONT_ICON_CAPSLOCK), plus one more genuinely unused trailing tile.
|
||||||
|
* FONT_ICON_SHIFT/FONT_ICON_NEWLINE reuse existing-but-blank slots within
|
||||||
|
* the printable range instead of more trailing tiles - see their own
|
||||||
|
* comments.
|
||||||
*/
|
*/
|
||||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
#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;
|
extern font_t FONT_DEFAULT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -187,3 +187,37 @@ int32_t textMeasure(
|
|||||||
|
|
||||||
return spriteCount;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,3 +104,17 @@ int32_t textMeasure(
|
|||||||
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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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, \
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -67,6 +110,10 @@ void cutsceneSystemNext() {
|
|||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -127,6 +127,56 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
|||||||
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
||||||
.init = cutsceneBattleForceActionStart,
|
.init = cutsceneBattleForceActionStart,
|
||||||
.update = cutsceneBattleForceActionUpdate
|
.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,6 +26,9 @@
|
|||||||
#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"
|
||||||
@@ -29,6 +36,8 @@
|
|||||||
#include "battle/cutscenestartbattle.h"
|
#include "battle/cutscenestartbattle.h"
|
||||||
#include "battle/cutscenebattlewaitstate.h"
|
#include "battle/cutscenebattlewaitstate.h"
|
||||||
#include "battle/cutscenebattleforceaction.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;
|
||||||
|
|
||||||
@@ -59,6 +68,16 @@ typedef enum {
|
|||||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||||
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
||||||
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
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;
|
||||||
@@ -91,6 +110,14 @@ struct cutsceneitem_s {
|
|||||||
cutsceneshake_t shake;
|
cutsceneshake_t shake;
|
||||||
cutscenebattlewaitstate_t battleWaitState;
|
cutscenebattlewaitstate_t battleWaitState;
|
||||||
cutscenebattleforceaction_t battleForceAction;
|
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
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
|||||||
+18
-6
@@ -65,16 +65,18 @@ errorret_t saveUpdate() {
|
|||||||
} else {
|
} else {
|
||||||
// Device was found, cache in the data.
|
// Device was found, cache in the data.
|
||||||
errorChain(saveLoadSettings());
|
errorChain(saveLoadSettings());
|
||||||
// Reset each slot and try load from file, updating cache along the way.
|
errorChain(saveLoadAllSlots());
|
||||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
|
||||||
SAVE.slotCurrent = i;
|
|
||||||
saveSlotInit(&SAVE.slot);
|
|
||||||
errorChain(saveLoadSlot());// Load slot updates the cache.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset slot, implying no slot has been selected.
|
// Reset slot, implying no slot has been selected.
|
||||||
SAVE.slotCurrent = 0xFF;
|
SAVE.slotCurrent = 0xFF;
|
||||||
|
|
||||||
|
// TEST: write settings back out immediately once a device is
|
||||||
|
// selected, to confirm the write path actually works on hardware.
|
||||||
|
errorChain(saveDeviceSettingsWrite(
|
||||||
|
&SAVE.devices[SAVE.deviceCurrent],
|
||||||
|
&SAVE.settings
|
||||||
|
));
|
||||||
|
|
||||||
SAVE.findingAvailableDevice = false;
|
SAVE.findingAvailableDevice = false;
|
||||||
SAVE.findAvailableCallback(
|
SAVE.findAvailableCallback(
|
||||||
&SAVE.devices[SAVE.deviceCurrent],
|
&SAVE.devices[SAVE.deviceCurrent],
|
||||||
@@ -222,6 +224,16 @@ errorret_t saveLoadSlot() {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorret_t saveLoadAllSlots() {
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
SAVE.slotCurrent = i;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
errorChain(saveLoadSlot());// Load slot updates the cache.
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t saveDispose() {
|
errorret_t saveDispose() {
|
||||||
// Dispose each device.
|
// Dispose each device.
|
||||||
savedevice_t *device = &SAVE.devices[0];
|
savedevice_t *device = &SAVE.devices[0];
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ errorret_t saveSaveSlot();
|
|||||||
*/
|
*/
|
||||||
errorret_t saveLoadSlot();
|
errorret_t saveLoadSlot();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets and loads every slot from the current device, updating the caches
|
||||||
|
* along the way. Leaves SAVE.slotCurrent pointing at the last slot index
|
||||||
|
* once done.
|
||||||
|
*
|
||||||
|
* @return Error state if any.
|
||||||
|
*/
|
||||||
|
errorret_t saveLoadAllSlots();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes of the save system.
|
* Disposes of the save system.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
#include "yyjson.h"
|
#include "yyjson.h"
|
||||||
|
|
||||||
#define SAVE_SLOT_NAME_LENGTH 8
|
#define SAVE_SLOT_NAME_LENGTH 8
|
||||||
|
#ifndef SAVE_SLOT_COUNT
|
||||||
#define SAVE_SLOT_COUNT 3
|
#define SAVE_SLOT_COUNT 3
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
||||||
|
|||||||
@@ -9,42 +9,40 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
#include "save/save.h"
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
|
||||||
int32_t testData = 69;
|
// Loaded lazily and kept resident for the rest of the process - this scene
|
||||||
|
// only ever runs once at boot, but there's no reason to unlock it (same
|
||||||
void testCallback(savedevice_t *device, void *user) {
|
// lifetime convention as e.g. LOCALE.entry).
|
||||||
if(device == NULL) {
|
static assetentry_t *INITIAL_CUTSCENE_ENTRY = NULL;
|
||||||
consolePrint("No save device found.");
|
|
||||||
} else {
|
|
||||||
uint8_t index = (uint8_t)(device - &SAVE.devices[0]);
|
|
||||||
consolePrint(
|
|
||||||
"Found save device %u: %s", (uint32_t)index, device->reasonKey
|
|
||||||
);
|
|
||||||
|
|
||||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
|
||||||
if(saveSlotInUse(&SAVE.caches[i])) {
|
|
||||||
consolePrint(
|
|
||||||
"Slot %u is in use: %s", (uint32_t)i, SAVE.caches[i].name
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
consolePrint("Slot %u is not in use.", (uint32_t)i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||||
assertNotNull(sceneData, "Scene data cannot be null");
|
assertNotNull(sceneData, "Scene data cannot be null");
|
||||||
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
||||||
|
|
||||||
consolePrint("Going to find a save device.");
|
// Set background color to black for the initial scene
|
||||||
saveFindAvailableDevice(
|
SCREEN.background = COLOR_BLACK;
|
||||||
testCallback,
|
|
||||||
&testData
|
// Runtime-loaded from assets/cutscenes/initial.cts (authored at
|
||||||
|
// assetsraw/cutscenes/initial.jsonc via `python3 -m tools.asset.cutscene`)
|
||||||
|
// - checks for a save device, retrying on failure, then hands off to the
|
||||||
|
// main menu scene via a plain CUTSCENE_SCENE item (no native callback
|
||||||
|
// needed here, unlike the main menu's own start-game cutscene).
|
||||||
|
if(INITIAL_CUTSCENE_ENTRY == NULL) {
|
||||||
|
INITIAL_CUTSCENE_ENTRY = assetLock(
|
||||||
|
"cutscenes/initial.cts", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
errorret_t result = assetRequireLoaded(INITIAL_CUTSCENE_ENTRY);
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
errorCatch(errorPrint(result));
|
||||||
|
assertTrue(false, "Failed to load initial scene cutscene asset");
|
||||||
|
}
|
||||||
|
|
||||||
|
cutsceneSystemStartCutscene(&INITIAL_CUTSCENE_ENTRY->data.cutscene.cutscene);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "scene/scenebase.h"
|
#include "scene/scenebase.h"
|
||||||
|
|
||||||
// No per-scene state needed - the save globals and the two modal UI
|
|
||||||
// elements (see ui/frame/initial/) carry everything this scene cares
|
|
||||||
// about. A byte placeholder keeps the struct non-empty for portability.
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t reserved;
|
// uint32_t callbackState;
|
||||||
|
void *nothing;
|
||||||
} sceneinitial_t;
|
} sceneinitial_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,8 +6,65 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "scenemainmenu.h"
|
#include "scenemainmenu.h"
|
||||||
|
#include "ui/screen/mainmenu/uimainmenu.h"
|
||||||
|
#include "ui/dialog/save/uiselectsave.h"
|
||||||
|
#include "rpg/cutscene/cutscenesystem.h"
|
||||||
|
#include "scene/scene.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
// Loaded lazily on first Start Game click and kept resident for the rest
|
||||||
|
// of the process - it's tiny and reused every time, so there's no benefit
|
||||||
|
// to unlocking/reloading it between attempts (same lifetime convention as
|
||||||
|
// e.g. LOCALE.entry).
|
||||||
|
static assetentry_t *MAIN_MENU_START_GAME_CUTSCENE_ENTRY = NULL;
|
||||||
|
|
||||||
|
void sceneMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
|
||||||
|
if(slotIndex == UI_SELECT_SAVE_RESULT_NONE) {
|
||||||
|
uiMainMenuOpen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: load/start the game using the chosen save slot.
|
||||||
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sceneMainMenuOpenSelectSave(void *userData) {
|
||||||
|
uiSelectSaveOpen(
|
||||||
|
UI_SELECT_SAVE_TYPE_LOAD, sceneMainMenuSelectSaveResult, NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads every save slot before opening the load-game picker (checking/
|
||||||
|
// retry-on-error shape mirroring the initial scene's device lookup, see
|
||||||
|
// scene/initial/sceneinitial.c) - runtime-loaded from
|
||||||
|
// assets/cutscenes/main_menu_start_game.cts (authored at
|
||||||
|
// assetsraw/cutscenes/main_menu_start_game.json via
|
||||||
|
// `python3 -m tools.asset.cutscene`) rather than compiled in, since it's
|
||||||
|
// player-facing flow rather than core engine wiring. The file ends at the
|
||||||
|
// LOADED marker with no further action - sceneMainMenuOpenSelectSave is
|
||||||
|
// armed as the completion callback below instead of being baked into the
|
||||||
|
// cutscene itself, since a file can't store a native function pointer.
|
||||||
|
void sceneMainMenuStartGame(void) {
|
||||||
|
if(MAIN_MENU_START_GAME_CUTSCENE_ENTRY == NULL) {
|
||||||
|
MAIN_MENU_START_GAME_CUTSCENE_ENTRY = assetLock(
|
||||||
|
"cutscenes/main_menu_start_game.cts", ASSET_LOADER_TYPE_CUTSCENE, NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
errorret_t result = assetRequireLoaded(MAIN_MENU_START_GAME_CUTSCENE_ENTRY);
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
errorCatch(errorPrint(result));
|
||||||
|
assertTrue(false, "Failed to load main menu start-game cutscene asset");
|
||||||
|
}
|
||||||
|
|
||||||
|
cutsceneSystemStartCutscene(
|
||||||
|
&MAIN_MENU_START_GAME_CUTSCENE_ENTRY->data.cutscene.cutscene
|
||||||
|
);
|
||||||
|
cutsceneSystemSetOnComplete(sceneMainMenuOpenSelectSave);
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t sceneMainMenuInit(scenedata_t *sceneData) {
|
errorret_t sceneMainMenuInit(scenedata_t *sceneData) {
|
||||||
|
uiMainMenuOpen();
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "scene/scenebase.h"
|
#include "scene/scenebase.h"
|
||||||
|
|
||||||
// Empty for now -- the menu itself lives in ui/frame/mainmenu/uimainmenu.c,
|
// Empty - the menu itself lives in ui/screen/mainmenu/uimainmenu.c. A byte
|
||||||
// driven by the global UI element pipeline (see uimainmenu.c's own
|
// placeholder keeps the struct non-empty for portability.
|
||||||
// SCENE.current check for when it shows itself). A byte placeholder keeps
|
|
||||||
// the struct non-empty for portability.
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t reserved;
|
uint8_t reserved;
|
||||||
} scenemainmenu_t;
|
} scenemainmenu_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the main menu scene.
|
* Initializes the main menu scene by opening the main menu panel.
|
||||||
*
|
*
|
||||||
* @param sceneData The scene data used for this scene.
|
* @param sceneData The scene data used for this scene.
|
||||||
* @return An error if the init failed, or errorOk() if it succeeded.
|
* @return An error if the init failed, or errorOk() if it succeeded.
|
||||||
@@ -25,7 +23,15 @@ typedef struct {
|
|||||||
errorret_t sceneMainMenuInit(scenedata_t *sceneData);
|
errorret_t sceneMainMenuInit(scenedata_t *sceneData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the main menu scene. Currently a no-op -- the menu drives
|
* Starts the "Start Game" flow: runs a cutscene that loads every save
|
||||||
|
* slot (retrying on error, prompting if no save device is found) and
|
||||||
|
* opens the load-game picker once it succeeds. Called by
|
||||||
|
* ui/screen/mainmenu/uimainmenu.c when the player selects Start Game.
|
||||||
|
*/
|
||||||
|
void sceneMainMenuStartGame(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the main menu scene. Currently a no-op - the menu drives
|
||||||
* itself via the global UI element pipeline.
|
* itself via the global UI element pipeline.
|
||||||
*
|
*
|
||||||
* @param sceneData The scene data used for this scene.
|
* @param sceneData The scene data used for this scene.
|
||||||
@@ -34,7 +40,7 @@ errorret_t sceneMainMenuInit(scenedata_t *sceneData);
|
|||||||
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData);
|
errorret_t sceneMainMenuUpdate(scenedata_t *sceneData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the main menu scene. Currently a no-op -- the menu draws
|
* Renders the main menu scene. Currently a no-op - the menu draws
|
||||||
* itself via the global UI element pipeline.
|
* itself via the global UI element pipeline.
|
||||||
*
|
*
|
||||||
* @param sceneData The scene data used for this scene.
|
* @param sceneData The scene data used for this scene.
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
add_subdirectory(debug)
|
add_subdirectory(debug)
|
||||||
add_subdirectory(frame)
|
add_subdirectory(dialog)
|
||||||
|
add_subdirectory(screen)
|
||||||
add_subdirectory(focus)
|
add_subdirectory(focus)
|
||||||
add_subdirectory(overlay)
|
add_subdirectory(overlay)
|
||||||
add_subdirectory(rpg)
|
add_subdirectory(rpg)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
|
uiconfirm.c
|
||||||
|
)
|
||||||
|
|
||||||
|
add_subdirectory(save)
|
||||||
|
add_subdirectory(keyboard)
|
||||||
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
event.c
|
uikeyboard.c
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uikeyboard.h"
|
||||||
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "ui/widget/uibutton.h"
|
||||||
|
#include "ui/dialog/uiconfirm.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "display/text/font.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "time/time.h"
|
||||||
|
|
||||||
|
// Key cell size, expressed in font tiles so it scales if the font ever
|
||||||
|
// does. Row height is derived from this ratio in uiKeyboardDraw rather
|
||||||
|
// than being its own fixed constant, so a key cell keeps its proportions
|
||||||
|
// even when contentWidth is stretched wider by a long title/text label.
|
||||||
|
#define UI_KEYBOARD_KEY_WIDTH ((float_t)(FONT_DEFAULT_TILE_WIDTH * 4))
|
||||||
|
#define UI_KEYBOARD_KEY_HEIGHT ((float_t)(FONT_DEFAULT_TILE_HEIGHT * 2))
|
||||||
|
|
||||||
|
uikeyboard_t UI_KEYBOARD;
|
||||||
|
|
||||||
|
// String literals have static storage duration, so these stay valid for
|
||||||
|
// as long as the process runs - safe for uiButtonInit's non-copying
|
||||||
|
// label contract. "" cells are blank filler/reserved slots, NULL is
|
||||||
|
// shorthand for the same thing where there's no key at all - see
|
||||||
|
// uikeyboard.h and uiKeyboardNormalizeKey.
|
||||||
|
//
|
||||||
|
// Unshifted, caps off - also the canonical table (see
|
||||||
|
// uiKeyboardGetCanonicalKey).
|
||||||
|
static const char_t *const UI_KEYBOARD_KEYS_NONE[UI_KEYBOARD_KEY_COUNT] = {
|
||||||
|
// 1 2 3 4 5 6 7 8 9 0 DEL
|
||||||
|
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
||||||
|
UI_KEYBOARD_KEY_BACKSPACE,
|
||||||
|
|
||||||
|
// q w e r t y u i o p
|
||||||
|
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", NULL,
|
||||||
|
|
||||||
|
// CAPS a s d f g h j k l (NEWLINE)
|
||||||
|
UI_KEYBOARD_KEY_CAPS,
|
||||||
|
"a", "s", "d", "f", "g", "h", "j", "k", "l", NULL,
|
||||||
|
|
||||||
|
// SHIFT z x c v b n m SHIFT
|
||||||
|
UI_KEYBOARD_KEY_SHIFT,
|
||||||
|
NULL,
|
||||||
|
"z", "x", "c", "v", "b", "n", "m",
|
||||||
|
NULL,
|
||||||
|
UI_KEYBOARD_KEY_SHIFT,
|
||||||
|
|
||||||
|
// (CANCEL) SPACE (CONFIRM)
|
||||||
|
NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Caps on - letters uppercase; caps doesn't affect digits/symbols on a
|
||||||
|
// physical keyboard, so this table is otherwise identical to _NONE.
|
||||||
|
static const char_t *const UI_KEYBOARD_KEYS_CAPS[UI_KEYBOARD_KEY_COUNT] = {
|
||||||
|
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0",
|
||||||
|
UI_KEYBOARD_KEY_BACKSPACE,
|
||||||
|
|
||||||
|
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL,
|
||||||
|
|
||||||
|
UI_KEYBOARD_KEY_CAPS,
|
||||||
|
"A", "S", "D", "F", "G", "H", "J", "K", "L", NULL,
|
||||||
|
|
||||||
|
UI_KEYBOARD_KEY_SHIFT,
|
||||||
|
NULL,
|
||||||
|
"Z", "X", "C", "V", "B", "N", "M",
|
||||||
|
NULL,
|
||||||
|
UI_KEYBOARD_KEY_SHIFT,
|
||||||
|
|
||||||
|
NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shift held - letters uppercase, digits become their US-layout shift
|
||||||
|
// symbols, '-'/'=' become '_'/'+'. Takes priority over caps (see
|
||||||
|
// uiKeyboardGetChar).
|
||||||
|
static const char_t *const UI_KEYBOARD_KEYS_SHIFT[UI_KEYBOARD_KEY_COUNT] = {
|
||||||
|
"!", "@", "#", "$", "%", "^", "&", "*", "(", ")",
|
||||||
|
UI_KEYBOARD_KEY_BACKSPACE,
|
||||||
|
|
||||||
|
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL,
|
||||||
|
|
||||||
|
UI_KEYBOARD_KEY_CAPS,
|
||||||
|
"A", "S", "D", "F", "G", "H", "J", "K", "L", NULL,
|
||||||
|
|
||||||
|
UI_KEYBOARD_KEY_SHIFT,
|
||||||
|
NULL,
|
||||||
|
"Z", "X", "C", "V", "B", "N", "M",
|
||||||
|
NULL,
|
||||||
|
UI_KEYBOARD_KEY_SHIFT,
|
||||||
|
|
||||||
|
NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL,
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collapses a table entry's NULL ("no key here") down to "" (blank
|
||||||
|
// filler), the only form of "nothing" callers need to handle.
|
||||||
|
const char_t * uiKeyboardNormalizeKey(const char_t *key) {
|
||||||
|
return key != NULL ? key : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift takes priority over caps - there's no fourth table for "shift
|
||||||
|
// while caps is on", unlike a physical keyboard.
|
||||||
|
const char_t *const * uiKeyboardSelectTable(
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
) {
|
||||||
|
if(shift) return UI_KEYBOARD_KEYS_SHIFT;
|
||||||
|
if(caps) return UI_KEYBOARD_KEYS_CAPS;
|
||||||
|
return UI_KEYBOARD_KEYS_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t uiKeyboardBuildItems(
|
||||||
|
uimenuitem_t *items,
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
) {
|
||||||
|
assertNotNull(items, "Items cannot be NULL");
|
||||||
|
const char_t *const *table = uiKeyboardSelectTable(shift, caps);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) {
|
||||||
|
items[i].type = UI_MENU_WIDGET_TYPE_BUTTON;
|
||||||
|
uiButtonInit(&items[i].button, uiKeyboardNormalizeKey(table[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return UI_KEYBOARD_KEY_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardUpdateDisplay(
|
||||||
|
uimenuitem_t *items,
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
) {
|
||||||
|
assertNotNull(items, "Items cannot be NULL");
|
||||||
|
const char_t *const *table = uiKeyboardSelectTable(shift, caps);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) {
|
||||||
|
// These reserved slots are blank in every shift/caps table - their
|
||||||
|
// real labels (NEWLINE/CANCEL/CONFIRM) are patched in once at open
|
||||||
|
// time and aren't affected by shift/caps, so leave them alone here.
|
||||||
|
if(
|
||||||
|
i == UI_KEYBOARD_NEWLINE_INDEX ||
|
||||||
|
i == UI_KEYBOARD_CANCEL_INDEX ||
|
||||||
|
i == UI_KEYBOARD_CONFIRM_INDEX
|
||||||
|
) continue;
|
||||||
|
|
||||||
|
items[i].button.label.text = uiKeyboardNormalizeKey(table[i]);
|
||||||
|
items[i].button.label.dirty = true;
|
||||||
|
|
||||||
|
const char_t *canonical = uiKeyboardGetCanonicalKey(i);
|
||||||
|
if(stringCompare(canonical, UI_KEYBOARD_KEY_CAPS) == 0) {
|
||||||
|
uiButtonSetActive(&items[i].button, caps);
|
||||||
|
} else if(stringCompare(canonical, UI_KEYBOARD_KEY_SHIFT) == 0) {
|
||||||
|
uiButtonSetActive(&items[i].button, shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char_t * uiKeyboardGetCanonicalKey(const uint8_t index) {
|
||||||
|
assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
|
||||||
|
return uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiKeyboardIsShiftSensitive(const uint8_t index) {
|
||||||
|
assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
|
||||||
|
return stringCompare(
|
||||||
|
uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]),
|
||||||
|
uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_SHIFT[index])
|
||||||
|
) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t uiKeyboardGetChar(
|
||||||
|
const uint8_t index,
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
) {
|
||||||
|
assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range");
|
||||||
|
const char_t *const *table = uiKeyboardSelectTable(shift, caps);
|
||||||
|
return uiKeyboardNormalizeKey(table[index])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardFocusConfirm(void) {
|
||||||
|
uiMenuSetPosition(
|
||||||
|
&UI_KEYBOARD.menu,
|
||||||
|
UI_KEYBOARD_CONFIRM_INDEX % UI_KEYBOARD_COLUMNS,
|
||||||
|
UI_KEYBOARD_CONFIRM_INDEX / UI_KEYBOARD_COLUMNS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiKeyboardTextIsBlank(void) {
|
||||||
|
for(const char_t *c = UI_KEYBOARD.text; *c != '\0'; c++) {
|
||||||
|
if(!stringIsWhitespace(*c)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardAppendChar(const char_t c) {
|
||||||
|
size_t length = strlen(UI_KEYBOARD.text);
|
||||||
|
|
||||||
|
// Full - drop the last character instead of ignoring the keypress, so
|
||||||
|
// the new one overrides it. Falls through to the normal append below,
|
||||||
|
// which now has room again.
|
||||||
|
if(length >= UI_KEYBOARD.maxLength) {
|
||||||
|
length--;
|
||||||
|
UI_KEYBOARD.text[length] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(c == '\n') {
|
||||||
|
uint8_t lines = 1;
|
||||||
|
for(size_t i = 0; i < length; i++) {
|
||||||
|
if(UI_KEYBOARD.text[i] == '\n') lines++;
|
||||||
|
}
|
||||||
|
if(lines >= UI_KEYBOARD.lineCount) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_KEYBOARD.text[length] = c;
|
||||||
|
UI_KEYBOARD.text[length + 1] = '\0';
|
||||||
|
UI_KEYBOARD.textLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
|
||||||
|
|
||||||
|
// Nothing more can be typed - jump focus to confirm so accepting is a
|
||||||
|
// single button press away.
|
||||||
|
if(length + 1 >= UI_KEYBOARD.maxLength) uiKeyboardFocusConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardBackspace(void) {
|
||||||
|
size_t length = strlen(UI_KEYBOARD.text);
|
||||||
|
if(length == 0) return;
|
||||||
|
|
||||||
|
UI_KEYBOARD.text[length - 1] = '\0';
|
||||||
|
UI_KEYBOARD.textLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardMenuClosed(const uimenu_t *menu) {
|
||||||
|
if(UI_KEYBOARD.onInput != NULL) {
|
||||||
|
UI_KEYBOARD.onInput(
|
||||||
|
UI_KEYBOARD.text, UI_KEYBOARD.confirmed, UI_KEYBOARD.user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiKeyboardMenuCancel(const uimenu_t *menu) {
|
||||||
|
if(strlen(UI_KEYBOARD.text) > 0) {
|
||||||
|
uiKeyboardBackspace();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing left to delete - close, but only if this dialog is even
|
||||||
|
// allowed to be cancelled. Closed explicitly (rather than returning
|
||||||
|
// false to fall through to uiFocusUpdate's default pop) since that
|
||||||
|
// path only pops the focus stack - it never clears UI_KEYBOARD.open,
|
||||||
|
// which would leave uiKeyboardDraw drawing a focus-less dialog forever
|
||||||
|
// and the next uiKeyboardOpen() tripping its already-open assert.
|
||||||
|
if(!UI_KEYBOARD.cancel) return true;
|
||||||
|
|
||||||
|
uiKeyboardDoCancel();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user) {
|
||||||
|
if(!result) return;
|
||||||
|
|
||||||
|
UI_KEYBOARD.confirmed = true;
|
||||||
|
uiKeyboardClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardDoCancel(void) {
|
||||||
|
if(UI_KEYBOARD.cancelConfirm) {
|
||||||
|
uiConfirmOpen(
|
||||||
|
UI_KEYBOARD.cancelConfirmQuestion,
|
||||||
|
uiKeyboardCancelConfirmSecondaryResult, NULL
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_KEYBOARD.confirmed = false;
|
||||||
|
uiKeyboardClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user) {
|
||||||
|
if(!result) return;
|
||||||
|
|
||||||
|
UI_KEYBOARD.confirmed = false;
|
||||||
|
uiKeyboardClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardMenuSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
if(index == UI_KEYBOARD_CONFIRM_INDEX) {
|
||||||
|
if(UI_KEYBOARD.trimmed) {
|
||||||
|
stringTrim(UI_KEYBOARD.text);
|
||||||
|
UI_KEYBOARD.textLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
|
||||||
|
}
|
||||||
|
if(!UI_KEYBOARD.allowBlank && uiKeyboardTextIsBlank()) return;
|
||||||
|
|
||||||
|
if(UI_KEYBOARD.confirm) {
|
||||||
|
uiConfirmOpen(
|
||||||
|
UI_KEYBOARD.confirmQuestion, uiKeyboardConfirmSecondaryResult, NULL
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_KEYBOARD.confirmed = true;
|
||||||
|
uiKeyboardClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_CANCEL_INDEX) {
|
||||||
|
uiKeyboardDoCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(index == UI_KEYBOARD_NEWLINE_INDEX && UI_KEYBOARD.lineCount > 1) {
|
||||||
|
if(
|
||||||
|
UI_KEYBOARD.onKeyPress != NULL &&
|
||||||
|
!UI_KEYBOARD.onKeyPress('\n', UI_KEYBOARD.user)
|
||||||
|
) return;
|
||||||
|
uiKeyboardAppendChar('\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The canonical (unshifted) key, independent of whatever character
|
||||||
|
// it's currently drawn as - see uiKeyboardGetCanonicalKey. Used to
|
||||||
|
// identify the key, not item->button.label.text, which changes with
|
||||||
|
// shift/caps.
|
||||||
|
const char_t *key = uiKeyboardGetCanonicalKey(index);
|
||||||
|
|
||||||
|
// Blank filler/reserved cell (includes an unused cancel/newline slot).
|
||||||
|
if(key[0] == '\0') return;
|
||||||
|
|
||||||
|
if(stringCompare(key, UI_KEYBOARD_KEY_BACKSPACE) == 0) {
|
||||||
|
uiKeyboardBackspace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stringCompare(key, UI_KEYBOARD_KEY_SHIFT) == 0) {
|
||||||
|
UI_KEYBOARD.shift = !UI_KEYBOARD.shift;
|
||||||
|
uiKeyboardUpdateDisplay(
|
||||||
|
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stringCompare(key, UI_KEYBOARD_KEY_CAPS) == 0) {
|
||||||
|
UI_KEYBOARD.caps = !UI_KEYBOARD.caps;
|
||||||
|
uiKeyboardUpdateDisplay(
|
||||||
|
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t c;
|
||||||
|
if(stringCompare(key, UI_KEYBOARD_KEY_SPACE) == 0) {
|
||||||
|
c = ' ';
|
||||||
|
} else {
|
||||||
|
c = uiKeyboardGetChar(index, UI_KEYBOARD.shift, UI_KEYBOARD.caps);
|
||||||
|
|
||||||
|
// Shift is a one-shot, consumed by any letter/digit/-/= it actually
|
||||||
|
// affected (and the display refreshed to drop back to whatever caps
|
||||||
|
// alone says); caps is a persistent toggle.
|
||||||
|
if(UI_KEYBOARD.shift && uiKeyboardIsShiftSensitive(index)) {
|
||||||
|
UI_KEYBOARD.shift = false;
|
||||||
|
uiKeyboardUpdateDisplay(
|
||||||
|
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_KEYBOARD.onKeyPress != NULL && !UI_KEYBOARD.onKeyPress(c, UI_KEYBOARD.user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uiKeyboardAppendChar(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiKeyboardInit(void) {
|
||||||
|
memoryZero(&UI_KEYBOARD, sizeof(uikeyboard_t));
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&UI_KEYBOARD.titleLabel, UI_KEYBOARD.titleText,
|
||||||
|
UI_KEYBOARD.titleSprites, UI_KEYBOARD_TITLE_SPRITES_MAX
|
||||||
|
);
|
||||||
|
uiLabelInit(
|
||||||
|
&UI_KEYBOARD.textLabel, UI_KEYBOARD.text,
|
||||||
|
UI_KEYBOARD.textSprites, UI_KEYBOARD_TEXT_SPRITES_MAX
|
||||||
|
);
|
||||||
|
uiLabelInit(
|
||||||
|
&UI_KEYBOARD.cursorLabel, UI_KEYBOARD_CURSOR_TEXT,
|
||||||
|
UI_KEYBOARD.cursorSprites, UI_KEYBOARD_CURSOR_SPRITES_MAX
|
||||||
|
);
|
||||||
|
UI_KEYBOARD.cursorLabel.dirty = true;
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiKeyboardIsOpen(void) {
|
||||||
|
return UI_KEYBOARD.open;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardOpen(const uikeyboardopen_t *open) {
|
||||||
|
assertNotNull(open, "Open parameters cannot be NULL");
|
||||||
|
assertFalse(UI_KEYBOARD.open, "Keyboard is already open");
|
||||||
|
assertTrue(
|
||||||
|
open->maxLength <= UI_KEYBOARD_TEXT_MAX - 1,
|
||||||
|
"maxLength exceeds UI_KEYBOARD_TEXT_MAX"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
open->lineCount <= UI_KEYBOARD_LINE_COUNT_MAX,
|
||||||
|
"lineCount exceeds UI_KEYBOARD_LINE_COUNT_MAX"
|
||||||
|
);
|
||||||
|
|
||||||
|
stringCopy(
|
||||||
|
UI_KEYBOARD.titleText,
|
||||||
|
open->title != NULL ? open->title : UI_KEYBOARD_TITLE_DEFAULT,
|
||||||
|
UI_KEYBOARD_TITLE_TEXT_MAX - 1
|
||||||
|
);
|
||||||
|
UI_KEYBOARD.titleLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_KEYBOARD.titleLabel);
|
||||||
|
|
||||||
|
UI_KEYBOARD.text[0] = '\0';
|
||||||
|
UI_KEYBOARD.textLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
|
||||||
|
|
||||||
|
UI_KEYBOARD.onInput = open->onInput;
|
||||||
|
UI_KEYBOARD.onKeyPress = open->onKeyPress;
|
||||||
|
UI_KEYBOARD.cancel = open->cancel;
|
||||||
|
UI_KEYBOARD.maxLength = open->maxLength == 0 ?
|
||||||
|
UI_KEYBOARD_TEXT_MAX - 1 : open->maxLength;
|
||||||
|
UI_KEYBOARD.lineCount = open->lineCount == 0 ? 1 : open->lineCount;
|
||||||
|
UI_KEYBOARD.confirm = open->confirm;
|
||||||
|
stringCopy(
|
||||||
|
UI_KEYBOARD.confirmQuestion,
|
||||||
|
open->confirmLabel != NULL ?
|
||||||
|
open->confirmLabel : UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT,
|
||||||
|
UI_KEYBOARD_CONFIRM_QUESTION_MAX - 1
|
||||||
|
);
|
||||||
|
UI_KEYBOARD.trimmed = open->trimmed;
|
||||||
|
UI_KEYBOARD.allowBlank = open->allowBlank;
|
||||||
|
UI_KEYBOARD.user = open->user;
|
||||||
|
UI_KEYBOARD.confirmed = false;
|
||||||
|
UI_KEYBOARD.caps = false;
|
||||||
|
UI_KEYBOARD.shift = false;
|
||||||
|
UI_KEYBOARD.open = true;
|
||||||
|
|
||||||
|
// Builds the full fixed grid, including blank filler at the
|
||||||
|
// newline/confirm/cancel reserved slots - patched with real buttons
|
||||||
|
// below where applicable. Letters start lowercase and digits
|
||||||
|
// unshifted, matching caps/shift both being reset to false above.
|
||||||
|
uiKeyboardBuildItems(UI_KEYBOARD.items, false, false);
|
||||||
|
|
||||||
|
if(UI_KEYBOARD.lineCount > 1) {
|
||||||
|
uiButtonInit(
|
||||||
|
&UI_KEYBOARD.items[UI_KEYBOARD_NEWLINE_INDEX].button,
|
||||||
|
UI_KEYBOARD_KEY_NEWLINE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uiButtonInit(
|
||||||
|
&UI_KEYBOARD.items[UI_KEYBOARD_CONFIRM_INDEX].button,
|
||||||
|
UI_KEYBOARD_CONFIRM_LABEL
|
||||||
|
);
|
||||||
|
|
||||||
|
if(UI_KEYBOARD.cancel) {
|
||||||
|
uiButtonInit(
|
||||||
|
&UI_KEYBOARD.items[UI_KEYBOARD_CANCEL_INDEX].button,
|
||||||
|
UI_KEYBOARD_CANCEL_LABEL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uiMenuInit(
|
||||||
|
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
|
||||||
|
);
|
||||||
|
uiMenuSetItems(
|
||||||
|
&UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_KEY_COUNT,
|
||||||
|
UI_KEYBOARD_COLUMNS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Back deletes a character rather than closing the dialog outright -
|
||||||
|
// see uiKeyboardMenuCancel.
|
||||||
|
uiMenuSetCancelCallback(&UI_KEYBOARD.menu, uiKeyboardMenuCancel);
|
||||||
|
uiMenuOpen(&UI_KEYBOARD.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiKeyboardClose(void) {
|
||||||
|
if(!UI_KEYBOARD.open) return;
|
||||||
|
UI_KEYBOARD.open = false;
|
||||||
|
|
||||||
|
if(uiMenuIsActive(&UI_KEYBOARD.menu)) {
|
||||||
|
uiMenuClose(&UI_KEYBOARD.menu);
|
||||||
|
} else {
|
||||||
|
uiKeyboardMenuClosed(NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiKeyboardDraw(void) {
|
||||||
|
if(!UI_KEYBOARD.open) errorOk();
|
||||||
|
|
||||||
|
float_t x = (float_t)SCREEN.scanX;
|
||||||
|
float_t y = (float_t)SCREEN.scanY;
|
||||||
|
float_t width = (float_t)SCREEN.scanWidth;
|
||||||
|
float_t height = (float_t)SCREEN.scanHeight;
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
// When multi-line, reserve the full lineCount rows up front so the
|
||||||
|
// dialog doesn't resize as newlines are typed - otherwise fall back to
|
||||||
|
// the label's own measured height (floored at one row for an empty
|
||||||
|
// string, which measures to zero).
|
||||||
|
float_t textHeight = UI_KEYBOARD.lineCount > 1 ?
|
||||||
|
(float_t)UI_KEYBOARD.lineCount * rowHeight :
|
||||||
|
mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height);
|
||||||
|
uint8_t columns = UI_KEYBOARD.menu.columns;
|
||||||
|
uint8_t rows = (UI_KEYBOARD.menu.itemCount + columns - 1) / columns;
|
||||||
|
|
||||||
|
float_t contentWidth = mathMax(
|
||||||
|
mathMax((float_t)UI_KEYBOARD.titleLabel.width, (float_t)UI_KEYBOARD.textLabel.width),
|
||||||
|
(float_t)columns * UI_KEYBOARD_KEY_WIDTH
|
||||||
|
);
|
||||||
|
float_t colStep = contentWidth / (float_t)columns;
|
||||||
|
float_t keyRowHeight = colStep * (UI_KEYBOARD_KEY_HEIGHT / UI_KEYBOARD_KEY_WIDTH);
|
||||||
|
float_t contentHeight = rowHeight + UI_FRAME_PADDING_Y
|
||||||
|
+ textHeight + UI_FRAME_PADDING_Y
|
||||||
|
+ ((float_t)rows * keyRowHeight);
|
||||||
|
|
||||||
|
// The frame now fills the whole screen, but the title/text/keys still
|
||||||
|
// lay out as one content block - center that block within the screen.
|
||||||
|
float_t contentX = x + (width - contentWidth) * 0.5f;
|
||||||
|
float_t contentY = y + (height - contentHeight) * 0.5f;
|
||||||
|
|
||||||
|
uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX);
|
||||||
|
uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY);
|
||||||
|
errorChain(uiLabelRender(&UI_KEYBOARD.titleLabel, COLOR_WHITE));
|
||||||
|
|
||||||
|
float_t textY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
||||||
|
uiLabelSetX(&UI_KEYBOARD.textLabel, contentX);
|
||||||
|
uiLabelSetY(&UI_KEYBOARD.textLabel, textY);
|
||||||
|
errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE));
|
||||||
|
|
||||||
|
// Insertion point is always the end of text - there's no mid-string
|
||||||
|
// cursor movement - so find it by counting characters since the last
|
||||||
|
// newline (column) and newlines overall (line). Once maxLength is
|
||||||
|
// reached, a new key overrides the last character (see
|
||||||
|
// uiKeyboardAppendChar) rather than appending one, so stop one
|
||||||
|
// character short here too - the cursor should sit on that last
|
||||||
|
// character, not past it, to reflect what typing will actually do.
|
||||||
|
size_t textLength = strlen(UI_KEYBOARD.text);
|
||||||
|
size_t cursorLimit = textLength >= UI_KEYBOARD.maxLength ?
|
||||||
|
textLength - 1 : textLength;
|
||||||
|
|
||||||
|
uint8_t cursorLine = 0;
|
||||||
|
uint8_t cursorCol = 0;
|
||||||
|
for(size_t i = 0; i < cursorLimit; i++) {
|
||||||
|
if(UI_KEYBOARD.text[i] == '\n') { cursorLine++; cursorCol = 0; }
|
||||||
|
else cursorCol++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mathModFloat(TIME.time, UI_KEYBOARD_CURSOR_BLINK_RATE * 2.0f) <
|
||||||
|
UI_KEYBOARD_CURSOR_BLINK_RATE
|
||||||
|
) {
|
||||||
|
uiLabelSetX(
|
||||||
|
&UI_KEYBOARD.cursorLabel,
|
||||||
|
contentX + (float_t)cursorCol * (float_t)FONT_DEFAULT_TILE_WIDTH
|
||||||
|
);
|
||||||
|
uiLabelSetY(&UI_KEYBOARD.cursorLabel, textY + (float_t)cursorLine * rowHeight);
|
||||||
|
errorChain(uiLabelRender(&UI_KEYBOARD.cursorLabel, COLOR_WHITE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawn directly rather than via uiMenuDraw - that helper hardcodes row
|
||||||
|
// spacing to the font's native tile height, which would keep the grid
|
||||||
|
// vertically compact no matter how much contentWidth grows. Focus
|
||||||
|
// highlighting is unaffected: it's applied to item->button.highlighted
|
||||||
|
// by the focus system independently of whoever calls uiButtonDraw.
|
||||||
|
float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y;
|
||||||
|
for(uint8_t i = 0; i < UI_KEYBOARD.menu.itemCount; i++) {
|
||||||
|
float_t ix = contentX + (float_t)(i % columns) * colStep;
|
||||||
|
float_t iy = menuY + (float_t)(i / columns) * keyRowHeight;
|
||||||
|
errorChain(uiButtonDraw(&UI_KEYBOARD.items[i].button, ix, iy));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiKeyboardDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
/**
|
||||||
|
* 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 "ui/widget/uilabel.h"
|
||||||
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "display/text/font.h"
|
||||||
|
|
||||||
|
// A fixed 11x5 QWERTY grid - the only layout that exists (numbers/
|
||||||
|
// symbols layouts were removed as separate concepts; digits already
|
||||||
|
// live on this one):
|
||||||
|
// 1 2 3 4 5 6 7 8 9 0 DEL
|
||||||
|
// q w e r t y u i o p
|
||||||
|
// CAPS a s d f g h j k l (NEWLINE)
|
||||||
|
// SHIFT z x c v b n m SHIFT
|
||||||
|
// (CANCEL) SPACE (CONFIRM)
|
||||||
|
// Rows shorter than 11 columns are padded with blank filler buttons
|
||||||
|
// (empty label) so every key lands in its intended position - the menu/
|
||||||
|
// focus grid only supports uniform columns, it has no concept of a key
|
||||||
|
// spanning multiple cells or a row being narrower than the grid, so this
|
||||||
|
// is the only way to get real positioning out of it. A filler cell is
|
||||||
|
// still a focusable/navigable-to grid slot, it just does nothing when
|
||||||
|
// selected - see uiKeyboardMenuSelected's blank-label check. A
|
||||||
|
// UI_KEYBOARD_KEYS_* table may use NULL for a slot with no key at all -
|
||||||
|
// see uiKeyboardGetCanonicalKey, which normalizes it to the same blank
|
||||||
|
// filler behavior.
|
||||||
|
//
|
||||||
|
// NEWLINE/CANCEL/CONFIRM are reserved slots the tables always leave
|
||||||
|
// blank - uiKeyboardOpen patches them with real buttons depending on
|
||||||
|
// lineCount/cancel (CONFIRM is unconditional, just needed as a fixed
|
||||||
|
// slot addressable by constant).
|
||||||
|
#define UI_KEYBOARD_COLUMNS 11
|
||||||
|
#define UI_KEYBOARD_ROWS 5
|
||||||
|
#define UI_KEYBOARD_KEY_COUNT (UI_KEYBOARD_COLUMNS * UI_KEYBOARD_ROWS)
|
||||||
|
|
||||||
|
// Reserved slot indices uiKeyboardOpen patches with real buttons.
|
||||||
|
#define UI_KEYBOARD_NEWLINE_INDEX 32
|
||||||
|
#define UI_KEYBOARD_CANCEL_INDEX 44
|
||||||
|
#define UI_KEYBOARD_CONFIRM_INDEX 54
|
||||||
|
|
||||||
|
// Labels uiKeyboardMenuSelected checks for by pointer/string compare to
|
||||||
|
// tell special keys apart from an ordinary single-character key. A blank
|
||||||
|
// ("") label is a filler cell - see above. Identical across every
|
||||||
|
// UI_KEYBOARD_KEYS_* table, since these don't change with shift/caps.
|
||||||
|
#define UI_KEYBOARD_KEY_SPACE FONT_ICON_SPACE
|
||||||
|
#define UI_KEYBOARD_KEY_BACKSPACE FONT_ICON_BACKSPACE
|
||||||
|
#define UI_KEYBOARD_KEY_SHIFT FONT_ICON_SHIFT
|
||||||
|
#define UI_KEYBOARD_KEY_CAPS FONT_ICON_CAPSLOCK
|
||||||
|
// Only patched into UI_KEYBOARD_NEWLINE_INDEX when
|
||||||
|
// uikeyboardopen_t.lineCount > 1 - otherwise that slot stays blank.
|
||||||
|
#define UI_KEYBOARD_KEY_NEWLINE FONT_ICON_NEWLINE
|
||||||
|
|
||||||
|
#define UI_KEYBOARD_TITLE_TEXT_MAX 64
|
||||||
|
#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
|
||||||
|
#define UI_KEYBOARD_TEXT_MAX 64
|
||||||
|
#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
|
||||||
|
// The grid is a fixed size - only one layout exists today, so this is
|
||||||
|
// just its key count.
|
||||||
|
#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_KEY_COUNT
|
||||||
|
|
||||||
|
// Hardcoded for now - a uiModalOpen-style title/button text override is
|
||||||
|
// planned (see uikeyboardopen_t) but not built yet. Patched into the
|
||||||
|
// grid's reserved slots (UI_KEYBOARD_CONFIRM_INDEX etc.) rather than
|
||||||
|
// appended, since the grid is a fixed shape.
|
||||||
|
#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
|
||||||
|
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
|
||||||
|
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
|
||||||
|
|
||||||
|
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
|
||||||
|
// Used when uikeyboardopen_t.confirmLabel is NULL.
|
||||||
|
#define UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT "Is this correct?"
|
||||||
|
|
||||||
|
#define UI_KEYBOARD_CANCEL_QUESTION_MAX 64
|
||||||
|
// Used when uikeyboardopen_t.cancelConfirmLabel is NULL.
|
||||||
|
#define UI_KEYBOARD_CANCEL_QUESTION_DEFAULT "Discard your changes?"
|
||||||
|
|
||||||
|
// Maximum number of lines uikeyboardopen_t.lineCount may request.
|
||||||
|
#define UI_KEYBOARD_LINE_COUNT_MAX 4
|
||||||
|
|
||||||
|
// Marks where the next typed character will be inserted (always the end
|
||||||
|
// of the current text - there's no mid-string cursor movement).
|
||||||
|
#define UI_KEYBOARD_CURSOR_TEXT "_"
|
||||||
|
#define UI_KEYBOARD_CURSOR_SPRITES_MAX 1
|
||||||
|
// Seconds per on/off half-cycle of the cursor blink.
|
||||||
|
#define UI_KEYBOARD_CURSOR_BLINK_RATE 0.5f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked once the keyboard dialog is dismissed.
|
||||||
|
*
|
||||||
|
* @param text The entered text. Only meaningful when confirmed is true;
|
||||||
|
* points at UI_KEYBOARD's own buffer, so copy it out if it needs to
|
||||||
|
* outlive the next uiKeyboardOpen call.
|
||||||
|
* @param confirmed True if the confirm button was picked, false if
|
||||||
|
* cancelled.
|
||||||
|
* @param user Arbitrary pointer passed via uikeyboardopen_t.user.
|
||||||
|
*/
|
||||||
|
typedef void (*uikeyboardinputcallback_t)(
|
||||||
|
const char_t *text,
|
||||||
|
const bool_t confirmed,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked before a key press is applied, giving the caller a
|
||||||
|
* chance to reject it - e.g. an email field rejecting "%".
|
||||||
|
*
|
||||||
|
* @param c The character about to be entered (SPACE included, DEL/
|
||||||
|
* confirm/cancel excluded - those aren't text being entered).
|
||||||
|
* @param user Arbitrary pointer passed via uikeyboardopen_t.user.
|
||||||
|
* @returns True to allow the character, false to reject it (ignored,
|
||||||
|
* nothing is entered).
|
||||||
|
*/
|
||||||
|
typedef bool_t (*uikeyboardkeypresscallback_t)(const char_t c, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for uiKeyboardOpen. Expect this to grow (e.g. button text
|
||||||
|
* overrides) as more dialogs start using the keyboard.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
// Title text shown above the entered text; copied internally, safe to
|
||||||
|
// be transient. Displayed as-is - resolve any locale string yourself
|
||||||
|
// before passing it in. NULL uses UI_KEYBOARD_TITLE_DEFAULT ("ENTER
|
||||||
|
// YOUR TEXT").
|
||||||
|
const char_t *title;
|
||||||
|
|
||||||
|
// Fired once the dialog is dismissed, confirmed or cancelled. May be
|
||||||
|
// NULL.
|
||||||
|
uikeyboardinputcallback_t onInput;
|
||||||
|
|
||||||
|
// Fired for each key press before it's applied. May be NULL to allow
|
||||||
|
// everything.
|
||||||
|
uikeyboardkeypresscallback_t onKeyPress;
|
||||||
|
|
||||||
|
// Whether the cancel button is shown, and whether pressing back with
|
||||||
|
// no text entered closes the dialog - see uiKeyboardMenuCancel.
|
||||||
|
bool_t cancel;
|
||||||
|
|
||||||
|
// Maximum length of the entered text, in characters excluding the
|
||||||
|
// null terminator. 0 defaults to the buffer's full capacity
|
||||||
|
// (UI_KEYBOARD_TEXT_MAX - 1). Must not exceed that.
|
||||||
|
size_t maxLength;
|
||||||
|
|
||||||
|
// When true, picking confirm opens a second uiConfirm "are you sure"
|
||||||
|
// dialog (see confirmLabel) on top of the keyboard before actually
|
||||||
|
// closing it - picking Cancel there returns to the keyboard with the
|
||||||
|
// text untouched.
|
||||||
|
bool_t confirm;
|
||||||
|
|
||||||
|
// Question text/locale message ID for the second confirm dialog when
|
||||||
|
// confirm is true; copied internally, safe to be transient. NULL uses
|
||||||
|
// UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT. Ignored when confirm is false.
|
||||||
|
const char_t *confirmLabel;
|
||||||
|
|
||||||
|
// When true, actually cancelling (the CANCEL button, or back with
|
||||||
|
// nothing left to delete - see uiKeyboardMenuCancel) opens a second
|
||||||
|
// uiConfirm "are you sure" dialog (see cancelConfirmLabel) before the
|
||||||
|
// keyboard closes - picking Cancel there returns to the keyboard with
|
||||||
|
// the text untouched. Ignored when cancel is false, since there's
|
||||||
|
// nothing to confirm.
|
||||||
|
bool_t cancelConfirm;
|
||||||
|
|
||||||
|
// Question text/locale message ID for the cancel confirm dialog when
|
||||||
|
// cancelConfirm is true; copied internally, safe to be transient. NULL
|
||||||
|
// uses UI_KEYBOARD_CANCEL_QUESTION_DEFAULT. Ignored when cancelConfirm
|
||||||
|
// is false.
|
||||||
|
const char_t *cancelConfirmLabel;
|
||||||
|
|
||||||
|
// If true, leading/trailing whitespace is trimmed from the entered
|
||||||
|
// text when confirm is pressed, before it's checked against
|
||||||
|
// allowBlank or passed to onInput.
|
||||||
|
bool_t trimmed;
|
||||||
|
|
||||||
|
// If true, a blank result is accepted when confirm is pressed - blank
|
||||||
|
// means an empty string, or (when trimmed is false) a string of only
|
||||||
|
// spaces. When false, pressing confirm does nothing while the text is
|
||||||
|
// blank - see uiKeyboardTextIsBlank.
|
||||||
|
bool_t allowBlank;
|
||||||
|
|
||||||
|
// Maximum number of lines the entered text can span, from 1 to
|
||||||
|
// UI_KEYBOARD_LINE_COUNT_MAX. 0 defaults to 1. When > 1, a NEWLINE key
|
||||||
|
// is added to the grid, capped at inserting lineCount - 1 newlines.
|
||||||
|
uint8_t lineCount;
|
||||||
|
|
||||||
|
// Arbitrary pointer passed to onInput/onKeyPress.
|
||||||
|
void *user;
|
||||||
|
} uikeyboardopen_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uilabel_t titleLabel;
|
||||||
|
char_t titleText[UI_KEYBOARD_TITLE_TEXT_MAX];
|
||||||
|
spritebatchsprite_t titleSprites[UI_KEYBOARD_TITLE_SPRITES_MAX];
|
||||||
|
|
||||||
|
uilabel_t textLabel;
|
||||||
|
char_t text[UI_KEYBOARD_TEXT_MAX];
|
||||||
|
spritebatchsprite_t textSprites[UI_KEYBOARD_TEXT_SPRITES_MAX];
|
||||||
|
|
||||||
|
// Blinking insertion-point marker, positioned each draw at the end of
|
||||||
|
// text - a separate label rather than appending to text/textSprites,
|
||||||
|
// since neither buffer has spare room for it (see
|
||||||
|
// UI_KEYBOARD_TEXT_MAX/UI_KEYBOARD_TEXT_SPRITES_MAX).
|
||||||
|
uilabel_t cursorLabel;
|
||||||
|
spritebatchsprite_t cursorSprites[UI_KEYBOARD_CURSOR_SPRITES_MAX];
|
||||||
|
|
||||||
|
uimenu_t menu;
|
||||||
|
uimenuitem_t items[UI_KEYBOARD_ITEMS_MAX];
|
||||||
|
|
||||||
|
char_t confirmQuestion[UI_KEYBOARD_CONFIRM_QUESTION_MAX];
|
||||||
|
char_t cancelConfirmQuestion[UI_KEYBOARD_CANCEL_QUESTION_MAX];
|
||||||
|
|
||||||
|
uikeyboardinputcallback_t onInput;
|
||||||
|
uikeyboardkeypresscallback_t onKeyPress;
|
||||||
|
void *user;
|
||||||
|
size_t maxLength;
|
||||||
|
uint8_t lineCount;
|
||||||
|
bool_t cancel;
|
||||||
|
bool_t confirm;
|
||||||
|
bool_t cancelConfirm;
|
||||||
|
bool_t trimmed;
|
||||||
|
bool_t allowBlank;
|
||||||
|
bool_t open;
|
||||||
|
bool_t confirmed;
|
||||||
|
|
||||||
|
// Persistent caps-lock toggle, and a one-shot shift consumed by the
|
||||||
|
// next letter/digit/-/= typed - see uiKeyboardMenuSelected. Key labels
|
||||||
|
// are redrawn to match (see uiKeyboardUpdateDisplay) rather than
|
||||||
|
// staying static.
|
||||||
|
bool_t caps;
|
||||||
|
bool_t shift;
|
||||||
|
} uikeyboard_t;
|
||||||
|
|
||||||
|
extern uikeyboard_t UI_KEYBOARD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the on-screen keyboard.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiKeyboardInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the on-screen keyboard: a semi-transparent black backdrop
|
||||||
|
* covering the whole screen, then its own centered frame with the title,
|
||||||
|
* currently entered text, and the key grid. No-op when not open.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiKeyboardDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the keyboard dialog is currently open.
|
||||||
|
*
|
||||||
|
* @returns True if open.
|
||||||
|
*/
|
||||||
|
bool_t uiKeyboardIsOpen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the on-screen keyboard with an empty text buffer.
|
||||||
|
*
|
||||||
|
* @param open Parameters for this dialog; copied internally, safe to be
|
||||||
|
* transient. Cannot be NULL.
|
||||||
|
*/
|
||||||
|
void uiKeyboardOpen(const uikeyboardopen_t *open);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the keyboard dialog, invoking the result callback set by
|
||||||
|
* uiKeyboardOpen as cancelled (confirmed=false) unless the confirm
|
||||||
|
* button was what triggered this close. No-op when already closed.
|
||||||
|
*/
|
||||||
|
void uiKeyboardClose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the on-screen keyboard.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiKeyboardDispose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a character to the currently entered text, silently ignoring
|
||||||
|
* it once UI_KEYBOARD.maxLength is reached, or (for '\n') once the text
|
||||||
|
* already has UI_KEYBOARD.lineCount - 1 newlines. Does not consult
|
||||||
|
* onKeyPress - that happens in uiKeyboardMenuSelected before this is
|
||||||
|
* called. Moves focus to the confirm button once this append fills the
|
||||||
|
* text to maxLength - see uiKeyboardFocusConfirm.
|
||||||
|
*
|
||||||
|
* @param c The character to append.
|
||||||
|
*/
|
||||||
|
void uiKeyboardAppendChar(const char_t c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the menu's focus to the confirm button.
|
||||||
|
*/
|
||||||
|
void uiKeyboardFocusConfirm(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the currently entered text counts as blank for
|
||||||
|
* UI_KEYBOARD.allowBlank purposes - an empty string, or (when
|
||||||
|
* UI_KEYBOARD.trimmed is false, so whitespace-only text was never
|
||||||
|
* reduced to empty) a string of only whitespace.
|
||||||
|
*
|
||||||
|
* @returns True if blank.
|
||||||
|
*/
|
||||||
|
bool_t uiKeyboardTextIsBlank(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the last character of the currently entered text. No-op when
|
||||||
|
* already empty.
|
||||||
|
*/
|
||||||
|
void uiKeyboardBackspace(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal menu callback - routes a picked key/confirm/cancel item to
|
||||||
|
* uiKeyboardAppendChar/uiKeyboardBackspace/uiKeyboardClose, consulting
|
||||||
|
* onKeyPress before a character key is applied. When UI_KEYBOARD.confirm
|
||||||
|
* is set, picking confirm opens a second uiConfirm dialog (see
|
||||||
|
* uiKeyboardConfirmSecondaryResult) instead of closing immediately.
|
||||||
|
* SHIFT/CAPS toggle UI_KEYBOARD.shift/caps instead of entering anything.
|
||||||
|
* A blank ("") label is a filler/reserved grid cell and is a no-op.
|
||||||
|
*
|
||||||
|
* @param menu The keyboard's menu.
|
||||||
|
* @param index Index of the picked item.
|
||||||
|
* @param item The picked item.
|
||||||
|
*/
|
||||||
|
void uiKeyboardMenuSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result callback for the second "are you sure" uiConfirm dialog opened
|
||||||
|
* when UI_KEYBOARD.confirm is set - see uiKeyboardMenuSelected.
|
||||||
|
*
|
||||||
|
* @param result True if Confirm was picked there, false if Cancel.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually cancels the keyboard - sets confirmed to false and closes the
|
||||||
|
* dialog. Shared by uiKeyboardMenuCancel (back with nothing to delete)
|
||||||
|
* and uiKeyboardMenuSelected (the CANCEL button); called directly when
|
||||||
|
* UI_KEYBOARD.cancelConfirm is false, or as the result callback (see
|
||||||
|
* uiKeyboardCancelConfirmSecondaryResult) once the "are you sure" dialog
|
||||||
|
* confirms it when cancelConfirm is true.
|
||||||
|
*/
|
||||||
|
void uiKeyboardDoCancel(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result callback for the "are you sure" uiConfirm dialog opened when
|
||||||
|
* UI_KEYBOARD.cancelConfirm is set - see uiKeyboardDoCancel.
|
||||||
|
*
|
||||||
|
* @param result True if Confirm was picked there, false if Cancel.
|
||||||
|
* @param user Unused.
|
||||||
|
*/
|
||||||
|
void uiKeyboardCancelConfirmSecondaryResult(const bool_t result, void *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal menu callback - fires the result callback set by
|
||||||
|
* uiKeyboardOpen once the menu finishes closing.
|
||||||
|
*
|
||||||
|
* @param menu Unused; matches uimenuclosedcallback_t.
|
||||||
|
*/
|
||||||
|
void uiKeyboardMenuClosed(const uimenu_t *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal menu cancel callback (see uiMenuSetCancelCallback): back
|
||||||
|
* deletes the last entered character; with nothing left to delete, it
|
||||||
|
* closes the dialog as cancelled via uiKeyboardClose, but only when
|
||||||
|
* UI_KEYBOARD.cancel allows it - otherwise back is swallowed and does
|
||||||
|
* nothing.
|
||||||
|
*
|
||||||
|
* @param menu The keyboard's menu.
|
||||||
|
* @returns Always true - this always fully handles back itself
|
||||||
|
* (backspace, explicit close, or swallow), never falling through to the
|
||||||
|
* default pop.
|
||||||
|
*/
|
||||||
|
bool_t uiKeyboardMenuCancel(const uimenu_t *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapses a UI_KEYBOARD_KEYS_* table entry's NULL ("no key here") down
|
||||||
|
* to "" (blank filler), the only form of "nothing" callers need to
|
||||||
|
* handle.
|
||||||
|
*
|
||||||
|
* @param key A table entry, possibly NULL.
|
||||||
|
* @returns key, or "" if key was NULL.
|
||||||
|
*/
|
||||||
|
const char_t * uiKeyboardNormalizeKey(const char_t *key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks which of UI_KEYBOARD_KEYS_NONE/_CAPS/_SHIFT is active for the
|
||||||
|
* given state - shift takes priority over caps when both are set.
|
||||||
|
*
|
||||||
|
* @param shift Current shift state.
|
||||||
|
* @param caps Current caps state.
|
||||||
|
* @returns The selected table.
|
||||||
|
*/
|
||||||
|
const char_t *const * uiKeyboardSelectTable(
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the key grid's buttons into items, starting at index 0, one
|
||||||
|
* UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_KEY_COUNT of
|
||||||
|
* them, in row-major order across UI_KEYBOARD_COLUMNS columns). Each
|
||||||
|
* button's label is a single character, one of the UI_KEYBOARD_KEY_*
|
||||||
|
* literals, or "" for a blank filler/reserved slot, read straight from
|
||||||
|
* whichever of the three UI_KEYBOARD_KEYS_* tables shift/caps selects
|
||||||
|
* (see uiKeyboardGetChar) - can be refreshed later with
|
||||||
|
* uiKeyboardUpdateDisplay. See uiKeyboardGetCanonicalKey for identifying
|
||||||
|
* a picked key independent of its displayed character.
|
||||||
|
*
|
||||||
|
* @param items Destination array; must have room for at least
|
||||||
|
* UI_KEYBOARD_KEY_COUNT entries.
|
||||||
|
* @param shift Initial shift state - see uiKeyboardGetChar.
|
||||||
|
* @param caps Initial caps state - see uiKeyboardGetChar.
|
||||||
|
* @returns The number of items written (UI_KEYBOARD_KEY_COUNT).
|
||||||
|
*/
|
||||||
|
uint8_t uiKeyboardBuildItems(
|
||||||
|
uimenuitem_t *items,
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repoints every key already built into items by uiKeyboardBuildItems at
|
||||||
|
* whichever UI_KEYBOARD_KEYS_* table the new shift/caps state selects,
|
||||||
|
* marking all of them dirty so they redraw. Unconditional (not just the
|
||||||
|
* letter/digit/-/= keys that actually differ between tables) - simpler
|
||||||
|
* than tracking which changed, and cheap since this only runs on an
|
||||||
|
* infrequent SHIFT/CAPS press.
|
||||||
|
*
|
||||||
|
* @param items The same array passed to uiKeyboardBuildItems.
|
||||||
|
* @param shift New shift state - see uiKeyboardGetChar.
|
||||||
|
* @param caps New caps state - see uiKeyboardGetChar.
|
||||||
|
*/
|
||||||
|
void uiKeyboardUpdateDisplay(
|
||||||
|
uimenuitem_t *items,
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a grid slot's canonical (unshifted, no-caps) key label,
|
||||||
|
* independent of whatever character it's currently drawn as - use this
|
||||||
|
* to identify which key was picked instead of reading a selected item's
|
||||||
|
* own (possibly shifted) label.text.
|
||||||
|
*
|
||||||
|
* @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
|
||||||
|
* @returns The slot's canonical label.
|
||||||
|
*/
|
||||||
|
const char_t * uiKeyboardGetCanonicalKey(const uint8_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if a grid slot's character actually differs between the
|
||||||
|
* unshifted and shifted tables - i.e. a letter, digit, '-', or '='. False
|
||||||
|
* for a slot whose UI_KEYBOARD_KEYS_* entry is the same in every table
|
||||||
|
* (SPACE, DEL, SHIFT, CAPS, or blank filler).
|
||||||
|
*
|
||||||
|
* @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
|
||||||
|
* @returns True if shift affects this slot's character.
|
||||||
|
*/
|
||||||
|
bool_t uiKeyboardIsShiftSensitive(const uint8_t index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the character a grid slot represents under the given shift/
|
||||||
|
* caps state, straight from whichever UI_KEYBOARD_KEYS_* table they
|
||||||
|
* select - shift takes priority over caps when both are set (so, unlike
|
||||||
|
* a physical keyboard, shift while caps is on still gives uppercase, not
|
||||||
|
* lowercase - there is no fourth table for that combination). For a slot
|
||||||
|
* whose label isn't a single character (a blank filler cell, or a
|
||||||
|
* multi-character special key like "SPACE"), returns that label's first
|
||||||
|
* byte ('\0' for blank).
|
||||||
|
*
|
||||||
|
* @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1.
|
||||||
|
* @param shift Current shift state (one-shot, see UI_KEYBOARD.shift).
|
||||||
|
* @param caps Current caps state (persistent toggle, see UI_KEYBOARD.caps).
|
||||||
|
* @returns The character to display/type.
|
||||||
|
*/
|
||||||
|
char_t uiKeyboardGetChar(
|
||||||
|
const uint8_t index,
|
||||||
|
const bool_t shift,
|
||||||
|
const bool_t caps
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
uisaveslot.c
|
||||||
|
uiselectsave.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uisaveslot.h"
|
||||||
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
|
void uiSaveSlotInit(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uint8_t slotIndex,
|
||||||
|
const char_t *fileName
|
||||||
|
) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
assertNotNull(fileName, "File name cannot be NULL");
|
||||||
|
|
||||||
|
memoryZero(saveSlot, sizeof(uisaveslot_t));
|
||||||
|
saveSlot->slotIndex = slotIndex;
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&saveSlot->numberLabel, saveSlot->numberText,
|
||||||
|
saveSlot->numberSprites, UI_SAVE_SLOT_NUMBER_SPRITES_MAX
|
||||||
|
);
|
||||||
|
errorret_t result = assetLocaleGetStringWithVA(
|
||||||
|
&LOCALE.entry->data.locale, "ui.save_slot.number_format", 0,
|
||||||
|
saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX,
|
||||||
|
slotIndex + 1
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
saveSlot->numberLabel.dirty = true;
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&saveSlot->nameLabel, saveSlot->nameText,
|
||||||
|
saveSlot->nameSprites, UI_SAVE_SLOT_NAME_SPRITES_MAX
|
||||||
|
);
|
||||||
|
stringCopy(saveSlot->nameText, fileName, UI_SAVE_SLOT_NAME_TEXT_MAX);
|
||||||
|
saveSlot->nameLabel.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
return saveSlot->highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
saveSlot->highlighted = highlighted;
|
||||||
|
}
|
||||||
|
|
||||||
|
uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
return saveSlot->displayType;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSaveSlotSetDisplayType(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uisaveslotdisplaytype_t displayType
|
||||||
|
) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
saveSlot->displayType = displayType;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSaveSlotDraw(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
) {
|
||||||
|
assertNotNull(saveSlot, "Save slot cannot be NULL");
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
const float_t contentX = x + UI_FRAME_START_X;
|
||||||
|
const float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
|
||||||
|
color_t color;
|
||||||
|
if(saveSlot->highlighted) {
|
||||||
|
color = COLOR_RED;
|
||||||
|
} else if(saveSlot->displayType == UI_SAVE_SLOT_DISPLAY_TYPE_DELETE) {
|
||||||
|
color = COLOR_ORANGE;
|
||||||
|
} else {
|
||||||
|
color = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiLabelSetX(&saveSlot->numberLabel, contentX);
|
||||||
|
uiLabelSetY(&saveSlot->numberLabel, contentY);
|
||||||
|
errorChain(uiLabelRender(&saveSlot->numberLabel, color));
|
||||||
|
|
||||||
|
const float_t nameY = contentY + (float_t)saveSlot->numberLabel.height +
|
||||||
|
UI_FRAME_PADDING_Y;
|
||||||
|
uiLabelSetX(&saveSlot->nameLabel, contentX);
|
||||||
|
uiLabelSetY(&saveSlot->nameLabel, nameY);
|
||||||
|
errorChain(uiLabelRender(&saveSlot->nameLabel, color));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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 "ui/widget/uilabel.h"
|
||||||
|
|
||||||
|
#define UI_SAVE_SLOT_NUMBER_TEXT_MAX 16
|
||||||
|
#define UI_SAVE_SLOT_NUMBER_SPRITES_MAX UI_SAVE_SLOT_NUMBER_TEXT_MAX
|
||||||
|
#define UI_SAVE_SLOT_NAME_TEXT_MAX 64
|
||||||
|
#define UI_SAVE_SLOT_NAME_SPRITES_MAX UI_SAVE_SLOT_NAME_TEXT_MAX
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a save slot entry should be styled: NORMAL for everyday
|
||||||
|
* save/load browsing, DELETE to visually flag the row as part of a
|
||||||
|
* destructive delete flow.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL,
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE
|
||||||
|
} uisaveslotdisplaytype_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single framed save slot entry: a slot number and a file name. Later
|
||||||
|
* this will grow to show more per-slot save data (level, playtime,
|
||||||
|
* etc).
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t slotIndex;
|
||||||
|
uisaveslotdisplaytype_t displayType;
|
||||||
|
|
||||||
|
uilabel_t numberLabel;
|
||||||
|
char_t numberText[UI_SAVE_SLOT_NUMBER_TEXT_MAX];
|
||||||
|
spritebatchsprite_t numberSprites[UI_SAVE_SLOT_NUMBER_SPRITES_MAX];
|
||||||
|
|
||||||
|
uilabel_t nameLabel;
|
||||||
|
char_t nameText[UI_SAVE_SLOT_NAME_TEXT_MAX];
|
||||||
|
spritebatchsprite_t nameSprites[UI_SAVE_SLOT_NAME_SPRITES_MAX];
|
||||||
|
|
||||||
|
bool_t highlighted;
|
||||||
|
} uisaveslot_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a save slot widget with the given slot index and display
|
||||||
|
* file name.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to initialize.
|
||||||
|
* @param slotIndex The save slot index this widget represents.
|
||||||
|
* @param fileName Display file name; copied internally, safe to be
|
||||||
|
* transient.
|
||||||
|
*/
|
||||||
|
void uiSaveSlotInit(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uint8_t slotIndex,
|
||||||
|
const char_t *fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the save slot widget is highlighted.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to query.
|
||||||
|
* @returns True if highlighted.
|
||||||
|
*/
|
||||||
|
bool_t uiSaveSlotIsHighlighted(const uisaveslot_t *saveSlot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the highlighted state of the save slot widget.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to update.
|
||||||
|
* @param highlighted The new highlighted state.
|
||||||
|
*/
|
||||||
|
void uiSaveSlotSetHighlighted(uisaveslot_t *saveSlot, const bool_t highlighted);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the save slot widget's display type.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to query.
|
||||||
|
* @returns The display type.
|
||||||
|
*/
|
||||||
|
uisaveslotdisplaytype_t uiSaveSlotGetDisplayType(const uisaveslot_t *saveSlot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the save slot widget's display type.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to update.
|
||||||
|
* @param displayType The new display type.
|
||||||
|
*/
|
||||||
|
void uiSaveSlotSetDisplayType(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const uisaveslotdisplaytype_t displayType
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the save slot widget as a framed box at the given position and
|
||||||
|
* size, showing its slot number and file name.
|
||||||
|
*
|
||||||
|
* @param saveSlot The save slot widget to draw.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Frame width.
|
||||||
|
* @param height Frame height.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSaveSlotDraw(
|
||||||
|
uisaveslot_t *saveSlot,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiselectsave.h"
|
||||||
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "ui/dialog/uiconfirm.h"
|
||||||
|
#include "ui/dialog/keyboard/uikeyboard.h"
|
||||||
|
#include "ui/overlay/uifatalerror.h"
|
||||||
|
#include "save/save.h"
|
||||||
|
#include "time/timeepoch.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
|
uiselectsave_t UI_SELECT_SAVE;
|
||||||
|
|
||||||
|
void uiSelectSaveFormatSlot(
|
||||||
|
const uint8_t index,
|
||||||
|
char_t *buffer,
|
||||||
|
const size_t bufferSize
|
||||||
|
) {
|
||||||
|
saveslotcache_t *cache = &SAVE.caches[index];
|
||||||
|
|
||||||
|
if(!saveSlotInUse(cache)) {
|
||||||
|
errorret_t result = assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.empty", 0,
|
||||||
|
buffer, bufferSize
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char_t dateText[32];
|
||||||
|
timeEpochFormat(cache->time, "%Y-%m-%d %H:%M", dateText, sizeof(dateText));
|
||||||
|
|
||||||
|
errorret_t result = assetLocaleGetStringWithVA(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.slot_format", 0,
|
||||||
|
buffer, bufferSize,
|
||||||
|
cache->name, cache->playerLevel, dateText
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveRefreshSlot(const uint8_t index) {
|
||||||
|
char_t fileName[UI_SAVE_SLOT_NAME_TEXT_MAX];
|
||||||
|
uiSelectSaveFormatSlot(index, fileName, sizeof(fileName));
|
||||||
|
uiSaveSlotInit(&UI_SELECT_SAVE.slots[index], index, fileName);
|
||||||
|
|
||||||
|
uisaveslotdisplaytype_t displayType =
|
||||||
|
UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE ?
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
|
||||||
|
uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[index], displayType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) {
|
||||||
|
if(!result) return;
|
||||||
|
|
||||||
|
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||||
|
const uint8_t index = UI_SELECT_SAVE.pendingDeleteIndex;
|
||||||
|
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
errorret_t writeResult = saveDeviceSlotWrite(
|
||||||
|
&SAVE.devices[SAVE.deviceCurrent], &slot, index
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(writeResult)) {
|
||||||
|
errorCatch(errorPrint(writeResult));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVE.caches[index] = slot.cachedData;
|
||||||
|
|
||||||
|
uiSelectSaveRefreshSlot(index);
|
||||||
|
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[index], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveConfirmDelete(const uint8_t index) {
|
||||||
|
UI_SELECT_SAVE.pendingDeleteIndex = index;
|
||||||
|
uiConfirmOpen(
|
||||||
|
"ui.select_save.delete_confirm", uiSelectSaveDeleteConfirmed, NULL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveNameEntered(
|
||||||
|
const char_t *text,
|
||||||
|
const bool_t confirmed,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
if(!confirmed) return;
|
||||||
|
|
||||||
|
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||||
|
const uint8_t index = UI_SELECT_SAVE.pendingNameIndex;
|
||||||
|
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
stringCopy(slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
|
||||||
|
|
||||||
|
errorret_t writeResult = saveDeviceSlotWrite(
|
||||||
|
&SAVE.devices[SAVE.deviceCurrent], &slot, index
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(writeResult)) {
|
||||||
|
errorCatch(errorPrint(writeResult));
|
||||||
|
uiFatalErrorOpen(writeResult.state->message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SAVE.caches[index] = slot.cachedData;
|
||||||
|
uiSelectSaveRefreshSlot(index);
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.result = index;
|
||||||
|
uiSelectSaveClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveNameEmptySlot(const uint8_t index) {
|
||||||
|
UI_SELECT_SAVE.pendingNameIndex = index;
|
||||||
|
|
||||||
|
char_t title[UI_KEYBOARD_TITLE_TEXT_MAX];
|
||||||
|
errorret_t result = assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.name_title", 0,
|
||||||
|
title, sizeof(title)
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
|
||||||
|
uikeyboardopen_t open = {
|
||||||
|
.title = title,
|
||||||
|
.onInput = uiSelectSaveNameEntered,
|
||||||
|
.cancel = true,
|
||||||
|
.maxLength = SAVE_SLOT_NAME_LENGTH,
|
||||||
|
.trimmed = true,
|
||||||
|
.allowBlank = false
|
||||||
|
};
|
||||||
|
uiKeyboardOpen(&open);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSetActionLabel(const uiselectsavetype_t type) {
|
||||||
|
const char_t *labelKey = type == UI_SELECT_SAVE_TYPE_LOAD ?
|
||||||
|
"ui.select_save.delete_mode" : "ui.confirm.cancel";
|
||||||
|
|
||||||
|
errorret_t result = assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, labelKey, 0,
|
||||||
|
UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) errorCatch(result);
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button.label.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSwitchType(const uiselectsavetype_t type) {
|
||||||
|
UI_SELECT_SAVE.type = type;
|
||||||
|
|
||||||
|
uisaveslotdisplaytype_t displayType = type == UI_SELECT_SAVE_TYPE_DELETE ?
|
||||||
|
UI_SAVE_SLOT_DISPLAY_TYPE_DELETE : UI_SAVE_SLOT_DISPLAY_TYPE_NORMAL;
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSaveSlotSetDisplayType(&UI_SELECT_SAVE.slots[i], displayType);
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSelectSaveSetActionLabel(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSlotSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
if(UI_SELECT_SAVE.hasAction && index == UI_SELECT_SAVE_ACTION_INDEX) {
|
||||||
|
uiSelectSaveSwitchType(
|
||||||
|
UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_LOAD ?
|
||||||
|
UI_SELECT_SAVE_TYPE_DELETE : UI_SELECT_SAVE_TYPE_LOAD
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_SELECT_SAVE.type == UI_SELECT_SAVE_TYPE_DELETE) {
|
||||||
|
uiSelectSaveConfirmDelete(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!saveSlotInUse(&SAVE.caches[index])) {
|
||||||
|
uiSelectSaveNameEmptySlot(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.result = index;
|
||||||
|
uiSelectSaveClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSlotChanged(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], i == index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The action row sits fixed below the scrollable list, so it never
|
||||||
|
// needs to be scrolled into view.
|
||||||
|
if(index >= SAVE_SLOT_COUNT) return;
|
||||||
|
|
||||||
|
uiScrollingEnsureVisible(
|
||||||
|
&UI_SELECT_SAVE.scroll, index, SAVE_SLOT_COUNT, UI_SELECT_SAVE_VISIBLE_ROWS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveSlotClosed(const uimenu_t *menu) {
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSaveSlotSetHighlighted(&UI_SELECT_SAVE.slots[i], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_SELECT_SAVE.callback != NULL) {
|
||||||
|
UI_SELECT_SAVE.callback(UI_SELECT_SAVE.result, UI_SELECT_SAVE.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSelectSaveInit(void) {
|
||||||
|
memoryZero(&UI_SELECT_SAVE, sizeof(uiselectsave_t));
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&UI_SELECT_SAVE.titleLabel, UI_SELECT_SAVE.titleText,
|
||||||
|
UI_SELECT_SAVE.titleSprites, UI_SELECT_SAVE_TITLE_SPRITES_MAX
|
||||||
|
);
|
||||||
|
errorChain(assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, "ui.select_save.title", 0,
|
||||||
|
UI_SELECT_SAVE.titleText, UI_SELECT_SAVE_TITLE_TEXT_MAX
|
||||||
|
));
|
||||||
|
UI_SELECT_SAVE.titleLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_SELECT_SAVE.titleLabel);
|
||||||
|
|
||||||
|
uiMenuInit(
|
||||||
|
&UI_SELECT_SAVE.slotMenu, uiSelectSaveSlotSelected, uiSelectSaveSlotClosed,
|
||||||
|
uiSelectSaveSlotChanged
|
||||||
|
);
|
||||||
|
uiMenuSetItems(
|
||||||
|
&UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, SAVE_SLOT_COUNT, 1
|
||||||
|
);
|
||||||
|
UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].type =
|
||||||
|
UI_MENU_WIDGET_TYPE_BUTTON;
|
||||||
|
uiButtonInit(
|
||||||
|
&UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
|
||||||
|
UI_SELECT_SAVE.actionText
|
||||||
|
);
|
||||||
|
|
||||||
|
uiScrollingInit(&UI_SELECT_SAVE.scroll);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSelectSaveDraw(void) {
|
||||||
|
if(!uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) errorOk();
|
||||||
|
|
||||||
|
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
const float_t rowFrameHeight =
|
||||||
|
(UI_FRAME_START_Y * 2) + (rowHeight * 2) + UI_FRAME_PADDING_Y;
|
||||||
|
const float_t listHeight =
|
||||||
|
(rowFrameHeight * (float_t)UI_SELECT_SAVE_VISIBLE_ROWS) +
|
||||||
|
(UI_SELECT_SAVE_ROW_GAP * (float_t)(UI_SELECT_SAVE_VISIBLE_ROWS - 1));
|
||||||
|
|
||||||
|
const float_t width = UI_SELECT_SAVE_WIDTH;
|
||||||
|
const float_t height = (UI_FRAME_START_Y * 2)
|
||||||
|
+ (float_t)UI_SELECT_SAVE.titleLabel.height + UI_FRAME_PADDING_Y
|
||||||
|
+ listHeight
|
||||||
|
+ (UI_SELECT_SAVE.hasAction ? UI_FRAME_PADDING_Y + rowHeight : 0.0f);
|
||||||
|
|
||||||
|
const float_t x = (float_t)SCREEN.scanX +
|
||||||
|
((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||||
|
const float_t y = (float_t)SCREEN.scanY +
|
||||||
|
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
const float_t contentX = x + UI_FRAME_START_X;
|
||||||
|
const float_t contentY = y + UI_FRAME_START_Y;
|
||||||
|
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||||
|
|
||||||
|
uiLabelSetX(&UI_SELECT_SAVE.titleLabel, contentX);
|
||||||
|
uiLabelSetY(&UI_SELECT_SAVE.titleLabel, contentY);
|
||||||
|
errorChain(uiLabelRender(&UI_SELECT_SAVE.titleLabel, COLOR_WHITE));
|
||||||
|
|
||||||
|
const float_t listY = contentY + (float_t)UI_SELECT_SAVE.titleLabel.height +
|
||||||
|
UI_FRAME_PADDING_Y;
|
||||||
|
|
||||||
|
const uint8_t offset = UI_SELECT_SAVE.scroll.offset;
|
||||||
|
uint8_t visibleEnd = (uint8_t)(offset + UI_SELECT_SAVE_VISIBLE_ROWS);
|
||||||
|
if(visibleEnd > SAVE_SLOT_COUNT) visibleEnd = SAVE_SLOT_COUNT;
|
||||||
|
|
||||||
|
for(uint8_t i = offset; i < visibleEnd; i++) {
|
||||||
|
const float_t iy =
|
||||||
|
listY + (float_t)(i - offset) * (rowFrameHeight + UI_SELECT_SAVE_ROW_GAP);
|
||||||
|
errorChain(uiSaveSlotDraw(
|
||||||
|
&UI_SELECT_SAVE.slots[i], contentX, iy, contentWidth, rowFrameHeight
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_SELECT_SAVE.hasAction) {
|
||||||
|
const float_t actionY = listY + listHeight + UI_FRAME_PADDING_Y;
|
||||||
|
errorChain(uiButtonDraw(
|
||||||
|
&UI_SELECT_SAVE.slotItems[UI_SELECT_SAVE_ACTION_INDEX].button,
|
||||||
|
contentX, actionY
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiSelectSaveIsOpen(void) {
|
||||||
|
return uiMenuIsActive(&UI_SELECT_SAVE.slotMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveOpen(
|
||||||
|
const uiselectsavetype_t type,
|
||||||
|
uiselectsaveresultcallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(callback, "Callback cannot be NULL");
|
||||||
|
if(uiMenuIsActive(&UI_SELECT_SAVE.slotMenu)) return;
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.type = type;
|
||||||
|
UI_SELECT_SAVE.callback = callback;
|
||||||
|
UI_SELECT_SAVE.user = user;
|
||||||
|
UI_SELECT_SAVE.result = UI_SELECT_SAVE_RESULT_NONE;
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
uiSelectSaveRefreshSlot(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_SELECT_SAVE.hasAction =
|
||||||
|
type == UI_SELECT_SAVE_TYPE_LOAD || type == UI_SELECT_SAVE_TYPE_DELETE;
|
||||||
|
uint8_t itemCount = SAVE_SLOT_COUNT;
|
||||||
|
if(UI_SELECT_SAVE.hasAction) {
|
||||||
|
uiSelectSaveSetActionLabel(type);
|
||||||
|
itemCount = UI_SELECT_SAVE_ITEM_CAPACITY;
|
||||||
|
}
|
||||||
|
uiMenuSetItems(&UI_SELECT_SAVE.slotMenu, UI_SELECT_SAVE.slotItems, itemCount, 1);
|
||||||
|
|
||||||
|
uiScrollingInit(&UI_SELECT_SAVE.scroll);
|
||||||
|
uiMenuOpen(&UI_SELECT_SAVE.slotMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSelectSaveClose(void) {
|
||||||
|
uiMenuClose(&UI_SELECT_SAVE.slotMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiSelectSaveDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* 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 "ui/widget/uilabel.h"
|
||||||
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "ui/widget/uiscrolling.h"
|
||||||
|
#include "ui/dialog/save/uisaveslot.h"
|
||||||
|
#include "save/saveslot.h"
|
||||||
|
|
||||||
|
#define UI_SELECT_SAVE_TITLE_TEXT_MAX 64
|
||||||
|
#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
|
||||||
|
#define UI_SELECT_SAVE_ACTION_LABEL_MAX 32
|
||||||
|
#define UI_SELECT_SAVE_VISIBLE_ROWS (SAVE_SLOT_COUNT < 3 ? SAVE_SLOT_COUNT : 3)
|
||||||
|
#define UI_SELECT_SAVE_WIDTH 260.0f
|
||||||
|
#define UI_SELECT_SAVE_ROW_GAP 4.0f
|
||||||
|
#define UI_SELECT_SAVE_RESULT_NONE 0xFF
|
||||||
|
|
||||||
|
// Slot rows plus one trailing action row (switch-to-delete/cancel), only
|
||||||
|
// present for the LOAD and DELETE types - see uiSelectSaveOpen.
|
||||||
|
#define UI_SELECT_SAVE_ITEM_CAPACITY (SAVE_SLOT_COUNT + 1)
|
||||||
|
#define UI_SELECT_SAVE_ACTION_INDEX SAVE_SLOT_COUNT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action the select-save panel is being used for. The caller
|
||||||
|
* chooses this when calling uiSelectSaveOpen.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
UI_SELECT_SAVE_TYPE_SAVE,
|
||||||
|
UI_SELECT_SAVE_TYPE_LOAD,
|
||||||
|
UI_SELECT_SAVE_TYPE_DELETE
|
||||||
|
} uiselectsavetype_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked once the select-save panel closes: slotIndex is the
|
||||||
|
* chosen slot, or UI_SELECT_SAVE_RESULT_NONE if backed out of without
|
||||||
|
* choosing.
|
||||||
|
*
|
||||||
|
* @param slotIndex The chosen slot, or UI_SELECT_SAVE_RESULT_NONE.
|
||||||
|
* @param user Arbitrary pointer passed to uiSelectSaveOpen.
|
||||||
|
*/
|
||||||
|
typedef void (*uiselectsaveresultcallback_t)(
|
||||||
|
const uint8_t slotIndex, void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uilabel_t titleLabel;
|
||||||
|
char_t titleText[UI_SELECT_SAVE_TITLE_TEXT_MAX];
|
||||||
|
spritebatchsprite_t titleSprites[UI_SELECT_SAVE_TITLE_SPRITES_MAX];
|
||||||
|
|
||||||
|
uiselectsavetype_t type;
|
||||||
|
bool_t hasAction;
|
||||||
|
char_t actionText[UI_SELECT_SAVE_ACTION_LABEL_MAX];
|
||||||
|
|
||||||
|
uimenu_t slotMenu;
|
||||||
|
uimenuitem_t slotItems[UI_SELECT_SAVE_ITEM_CAPACITY];
|
||||||
|
uisaveslot_t slots[SAVE_SLOT_COUNT];
|
||||||
|
uiscrolling_t scroll;
|
||||||
|
uint8_t pendingDeleteIndex;
|
||||||
|
uint8_t pendingNameIndex;
|
||||||
|
|
||||||
|
uiselectsaveresultcallback_t callback;
|
||||||
|
void *user;
|
||||||
|
uint8_t result;
|
||||||
|
} uiselectsave_t;
|
||||||
|
|
||||||
|
extern uiselectsave_t UI_SELECT_SAVE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the select-save panel.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSelectSaveInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the select-save panel: a centered frame with a title, a
|
||||||
|
* scrolling list of save/save.h's slots (each shown as a framed
|
||||||
|
* uisaveslot_t, showing at most UI_SELECT_SAVE_VISIBLE_ROWS at a time),
|
||||||
|
* and - when UI_SELECT_SAVE.hasAction is set - the switch-to-delete/
|
||||||
|
* cancel action row fixed below the scrollable list, outside it. No-op
|
||||||
|
* when not open.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSelectSaveDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the select-save panel is currently open.
|
||||||
|
*
|
||||||
|
* @returns True if open.
|
||||||
|
*/
|
||||||
|
bool_t uiSelectSaveIsOpen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the select-save panel for the given action, refreshing each row
|
||||||
|
* from the current SAVE.caches state. callback is invoked exactly once
|
||||||
|
* with the result, whether a slot was chosen or the panel was backed
|
||||||
|
* out of without choosing.
|
||||||
|
*
|
||||||
|
* UI_SELECT_SAVE_TYPE_LOAD adds a trailing row to switch to
|
||||||
|
* UI_SELECT_SAVE_TYPE_DELETE in place, and UI_SELECT_SAVE_TYPE_DELETE
|
||||||
|
* adds a trailing "Cancel" row to switch back to
|
||||||
|
* UI_SELECT_SAVE_TYPE_LOAD - neither closes the panel or invokes
|
||||||
|
* callback, they just toggle UI_SELECT_SAVE.type and the slot rows'
|
||||||
|
* styling. UI_SELECT_SAVE_TYPE_SAVE has no trailing row.
|
||||||
|
*
|
||||||
|
* @param type The action the caller is opening the panel for.
|
||||||
|
* @param callback Called with the result once the panel closes. Cannot
|
||||||
|
* be NULL.
|
||||||
|
* @param user Arbitrary pointer passed through to callback.
|
||||||
|
*/
|
||||||
|
void uiSelectSaveOpen(
|
||||||
|
const uiselectsavetype_t type,
|
||||||
|
uiselectsaveresultcallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the select-save panel. No-op when already closed.
|
||||||
|
*/
|
||||||
|
void uiSelectSaveClose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the select-save panel.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiSelectSaveDispose(void);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiconfirm.h"
|
||||||
|
#include "ui/widget/uimodal.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
#define UI_CONFIRM_INDEX_CONFIRM 0
|
||||||
|
#define UI_CONFIRM_INDEX_CANCEL 1
|
||||||
|
|
||||||
|
uiconfirm_t UI_CONFIRM;
|
||||||
|
|
||||||
|
void uiConfirmModalCallback(const uint8_t optionIndex, void *user) {
|
||||||
|
UI_CONFIRM.result = optionIndex == UI_CONFIRM_INDEX_CONFIRM;
|
||||||
|
if(UI_CONFIRM.callback != NULL) {
|
||||||
|
UI_CONFIRM.callback(UI_CONFIRM.result, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiConfirmInit(void) {
|
||||||
|
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiConfirmIsOpen(void) {
|
||||||
|
return uiModalIsOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiConfirmGetResult(void) {
|
||||||
|
return UI_CONFIRM.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiConfirmOpen(
|
||||||
|
const char_t *question,
|
||||||
|
uiconfirmcallback_t callback,
|
||||||
|
void *user
|
||||||
|
) {
|
||||||
|
assertNotNull(question, "Question cannot be NULL");
|
||||||
|
|
||||||
|
UI_CONFIRM.callback = callback;
|
||||||
|
UI_CONFIRM.result = false;
|
||||||
|
|
||||||
|
const char_t *options[] = {
|
||||||
|
"ui.confirm.confirm",
|
||||||
|
"ui.confirm.cancel"
|
||||||
|
};
|
||||||
|
uiModalOpen(
|
||||||
|
NULL, question, options, sizeof(options) / sizeof(options[0]),
|
||||||
|
uiConfirmModalCallback, NULL, user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiConfirmClose(void) {
|
||||||
|
uiModalClose(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiConfirmDispose(void) {
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -7,33 +7,21 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
|
||||||
|
|
||||||
#define UI_CONFIRM_TEXT_MAX 256
|
|
||||||
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
|
||||||
#define UI_CONFIRM_INDEX_CONFIRM 0
|
|
||||||
#define UI_CONFIRM_INDEX_CANCEL 1
|
|
||||||
#define UI_CONFIRM_ITEM_COUNT 2
|
|
||||||
#define UI_CONFIRM_LABEL_MAX 32
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback invoked once a confirm dialog is dismissed.
|
* Callback invoked once a confirm dialog is dismissed. Back/cancel
|
||||||
|
* input is disabled while it's open, so the dialog can only be
|
||||||
|
* dismissed by picking Confirm or Cancel.
|
||||||
*
|
*
|
||||||
* @param result True if Confirm was selected, false if Cancel was
|
* @param result True if Confirm was selected, false if Cancel was
|
||||||
* selected or the dialog was backed out of.
|
* selected.
|
||||||
* @param user Arbitrary pointer passed to uiConfirmOpen.
|
* @param user Arbitrary pointer passed to uiConfirmOpen.
|
||||||
*/
|
*/
|
||||||
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
|
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t text[UI_CONFIRM_TEXT_MAX];
|
|
||||||
uimenu_t menu;
|
|
||||||
uimenuitem_t items[UI_CONFIRM_ITEM_COUNT];
|
|
||||||
uiconfirmcallback_t callback;
|
uiconfirmcallback_t callback;
|
||||||
void *user;
|
|
||||||
bool_t result;
|
bool_t result;
|
||||||
char_t confirmLabel[UI_CONFIRM_LABEL_MAX];
|
|
||||||
char_t cancelLabel[UI_CONFIRM_LABEL_MAX];
|
|
||||||
} uiconfirm_t;
|
} uiconfirm_t;
|
||||||
|
|
||||||
extern uiconfirm_t UI_CONFIRM;
|
extern uiconfirm_t UI_CONFIRM;
|
||||||
@@ -45,15 +33,6 @@ extern uiconfirm_t UI_CONFIRM;
|
|||||||
*/
|
*/
|
||||||
errorret_t uiConfirmInit(void);
|
errorret_t uiConfirmInit(void);
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the confirm dialog: a semi-transparent black backdrop covering
|
|
||||||
* the whole screen, then its own centered frame with the question text
|
|
||||||
* and Confirm/Cancel buttons. No-op when not open.
|
|
||||||
*
|
|
||||||
* @return Any error that occurs.
|
|
||||||
*/
|
|
||||||
errorret_t uiConfirmDraw(void);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when the confirm dialog is currently open.
|
* Returns true when the confirm dialog is currently open.
|
||||||
*
|
*
|
||||||
@@ -69,11 +48,12 @@ bool_t uiConfirmIsOpen(void);
|
|||||||
bool_t uiConfirmGetResult(void);
|
bool_t uiConfirmGetResult(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the confirm dialog with the given question text. callback is
|
* Opens a confirm dialog, backed by the shared uimodal_t, with the given
|
||||||
* invoked exactly once with the result, whether the dialog was
|
* question text and Confirm/Cancel buttons. callback is invoked exactly
|
||||||
* dismissed by selecting a button or by pressing cancel/back.
|
* once with the result once a button is selected.
|
||||||
*
|
*
|
||||||
* @param question Display text; copied internally, safe to be transient.
|
* @param question Display text, or a locale message ID; copied
|
||||||
|
* internally by uimodal, safe to be transient.
|
||||||
* @param callback Called with the result once the dialog closes. May be
|
* @param callback Called with the result once the dialog closes. May be
|
||||||
* NULL.
|
* NULL.
|
||||||
* @param user Arbitrary pointer passed through to callback.
|
* @param user Arbitrary pointer passed through to callback.
|
||||||
@@ -59,9 +59,14 @@ uifocusitem_t * uiFocusPush(
|
|||||||
void uiFocusPop(void) {
|
void uiFocusPop(void) {
|
||||||
assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
|
assertTrue(UI_FOCUS.count > 0, "UI focus stack underflow");
|
||||||
|
|
||||||
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1];
|
// Count is decremented before the closed callback fires so that a
|
||||||
if(item->closed != NULL) item->closed(item);
|
// push triggered from within it (e.g. reopening a menu once another
|
||||||
|
// one reports it closed with no result) lands in the now-free slot
|
||||||
|
// instead of one past it, which would silently strand the new item
|
||||||
|
// above the tracked stack top.
|
||||||
UI_FOCUS.count--;
|
UI_FOCUS.count--;
|
||||||
|
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count];
|
||||||
|
if(item->closed != NULL) item->closed(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiFocusPopItem(uifocusitem_t *item) {
|
void uiFocusPopItem(uifocusitem_t *item) {
|
||||||
@@ -113,9 +118,17 @@ void uiFocusMoveDirection(
|
|||||||
) {
|
) {
|
||||||
if(m->direction != dir) continue;
|
if(m->direction != dir) continue;
|
||||||
|
|
||||||
uint8_t x = (uint8_t)(item->x + m->dx);
|
// Widened to int16_t so a move off the left/top edge (e.g. x=0,
|
||||||
uint8_t y = (uint8_t)(item->y + m->dy);
|
// dx=-1) stays negative here instead of wrapping straight to 255 via
|
||||||
uiFocusSetPosition(item, x, y);
|
// a uint8_t truncation - that would then land on `255 % cols`
|
||||||
|
// (column 3 of a 14-wide grid, not the intended last column) rather
|
||||||
|
// than wrapping to the row/column's actual last cell.
|
||||||
|
int16_t x = (int16_t)item->x + m->dx;
|
||||||
|
int16_t y = (int16_t)item->y + m->dy;
|
||||||
|
if(x < 0) x += item->cols;
|
||||||
|
if(y < 0) y += item->rows;
|
||||||
|
|
||||||
|
uiFocusSetPosition(item, (uint8_t)x, (uint8_t)y);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +161,8 @@ void uiFocusUpdate(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(inputPressed(INPUT_ACTION_CANCEL)) {
|
if(inputPressed(INPUT_ACTION_CANCEL)) {
|
||||||
uiFocusPop();
|
if(item->cancel != NULL && item->cancel(item)) return;
|
||||||
|
if(!item->disableBack) uiFocusPop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,5 +53,19 @@ struct uifocusitem_s {
|
|||||||
uifocusitemcallback_t changed;
|
uifocusitemcallback_t changed;
|
||||||
uifocusitemcallback_t closed;
|
uifocusitemcallback_t closed;
|
||||||
uifocusitemdirectioncallback_t direction;
|
uifocusitemdirectioncallback_t direction;
|
||||||
|
|
||||||
|
// Called when INPUT_ACTION_CANCEL is pressed, before disableBack/the
|
||||||
|
// default pop is applied; may be NULL. Returns true if fully handled
|
||||||
|
// (e.g. it did something else instead, or popped itself), which skips
|
||||||
|
// the default pop; false to fall through to the default disableBack/
|
||||||
|
// pop behavior below.
|
||||||
|
uifocusitemcallback_t cancel;
|
||||||
|
|
||||||
void *user;
|
void *user;
|
||||||
|
|
||||||
|
// When true, INPUT_ACTION_CANCEL is ignored while this item is the
|
||||||
|
// topmost focus item - it does not pop. Does not affect a
|
||||||
|
// programmatic uiFocusPop()/uiFocusPopItem() call. Only consulted
|
||||||
|
// when cancel is unset or returns false.
|
||||||
|
bool_t disableBack;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "uiconfirm.h"
|
|
||||||
#include "ui/widget/uiframe.h"
|
|
||||||
#include "assert/assert.h"
|
|
||||||
#include "util/memory.h"
|
|
||||||
#include "util/string.h"
|
|
||||||
#include "util/math.h"
|
|
||||||
#include "display/screen/screen.h"
|
|
||||||
#include "display/text/text.h"
|
|
||||||
#include "display/color.h"
|
|
||||||
#include "display/spritebatch/spritebatch.h"
|
|
||||||
#include "display/texture/texture.h"
|
|
||||||
#include "display/shader/shaderunlit.h"
|
|
||||||
#include "locale/localemanager.h"
|
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
|
||||||
|
|
||||||
#define UI_CONFIRM_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
|
||||||
|
|
||||||
uiconfirm_t UI_CONFIRM;
|
|
||||||
|
|
||||||
void uiConfirmSelected(
|
|
||||||
const uimenu_t *menu,
|
|
||||||
const uint8_t index,
|
|
||||||
const uimenuitem_t *item
|
|
||||||
) {
|
|
||||||
UI_CONFIRM.result = index == UI_CONFIRM_INDEX_CONFIRM;
|
|
||||||
uiConfirmClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiConfirmClosed(const uimenu_t *menu) {
|
|
||||||
if(UI_CONFIRM.callback != NULL) {
|
|
||||||
UI_CONFIRM.callback(UI_CONFIRM.result, UI_CONFIRM.user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t uiConfirmInit(void) {
|
|
||||||
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
|
|
||||||
|
|
||||||
errorChain(assetLocaleGetString(
|
|
||||||
&LOCALE.entry->data.locale,
|
|
||||||
"ui.confirm.confirm",
|
|
||||||
0,
|
|
||||||
UI_CONFIRM.confirmLabel,
|
|
||||||
UI_CONFIRM_LABEL_MAX
|
|
||||||
));
|
|
||||||
errorChain(assetLocaleGetString(
|
|
||||||
&LOCALE.entry->data.locale,
|
|
||||||
"ui.confirm.cancel",
|
|
||||||
0,
|
|
||||||
UI_CONFIRM.cancelLabel,
|
|
||||||
UI_CONFIRM_LABEL_MAX
|
|
||||||
));
|
|
||||||
|
|
||||||
MENU_BEGIN(
|
|
||||||
&UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL
|
|
||||||
);
|
|
||||||
MENU_BUTTON(UI_CONFIRM.confirmLabel);
|
|
||||||
MENU_BUTTON(UI_CONFIRM.cancelLabel);
|
|
||||||
|
|
||||||
MENU_END(UI_CONFIRM.items, menuIndex);
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t uiConfirmDraw(void) {
|
|
||||||
if(!uiMenuIsActive(&UI_CONFIRM.menu)) errorOk();
|
|
||||||
|
|
||||||
spritebatchsprite_t backdropSprite = {
|
|
||||||
.min = { 0.0f, 0.0f, 0.0f },
|
|
||||||
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
shadermaterial_t backdropMaterial = {
|
|
||||||
.unlit = {
|
|
||||||
.color = UI_CONFIRM_BACKDROP_COLOR,
|
|
||||||
.texture = &TEXTURE_WHITE
|
|
||||||
}
|
|
||||||
};
|
|
||||||
errorChain(
|
|
||||||
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
|
||||||
);
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
|
|
||||||
int32_t textW, textH;
|
|
||||||
textMeasure(UI_CONFIRM.text, &FONT_DEFAULT, &textW, &textH);
|
|
||||||
|
|
||||||
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
|
||||||
float_t width = mathMax(
|
|
||||||
(float_t)textW + (UI_FRAME_START_X * 2), UI_CONFIRM_MIN_WIDTH
|
|
||||||
);
|
|
||||||
float_t height = (UI_FRAME_START_Y * 2) + rowHeight + UI_FRAME_PADDING_Y +
|
|
||||||
rowHeight;
|
|
||||||
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
|
||||||
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
|
||||||
|
|
||||||
float_t contentX = x + UI_FRAME_START_X;
|
|
||||||
float_t contentY = y + UI_FRAME_START_Y;
|
|
||||||
float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
|
||||||
|
|
||||||
errorChain(textDraw(contentX, contentY, UI_CONFIRM.text, COLOR_WHITE, &FONT_DEFAULT));
|
|
||||||
|
|
||||||
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
|
||||||
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
|
|
||||||
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t uiConfirmIsOpen(void) {
|
|
||||||
return uiMenuIsActive(&UI_CONFIRM.menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_t uiConfirmGetResult(void) {
|
|
||||||
return UI_CONFIRM.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiConfirmOpen(
|
|
||||||
const char_t *question,
|
|
||||||
uiconfirmcallback_t callback,
|
|
||||||
void *user
|
|
||||||
) {
|
|
||||||
assertNotNull(question, "Question cannot be NULL");
|
|
||||||
stringCopy(UI_CONFIRM.text, question, UI_CONFIRM_TEXT_MAX);
|
|
||||||
UI_CONFIRM.callback = callback;
|
|
||||||
UI_CONFIRM.user = user;
|
|
||||||
UI_CONFIRM.result = false;
|
|
||||||
uiMenuOpen(&UI_CONFIRM.menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
void uiConfirmClose(void) {
|
|
||||||
uiMenuClose(&UI_CONFIRM.menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t uiConfirmDispose(void) {
|
|
||||||
errorOk();
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "error/error.h"
|
|
||||||
#include "ui/widget/uilabel.h"
|
|
||||||
#include "ui/widget/uimenu.h"
|
|
||||||
|
|
||||||
#define UI_MODAL_TITLE_TEXT_MAX 64
|
|
||||||
#define UI_MODAL_TITLE_SPRITES_MAX UI_MODAL_TITLE_TEXT_MAX
|
|
||||||
#define UI_MODAL_MESSAGE_TEXT_MAX 256
|
|
||||||
#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX
|
|
||||||
#define UI_MODAL_OPTIONS_MAX 4
|
|
||||||
#define UI_MODAL_MIN_WIDTH 160.0f
|
|
||||||
#define UI_MODAL_RESULT_NONE 0xFF
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback invoked once a modal is dismissed.
|
|
||||||
*
|
|
||||||
* @param optionIndex Index into the options array passed to uiModalOpen
|
|
||||||
* that was selected, or UI_MODAL_RESULT_NONE if backed out of without
|
|
||||||
* selecting an option.
|
|
||||||
* @param user Arbitrary pointer passed to uiModalOpen.
|
|
||||||
*/
|
|
||||||
typedef void (*uimodalcallback_t)(const uint8_t optionIndex, void *user);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uilabel_t titleLabel;
|
|
||||||
char_t titleText[UI_MODAL_TITLE_TEXT_MAX];
|
|
||||||
spritebatchsprite_t titleSprites[UI_MODAL_TITLE_SPRITES_MAX];
|
|
||||||
|
|
||||||
uilabel_t messageLabel;
|
|
||||||
char_t messageText[UI_MODAL_MESSAGE_TEXT_MAX];
|
|
||||||
spritebatchsprite_t messageSprites[UI_MODAL_MESSAGE_SPRITES_MAX];
|
|
||||||
|
|
||||||
uimenu_t menu;
|
|
||||||
uimenuitem_t options[UI_MODAL_OPTIONS_MAX];
|
|
||||||
|
|
||||||
uimodalcallback_t callback;
|
|
||||||
void *user;
|
|
||||||
uint8_t result;
|
|
||||||
} uimodal_t;
|
|
||||||
|
|
||||||
extern uimodal_t UI_MODAL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the modal dialog.
|
|
||||||
*
|
|
||||||
* @return Any error that occurs.
|
|
||||||
*/
|
|
||||||
errorret_t uiModalInit(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the modal dialog: a semi-transparent black backdrop covering the
|
|
||||||
* whole screen, then its own centered frame with the title, message, and
|
|
||||||
* option buttons. No-op when not open.
|
|
||||||
*
|
|
||||||
* @return Any error that occurs.
|
|
||||||
*/
|
|
||||||
errorret_t uiModalDraw(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true when the modal dialog is currently open.
|
|
||||||
*
|
|
||||||
* @returns True if open.
|
|
||||||
*/
|
|
||||||
bool_t uiModalIsOpen(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the result of the most recently dismissed modal.
|
|
||||||
*
|
|
||||||
* @returns The selected option index, or UI_MODAL_RESULT_NONE.
|
|
||||||
*/
|
|
||||||
uint8_t uiModalGetResult(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the modal dialog with the given title, message, and options.
|
|
||||||
* callback is invoked exactly once with the result, whether the dialog
|
|
||||||
* was dismissed by selecting an option or by pressing cancel/back.
|
|
||||||
*
|
|
||||||
* @param title Display title; copied internally, safe to be transient.
|
|
||||||
* @param message Display message; copied internally, safe to be
|
|
||||||
* transient.
|
|
||||||
* @param options Array of option label strings; NOT copied internally,
|
|
||||||
* the pointers are stored directly by the underlying buttons, so they
|
|
||||||
* must remain valid for as long as the modal is open (e.g. string
|
|
||||||
* literals or locale-owned strings).
|
|
||||||
* @param optionCount Number of options, from 1 to UI_MODAL_OPTIONS_MAX.
|
|
||||||
* @param callback Called with the result once the dialog closes. May be
|
|
||||||
* NULL.
|
|
||||||
* @param user Arbitrary pointer passed through to callback.
|
|
||||||
*/
|
|
||||||
void uiModalOpen(
|
|
||||||
const char_t *title,
|
|
||||||
const char_t *message,
|
|
||||||
const char_t **options,
|
|
||||||
const uint8_t optionCount,
|
|
||||||
uimodalcallback_t callback,
|
|
||||||
void *user
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the modal dialog. No-op when already closed.
|
|
||||||
*/
|
|
||||||
void uiModalClose(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disposes of the modal dialog.
|
|
||||||
*
|
|
||||||
* @return Any error that occurs.
|
|
||||||
*/
|
|
||||||
errorret_t uiModalDispose(void);
|
|
||||||
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
uicrop.c
|
uicrop.c
|
||||||
uifullbox.c
|
uifullbox.c
|
||||||
uiloading.c
|
uiloading.c
|
||||||
|
uifatalerror.c
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uifatalerror.h"
|
||||||
|
#include "ui/widget/uiframe.h"
|
||||||
|
#include "ui/widget/uibutton.h"
|
||||||
|
#include "engine/engine.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "util/math.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
#include "display/text/font.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
|
||||||
|
uifatalerror_t UI_FATAL_ERROR;
|
||||||
|
|
||||||
|
void uiFatalErrorMenuSelected(
|
||||||
|
const uimenu_t *menu,
|
||||||
|
const uint8_t index,
|
||||||
|
const uimenuitem_t *item
|
||||||
|
) {
|
||||||
|
ENGINE.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiFatalErrorInit(void) {
|
||||||
|
memoryZero(&UI_FATAL_ERROR, sizeof(uifatalerror_t));
|
||||||
|
|
||||||
|
uiLabelInit(
|
||||||
|
&UI_FATAL_ERROR.messageLabel, UI_FATAL_ERROR.messageText,
|
||||||
|
UI_FATAL_ERROR.messageSprites, UI_FATAL_ERROR_MESSAGE_SPRITES_MAX
|
||||||
|
);
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiFatalErrorOpen(const char_t *message) {
|
||||||
|
assertNotNull(message, "Message cannot be NULL");
|
||||||
|
// This is the last line of defense - it must never itself fail, so a
|
||||||
|
// second fatal error while one is already showing is silently dropped
|
||||||
|
// rather than asserting.
|
||||||
|
if(UI_FATAL_ERROR.open) return;
|
||||||
|
|
||||||
|
stringCopy(
|
||||||
|
UI_FATAL_ERROR.messageText, message, UI_FATAL_ERROR_MESSAGE_MAX - 1
|
||||||
|
);
|
||||||
|
UI_FATAL_ERROR.messageLabel.dirty = true;
|
||||||
|
uiLabelRebuffer(&UI_FATAL_ERROR.messageLabel);
|
||||||
|
|
||||||
|
UI_FATAL_ERROR.open = true;
|
||||||
|
|
||||||
|
uiMenuInit(&UI_FATAL_ERROR.menu, uiFatalErrorMenuSelected, NULL, NULL);
|
||||||
|
UI_FATAL_ERROR.items[0].type = UI_MENU_WIDGET_TYPE_BUTTON;
|
||||||
|
uiButtonInit(&UI_FATAL_ERROR.items[0].button, UI_FATAL_ERROR_QUIT_LABEL);
|
||||||
|
uiMenuSetItems(
|
||||||
|
&UI_FATAL_ERROR.menu, UI_FATAL_ERROR.items, UI_FATAL_ERROR_ITEM_COUNT, 1
|
||||||
|
);
|
||||||
|
// No way out but the QUIT button - this is a fatal, unrecoverable
|
||||||
|
// condition.
|
||||||
|
uiMenuSetDisableBack(&UI_FATAL_ERROR.menu, true);
|
||||||
|
uiMenuOpen(&UI_FATAL_ERROR.menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t uiFatalErrorIsOpen(void) {
|
||||||
|
return UI_FATAL_ERROR.open;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiFatalErrorDraw(void) {
|
||||||
|
if(!UI_FATAL_ERROR.open) errorOk();
|
||||||
|
|
||||||
|
const float_t x = (float_t)SCREEN.scanX;
|
||||||
|
const float_t y = (float_t)SCREEN.scanY;
|
||||||
|
const float_t width = (float_t)SCREEN.scanWidth;
|
||||||
|
const float_t height = (float_t)SCREEN.scanHeight;
|
||||||
|
|
||||||
|
errorChain(uiFrameDraw(x, y, width, height));
|
||||||
|
|
||||||
|
const float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
const float_t contentWidth = mathMax(
|
||||||
|
(float_t)UI_FATAL_ERROR.messageLabel.width,
|
||||||
|
(float_t)UI_FATAL_ERROR.items[0].button.label.width
|
||||||
|
);
|
||||||
|
const float_t contentHeight = (float_t)UI_FATAL_ERROR.messageLabel.height +
|
||||||
|
UI_FRAME_PADDING_Y + rowHeight;
|
||||||
|
|
||||||
|
const float_t contentX = x + (width - contentWidth) * 0.5f;
|
||||||
|
const float_t contentY = y + (height - contentHeight) * 0.5f;
|
||||||
|
|
||||||
|
uiLabelSetX(&UI_FATAL_ERROR.messageLabel, contentX);
|
||||||
|
uiLabelSetY(&UI_FATAL_ERROR.messageLabel, contentY);
|
||||||
|
errorChain(uiLabelRender(&UI_FATAL_ERROR.messageLabel, COLOR_WHITE));
|
||||||
|
|
||||||
|
const float_t menuY = contentY +
|
||||||
|
(float_t)UI_FATAL_ERROR.messageLabel.height + UI_FRAME_PADDING_Y;
|
||||||
|
errorChain(uiMenuDraw(&UI_FATAL_ERROR.menu, contentX, menuY, contentWidth, rowHeight));
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiFatalErrorDispose(void) {
|
||||||
|
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 "error/error.h"
|
||||||
|
#include "ui/widget/uilabel.h"
|
||||||
|
#include "ui/widget/uimenu.h"
|
||||||
|
|
||||||
|
#define UI_FATAL_ERROR_MESSAGE_MAX 256
|
||||||
|
#define UI_FATAL_ERROR_MESSAGE_SPRITES_MAX UI_FATAL_ERROR_MESSAGE_MAX
|
||||||
|
#define UI_FATAL_ERROR_ITEM_COUNT 1
|
||||||
|
#define UI_FATAL_ERROR_QUIT_LABEL "QUIT"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool_t open;
|
||||||
|
|
||||||
|
uilabel_t messageLabel;
|
||||||
|
char_t messageText[UI_FATAL_ERROR_MESSAGE_MAX];
|
||||||
|
spritebatchsprite_t messageSprites[UI_FATAL_ERROR_MESSAGE_SPRITES_MAX];
|
||||||
|
|
||||||
|
uimenu_t menu;
|
||||||
|
uimenuitem_t items[UI_FATAL_ERROR_ITEM_COUNT];
|
||||||
|
} uifatalerror_t;
|
||||||
|
|
||||||
|
extern uifatalerror_t UI_FATAL_ERROR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the fatal error overlay.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFatalErrorInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the fatal error overlay, a fullscreen, non-dismissible display
|
||||||
|
* for an unrecoverable error - the only way out is its QUIT button,
|
||||||
|
* which sets ENGINE.running to false. Intended as a last-resort, nicer-
|
||||||
|
* looking alternative to a raw assert crash for a genuinely fatal
|
||||||
|
* condition, so it must never itself fail: a second call while already
|
||||||
|
* open is silently ignored rather than asserting, and it draws above
|
||||||
|
* every other UI element.
|
||||||
|
*
|
||||||
|
* @param message The error text to display; copied internally, safe to
|
||||||
|
* be transient. Truncated to UI_FATAL_ERROR_MESSAGE_MAX - 1 characters.
|
||||||
|
*/
|
||||||
|
void uiFatalErrorOpen(const char_t *message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the fatal error overlay is currently open.
|
||||||
|
*
|
||||||
|
* @returns True if open.
|
||||||
|
*/
|
||||||
|
bool_t uiFatalErrorIsOpen(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the fatal error overlay. No-op when not open.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFatalErrorDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the fatal error overlay.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFatalErrorDispose(void);
|
||||||
@@ -20,12 +20,6 @@ uifullbox_t UI_FULLBOX_OVER;
|
|||||||
void uiFullboxInit(uifullbox_t *fullbox) {
|
void uiFullboxInit(uifullbox_t *fullbox) {
|
||||||
assertNotNull(fullbox, "fullbox must not be NULL");
|
assertNotNull(fullbox, "fullbox must not be NULL");
|
||||||
memoryZero(fullbox, sizeof(uifullbox_t));
|
memoryZero(fullbox, sizeof(uifullbox_t));
|
||||||
eventInit(
|
|
||||||
&fullbox->onTransitionEnd,
|
|
||||||
fullbox->onTransitionEndCallbacks,
|
|
||||||
fullbox->onTransitionEndUsers,
|
|
||||||
4
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
|
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
|
||||||
@@ -35,7 +29,9 @@ void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) {
|
|||||||
fullbox->time += delta;
|
fullbox->time += delta;
|
||||||
if(fullbox->time >= fullbox->duration) {
|
if(fullbox->time >= fullbox->duration) {
|
||||||
fullbox->time = fullbox->duration;
|
fullbox->time = fullbox->duration;
|
||||||
eventInvoke(&fullbox->onTransitionEnd, fullbox);
|
if(fullbox->onTransitionEnd) {
|
||||||
|
fullbox->onTransitionEnd(fullbox, fullbox->onTransitionEndUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,26 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "display/color.h"
|
#include "display/color.h"
|
||||||
#include "animation/easing.h"
|
#include "animation/easing.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct uifullbox_s uifullbox_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback fired once when a fullbox transition completes.
|
||||||
|
*
|
||||||
|
* @param fullbox The uifullbox_t the transition ran on.
|
||||||
|
* @param user The user pointer passed alongside the callback.
|
||||||
|
*/
|
||||||
|
typedef void (*uifullboxcallback_t)(uifullbox_t *fullbox, void *user);
|
||||||
|
|
||||||
|
struct uifullbox_s {
|
||||||
color_t fromColor;
|
color_t fromColor;
|
||||||
color_t toColor;
|
color_t toColor;
|
||||||
float_t duration;
|
float_t duration;
|
||||||
float_t time;
|
float_t time;
|
||||||
easingtype_t easing;
|
easingtype_t easing;
|
||||||
eventcallback_t onTransitionEndCallbacks[4];
|
uifullboxcallback_t onTransitionEnd;
|
||||||
void *onTransitionEndUsers[4];
|
void *onTransitionEndUser;
|
||||||
event_t onTransitionEnd;
|
};
|
||||||
} uifullbox_t;
|
|
||||||
|
|
||||||
extern uifullbox_t UI_FULLBOX_UNDER;
|
extern uifullbox_t UI_FULLBOX_UNDER;
|
||||||
extern uifullbox_t UI_FULLBOX_OVER;
|
extern uifullbox_t UI_FULLBOX_OVER;
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ uiloading_t UI_LOADING;
|
|||||||
|
|
||||||
errorret_t uiLoadingInit(void) {
|
errorret_t uiLoadingInit(void) {
|
||||||
memoryZero(&UI_LOADING, sizeof(uiloading_t));
|
memoryZero(&UI_LOADING, sizeof(uiloading_t));
|
||||||
eventInit(
|
|
||||||
&UI_LOADING.onTransitionEnd,
|
|
||||||
UI_LOADING.onTransitionEndCallbacks,
|
|
||||||
UI_LOADING.onTransitionEndUsers,
|
|
||||||
4
|
|
||||||
);
|
|
||||||
|
|
||||||
errorChain(assetLocaleGetString(
|
errorChain(assetLocaleGetString(
|
||||||
&LOCALE.entry->data.locale,
|
&LOCALE.entry->data.locale,
|
||||||
@@ -43,7 +37,9 @@ errorret_t uiLoadingUpdate(void) {
|
|||||||
UI_LOADING.time += TIME.delta;
|
UI_LOADING.time += TIME.delta;
|
||||||
if(UI_LOADING.time >= UI_LOADING.duration) {
|
if(UI_LOADING.time >= UI_LOADING.duration) {
|
||||||
UI_LOADING.time = UI_LOADING.duration;
|
UI_LOADING.time = UI_LOADING.duration;
|
||||||
eventInvoke(&UI_LOADING.onTransitionEnd, &UI_LOADING);
|
if(UI_LOADING.onTransitionEnd) {
|
||||||
|
UI_LOADING.onTransitionEnd(&UI_LOADING, UI_LOADING.onTransitionEndUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -76,26 +72,21 @@ errorret_t uiLoadingDraw(void) {
|
|||||||
return spriteBatchFlush();
|
return spriteBatchFlush();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void uiLoadingTransition(
|
void uiLoadingTransition(
|
||||||
const float_t from,
|
const float_t from,
|
||||||
const float_t to,
|
const float_t to,
|
||||||
const eventcallback_t callback,
|
const uiloadingcallback_t callback,
|
||||||
void *user
|
void *user
|
||||||
) {
|
) {
|
||||||
UI_LOADING.fromAlpha = from;
|
UI_LOADING.fromAlpha = from;
|
||||||
UI_LOADING.toAlpha = to;
|
UI_LOADING.toAlpha = to;
|
||||||
UI_LOADING.duration = UI_LOADING_FADE_DURATION;
|
UI_LOADING.duration = UI_LOADING_FADE_DURATION;
|
||||||
UI_LOADING.time = 0.0f;
|
UI_LOADING.time = 0.0f;
|
||||||
eventInit(
|
UI_LOADING.onTransitionEnd = callback;
|
||||||
&UI_LOADING.onTransitionEnd,
|
UI_LOADING.onTransitionEndUser = user;
|
||||||
UI_LOADING.onTransitionEndCallbacks,
|
|
||||||
UI_LOADING.onTransitionEndUsers,
|
|
||||||
4
|
|
||||||
);
|
|
||||||
if(callback) eventSubscribe(&UI_LOADING.onTransitionEnd, callback, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiLoadingShow(eventcallback_t callback, void *user) {
|
void uiLoadingShow(uiloadingcallback_t callback, void *user) {
|
||||||
uiLoadingTransition(0.0f, 1.0f, callback, user);
|
uiLoadingTransition(0.0f, 1.0f, callback, user);
|
||||||
uiFullboxTransition(
|
uiFullboxTransition(
|
||||||
&UI_FULLBOX_OVER,
|
&UI_FULLBOX_OVER,
|
||||||
@@ -106,7 +97,7 @@ void uiLoadingShow(eventcallback_t callback, void *user) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiLoadingHide(eventcallback_t callback, void *user) {
|
void uiLoadingHide(uiloadingcallback_t callback, void *user) {
|
||||||
uiLoadingTransition(1.0f, 0.0f, callback, user);
|
uiLoadingTransition(1.0f, 0.0f, callback, user);
|
||||||
uiFullboxTransition(
|
uiFullboxTransition(
|
||||||
&UI_FULLBOX_OVER,
|
&UI_FULLBOX_OVER,
|
||||||
|
|||||||
@@ -7,22 +7,30 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "event/event.h"
|
|
||||||
|
|
||||||
#define UI_LOADING_FADE_DURATION 0.5f
|
#define UI_LOADING_FADE_DURATION 0.5f
|
||||||
#define UI_LOADING_MARGIN 8.0f
|
#define UI_LOADING_MARGIN 8.0f
|
||||||
#define UI_LOADING_TEXT_MAX 32
|
#define UI_LOADING_TEXT_MAX 32
|
||||||
|
|
||||||
typedef struct {
|
typedef struct uiloading_s uiloading_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback fired once when a loading indicator fade transition completes.
|
||||||
|
*
|
||||||
|
* @param loading The uiloading_t the transition ran on.
|
||||||
|
* @param user The user pointer passed alongside the callback.
|
||||||
|
*/
|
||||||
|
typedef void (*uiloadingcallback_t)(uiloading_t *loading, void *user);
|
||||||
|
|
||||||
|
struct uiloading_s {
|
||||||
float_t fromAlpha;
|
float_t fromAlpha;
|
||||||
float_t toAlpha;
|
float_t toAlpha;
|
||||||
float_t duration;
|
float_t duration;
|
||||||
float_t time;
|
float_t time;
|
||||||
eventcallback_t onTransitionEndCallbacks[4];
|
uiloadingcallback_t onTransitionEnd;
|
||||||
void *onTransitionEndUsers[4];
|
void *onTransitionEndUser;
|
||||||
event_t onTransitionEnd;
|
|
||||||
char_t text[UI_LOADING_TEXT_MAX];
|
char_t text[UI_LOADING_TEXT_MAX];
|
||||||
} uiloading_t;
|
};
|
||||||
|
|
||||||
extern uiloading_t UI_LOADING;
|
extern uiloading_t UI_LOADING;
|
||||||
|
|
||||||
@@ -48,13 +56,29 @@ errorret_t uiLoadingUpdate(void);
|
|||||||
*/
|
*/
|
||||||
errorret_t uiLoadingDraw(void);
|
errorret_t uiLoadingDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begins a loading indicator fade transition, replacing any transition
|
||||||
|
* already in progress along with its pending callback.
|
||||||
|
*
|
||||||
|
* @param from Starting alpha.
|
||||||
|
* @param to Ending alpha.
|
||||||
|
* @param callback Called when the transition completes. May be NULL.
|
||||||
|
* @param user Forwarded to the callback unchanged.
|
||||||
|
*/
|
||||||
|
void uiLoadingTransition(
|
||||||
|
const float_t from,
|
||||||
|
const float_t to,
|
||||||
|
const uiloadingcallback_t callback,
|
||||||
|
void *user
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fades the loading indicator in. Invokes callback when fully visible.
|
* Fades the loading indicator in. Invokes callback when fully visible.
|
||||||
*
|
*
|
||||||
* @param callback Called when the fade-in completes. May be NULL.
|
* @param callback Called when the fade-in completes. May be NULL.
|
||||||
* @param user Forwarded to the callback unchanged.
|
* @param user Forwarded to the callback unchanged.
|
||||||
*/
|
*/
|
||||||
void uiLoadingShow(eventcallback_t callback, void *user);
|
void uiLoadingShow(uiloadingcallback_t callback, void *user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fades the loading indicator out. Invokes callback when fully hidden.
|
* Fades the loading indicator out. Invokes callback when fully hidden.
|
||||||
@@ -62,4 +86,4 @@ void uiLoadingShow(eventcallback_t callback, void *user);
|
|||||||
* @param callback Called when the fade-out completes. May be NULL.
|
* @param callback Called when the fade-out completes. May be NULL.
|
||||||
* @param user Forwarded to the callback unchanged.
|
* @param user Forwarded to the callback unchanged.
|
||||||
*/
|
*/
|
||||||
void uiLoadingHide(eventcallback_t callback, void *user);
|
void uiLoadingHide(uiloadingcallback_t callback, void *user);
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|
||||||
PUBLIC
|
|
||||||
uiconfirm.c
|
|
||||||
uimodal.c
|
|
||||||
)
|
|
||||||
|
|
||||||
add_subdirectory(game)
|
add_subdirectory(game)
|
||||||
add_subdirectory(mainmenu)
|
add_subdirectory(mainmenu)
|
||||||
add_subdirectory(battle)
|
add_subdirectory(battle)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user