Compare commits
38 Commits
128f9ab9d4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a858cc424 | |||
| c0292842a5 | |||
| 717902462b | |||
| 1d73b9d224 | |||
| 51f262efa0 | |||
| a6e4e3f71f | |||
| 4387d223b9 | |||
| 00bfaf6360 | |||
| 709bd7be52 | |||
| 80f4348e21 | |||
| e630827b34 | |||
| 106d9b0fc0 | |||
| 4a26f79945 | |||
| 1a199f6ce3 | |||
| ec59e77867 | |||
| cb28d2b611 | |||
| 27b6ddf5cb | |||
| 475c865e33 | |||
| 0b4ba062bb | |||
| 28f5e66662 | |||
| e225a076f0 | |||
| 774c8ad0f8 | |||
| 52d1e7414d | |||
| af4cb53e5f | |||
| c019271e12 | |||
| 82ae2bce9d | |||
| 092e259a06 | |||
| 08b4bbfe91 | |||
| 560c51cf27 | |||
| 674f86b18a | |||
| e3f10e0926 | |||
| d7223d7387 | |||
| 45331c2a60 | |||
| 33f50a2c69 | |||
| 6c8e4d5cbd | |||
| 3b7215876a | |||
| c74f5890bd | |||
| fbd3c71ba7 |
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||
|
||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||
option(DUSK_NETWORK "Enable network support" ON)
|
||||
|
||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
||||
DUSK_VERSION="${DUSK_VERSION}"
|
||||
)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_NETWORK
|
||||
)
|
||||
endif()
|
||||
|
||||
# Toolchains
|
||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -5,11 +5,107 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||
|
||||
# Initial Scene
|
||||
msgid "initial.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "initial.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "initial.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "initial.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "initial.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "initial.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
|
||||
# Main Menu Scene
|
||||
msgid "main_menu.start_game"
|
||||
msgstr "Start Game"
|
||||
|
||||
msgid "main_menu.options"
|
||||
msgstr "Options"
|
||||
|
||||
msgid "main_menu.quit"
|
||||
msgstr "Quit Game"
|
||||
|
||||
msgid "main_menu.quit_confirm"
|
||||
msgstr "Are you sure you want to quit?"
|
||||
|
||||
msgid "main_menu.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "main_menu.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "main_menu.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "main_menu.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "main_menu.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "main_menu.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
msgid "main_menu.save_load_error.title"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "main_menu.save_load_error.message"
|
||||
msgstr "Failed to load save data. Please try again."
|
||||
|
||||
msgid "main_menu.save_load_error.retry"
|
||||
msgstr "Try Again"
|
||||
|
||||
|
||||
# Select Save Screen
|
||||
msgid "ui.select_save.title"
|
||||
msgstr "Select Save"
|
||||
|
||||
msgid "ui.select_save.empty"
|
||||
msgstr "Empty Slot"
|
||||
|
||||
msgid "ui.select_save.slot_format"
|
||||
msgstr "%s Lv.%d %s"
|
||||
|
||||
msgid "ui.select_save.delete_mode"
|
||||
msgstr "Delete a Save"
|
||||
|
||||
msgid "ui.select_save.delete_confirm"
|
||||
msgstr "Are you sure you want to delete this save?"
|
||||
|
||||
msgid "ui.select_save.name_title"
|
||||
msgstr "Enter game save file name"
|
||||
|
||||
msgid "ui.save_slot.number_format"
|
||||
msgstr "Slot %d"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Welcome"
|
||||
|
||||
msgid "save.linux.mkdirp_failed"
|
||||
msgstr "Failed to create save directory, check the disk is not full or write-protected."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
@@ -112,6 +208,58 @@ msgstr "Yes"
|
||||
msgid "ui.initial.create_save.no"
|
||||
msgstr "No"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.confirm"
|
||||
msgstr "Confirm"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.attack"
|
||||
msgstr "Attack"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.flee"
|
||||
msgstr "Flee"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.target_format"
|
||||
msgstr "Enemy %u (%u/%u HP)"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.hp_format"
|
||||
msgstr "HP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.mp_format"
|
||||
msgstr "MP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
||||
msgid "ui.settings.audio.placeholder"
|
||||
msgstr "No audio settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
||||
msgid "ui.settings.display.placeholder"
|
||||
msgstr "No display settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
||||
msgid "ui.settings.input.placeholder"
|
||||
msgstr "No input settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
||||
msgid "ui.backpack.category_format"
|
||||
msgstr "Category %u"
|
||||
|
||||
#: src/dusk/ui/overlay/uiloading.c
|
||||
msgid "ui.loading.text"
|
||||
msgstr "loading"
|
||||
|
||||
#: src/dusk/ui/overlay/uiautosave.c
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: es\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Bienvenido"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "Pantalla"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "Deadzone"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "Se aplica después de reiniciar la aplicación."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "¿Descartar los cambios no guardados?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Personajes"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Objetos"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Configuración"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_success"
|
||||
msgstr "Partida guardada."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_cancelled"
|
||||
msgstr "Guardado cancelado."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_unavailable"
|
||||
msgstr "No se puede guardar: no se encontró ningún dispositivo de guardado."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_temporary"
|
||||
msgstr "Esta sesión es temporal - no se encontró ningún dispositivo de guardado, por lo que guardar está deshabilitado."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_create_confirm"
|
||||
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_failed_format"
|
||||
msgstr "Error al guardar: %s"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_check_failed_format"
|
||||
msgstr "No se puede guardar: %s"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.message"
|
||||
msgstr "No se encontró ningún dispositivo de guardado. Puedes continuar, pero\nel progreso no se guardará."
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.retry"
|
||||
msgstr "Reintentar"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.continue"
|
||||
msgstr "Continuar de todos modos"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.message"
|
||||
msgstr "No se encontraron datos guardados. ¿Crear una partida nueva?"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.yes"
|
||||
msgstr "Sí"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.no"
|
||||
msgstr "No"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "Poción"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "Papa"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "Manzana"
|
||||
@@ -1,126 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: ja\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"歓迎"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "一般"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "入力"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "表示"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "オーディオ"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "デッドゾーン"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "言語"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "アプリケーションを再起動すると適用されます。"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "適用"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "未保存の変更を破棄しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "キャラクター"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "アイテム"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save"
|
||||
msgstr "セーブ"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_success"
|
||||
msgstr "セーブしました。"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_cancelled"
|
||||
msgstr "セーブをキャンセルしました。"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_unavailable"
|
||||
msgstr "セーブできません - セーブデバイスが見つかりません。"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_temporary"
|
||||
msgstr "このセッションは一時的です - セーブデバイスが見つからなかったため、セーブは無効になっています。"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_create_confirm"
|
||||
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_failed_format"
|
||||
msgstr "セーブに失敗しました: %s"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_check_failed_format"
|
||||
msgstr "セーブできません: %s"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.message"
|
||||
msgstr "セーブデバイスが見つかりません。続行できますが、\n進行状況は保存されません。"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.retry"
|
||||
msgstr "再試行"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.continue"
|
||||
msgstr "続行する"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.message"
|
||||
msgstr "セーブデータが見つかりません。新しいセーブを作成しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.yes"
|
||||
msgstr "はい"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.no"
|
||||
msgstr "いいえ"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "ポーション"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "ジャガイモ"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "リンゴ"
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+47
-1
@@ -65,6 +65,23 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
)
|
||||
endif()
|
||||
|
||||
# Generate PARAM.SFO as a normal tracked build output (instead of letting
|
||||
# create_pbp_file() auto-generate + delete it) so it can be reused below by
|
||||
# a properly dependency-tracked EBOOT.PBP repack step.
|
||||
set(DUSK_PSP_PARAM_SFO "${DUSK_BUILD_DIR}/PARAM.SFO")
|
||||
add_custom_command(
|
||||
OUTPUT "${DUSK_PSP_PARAM_SFO}"
|
||||
COMMAND "$ENV{PSPDEV}/bin/mksfoex" "-d" "MEMSIZE=1" "-s" "APP_VER=01.00"
|
||||
"${DUSK_BINARY_TARGET_NAME}" "${DUSK_PSP_PARAM_SFO}"
|
||||
COMMENT "Generating PARAM.SFO for ${DUSK_BINARY_TARGET_NAME}"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(DuskPspParamSfo DEPENDS "${DUSK_PSP_PARAM_SFO}")
|
||||
|
||||
# create_pbp_file()'s own POST_BUILD chain (below) also consumes
|
||||
# DUSK_PSP_PARAM_SFO, so make sure it exists before that chain runs.
|
||||
add_dependencies(${DUSK_BINARY_TARGET_NAME} DuskPspParamSfo)
|
||||
|
||||
# Postbuild, create .pbp file for PSP.
|
||||
create_pbp_file(
|
||||
TARGET "${DUSK_BINARY_TARGET_NAME}"
|
||||
@@ -74,4 +91,33 @@ create_pbp_file(
|
||||
TITLE "${DUSK_BINARY_TARGET_NAME}"
|
||||
PSAR_PATH ${DUSK_ASSETS_ZIP}
|
||||
VERSION 01.00
|
||||
)
|
||||
SFO_PATH "${DUSK_PSP_PARAM_SFO}"
|
||||
OUTPUT_DIR "${DUSK_BUILD_DIR}"
|
||||
)
|
||||
|
||||
# CreatePBP.cmake's pack-pbp step is a POST_BUILD command tied to the
|
||||
# executable target, so it only reruns when the ELF itself relinks. That
|
||||
# means regenerating dusk.dsk (assets) alone, without touching any C
|
||||
# source, silently leaves EBOOT.PBP embedding a stale asset pak. Repack it
|
||||
# here as a normal file-tracked custom command depending on both the
|
||||
# executable and the asset zip, so EBOOT.PBP always reflects the current
|
||||
# assets even when nothing else about the build changed.
|
||||
set(DUSK_PSP_EBOOT "${DUSK_BUILD_DIR}/EBOOT.PBP")
|
||||
if(BUILD_PRX)
|
||||
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>.prx")
|
||||
else()
|
||||
set(DUSK_PSP_EXECUTABLE "$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>")
|
||||
endif()
|
||||
add_custom_command(
|
||||
OUTPUT "${DUSK_PSP_EBOOT}"
|
||||
COMMAND "$ENV{PSPDEV}/bin/pack-pbp" "${DUSK_PSP_EBOOT}" "${DUSK_PSP_PARAM_SFO}"
|
||||
"NULL" "NULL" "NULL" "NULL" "NULL"
|
||||
"${DUSK_PSP_EXECUTABLE}" "${DUSK_ASSETS_ZIP}"
|
||||
DEPENDS
|
||||
"$<TARGET_FILE:${DUSK_BINARY_TARGET_NAME}>"
|
||||
"${DUSK_PSP_PARAM_SFO}"
|
||||
"${DUSK_ASSETS_ZIP}"
|
||||
COMMENT "Repacking EBOOT.PBP (tracks executable + asset pak freshness)"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(DuskPspEbootRepack ALL DEPENDS "${DUSK_PSP_EBOOT}")
|
||||
@@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_WII
|
||||
)
|
||||
|
||||
# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h.
|
||||
set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING
|
||||
"Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\
|
||||
compatible memory card emulation), or SD (SD card via libfat)"
|
||||
)
|
||||
set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD")
|
||||
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD}
|
||||
)
|
||||
|
||||
# Generate Homebrew Channel meta.xml from project identity variables
|
||||
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||
configure_file(
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
add_subdirectory(dusk)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
add_subdirectory(dusknetwork)
|
||||
endif()
|
||||
|
||||
if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "knulli")
|
||||
add_subdirectory(dusklinux)
|
||||
add_subdirectory(dusksdl2)
|
||||
|
||||
@@ -53,7 +53,6 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(animation)
|
||||
add_subdirectory(event)
|
||||
add_subdirectory(assert)
|
||||
add_subdirectory(asset)
|
||||
add_subdirectory(console)
|
||||
@@ -68,7 +67,6 @@ add_subdirectory(scene)
|
||||
add_subdirectory(system)
|
||||
add_subdirectory(time)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(network)
|
||||
add_subdirectory(save)
|
||||
add_subdirectory(util)
|
||||
add_subdirectory(thread)
|
||||
@@ -7,7 +7,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
asset.c
|
||||
assetbatch.c
|
||||
assetfile.c
|
||||
)
|
||||
|
||||
|
||||
@@ -322,7 +322,9 @@ errorret_t assetUpdate(void) {
|
||||
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
assetentry_t *loadedEntry = loading->entry;
|
||||
loading->entry = NULL;
|
||||
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
||||
if(loadedEntry->onLoaded) {
|
||||
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
|
||||
}
|
||||
}
|
||||
|
||||
loading++;
|
||||
@@ -346,8 +348,8 @@ errorret_t assetUpdate(void) {
|
||||
assetentry_t *errEntry = loading->entry;
|
||||
loading->entry = NULL;
|
||||
threadMutexUnlock(&loading->mutex);
|
||||
eventInvoke(&errEntry->onError, errEntry);
|
||||
errorThrow("Failed to load asset asynchronously.");
|
||||
if(errEntry->onError) errEntry->onError(errEntry, errEntry->onErrorUser);
|
||||
loading++;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetbatch.h"
|
||||
#include "asset.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include <unistd.h>
|
||||
|
||||
void assetBatchInit(
|
||||
assetbatch_t *batch,
|
||||
const uint16_t count,
|
||||
const assetbatchdesc_t *descs
|
||||
) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
assertNotNull(descs, "Descs cannot be NULL.");
|
||||
assertTrue(count > 0, "Count must be greater than 0.");
|
||||
assertTrue(
|
||||
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
|
||||
);
|
||||
|
||||
memoryZero(batch, sizeof(assetbatch_t));
|
||||
batch->count = count;
|
||||
|
||||
eventInit(
|
||||
&batch->onLoaded,
|
||||
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&batch->onEntryLoaded,
|
||||
batch->onEntryLoadedCallbacks,
|
||||
batch->onEntryLoadedUsers,
|
||||
ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&batch->onError,
|
||||
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&batch->onEntryError,
|
||||
batch->onEntryErrorCallbacks,
|
||||
batch->onEntryErrorUsers,
|
||||
ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
|
||||
for(uint16_t i = 0; i < count; i++) {
|
||||
batch->inputs[i] = descs[i].input;
|
||||
batch->entries[i] = assetLock(
|
||||
descs[i].path, descs[i].type, &batch->inputs[i]
|
||||
);
|
||||
|
||||
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
// Already loaded (cached) - count it now, no subscription needed.
|
||||
batch->loadedCount++;
|
||||
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
batch->errorCount++;
|
||||
} else {
|
||||
eventSubscribe(
|
||||
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
|
||||
);
|
||||
eventSubscribe(
|
||||
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void assetBatchLock(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
assetEntryLock(batch->entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void assetBatchUnlock(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
assetEntryUnlock(batch->entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t assetBatchHasError(const assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
|
||||
bool_t allDone;
|
||||
do {
|
||||
allDone = true;
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
const assetentrystate_t state = batch->entries[i]->state;
|
||||
if(state == ASSET_ENTRY_STATE_ERROR) {
|
||||
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
|
||||
}
|
||||
if(state != ASSET_ENTRY_STATE_LOADED) {
|
||||
allDone = false;
|
||||
}
|
||||
}
|
||||
if(!allDone) {
|
||||
usleep(1000);
|
||||
errorChain(assetUpdate());
|
||||
}
|
||||
} while(!allDone);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void assetBatchDispose(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
if(batch->entries[i]) {
|
||||
// Unsubscribe while we still hold a lock so the entry is live.
|
||||
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
|
||||
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
|
||||
assetUnlockEntry(batch->entries[i]);
|
||||
}
|
||||
}
|
||||
memoryZero(batch, sizeof(assetbatch_t));
|
||||
}
|
||||
|
||||
void assetBatchEntryOnLoadedCb(void *params, void *user) {
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
assetbatch_t *batch = (assetbatch_t *)user;
|
||||
|
||||
batch->loadedCount++;
|
||||
eventInvoke(&batch->onEntryLoaded, entry);
|
||||
|
||||
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
||||
if(batch->errorCount == 0) {
|
||||
eventInvoke(&batch->onLoaded, batch);
|
||||
} else {
|
||||
eventInvoke(&batch->onError, batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void assetBatchEntryOnErrorCb(void *params, void *user) {
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
assetbatch_t *batch = (assetbatch_t *)user;
|
||||
|
||||
batch->errorCount++;
|
||||
eventInvoke(&batch->onEntryError, entry);
|
||||
|
||||
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
||||
eventInvoke(&batch->onError, batch);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#define ASSET_BATCH_COUNT_MAX 64
|
||||
#define ASSET_BATCH_EVENT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
const char_t *path;
|
||||
assetloadertype_t type;
|
||||
assetloaderinput_t input;
|
||||
} assetbatchdesc_t;
|
||||
|
||||
typedef struct {
|
||||
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
|
||||
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
|
||||
uint16_t count;
|
||||
uint16_t loadedCount;
|
||||
uint16_t errorCount;
|
||||
|
||||
/** Fires once when every entry loaded. params = assetbatch_t * */
|
||||
event_t onLoaded;
|
||||
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
||||
|
||||
/** Fires each time a single entry loads. params = assetentry_t * */
|
||||
event_t onEntryLoaded;
|
||||
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
||||
|
||||
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
|
||||
|
||||
/** Fires each time a single entry errors. params = assetentry_t * */
|
||||
event_t onEntryError;
|
||||
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
|
||||
} assetbatch_t;
|
||||
|
||||
/**
|
||||
* Initialises the batch from an array of descriptors. Each entry is locked
|
||||
* and queued for loading immediately.
|
||||
*
|
||||
* @param batch Batch to initialise.
|
||||
* @param descs Array of entry descriptors (need not outlive this call).
|
||||
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
|
||||
*/
|
||||
void assetBatchInit(
|
||||
assetbatch_t *batch,
|
||||
uint16_t count,
|
||||
const assetbatchdesc_t *descs
|
||||
);
|
||||
|
||||
/**
|
||||
* Acquires one additional lock on every entry in the batch.
|
||||
*
|
||||
* @param batch Batch to lock.
|
||||
*/
|
||||
void assetBatchLock(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Releases one lock from every entry in the batch. When an entry's lock
|
||||
* count reaches zero it will be reaped on the next assetUpdate.
|
||||
*
|
||||
* @param batch Batch to unlock.
|
||||
*/
|
||||
void assetBatchUnlock(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Returns true if every entry in the batch has finished loading.
|
||||
*
|
||||
* @param batch Batch to query.
|
||||
*/
|
||||
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Returns true if any entry in the batch is in an error state.
|
||||
*
|
||||
* @param batch Batch to query.
|
||||
*/
|
||||
bool_t assetBatchHasError(const assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Blocks until every entry is loaded. Returns an error if any entry fails.
|
||||
*
|
||||
* @param batch Batch to wait on.
|
||||
*/
|
||||
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Releases the batch's lock on every entry and clears the batch. After this
|
||||
* call the batch struct may be reused with assetBatchInit.
|
||||
*
|
||||
* @param batch Batch to dispose.
|
||||
*/
|
||||
void assetBatchDispose(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Event trampoline invoked when a batch entry finishes loading.
|
||||
* Increments the loaded counter and fires batch-level events.
|
||||
*
|
||||
* @param params The loaded assetentry_t pointer.
|
||||
* @param user The owning assetbatch_t pointer.
|
||||
*/
|
||||
void assetBatchEntryOnLoadedCb(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Event trampoline invoked when a batch entry fails to load.
|
||||
* Increments the error counter and fires batch-level events.
|
||||
*
|
||||
* @param params The errored assetentry_t pointer.
|
||||
* @param user The owning assetbatch_t pointer.
|
||||
*/
|
||||
void assetBatchEntryOnErrorCb(void *params, void *user);
|
||||
@@ -87,13 +87,30 @@ errorret_t assetFileRead(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// I assume zip_fread takes buffer NULL for skipping?
|
||||
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
|
||||
if(bytesRead < 0) {
|
||||
errorThrow("Failed to read from asset file: %s", file->filename);
|
||||
// Some zip_fread() implementations (seen on PSP) reject a single call
|
||||
// asking for the entire (potentially large) file at once with EINVAL;
|
||||
// the line reader above only ever asks for up to 1024 bytes per call and
|
||||
// works fine, so read in bounded chunks here too.
|
||||
size_t totalRead = 0;
|
||||
uint8_t *dest = (uint8_t *)buffer;
|
||||
while(totalRead < bufferSize) {
|
||||
size_t chunkSize = mathMin(
|
||||
bufferSize - totalRead, ASSET_FILE_READ_CHUNK_MAX
|
||||
);
|
||||
zip_int64_t bytesRead = zip_fread(
|
||||
file->zipFile, dest + totalRead, chunkSize
|
||||
);
|
||||
if(bytesRead < 0) {
|
||||
errorThrow(
|
||||
"Failed to read from asset file: %s (%s)",
|
||||
file->filename, zip_file_strerror(file->zipFile)
|
||||
);
|
||||
}
|
||||
if(bytesRead == 0) break;
|
||||
totalRead += (size_t)bytesRead;
|
||||
}
|
||||
file->position += bytesRead;
|
||||
file->lastRead = bytesRead;
|
||||
file->position += totalRead;
|
||||
file->lastRead = totalRead;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
#define ASSET_FILE_NAME_MAX 48
|
||||
|
||||
// Max bytes requested per zip_fread() call in assetFileRead(). Some
|
||||
// zip_fread() implementations (seen on PSP) reject a single call asking
|
||||
// for very large amounts of data at once; the locale line reader has
|
||||
// always used 1024-byte reads successfully, so that's the proven-safe cap.
|
||||
#define ASSET_FILE_READ_CHUNK_MAX 1024
|
||||
|
||||
typedef struct assetfile_s assetfile_t;
|
||||
|
||||
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
|
||||
|
||||
@@ -16,4 +16,5 @@ add_subdirectory(display)
|
||||
add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(dmf)
|
||||
add_subdirectory(cutscene)
|
||||
@@ -35,22 +35,6 @@ void assetEntryInit(
|
||||
entry->input = NULL;
|
||||
}
|
||||
refInit(&entry->refs, entry, NULL, NULL, NULL);
|
||||
|
||||
eventInit(
|
||||
&entry->onLoaded,
|
||||
entry->onLoadedCallbacks, entry->onLoadedUsers,
|
||||
ASSET_ENTRY_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&entry->onUnloaded,
|
||||
entry->onUnloadedCallbacks, entry->onUnloadedUsers,
|
||||
ASSET_ENTRY_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&entry->onError,
|
||||
entry->onErrorCallbacks, entry->onErrorUsers,
|
||||
ASSET_ENTRY_EVENT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
void assetEntryLock(assetentry_t *entry) {
|
||||
@@ -97,7 +81,7 @@ errorret_t assetEntryDispose(assetentry_t *entry) {
|
||||
"Asset entry still refed at dispose time."
|
||||
);
|
||||
|
||||
eventInvoke(&entry->onUnloaded, entry);
|
||||
if(entry->onUnloaded) entry->onUnloaded(entry, entry->onUnloadedUser);
|
||||
errorChain(ASSET_LOADER_CALLBACKS[entry->type].dispose(entry));
|
||||
memoryZero(entry, sizeof(assetentry_t));
|
||||
errorOk();
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "event/event.h"
|
||||
#include "util/ref.h"
|
||||
|
||||
typedef enum {
|
||||
@@ -20,11 +19,17 @@ typedef enum {
|
||||
ASSET_ENTRY_STATE_ERROR
|
||||
} assetentrystate_t;
|
||||
|
||||
/** Maximum number of subscribers for each per-entry event. */
|
||||
#define ASSET_ENTRY_EVENT_MAX 2
|
||||
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
/**
|
||||
* A single asset entry callback. Each entry supports at most one subscriber
|
||||
* per event - a second assignment without clearing the first is a bug.
|
||||
*
|
||||
* @param entry The assetentry_t the event fired on.
|
||||
* @param user The user pointer passed alongside the callback.
|
||||
*/
|
||||
typedef void (*assetentrycallback_t)(assetentry_t *entry, void *user);
|
||||
|
||||
struct assetentry_s {
|
||||
char_t name[ASSET_FILE_NAME_MAX];
|
||||
assetloadertype_t type;
|
||||
@@ -33,30 +38,27 @@ struct assetentry_s {
|
||||
ref_t refs;
|
||||
assetloaderinput_t *input;
|
||||
assetloaderinput_t inputData;
|
||||
/**
|
||||
* Fired once when loading completes successfully (params = assetentry_t *).
|
||||
* Always invoked on the main thread.
|
||||
*/
|
||||
event_t onLoaded;
|
||||
eventcallback_t onLoadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
||||
void *onLoadedUsers[ASSET_ENTRY_EVENT_MAX];
|
||||
|
||||
/**
|
||||
* Fired once when the entry is disposed/reaped (params = assetentry_t *).
|
||||
* The asset data is still accessible when the callback runs.
|
||||
* Fired once when loading completes successfully.
|
||||
* Always invoked on the main thread.
|
||||
*/
|
||||
event_t onUnloaded;
|
||||
eventcallback_t onUnloadedCallbacks[ASSET_ENTRY_EVENT_MAX];
|
||||
void *onUnloadedUsers[ASSET_ENTRY_EVENT_MAX];
|
||||
assetentrycallback_t onLoaded;
|
||||
void *onLoadedUser;
|
||||
|
||||
/**
|
||||
* Fired once when loading fails (params = assetentry_t *).
|
||||
* Fired once when the entry is disposed/reaped. The asset data is still
|
||||
* accessible when the callback runs. Always invoked on the main thread.
|
||||
*/
|
||||
assetentrycallback_t onUnloaded;
|
||||
void *onUnloadedUser;
|
||||
|
||||
/**
|
||||
* Fired once when loading fails.
|
||||
* Always invoked on the main thread.
|
||||
*/
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[ASSET_ENTRY_EVENT_MAX];
|
||||
void *onErrorUsers[ASSET_ENTRY_EVENT_MAX];
|
||||
assetentrycallback_t onError;
|
||||
void *onErrorUser;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,4 +51,10 @@ assetloadercallbacks_t ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_COUNT] = {
|
||||
.loadAsync = assetChunkLoaderAsync,
|
||||
.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/json/assetjsonloader.h"
|
||||
#include "asset/loader/chunk/assetchunkloader.h"
|
||||
#include "asset/loader/cutscene/assetcutsceneloader.h"
|
||||
|
||||
typedef enum {
|
||||
ASSET_LOADER_TYPE_NULL,
|
||||
@@ -24,6 +25,7 @@ typedef enum {
|
||||
ASSET_LOADER_TYPE_LOCALE,
|
||||
ASSET_LOADER_TYPE_JSON,
|
||||
ASSET_LOADER_TYPE_CHUNK,
|
||||
ASSET_LOADER_TYPE_CUTSCENE,
|
||||
|
||||
ASSET_LOADER_TYPE_COUNT
|
||||
} assetloadertype_t;
|
||||
@@ -36,6 +38,7 @@ typedef union {
|
||||
assetlocaleloaderloading_t locale;
|
||||
assetjsonloaderloading_t json;
|
||||
assetchunkloaderloading_t chunk;
|
||||
assetcutsceneloaderloading_t cutscene;
|
||||
} assetloaderloading_t;
|
||||
|
||||
typedef union {
|
||||
@@ -46,6 +49,7 @@ typedef union {
|
||||
assetlocaleoutput_t locale;
|
||||
assetjsonoutput_t json;
|
||||
assetchunkoutput_t chunk;
|
||||
assetcutsceneoutput_t cutscene;
|
||||
} assetloaderoutput_t;
|
||||
|
||||
typedef union {
|
||||
@@ -54,6 +58,7 @@ typedef union {
|
||||
assetlocaleloaderinput_t locale;
|
||||
assetjsonloaderinput_t json;
|
||||
assetchunkloaderinput_t chunk;
|
||||
assetcutsceneloaderinput_t cutscene;
|
||||
} assetloaderinput_t;
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
|
||||
+1
-2
@@ -5,6 +5,5 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uiinitialnocard.c
|
||||
uiinitialcreatesave.c
|
||||
assetcutsceneloader.c
|
||||
)
|
||||
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetcutsceneloader.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/endian.h"
|
||||
#include "asset/loader/assetloading.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
// DCTS header: magic "DCTS" (4), version u32 LE (4), pauseType u8 (1),
|
||||
// itemCount u8 (1), poolSize u16 LE (2) = 12 bytes.
|
||||
#define ASSET_CUTSCENE_HEADER_SIZE 12
|
||||
|
||||
static uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset) {
|
||||
uint8_t value = data[*offset];
|
||||
*offset += sizeof(uint8_t);
|
||||
return value;
|
||||
}
|
||||
|
||||
static uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset) {
|
||||
uint16_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(uint16_t));
|
||||
*offset += sizeof(uint16_t);
|
||||
return endianLittleToHost16(value);
|
||||
}
|
||||
|
||||
static uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset) {
|
||||
uint32_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(uint32_t));
|
||||
*offset += sizeof(uint32_t);
|
||||
return endianLittleToHost32(value);
|
||||
}
|
||||
|
||||
static float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset) {
|
||||
float_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(float_t));
|
||||
*offset += sizeof(float_t);
|
||||
return endianLittleToHostFloat(value);
|
||||
}
|
||||
|
||||
static worldunit_t assetCutsceneReadWorldUnit(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
uint16_t value = assetCutsceneReadU16(data, offset);
|
||||
return (worldunit_t)value;
|
||||
}
|
||||
|
||||
static worldpos_t assetCutsceneReadWorldPos(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
worldpos_t pos;
|
||||
pos.x = assetCutsceneReadWorldUnit(data, offset);
|
||||
pos.y = assetCutsceneReadWorldUnit(data, offset);
|
||||
pos.z = assetCutsceneReadWorldUnit(data, offset);
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Copies a length-prefixed string directly into an item's own embedded
|
||||
// char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these
|
||||
// are never pool references, see the item-field inventory in the runtime
|
||||
// cutscene file design.
|
||||
static void assetCutsceneReadEmbeddedString(
|
||||
const uint8_t *data,
|
||||
size_t *offset,
|
||||
char_t *dest,
|
||||
const size_t destCapacity
|
||||
) {
|
||||
uint8_t len = assetCutsceneReadU8(data, offset);
|
||||
assertTrue(len < destCapacity, "Cutscene string exceeds field capacity");
|
||||
memoryCopy(dest, data + *offset, len);
|
||||
dest[len] = '\0';
|
||||
*offset += len;
|
||||
}
|
||||
|
||||
// Resolves a u16 pool offset (read from the item stream) to a real pointer
|
||||
// into the entry's own persistent pool allocation.
|
||||
static const char_t * assetCutsceneReadPoolString(
|
||||
const uint8_t *data,
|
||||
size_t *offset,
|
||||
const char_t *pool
|
||||
) {
|
||||
uint16_t poolOffset = assetCutsceneReadU16(data, offset);
|
||||
return pool + poolOffset;
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
|
||||
if(loading->loading.cutscene.state != ASSET_CUTSCENE_LOADING_STATE_READ_FILE) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assertNull(loading->loading.cutscene.data, "Data already defined?");
|
||||
|
||||
assetfile_t *file = &loading->loading.cutscene.file;
|
||||
assetLoaderErrorChain(loading,
|
||||
assetFileInit(file, loading->entry->name, NULL, NULL)
|
||||
);
|
||||
|
||||
uint8_t *data = memoryAllocate(file->size);
|
||||
assetLoaderErrorChain(loading, assetFileOpen(file));
|
||||
assetLoaderErrorChain(loading, assetFileRead(file, data, file->size));
|
||||
assertTrue(
|
||||
file->lastRead == file->size,
|
||||
"Failed to read entire cutscene file."
|
||||
);
|
||||
// Saved before assetFileDispose zeroes the whole assetfile_t struct
|
||||
// (including .size) - the sync phase needs the file's total length to
|
||||
// locate the pool region, which starts poolSize bytes before the end.
|
||||
loading->loading.cutscene.dataSize = (size_t)file->size;
|
||||
|
||||
assetLoaderErrorChain(loading, assetFileClose(file));
|
||||
assetLoaderErrorChain(loading, assetFileDispose(file));
|
||||
|
||||
loading->loading.cutscene.data = data;
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_PARSE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_SYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertTrue(loading->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
if(loading->loading.cutscene.state == ASSET_CUTSCENE_LOADING_STATE_INITIAL) {
|
||||
loading->loading.cutscene.state = ASSET_CUTSCENE_LOADING_STATE_READ_FILE;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_PENDING_ASYNC;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
assetcutsceneoutput_t *out = &loading->entry->data.cutscene;
|
||||
uint8_t *data = loading->loading.cutscene.data;
|
||||
assertNotNull(data, "Cutscene data should have been loaded by now.");
|
||||
|
||||
size_t fileSize = loading->loading.cutscene.dataSize;
|
||||
|
||||
if(data[0] != 'D' || data[1] != 'C' || data[2] != 'T' || data[3] != 'S') {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(loading, "Invalid cutscene file header");
|
||||
}
|
||||
|
||||
size_t offset = 4;
|
||||
uint32_t version = assetCutsceneReadU32(data, &offset);
|
||||
if(version != ASSET_CUTSCENE_FILE_VERSION) {
|
||||
memoryFree(data);
|
||||
assetLoaderErrorThrow(
|
||||
loading, "Unsupported cutscene file version %u", version
|
||||
);
|
||||
}
|
||||
|
||||
cutscenepause_t pauseType = (cutscenepause_t)assetCutsceneReadU8(data, &offset);
|
||||
uint8_t itemCount = assetCutsceneReadU8(data, &offset);
|
||||
uint16_t poolSize = assetCutsceneReadU16(data, &offset);
|
||||
assertTrue(offset == ASSET_CUTSCENE_HEADER_SIZE, "Cutscene header size mismatch");
|
||||
|
||||
out->pool = poolSize > 0 ? memoryAllocate(poolSize) : NULL;
|
||||
if(poolSize > 0) {
|
||||
size_t poolStart = fileSize - (size_t)poolSize;
|
||||
memoryCopy(out->pool, data + poolStart, poolSize);
|
||||
}
|
||||
const char_t *pool = out->pool;
|
||||
|
||||
out->items = memoryAllocate(itemCount * sizeof(cutsceneitem_t));
|
||||
memoryZero(out->items, itemCount * sizeof(cutsceneitem_t));
|
||||
|
||||
for(uint8_t i = 0; i < itemCount; i++) {
|
||||
cutsceneitem_t *item = &out->items[i];
|
||||
item->type = (cutsceneitemtype_t)assetCutsceneReadU8(data, &offset);
|
||||
|
||||
switch(item->type) {
|
||||
case CUTSCENE_ITEM_TYPE_TEXT:
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->text.text, CUTSCENE_TEXT_MAX_CHARS
|
||||
);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI:
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->textMini.text, CUTSCENE_TEXT_MINI_MAX_CHARS
|
||||
);
|
||||
item->textMini.position[0] = assetCutsceneReadFloat(data, &offset);
|
||||
item->textMini.position[1] = assetCutsceneReadFloat(data, &offset);
|
||||
item->textMini.position[2] = assetCutsceneReadFloat(data, &offset);
|
||||
item->textMini.duration = assetCutsceneReadFloat(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_TEXT_MINI_HIDE:
|
||||
item->textMiniHide.index = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_WAIT:
|
||||
item->wait = assetCutsceneReadFloat(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
|
||||
item->entityTeleport.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->entityTeleport.target = assetCutsceneReadWorldPos(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: {
|
||||
item->entityWalkTo.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->entityWalkTo.walkAround = assetCutsceneReadU8(data, &offset) != 0;
|
||||
uint8_t count = assetCutsceneReadU8(data, &offset);
|
||||
uint16_t poolOffset = assetCutsceneReadU16(data, &offset);
|
||||
item->entityWalkTo.count = count;
|
||||
item->entityWalkTo.positions = (const worldpos_t *)(pool + poolOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_FADE:
|
||||
item->fade.from.r = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.from.g = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.from.b = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.from.a = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.r = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.g = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.b = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.to.a = assetCutsceneReadU8(data, &offset);
|
||||
item->fade.duration = assetCutsceneReadFloat(data, &offset);
|
||||
item->fade.easing = (easingtype_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SET_PAUSE:
|
||||
item->setPause = (cutscenepause_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ITEM_GIVE:
|
||||
item->itemGive.item = (itemid_t)assetCutsceneReadU16(data, &offset);
|
||||
item->itemGive.quantity = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_REMOVE:
|
||||
item->entityRemove.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
|
||||
item->entityAdd.entityType = assetCutsceneReadU8(data, &offset);
|
||||
item->entityAdd.position = assetCutsceneReadWorldPos(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
|
||||
item->entityTurn.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->entityTurn.direction =
|
||||
(entitydir_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO_ENTITY:
|
||||
item->entityWalkToEntity.entityIndex =
|
||||
assetCutsceneReadU8(data, &offset);
|
||||
item->entityWalkToEntity.targetEntityIndex =
|
||||
assetCutsceneReadU8(data, &offset);
|
||||
item->entityWalkToEntity.offsetX =
|
||||
assetCutsceneReadWorldUnit(data, &offset);
|
||||
item->entityWalkToEntity.offsetY =
|
||||
assetCutsceneReadWorldUnit(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
|
||||
item->mapAreaRemove.areaId = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MAP_AREA_WAIT: {
|
||||
uint8_t count = assetCutsceneReadU8(data, &offset);
|
||||
uint16_t poolOffset = assetCutsceneReadU16(data, &offset);
|
||||
assertTrue(
|
||||
count <= CUTSCENE_MAP_AREA_WAIT_MAX,
|
||||
"Cutscene map area wait count exceeds maximum"
|
||||
);
|
||||
item->mapAreaWait.count = count;
|
||||
item->mapAreaWait.areaIds = (const uint8_t *)(pool + poolOffset);
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_START_BATTLE: {
|
||||
item->startBattle.encounterType =
|
||||
(battleencountertype_t)assetCutsceneReadU8(data, &offset);
|
||||
item->startBattle.fleeAvailable =
|
||||
assetCutsceneReadU8(data, &offset) != 0;
|
||||
uint8_t enemyCount = assetCutsceneReadU8(data, &offset);
|
||||
assertTrue(
|
||||
enemyCount <= CUTSCENE_START_BATTLE_ENEMY_COUNT_MAX,
|
||||
"Cutscene battle enemy count exceeds maximum"
|
||||
);
|
||||
item->startBattle.enemyCount = enemyCount;
|
||||
for(uint8_t e = 0; e < enemyCount; e++) {
|
||||
cutscenestartbattleenemy_t *enemy = &item->startBattle.enemies[e];
|
||||
enemy->stats.attack = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.defense = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.magic = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.speed = assetCutsceneReadU16(data, &offset);
|
||||
enemy->stats.luck = assetCutsceneReadU16(data, &offset);
|
||||
enemy->healthMax = assetCutsceneReadU16(data, &offset);
|
||||
enemy->mpMax = assetCutsceneReadU16(data, &offset);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_EMOJI:
|
||||
item->emoji.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->emoji.duration = assetCutsceneReadFloat(data, &offset);
|
||||
item->emoji.emojiType = (uiemojitype_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SHAKE:
|
||||
item->shake.amount = assetCutsceneReadU8(data, &offset);
|
||||
item->shake.duration = assetCutsceneReadFloat(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE:
|
||||
item->battleWaitState.state =
|
||||
(battlestate_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION:
|
||||
item->battleForceAction.fighterIndex = assetCutsceneReadU8(data, &offset);
|
||||
item->battleForceAction.targetIndex = assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MODAL: {
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS
|
||||
);
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modal.message, CUTSCENE_MODAL_MESSAGE_MAX_CHARS
|
||||
);
|
||||
// v1 only supports the message-only form: MODAL and MODAL_OPTIONS
|
||||
// share this same tag with no separate discriminator, and there is
|
||||
// no native-callback registry yet to resolve an options callback.
|
||||
// Read into a local first (not inline in the check below) - on a
|
||||
// release build with DUSK_ASSERTIONS_FAKED, an assert's condition
|
||||
// is never evaluated at all, so a byte-consuming call inside one
|
||||
// would silently desync every item after it. This is untrusted
|
||||
// file content anyway, so it gets a real error, not an assert.
|
||||
uint8_t optionCount = assetCutsceneReadU8(data, &offset);
|
||||
if(optionCount != 0) {
|
||||
memoryFree(data);
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene MODAL item with options is not supported in "
|
||||
"file-based cutscenes yet - use MODAL_OPTIONS_MARKERS instead"
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS: {
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modalOptionsMarkers.title,
|
||||
CUTSCENE_MODAL_TITLE_MAX_CHARS
|
||||
);
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->modalOptionsMarkers.message,
|
||||
CUTSCENE_MODAL_MESSAGE_MAX_CHARS
|
||||
);
|
||||
uint8_t optionCount = assetCutsceneReadU8(data, &offset);
|
||||
assertTrue(
|
||||
optionCount <= CUTSCENE_MODAL_OPTIONS_MARKERS_MAX,
|
||||
"Cutscene modal option count exceeds maximum"
|
||||
);
|
||||
item->modalOptionsMarkers.optionCount = optionCount;
|
||||
for(uint8_t o = 0; o < optionCount; o++) {
|
||||
item->modalOptionsMarkers.options[o] =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
item->modalOptionsMarkers.markers[o] =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MODAL_CLOSE:
|
||||
case CUTSCENE_ITEM_TYPE_RESTART:
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_PRINT:
|
||||
assetCutsceneReadEmbeddedString(
|
||||
data, &offset, item->print.text, CUTSCENE_PRINT_MAX_CHARS
|
||||
);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_MARKER:
|
||||
item->marker.name = assetCutsceneReadPoolString(data, &offset, pool);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SCENE:
|
||||
item->sceneChange.type = (scenetype_t)assetCutsceneReadU8(data, &offset);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK:
|
||||
item->saveDeviceCheck.successMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
item->saveDeviceCheck.failureMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
break;
|
||||
|
||||
case CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS:
|
||||
item->saveLoadAllSlots.successMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
item->saveLoadAllSlots.failureMarker =
|
||||
assetCutsceneReadPoolString(data, &offset, pool);
|
||||
break;
|
||||
|
||||
default:
|
||||
memoryFree(data);
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
assetLoaderErrorThrow(
|
||||
loading,
|
||||
"Cutscene item type %u is not supported in file-based cutscenes "
|
||||
"(item %u/%u, offset %u/%u, poolSize %u)",
|
||||
(uint32_t)item->type, (uint32_t)i, (uint32_t)itemCount,
|
||||
(uint32_t)offset, (uint32_t)fileSize, (uint32_t)poolSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.cutscene.data = NULL;
|
||||
|
||||
out->cutscene.items = out->items;
|
||||
out->cutscene.itemCount = itemCount;
|
||||
out->cutscene.pause = pauseType;
|
||||
out->cutscene.dataSize = 0;
|
||||
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry) {
|
||||
assertNotNull(entry, "Entry cannot be NULL");
|
||||
assertTrue(entry->type == ASSET_LOADER_TYPE_CUTSCENE, "Invalid type.");
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetcutsceneoutput_t *out = &entry->data.cutscene;
|
||||
|
||||
if(out->items != NULL) {
|
||||
memoryFree(out->items);
|
||||
out->items = NULL;
|
||||
}
|
||||
|
||||
if(out->pool != NULL) {
|
||||
memoryFree(out->pool);
|
||||
out->pool = NULL;
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/cutscene/cutscene.h"
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
#define ASSET_CUTSCENE_FILE_VERSION 1
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
} assetcutsceneloaderinput_t;
|
||||
|
||||
typedef enum {
|
||||
ASSET_CUTSCENE_LOADING_STATE_INITIAL,
|
||||
ASSET_CUTSCENE_LOADING_STATE_READ_FILE,
|
||||
ASSET_CUTSCENE_LOADING_STATE_PARSE
|
||||
} assetcutsceneloadingstate_t;
|
||||
|
||||
typedef struct {
|
||||
assetfile_t file;
|
||||
assetcutsceneloadingstate_t state;
|
||||
uint8_t *data;
|
||||
size_t dataSize;// Saved before assetFileDispose zeroes file.size.
|
||||
} assetcutsceneloaderloading_t;
|
||||
|
||||
// Runtime-loaded cutscene: items/pool are heap-allocated to the file's
|
||||
// actual declared sizes (not fixed-capacity), so an entry that never holds
|
||||
// a cutscene costs nothing extra in the shared assetloaderoutput_t union -
|
||||
// see assetchunkoutput_t.tiles for the same pattern.
|
||||
typedef struct {
|
||||
cutscene_t cutscene; // .items points at the items array below
|
||||
cutsceneitem_t *items;
|
||||
char_t *pool;
|
||||
} assetcutsceneoutput_t;
|
||||
|
||||
/**
|
||||
* Asynchronous loader for cutscene assets. Reads the raw DCTS file bytes
|
||||
* into the loading buffer so the sync phase can parse without blocking the
|
||||
* main thread on I/O.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderAsync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Synchronous loader for cutscene assets. Validates the DCTS binary
|
||||
* previously read by the async phase and decodes it into a heap-allocated
|
||||
* cutsceneitem_t array + string/data pool.
|
||||
*
|
||||
* @param loading Loading information for the asset being loaded.
|
||||
* @return Error code indicating success or failure of the load operation.
|
||||
*/
|
||||
errorret_t assetCutsceneLoaderSync(assetloading_t *loading);
|
||||
|
||||
/**
|
||||
* Disposer for cutscene assets.
|
||||
*
|
||||
* @param entry Asset entry containing the cutscene data to dispose.
|
||||
* @return Error code indicating success or failure of the dispose operation.
|
||||
*/
|
||||
errorret_t assetCutsceneDispose(assetentry_t *entry);
|
||||
@@ -17,7 +17,8 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = false;
|
||||
// CONSOLE.visible = false;
|
||||
CONSOLE.visible = true;
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "dusk.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
#define CONSOLE_LINE_MAX 512
|
||||
#define CONSOLE_LINE_MAX 128
|
||||
#define CONSOLE_HISTORY_MAX 16
|
||||
#define CONSOLE_EXEC_BUFFER_MAX 32
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ errorret_t spriteBatchBuffer(
|
||||
|
||||
// Buffer to the mesh vertices.
|
||||
spriteBatchBufferToMesh(
|
||||
sprites, batchCount, v, batchCount * QUAD_VERTEX_COUNT
|
||||
sprites + (count - remaining), batchCount, v, batchCount * QUAD_VERTEX_COUNT
|
||||
);
|
||||
SPRITEBATCH.spriteCount += batchCount;
|
||||
remaining -= batchCount;
|
||||
|
||||
@@ -76,11 +76,23 @@ const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
||||
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
||||
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // backslash (not drawn in source font)
|
||||
// Custom icon glyph, not a real backslash - a backspace symbol: Caps
|
||||
// Lock's up arrow (see FONT_ICON_CAPSLOCK) rotated 270 degrees to
|
||||
// point left instead, adapted to this font's fixed 6x10 tile (each of
|
||||
// Caps Lock's row widths becomes a column height here, centered
|
||||
// vertically). Backslash was never drawn anyway, and isn't a key this
|
||||
// virtual keyboard can type - see FONT_ICON_BACKSPACE.
|
||||
{ 0x00, 0x00, 0x08, 0x1F, 0x3F, 0x3F, 0x1F, 0x08, 0x00, 0x00 }, // FONT_ICON_BACKSPACE
|
||||
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // _ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ` (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E }, // _
|
||||
// Custom icon glyph, not a real backtick - an up arrow for Shift, same
|
||||
// head as Caps Lock's (see FONT_ICON_CAPSLOCK) but with a shaft 2px
|
||||
// thinner, plus one empty row near the bottom of it, so it reads as a
|
||||
// lighter/broken version of Caps Lock's thicker uninterrupted one.
|
||||
// Backtick was never drawn anyway, and isn't a key this virtual
|
||||
// keyboard can type - see FONT_ICON_SHIFT.
|
||||
{ 0x0C, 0x1E, 0x3F, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x0C, 0x00 }, // FONT_ICON_SHIFT
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
||||
@@ -108,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, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
||||
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // | (not drawn in source font)
|
||||
// Custom icon glyph, not a real pipe - a "return" arrow for the
|
||||
// keyboard's newline key: a vertical riser down the right side that
|
||||
// hooks left into a leftward-pointing arrowhead, i.e. "<-|" rotated
|
||||
// into an L. Pipe was never drawn anyway, and isn't a key this
|
||||
// virtual keyboard can type - see FONT_ICON_NEWLINE.
|
||||
{ 0x02, 0x02, 0x02, 0x02, 0x0E, 0x1E, 0x08, 0x00, 0x00, 0x00 }, // FONT_ICON_NEWLINE
|
||||
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ~ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||
// Custom icon glyph, not a real tilde - a spacebar symbol for the
|
||||
// keyboard's space key: an underscore with a tick at each end, like
|
||||
// "|___|". Tilde was never drawn anyway, and was explicitly dropped
|
||||
// from this virtual keyboard's own key set - see FONT_ICON_SPACE.
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x3F }, // FONT_ICON_SPACE
|
||||
// Custom icon glyph, not a real ASCII character - a thick, solid,
|
||||
// uninterrupted up arrow for Caps Lock (contrast FONT_ICON_SHIFT's
|
||||
// same head but a thinner, gapped shaft). Assigned to char code 127
|
||||
// (DEL) since that codepoint is never legitimately typed text and
|
||||
// (unlike 128+) is still a positive value regardless of whether this
|
||||
// platform's plain `char` is signed - see FONT_ICON_CAPSLOCK.
|
||||
{ 0x0C, 0x1E, 0x3F, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x00 }, // FONT_ICON_CAPSLOCK
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||
};
|
||||
|
||||
|
||||
@@ -28,10 +28,45 @@ typedef struct {
|
||||
/**
|
||||
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
||||
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
||||
* TEXT_CHAR_START ('!') plus a couple of unused trailing tiles.
|
||||
* TEXT_CHAR_START ('!'), one custom icon glyph in a trailing unused tile
|
||||
* (see FONT_ICON_CAPSLOCK), plus one more genuinely unused trailing tile.
|
||||
* FONT_ICON_SHIFT/FONT_ICON_NEWLINE reuse existing-but-blank slots within
|
||||
* the printable range instead of more trailing tiles - see their own
|
||||
* comments.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
||||
|
||||
/**
|
||||
* Custom (non-ASCII-meaning) icon glyphs baked into FONT_DEFAULT_GLYPHS
|
||||
* at otherwise-unused codepoints within the range this font covers -
|
||||
* safe to use anywhere a char_t string is expected, e.g. a uibutton_t
|
||||
* label. Kept below 128: char_t is a plain `char`, whose signedness
|
||||
* varies by platform, so a codepoint of 128 or above isn't safely
|
||||
* representable everywhere this engine targets.
|
||||
*/
|
||||
// Thick, solid, uninterrupted up arrow - Caps Lock. A trailing tile
|
||||
// (char code 127/DEL) that was never a real character to begin with.
|
||||
#define FONT_ICON_CAPSLOCK "\x7F"
|
||||
// Same up arrow, but with a thinner and gapped shaft - Shift. Reuses
|
||||
// the backtick's glyph slot: backtick was never drawn by this font
|
||||
// anyway, and isn't a key this engine's virtual keyboard can type.
|
||||
#define FONT_ICON_SHIFT "`"
|
||||
// A "return" arrow (down then left, with a leftward arrowhead) - the
|
||||
// keyboard's newline key. Reuses the pipe's glyph slot, for the same
|
||||
// reason as FONT_ICON_SHIFT.
|
||||
#define FONT_ICON_NEWLINE "|"
|
||||
// An underscore with a tick at each end ("|___|") - the keyboard's
|
||||
// space key. Reuses the tilde's glyph slot: tilde was never drawn by
|
||||
// this font, and was explicitly dropped from this engine's virtual
|
||||
// keyboard's own key set.
|
||||
#define FONT_ICON_SPACE "~"
|
||||
// Caps Lock's arrow rotated to point left - the keyboard's backspace
|
||||
// key. Reuses the backslash's glyph slot, for the same reason as
|
||||
// FONT_ICON_SHIFT/FONT_ICON_NEWLINE/FONT_ICON_SPACE. Last free reused
|
||||
// slot below 128 - one more custom icon after this needs
|
||||
// FONT_DEFAULT_COLUMNS/ROWS grown to make room.
|
||||
#define FONT_ICON_BACKSPACE "\\"
|
||||
|
||||
extern font_t FONT_DEFAULT;
|
||||
|
||||
/**
|
||||
|
||||
+113
-21
@@ -54,6 +54,62 @@ spritebatchsprite_t textGetSprite(
|
||||
return sprite;
|
||||
}
|
||||
|
||||
int32_t textBuffer(
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const char_t *text,
|
||||
font_t *font,
|
||||
spritebatchsprite_t *outSprites,
|
||||
const int32_t maxSprites,
|
||||
int32_t *charIndex,
|
||||
float_t *posX,
|
||||
float_t *posY
|
||||
) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
|
||||
if(outSprites == NULL) {
|
||||
int32_t count = 0;
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
while((c = text[i++]) != '\0') {
|
||||
if(c != ' ' && c != '\n') count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
assertNotNull(font, "Font cannot be NULL");
|
||||
assertTrue(maxSprites > 0, "Max sprites must be greater than zero");
|
||||
assertNotNull(posX, "Output posX cannot be NULL");
|
||||
assertNotNull(posY, "Output posY cannot be NULL");
|
||||
assertNotNull(charIndex, "Output charIndex cannot be NULL");
|
||||
|
||||
int32_t spriteIndex = 0;
|
||||
char_t c;
|
||||
for(;;) {
|
||||
c = text[*charIndex];
|
||||
if(c == '\0') break;
|
||||
(*charIndex)++;
|
||||
|
||||
if(c == '\n') {
|
||||
*posX = x;
|
||||
*posY += font->tileset->tileHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(c == ' ') {
|
||||
*posX += font->tileset->tileWidth;
|
||||
continue;
|
||||
}
|
||||
|
||||
outSprites[spriteIndex++] = textGetSprite((vec2){*posX, *posY}, c, font);
|
||||
*posX += font->tileset->tileWidth;
|
||||
|
||||
if(spriteIndex >= maxSprites) break;
|
||||
}
|
||||
|
||||
return spriteIndex;
|
||||
}
|
||||
|
||||
errorret_t textDraw(
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
@@ -62,42 +118,37 @@ errorret_t textDraw(
|
||||
font_t *font
|
||||
) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
int32_t length = strlen(text);
|
||||
if(length == 0) errorOk();
|
||||
|
||||
if(font == NULL) font = &FONT_DEFAULT;
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = color,
|
||||
.texture = font->texture
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
spritebatchsprite_t sprites[32];
|
||||
float_t posX = x;
|
||||
float_t posY = y;
|
||||
int32_t buffered = 0;
|
||||
int32_t charIndex = 0;
|
||||
do {
|
||||
buffered = textBuffer(
|
||||
x, y, text, font,
|
||||
sprites,
|
||||
sizeof(sprites) / sizeof(spritebatchsprite_t),
|
||||
&charIndex, &posX, &posY
|
||||
);
|
||||
errorChain(spriteBatchBuffer(sprites, buffered, &SHADER_UNLIT, material));
|
||||
} while(charIndex < length);
|
||||
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
while((c = text[i++]) != '\0') {
|
||||
if(c == '\n') {
|
||||
posX = x;
|
||||
posY += font->tileset->tileHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(c == ' ') {
|
||||
posX += font->tileset->tileWidth;
|
||||
continue;
|
||||
}
|
||||
|
||||
sprite = textGetSprite((vec2){posX, posY}, c, font);
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
posX += font->tileset->tileWidth;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void textMeasure(
|
||||
int32_t textMeasure(
|
||||
const char_t *text,
|
||||
const font_t *font,
|
||||
int32_t *outWidth,
|
||||
@@ -110,6 +161,7 @@ void textMeasure(
|
||||
int32_t width = 0;
|
||||
int32_t height = font->tileset->tileHeight;
|
||||
int32_t lineWidth = 0;
|
||||
int32_t spriteCount = 0;
|
||||
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
@@ -122,10 +174,50 @@ void textMeasure(
|
||||
}
|
||||
|
||||
lineWidth += font->tileset->tileWidth;
|
||||
|
||||
if(c != ' ') {
|
||||
spriteCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(lineWidth > width) width = lineWidth;
|
||||
|
||||
*outWidth = width;
|
||||
*outHeight = height;
|
||||
|
||||
return spriteCount;
|
||||
}
|
||||
|
||||
void textWrap(char_t *text, const font_t *font, const float_t maxWidth) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
assertNotNull(font, "Font cannot be NULL");
|
||||
|
||||
float_t fontWidth = (float_t)font->tileset->tileWidth;
|
||||
if(fontWidth <= 0.0f) return;
|
||||
|
||||
int32_t charsPerLine = (int32_t)(maxWidth / fontWidth);
|
||||
if(charsPerLine <= 0) return;
|
||||
|
||||
int32_t lineWidth = 0;
|
||||
int32_t lastSpace = -1;
|
||||
|
||||
for(int32_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '\n') {
|
||||
lineWidth = 0;
|
||||
lastSpace = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(text[i] == ' ') {
|
||||
lastSpace = i;
|
||||
}
|
||||
|
||||
lineWidth++;
|
||||
|
||||
if(lineWidth > charsPerLine && lastSpace != -1) {
|
||||
text[lastSpace] = '\n';
|
||||
lineWidth = i - lastSpace;
|
||||
lastSpace = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,37 @@ spritebatchsprite_t textGetSprite(
|
||||
const font_t *font
|
||||
);
|
||||
|
||||
/**
|
||||
* Buffers a string into sprites for rendering. If outSprites is NULL then
|
||||
* the function will only return the count of sprites necessary for the buffer.
|
||||
*
|
||||
* posX and posY are updated whilst buffering characters, if you need to do
|
||||
* buffering in sets then these will be reusable between buffer commands. Start
|
||||
* by setting these to x and y initially.
|
||||
*
|
||||
* @param x The x-coordinate to start buffering the text at.
|
||||
* @param y The y-coordinate to start buffering the text at.
|
||||
* @param text The null-terminated string of text to buffer.
|
||||
* @param font Font to use for rendering.
|
||||
* @param outSprites Pointer to an array of spritebatchsprite_t.
|
||||
* @param maxSprites The maximum number of sprites in outSprites.
|
||||
* @param charIndex Pointer to an int32_t to store the character indexed.
|
||||
* @param posX Pointer to a float_t to store the final x position.
|
||||
* @param posY Pointer to a float_t to store the final y position.
|
||||
* @return The count of sprites buffered.
|
||||
*/
|
||||
int32_t textBuffer(
|
||||
const float_t x,
|
||||
const float_t y,
|
||||
const char_t *text,
|
||||
font_t *font,
|
||||
spritebatchsprite_t *outSprites,
|
||||
const int32_t maxSprites,
|
||||
int32_t *charIndex,
|
||||
float_t *posX,
|
||||
float_t *posY
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws a string of text at the specified position.
|
||||
*
|
||||
@@ -65,10 +96,25 @@ errorret_t textDraw(
|
||||
* @param font Font to use for measurement.
|
||||
* @param outWidth Pointer to store the measured width in pixels.
|
||||
* @param outHeight Pointer to store the measured height in pixels.
|
||||
* @return The count of sprites that will be rendered for the given text.
|
||||
*/
|
||||
void textMeasure(
|
||||
int32_t textMeasure(
|
||||
const char_t *text,
|
||||
const font_t *font,
|
||||
int32_t *outWidth,
|
||||
int32_t *outHeight
|
||||
);
|
||||
|
||||
/**
|
||||
* Word-wraps text in place for display at up to maxWidth pixels wide,
|
||||
* by replacing the space nearest each overflow point with a newline.
|
||||
* Length is unchanged - this only ever swaps existing spaces for
|
||||
* newlines, never inserts characters - so it's always safe to call on a
|
||||
* fixed-size buffer. A single word wider than maxWidth on its own is
|
||||
* left unbroken.
|
||||
*
|
||||
* @param text Null-terminated, caller-owned buffer to wrap in place.
|
||||
* @param font Font to measure character width with.
|
||||
* @param maxWidth Maximum line width, in pixels.
|
||||
*/
|
||||
void textWrap(char_t *text, const font_t *font, const float_t maxWidth);
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
#include "asset/asset.h"
|
||||
#include "ui/ui.h"
|
||||
#include "assert/assert.h"
|
||||
#include "network/network.h"
|
||||
#ifdef DUSK_NETWORK
|
||||
#include "network/network.h"
|
||||
#endif
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
#include "save/save.h"
|
||||
#include "save/autosave.h"
|
||||
#include "save/save.h"\
|
||||
|
||||
engine_t ENGINE;
|
||||
|
||||
@@ -43,7 +44,9 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
errorChain(networkInit());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkInit());
|
||||
#endif
|
||||
errorChain(sceneInit());
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
@@ -61,9 +64,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
|
||||
errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
errorChain(networkUpdate());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkUpdate());
|
||||
#endif
|
||||
errorChain(saveUpdate());
|
||||
autoSaveUpdate();
|
||||
timeUpdate();
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
@@ -84,7 +88,9 @@ void engineExit(void) {
|
||||
|
||||
errorret_t engineDispose(void) {
|
||||
errorChain(sceneDispose());
|
||||
errorChain(networkDispose());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkDispose());
|
||||
#endif
|
||||
errorChain(rpgDispose());
|
||||
localeManagerDispose();
|
||||
errorChain(uiDispose());
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "event.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void eventInit(
|
||||
event_t *event,
|
||||
eventcallback_t *callbacks,
|
||||
void **users,
|
||||
size_t size
|
||||
) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
assertNotNull((void *)callbacks, "callbacks must not be NULL");
|
||||
assertTrue(size > 0, "size must be greater than 0");
|
||||
|
||||
event->callbacks = callbacks;
|
||||
event->users = users;
|
||||
event->size = size;
|
||||
event->count = 0;
|
||||
memoryZero(callbacks, sizeof(eventcallback_t) * size);
|
||||
if(users) memoryZero(users, sizeof(void *) * size);
|
||||
}
|
||||
|
||||
void eventSubscribe(event_t *event, eventcallback_t callback, void *user) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
assertNotNull(callback, "callback must not be NULL");
|
||||
|
||||
// Ensure callback isn't already susbcribed
|
||||
for(uint32_t i = 0; i < event->count; i++) {
|
||||
if(event->callbacks[i] != callback) continue;
|
||||
assertUnreachable("Callback already registered, cannot subscribe twice.");
|
||||
}
|
||||
|
||||
assertTrue(event->count < event->size, "event subscriber capacity exceeded");
|
||||
|
||||
event->callbacks[event->count] = callback;
|
||||
if(user) {
|
||||
assertNotNull(event->users, "Cannot add user pointer.");
|
||||
event->users[event->count] = user;
|
||||
}
|
||||
event->count++;
|
||||
}
|
||||
|
||||
void eventUnsubscribe(event_t *event, eventcallback_t callback) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
assertNotNull(callback, "callback must not be NULL");
|
||||
|
||||
for(uint32_t i = 0; i < event->count; i++) {
|
||||
if(event->callbacks[i] != callback) continue;
|
||||
|
||||
uint32_t last = event->count - 1;
|
||||
if(i != last) {
|
||||
event->callbacks[i] = event->callbacks[last];
|
||||
if(event->users) event->users[i] = event->users[last];
|
||||
}
|
||||
event->callbacks[last] = NULL;
|
||||
if(event->users) event->users[last] = NULL;
|
||||
event->count--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void eventInvoke(const event_t *event, void *params) {
|
||||
assertNotNull(event, "event must not be NULL");
|
||||
|
||||
for(uint32_t i = 0; i < event->count; i++) {
|
||||
void *u = event->users ? event->users[i] : NULL;
|
||||
event->callbacks[i](params, u);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef void (*eventcallback_t)(void *params, void *user);
|
||||
|
||||
typedef struct {
|
||||
eventcallback_t *callbacks;
|
||||
void **users;
|
||||
size_t size;
|
||||
uint32_t count;
|
||||
} event_t;
|
||||
|
||||
/**
|
||||
* Initializes an event, binding it to the provided backing arrays and clearing
|
||||
* all subscribers. May also be called to reset an event (re-clears subscribers
|
||||
* without changing the backing arrays or size).
|
||||
*
|
||||
* @param event The event to initialize.
|
||||
* @param callbacks Caller-owned array of at least `size` callback slots.
|
||||
* @param users Array of user pointers, matching each callback, or NULL.
|
||||
* @param size Capacity of both arrays, must match.
|
||||
*/
|
||||
void eventInit(
|
||||
event_t *event,
|
||||
eventcallback_t *callbacks,
|
||||
void **users,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscribes a callback to an event. The callback is invoked with params and
|
||||
* the provided user pointer each time the event fires. The same (callback,
|
||||
* user) pair may only be subscribed once.
|
||||
*
|
||||
* @param event The event to subscribe to.
|
||||
* @param callback The function to call when the event fires.
|
||||
* @param user Arbitrary pointer forwarded to the callback unchanged.
|
||||
*/
|
||||
void eventSubscribe(event_t *event, eventcallback_t callback, void *user);
|
||||
|
||||
/**
|
||||
* Removes a previously subscribed (callback, user) pair. Does nothing if the
|
||||
* pair is not currently subscribed.
|
||||
*
|
||||
* @param event The event to unsubscribe from.
|
||||
* @param callback The callback that was passed to eventSubscribe.
|
||||
*/
|
||||
void eventUnsubscribe(event_t *event, eventcallback_t callback);
|
||||
|
||||
/**
|
||||
* Invokes all subscribed callbacks, passing params and each subscriber's user
|
||||
* pointer.
|
||||
*
|
||||
* @param event The event to invoke.
|
||||
* @param params Arbitrary pointer forwarded to every callback unchanged.
|
||||
*/
|
||||
void eventInvoke(const event_t *event, void *params);
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "util/string.h"
|
||||
#include "util/math.h"
|
||||
#include "time/time.h"
|
||||
#include "event/event.h"
|
||||
|
||||
input_t INPUT;
|
||||
|
||||
@@ -22,20 +21,6 @@ errorret_t inputInit(void) {
|
||||
INPUT.actions[i].action = (inputaction_t)i;
|
||||
INPUT.actions[i].lastValue = 0.0f;
|
||||
INPUT.actions[i].currentValue = 0.0f;
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onPressed,
|
||||
INPUT.actions[i].onPressedCallbacks,
|
||||
INPUT.actions[i].onPressedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
|
||||
eventInit(
|
||||
&INPUT.actions[i].onReleased,
|
||||
INPUT.actions[i].onReleasedCallbacks,
|
||||
INPUT.actions[i].onReleasedUsers,
|
||||
INPUT_ACTION_CALLBACK_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
#ifdef inputInitPlatform
|
||||
@@ -106,8 +91,6 @@ void inputUpdate(void) {
|
||||
inputactiondata_t *act = &INPUT.actions[i];
|
||||
bool_t isDown = act->currentValue > 0.0f;
|
||||
bool_t wasDown = act->lastValue > 0.0f;
|
||||
if(isDown && !wasDown) eventInvoke(&act->onPressed, act);
|
||||
if(!isDown && wasDown) eventInvoke(&act->onReleased, act);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,8 @@
|
||||
#include "inputbutton.h"
|
||||
#include "inputaction.h"
|
||||
|
||||
#define INPUT_LISTENER_PRESSED_MAX 16
|
||||
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
||||
|
||||
typedef struct {
|
||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||
|
||||
inputplatform_t platform;
|
||||
} input_t;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#pragma once
|
||||
#include "time/time.h"
|
||||
#include "input/inputactiondefs.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#define INPUT_ACTION_CALLBACK_COUNT_MAX 4
|
||||
|
||||
@@ -21,13 +20,6 @@ typedef struct {
|
||||
float_t lastDynamicValue;
|
||||
float_t currentDynamicValue;
|
||||
#endif
|
||||
|
||||
eventcallback_t onPressedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onPressedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onPressed;
|
||||
eventcallback_t onReleasedCallbacks[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
void *onReleasedUsers[INPUT_ACTION_CALLBACK_COUNT_MAX];
|
||||
event_t onReleased;
|
||||
} inputactiondata_t;
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,17 +13,31 @@ typedef struct {
|
||||
const char_t *file;
|
||||
} localeinfo_t;
|
||||
|
||||
static const localeinfo_t LOCALE_EN_US = {
|
||||
static const localeinfo_t LOCALE_INFO_EN_US = {
|
||||
.name = "en-US",
|
||||
.file = "locale/en_US.po",
|
||||
};
|
||||
|
||||
static const localeinfo_t LOCALE_JP_JP = {
|
||||
static const localeinfo_t LOCALE_INFO_JP_JP = {
|
||||
.name = "ja-JP",
|
||||
.file = "locale/jp_JP.po",
|
||||
};
|
||||
|
||||
static const localeinfo_t LOCALE_ES_MX = {
|
||||
static const localeinfo_t LOCALE_INFO_ES_MX = {
|
||||
.name = "es-MX",
|
||||
.file = "locale/es_MX.po",
|
||||
};
|
||||
};
|
||||
|
||||
static const localeinfo_t * const LOCALE_INFO_LIST[] = {
|
||||
&LOCALE_INFO_EN_US,
|
||||
&LOCALE_INFO_JP_JP,
|
||||
&LOCALE_INFO_ES_MX
|
||||
};
|
||||
|
||||
#define LOCALE_INFO_LIST_COUNT ( \
|
||||
sizeof(LOCALE_INFO_LIST) / sizeof(LOCALE_INFO_LIST[0]) \
|
||||
)
|
||||
|
||||
#define LOCALE_DEFAULT LOCALE_INFO_EN_US
|
||||
|
||||
// EOF
|
||||
|
||||
@@ -9,40 +9,23 @@
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "save/save.h"
|
||||
#include "ui/ui.h"
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
|
||||
localemanager_t LOCALE;
|
||||
|
||||
const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT] = {
|
||||
&LOCALE_EN_US,
|
||||
&LOCALE_JP_JP,
|
||||
&LOCALE_ES_MX
|
||||
};
|
||||
|
||||
errorret_t localeManagerInit() {
|
||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
||||
// saveInit() runs before this (see engine.c) and, on most platforms,
|
||||
// has already loaded a persisted language choice into savemeta_t by
|
||||
// this point - see localeManagerGetByIndex().
|
||||
errorChain(localeManagerSetLocale(
|
||||
localeManagerGetByIndex(saveGetMeta()->language)
|
||||
));
|
||||
|
||||
// TODO: Set locale based on system locale.
|
||||
const localeinfo_t *locale = systemGetLocale();
|
||||
errorChain(localeManagerSetLocale(locale));
|
||||
consolePrint("Locale set to: %s", locale->name);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint8_t localeManagerGetIndex(const localeinfo_t *locale) {
|
||||
assertNotNull(locale, "Locale cannot be NULL");
|
||||
for(uint8_t i = 0; i < LOCALE_LIST_COUNT; i++) {
|
||||
if(stringCompare(LOCALE_LIST[i]->file, locale->file) == 0) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const localeinfo_t * localeManagerGetByIndex(const uint8_t index) {
|
||||
if(index >= LOCALE_LIST_COUNT) return LOCALE_LIST[0];
|
||||
return LOCALE_LIST[index];
|
||||
}
|
||||
|
||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||
assertNotNull(locale, "Locale cannot be NULL");
|
||||
|
||||
@@ -56,6 +39,9 @@ errorret_t localeManagerSetLocale(const localeinfo_t *locale) {
|
||||
assetEntryLock(LOCALE.entry);
|
||||
errorChain(assetRequireLoaded(LOCALE.entry));
|
||||
|
||||
// TODO : Trigger UI update.
|
||||
errorChain(uiUpdateTranslations());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "localemanager.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "locale/localeinfo.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
@@ -18,15 +18,6 @@ typedef struct {
|
||||
|
||||
extern localemanager_t LOCALE;
|
||||
|
||||
/**
|
||||
* Every locale the game supports, in a fixed, stable order - index into
|
||||
* this array is what gets persisted as savemeta_t.language (see
|
||||
* save/savemeta.h), so the order here must never change once shipped
|
||||
* (only append new locales at the end).
|
||||
*/
|
||||
#define LOCALE_LIST_COUNT 3
|
||||
extern const localeinfo_t * const LOCALE_LIST[LOCALE_LIST_COUNT];
|
||||
|
||||
/**
|
||||
* Initialize the locale system.
|
||||
*
|
||||
@@ -42,27 +33,6 @@ errorret_t localeManagerInit();
|
||||
*/
|
||||
errorret_t localeManagerSetLocale(const localeinfo_t *locale);
|
||||
|
||||
/**
|
||||
* Finds the LOCALE_LIST index matching the given locale's file, by
|
||||
* content rather than pointer identity (localeinfo_t instances are
|
||||
* declared `static const` in a header, so the same locale gets a
|
||||
* distinct pointer in every translation unit that references it).
|
||||
*
|
||||
* @param locale The locale to find.
|
||||
* @return The matching index in LOCALE_LIST, or 0 if not found.
|
||||
*/
|
||||
uint8_t localeManagerGetIndex(const localeinfo_t *locale);
|
||||
|
||||
/**
|
||||
* Gets the locale at the given LOCALE_LIST index, clamping to index 0
|
||||
* (LOCALE_EN_US) if out of range - e.g. a save file's persisted language
|
||||
* index from a future build with more locales than this one supports.
|
||||
*
|
||||
* @param index The index to look up.
|
||||
* @return The locale at that index, or LOCALE_LIST[0] if out of range.
|
||||
*/
|
||||
const localeinfo_t * localeManagerGetByIndex(const uint8_t index);
|
||||
|
||||
/**
|
||||
* Get a localized string for the given message ID.
|
||||
*
|
||||
|
||||
@@ -10,3 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(testbattle)
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
savevita.c
|
||||
savestreamvita.c
|
||||
testbattle.c
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "testbattle.h"
|
||||
#include "rpg/battle/battle.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
void testBattleStart(void) {
|
||||
battleInit();
|
||||
|
||||
const battlefighterstats_t allyOneStats =
|
||||
{ .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 };
|
||||
const battlefighterstats_t allyTwoStats =
|
||||
{ .attack = 8, .defense = 4, .magic = 0, .speed = 8, .luck = 0 };
|
||||
const battlefighterstats_t enemyOneStats =
|
||||
{ .attack = 6, .defense = 3, .magic = 0, .speed = 6, .luck = 0 };
|
||||
const battlefighterstats_t enemyTwoStats =
|
||||
{ .attack = 7, .defense = 3, .magic = 0, .speed = 5, .luck = 0 };
|
||||
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
allyOneStats, 30, 10
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
allyTwoStats, 25, 10
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyOneStats, 20, 5
|
||||
);
|
||||
battleAddFighter(
|
||||
BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyTwoStats, 20, 5
|
||||
);
|
||||
|
||||
battleStart(BATTLE_ENCOUNTER_REGULAR, true);
|
||||
sceneSet(SCENE_TYPE_BATTLE);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
/**
|
||||
* TEMPORARY test hook: sets up a hardcoded mock battle and switches to the
|
||||
* battle scene, so the battle scene (camera/fighters/HUD) can be seen and
|
||||
* played without a real encounter trigger yet.
|
||||
*/
|
||||
void testBattleStart(void);
|
||||
@@ -53,6 +53,25 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_WAIT(WAIT) \
|
||||
{ .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) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_CUTSCENE, \
|
||||
@@ -62,6 +81,124 @@ typedef struct cutscene_s {
|
||||
#define CUTSCENE_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) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO, \
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "cutscenesystem.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
@@ -16,11 +17,7 @@ void cutsceneSystemInit() {
|
||||
memoryZero(&CUTSCENE_SYSTEM, sizeof(cutscenesystem_t));
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutscene(const cutscene_t *cutscene) {
|
||||
cutsceneSystemStartCutsceneWith(cutscene, NULL, NULL);
|
||||
}
|
||||
|
||||
void cutsceneSystemStartCutsceneWith(
|
||||
void cutsceneSystemPrepare(
|
||||
const cutscene_t *cutscene,
|
||||
entity_t *interact,
|
||||
entity_t *interacted
|
||||
@@ -38,10 +35,49 @@ void cutsceneSystemStartCutsceneWith(
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_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.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();
|
||||
}
|
||||
|
||||
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() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return;
|
||||
|
||||
@@ -58,6 +94,13 @@ void cutsceneSystemNext() {
|
||||
if(
|
||||
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.currentItem = 0xFF;
|
||||
CUTSCENE_SYSTEM.pause = CUTSCENE_PAUSE_NONE;
|
||||
@@ -66,7 +109,11 @@ void cutsceneSystemNext() {
|
||||
CUTSCENE_SYSTEM.entityLastCreated = NULL;
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -76,6 +123,35 @@ void cutsceneSystemNext() {
|
||||
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() {
|
||||
if(CUTSCENE_SYSTEM.scene == NULL) return NULL;
|
||||
|
||||
@@ -144,6 +220,16 @@ uint8_t cutsceneSystemGetTextMiniId(const uint8_t 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() {
|
||||
CUTSCENE_SYSTEM.scene = NULL;
|
||||
CUTSCENE_SYSTEM.currentItem = 0xFF;
|
||||
@@ -154,4 +240,6 @@ void cutsceneSystemDispose() {
|
||||
CUTSCENE_SYSTEM.entityLastRef = NULL;
|
||||
CUTSCENE_SYSTEM.areaLastCreated = CUTSCENE_AREA_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.
|
||||
#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 {
|
||||
const cutscene_t *scene;
|
||||
uint8_t currentItem;
|
||||
@@ -32,12 +37,21 @@ typedef struct {
|
||||
uint8_t areaLastCreated;
|
||||
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).
|
||||
cutsceneitemdata_t data;
|
||||
|
||||
// Custom user data for the running cutscene, sized per-scene by
|
||||
// cutscene_t.dataSize.
|
||||
uint8_t userData[CUTSCENE_SYSTEM_SIZE_MAX];
|
||||
|
||||
// See cutsceneSystemSetOnComplete.
|
||||
cutscenecallback_t onComplete;
|
||||
} cutscenesystem_t;
|
||||
|
||||
extern cutscenesystem_t CUTSCENE_SYSTEM;
|
||||
@@ -67,6 +81,47 @@ void cutsceneSystemStartCutsceneWith(
|
||||
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.
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutsceneitem.c
|
||||
cutscenecallback.c
|
||||
cutsceneprint.c
|
||||
)
|
||||
|
||||
add_subdirectory(control)
|
||||
@@ -15,3 +16,4 @@ add_subdirectory(item)
|
||||
add_subdirectory(maparea)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(battle)
|
||||
add_subdirectory(save)
|
||||
|
||||
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenewait.c
|
||||
cutscenesetpause.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] = {
|
||||
.init = cutsceneBattleForceActionStart,
|
||||
.update = cutsceneBattleForceActionUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL] = {
|
||||
.init = cutsceneModalStart,
|
||||
.update = cutsceneModalUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
|
||||
.init = cutsceneModalOptionsMarkersStart,
|
||||
.update = cutsceneModalOptionsMarkersUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
|
||||
.init = cutsceneModalCloseStart,
|
||||
.update = cutsceneModalCloseUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_PRINT] = {
|
||||
.init = cutscenePrintStart,
|
||||
.update = cutscenePrintUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MARKER] = {
|
||||
.init = cutsceneMarkerStart,
|
||||
.update = cutsceneMarkerUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_RESTART] = {
|
||||
.init = cutsceneRestartStart,
|
||||
.update = cutsceneRestartUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SCENE] = {
|
||||
.init = cutsceneSceneStart,
|
||||
.update = cutsceneSceneUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
|
||||
.init = cutsceneSaveDeviceCheckStart,
|
||||
.update = cutsceneSaveDeviceCheckUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS] = {
|
||||
.init = cutsceneSaveLoadAllSlotsStart,
|
||||
.update = cutsceneSaveLoadAllSlotsUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_KEYBOARD] = {
|
||||
.init = cutsceneKeyboardStart,
|
||||
.update = cutsceneKeyboardUpdate
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
|
||||
#pragma once
|
||||
#include "cutscenecallback.h"
|
||||
#include "cutsceneprint.h"
|
||||
#include "control/cutscenewait.h"
|
||||
#include "control/cutscenesetpause.h"
|
||||
#include "control/cutsceneconcurrent.h"
|
||||
#include "control/cutscenemarker.h"
|
||||
#include "control/cutscenerestart.h"
|
||||
#include "control/cutscenescene.h"
|
||||
#include "entity/cutsceneentityteleport.h"
|
||||
#include "entity/cutsceneentitywalkto.h"
|
||||
#include "entity/cutsceneentityremove.h"
|
||||
@@ -22,6 +26,9 @@
|
||||
#include "ui/cutscenefade.h"
|
||||
#include "ui/cutsceneemoji.h"
|
||||
#include "ui/cutsceneshake.h"
|
||||
#include "ui/cutscenemodal.h"
|
||||
#include "ui/cutscenemodaloptionsmarkers.h"
|
||||
#include "ui/cutscenekeyboard.h"
|
||||
#include "item/cutsceneitemgive.h"
|
||||
#include "maparea/cutscenemapareaadd.h"
|
||||
#include "maparea/cutscenemaparearemove.h"
|
||||
@@ -29,6 +36,8 @@
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
#include "battle/cutscenebattlewaitstate.h"
|
||||
#include "battle/cutscenebattleforceaction.h"
|
||||
#include "save/cutscenesavedevicecheck.h"
|
||||
#include "save/cutscenesaveloadallslots.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
@@ -59,6 +68,16 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_SHAKE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE,
|
||||
CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION,
|
||||
CUTSCENE_ITEM_TYPE_MODAL,
|
||||
CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS,
|
||||
CUTSCENE_ITEM_TYPE_MODAL_CLOSE,
|
||||
CUTSCENE_ITEM_TYPE_PRINT,
|
||||
CUTSCENE_ITEM_TYPE_MARKER,
|
||||
CUTSCENE_ITEM_TYPE_RESTART,
|
||||
CUTSCENE_ITEM_TYPE_SCENE,
|
||||
CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK,
|
||||
CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS,
|
||||
CUTSCENE_ITEM_TYPE_KEYBOARD,
|
||||
|
||||
CUTSCENE_ITEM_TYPE_COUNT
|
||||
} cutsceneitemtype_t;
|
||||
@@ -91,6 +110,14 @@ struct cutsceneitem_s {
|
||||
cutsceneshake_t shake;
|
||||
cutscenebattlewaitstate_t battleWaitState;
|
||||
cutscenebattleforceaction_t battleForceAction;
|
||||
cutscenemodal_t modal;
|
||||
cutscenemodaloptionsmarkers_t modalOptionsMarkers;
|
||||
cutsceneprint_t print;
|
||||
cutscenemarker_t marker;
|
||||
cutscenescene_t sceneChange;
|
||||
cutscenesavedevicecheck_t saveDeviceCheck;
|
||||
cutscenesaveloadallslots_t saveLoadAllSlots;
|
||||
cutscenekeyboard_t keyboard;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutsceneitem.h"
|
||||
#include "console/console.h"
|
||||
|
||||
void cutscenePrintStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
consolePrint("%s", item->print.text);
|
||||
}
|
||||
|
||||
bool_t cutscenePrintUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
#define CUTSCENE_PRINT_MAX_CHARS 128
|
||||
|
||||
typedef struct {
|
||||
char_t text[CUTSCENE_PRINT_MAX_CHARS];
|
||||
} cutsceneprint_t;
|
||||
|
||||
/**
|
||||
* Starts a print item (prints the item's text to the console).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutscenePrintStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a print item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutscenePrintUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
+2
-5
@@ -5,9 +5,6 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uisettings.c
|
||||
uisettingsgeneral.c
|
||||
uisettingsinput.c
|
||||
uisettingsdisplay.c
|
||||
uisettingsaudio.c
|
||||
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
|
||||
cutsceneemoji.c
|
||||
cutsceneshake.c
|
||||
cutscenemodal.c
|
||||
cutscenemodaloptionsmarkers.c
|
||||
cutscenekeyboard.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "ui/dialog/keyboard/uikeyboard.h"
|
||||
|
||||
void cutsceneKeyboardStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
uiKeyboardOpen(&item->keyboard.open);
|
||||
}
|
||||
|
||||
bool_t cutsceneKeyboardUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
if(uiKeyboardIsOpen()) return false;
|
||||
|
||||
cutsceneSystemSetTextCache(UI_KEYBOARD.text);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "ui/dialog/keyboard/uikeyboard.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
// Passed straight through to uiKeyboardOpen - see uikeyboardopen_t.
|
||||
uikeyboardopen_t open;
|
||||
} cutscenekeyboard_t;
|
||||
|
||||
/**
|
||||
* Starts a keyboard item (opens the on-screen keyboard with the item's
|
||||
* open parameters, unmodified - see uiKeyboardOpen).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneKeyboardStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a keyboard item. Blocks the cutscene while the keyboard is
|
||||
* open; once it closes (confirmed or cancelled), caches its resulting
|
||||
* text via cutsceneSystemSetTextCache (see cutsceneSystemGetTextCache)
|
||||
* before completing, regardless of whether confirm or cancel was
|
||||
* picked - check UI_KEYBOARD.confirmed from the item's own onInput
|
||||
* callback if that distinction matters.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once the keyboard has closed; false while still open.
|
||||
*/
|
||||
bool_t cutsceneKeyboardUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "assert/assert.h"
|
||||
#include "ui/widget/uimodal.h"
|
||||
|
||||
void cutsceneModalStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenemodal_t *modal = &item->modal;
|
||||
assertTrue(
|
||||
modal->optionCount <= CUTSCENE_MODAL_OPTIONS_MAX,
|
||||
"Too many options for cutscene modal"
|
||||
);
|
||||
|
||||
uiModalOpen(
|
||||
modal->title,
|
||||
modal->message,
|
||||
modal->options,
|
||||
modal->optionCount,
|
||||
modal->callback,
|
||||
NULL,
|
||||
CUTSCENE_SYSTEM.userData
|
||||
);
|
||||
}
|
||||
|
||||
bool_t cutsceneModalUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return item->modal.optionCount == 0;
|
||||
}
|
||||
|
||||
void cutsceneModalCloseStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
uiModalClose(NULL);
|
||||
}
|
||||
|
||||
bool_t cutsceneModalCloseUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
#define CUTSCENE_MODAL_TITLE_MAX_CHARS 64
|
||||
#define CUTSCENE_MODAL_MESSAGE_MAX_CHARS 256
|
||||
#define CUTSCENE_MODAL_OPTIONS_MAX 4
|
||||
|
||||
/**
|
||||
* Callback invoked with the selected option once a cutscene modal
|
||||
* item's dialog closes. Back/cancel input is disabled while it's open
|
||||
* (see uiMenuSetDisableBack), so it must be dismissed by picking one.
|
||||
*
|
||||
* @param optionIndex Index into the options array that was selected.
|
||||
* @param userData CUTSCENE_SYSTEM.userData for the running cutscene.
|
||||
*/
|
||||
typedef void (*cutscenemodaloptioncallback_t)(
|
||||
const uint8_t optionIndex, void *userData
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
// Display text, or a locale message ID to translate - see
|
||||
// uiModalLocalize.
|
||||
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
||||
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
||||
|
||||
// NOT copied by this struct - these pointers are only read once,
|
||||
// while uiModalOpen is opening the modal in cutsceneModalStart, so
|
||||
// they only need to stay valid until then (e.g. string literals, as
|
||||
// CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a
|
||||
// locale message ID, same as title/message.
|
||||
const char_t **options;
|
||||
uint8_t optionCount;
|
||||
|
||||
cutscenemodaloptioncallback_t callback;
|
||||
} cutscenemodal_t;
|
||||
|
||||
/**
|
||||
* Starts a modal item (shows the modal dialog with the item's title,
|
||||
* message, and options, wiring callback to fire with the selected
|
||||
* option once it closes). optionCount may be 0 for a message-only
|
||||
* dialog with no option buttons.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneModalStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a modal item. A message-only modal (optionCount == 0, e.g.
|
||||
* from CUTSCENE_MODAL) always completes immediately - the cutscene
|
||||
* continues on to whatever follows this item right away, it does not
|
||||
* wait for the dialog to be dismissed. Script what should happen while
|
||||
* it's up, and how it gets closed, as later items in the same cutscene
|
||||
* (e.g. CUTSCENE_CALLBACK, CUTSCENE_WAIT, CUTSCENE_MODAL_CLOSE).
|
||||
*
|
||||
* A modal with options (optionCount > 0, e.g. from
|
||||
* CUTSCENE_MODAL_OPTIONS) never completes on its own - the cutscene
|
||||
* blocks here indefinitely. The option callback fires once the user
|
||||
* picks something, and it alone is responsible for moving the cutscene
|
||||
* on from there (typically via cutsceneGoTo or cutsceneRestart).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once immediately for a message-only modal; false
|
||||
* always for a modal with options.
|
||||
*/
|
||||
bool_t cutsceneModalUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts a modal-close item (closes the currently open modal, if any).
|
||||
* Whatever should happen next belongs in the cutscene's own item
|
||||
* sequence (e.g. a CUTSCENE_CALLBACK or CUTSCENE_PRINT placed right
|
||||
* after this item), not on this item itself.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneModalCloseStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a modal-close item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneModalCloseUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "ui/widget/uimodal.h"
|
||||
|
||||
void cutsceneModalOptionsMarkersCallback(
|
||||
const uint8_t optionIndex,
|
||||
void *userData
|
||||
) {
|
||||
const cutscenemodaloptionsmarkers_t *options =
|
||||
&cutsceneSystemGetCurrentItem()->modalOptionsMarkers;
|
||||
|
||||
// uiModalOpen disables back/cancel for a dialog with options, so
|
||||
// UI_MODAL_RESULT_NONE can't reach here via input - this fallback to
|
||||
// the last option's marker only matters if something ever force-closes
|
||||
// the modal directly (uiModalClose) before an option is picked.
|
||||
const uint8_t index =
|
||||
optionIndex < options->optionCount ? optionIndex : options->optionCount - 1;
|
||||
cutsceneGoTo(options->markers[index]);
|
||||
}
|
||||
|
||||
void cutsceneModalOptionsMarkersStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
const cutscenemodaloptionsmarkers_t *options = &item->modalOptionsMarkers;
|
||||
|
||||
uiModalOpen(
|
||||
options->title,
|
||||
options->message,
|
||||
options->options,
|
||||
options->optionCount,
|
||||
cutsceneModalOptionsMarkersCallback,
|
||||
NULL,
|
||||
CUTSCENE_SYSTEM.userData
|
||||
);
|
||||
}
|
||||
|
||||
bool_t cutsceneModalOptionsMarkersUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
// Backs CUTSCENE_MODAL_OPTIONS_ONE and CUTSCENE_MODAL_OPTIONS_TWO, so two
|
||||
// is the most either macro ever needs.
|
||||
#define CUTSCENE_MODAL_OPTIONS_MARKERS_MAX 2
|
||||
|
||||
typedef struct {
|
||||
// Display text, or a locale message ID to translate - see
|
||||
// uiModalLocalize.
|
||||
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
||||
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
||||
|
||||
// Each option's display text/locale message ID, and the marker to
|
||||
// cutsceneGoTo when that option is selected.
|
||||
const char_t *options[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
|
||||
const char_t *markers[CUTSCENE_MODAL_OPTIONS_MARKERS_MAX];
|
||||
uint8_t optionCount;
|
||||
} cutscenemodaloptionsmarkers_t;
|
||||
|
||||
/**
|
||||
* Starts a modal-options-markers item (shows the modal dialog with the
|
||||
* item's title, message, and options).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneModalOptionsMarkersStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a modal-options-markers item. Never completes on its own -
|
||||
* the cutscene blocks here indefinitely. Back/cancel input is disabled
|
||||
* while it's open (see uiMenuSetDisableBack), so it must be dismissed
|
||||
* by picking an option, which jumps straight to that option's marker
|
||||
* via cutsceneGoTo.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns false always.
|
||||
*/
|
||||
bool_t cutsceneModalOptionsMarkersUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -6,5 +6,4 @@
|
||||
# Sources
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
globalitemstore.c
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "globalitemstore.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
bool_t globalItemStoreIsCollected(
|
||||
const saveslot_t *file, const entityglobalid_t id
|
||||
) {
|
||||
assertNotNull(file, "Save slot cannot be NULL");
|
||||
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||
return file->globalItemCollected[id];
|
||||
}
|
||||
|
||||
void globalItemStoreSetCollected(
|
||||
saveslot_t *file, const entityglobalid_t id, const bool_t collected
|
||||
) {
|
||||
assertNotNull(file, "Save slot cannot be NULL");
|
||||
assertTrue(id < SAVE_GLOBAL_ITEM_COUNT_MAX, "Global item ID out of range");
|
||||
file->globalItemCollected[id] = collected;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
|
||||
/**
|
||||
* Checks whether the global entity with the given ID has already been
|
||||
* marked collected in the given save slot's data - e.g. so a global item
|
||||
* entity's init callback (see rpg/entity/global/entitygloballist.h) can
|
||||
* skip spawning itself if the player already picked it up in a prior
|
||||
* session, without needing to keep the entity itself alive to remember
|
||||
* that (which would need render/collision special-casing - this doesn't).
|
||||
*
|
||||
* @param file The save slot to check.
|
||||
* @param id The global entity ID to check.
|
||||
* @return True if already marked collected.
|
||||
*/
|
||||
bool_t globalItemStoreIsCollected(
|
||||
const saveslot_t *file, const entityglobalid_t id
|
||||
);
|
||||
|
||||
/**
|
||||
* Marks the global entity with the given ID as collected (or not) in the
|
||||
* given save slot's data. Does not itself write the save to disk - call
|
||||
* saveWriteSlot() separately once ready to persist it.
|
||||
*
|
||||
* @param file The save slot to write into.
|
||||
* @param id The global entity ID to mark.
|
||||
* @param collected The new collected state.
|
||||
*/
|
||||
void globalItemStoreSetCollected(
|
||||
saveslot_t *file, const entityglobalid_t id, const bool_t collected
|
||||
);
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "util/memory.h"
|
||||
#include "time/time.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "ui/frame/game/uigamemenu.h"
|
||||
#include "ui/screen/game/uigamemenu.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
void playerInit(entity_t *entity) {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "console/console.h"
|
||||
#include "event/event.h"
|
||||
#include "util/string.h"
|
||||
#include "rpg/entity/global/entityglobal.h"
|
||||
#include "rpg/entity/item/entityitem.h"
|
||||
@@ -137,8 +136,10 @@ void mapChunkUnload(chunk_t *chunk) {
|
||||
memorySet(chunk->areas, 0xFF, sizeof(chunk->areas));
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||
chunk->dcfEntry->onLoaded = NULL;
|
||||
chunk->dcfEntry->onLoadedUser = NULL;
|
||||
chunk->dcfEntry->onError = NULL;
|
||||
chunk->dcfEntry->onErrorUser = NULL;
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
}
|
||||
@@ -159,8 +160,10 @@ errorret_t mapChunkLoad(chunk_t *chunk) {
|
||||
mapChunkLoadingSlotClear(chunk);
|
||||
|
||||
if(chunk->dcfEntry != NULL) {
|
||||
eventUnsubscribe(&chunk->dcfEntry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&chunk->dcfEntry->onError, mapChunkLoadError);
|
||||
chunk->dcfEntry->onLoaded = NULL;
|
||||
chunk->dcfEntry->onLoadedUser = NULL;
|
||||
chunk->dcfEntry->onError = NULL;
|
||||
chunk->dcfEntry->onErrorUser = NULL;
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
}
|
||||
@@ -244,8 +247,12 @@ void mapChunkLoadNext() {
|
||||
continue;
|
||||
}
|
||||
|
||||
eventSubscribe(&entry->onLoaded, mapChunkLoaded, chunk);
|
||||
eventSubscribe(&entry->onError, mapChunkLoadError, chunk);
|
||||
assertNull(entry->onLoaded, "Entry already has an onLoaded subscriber.");
|
||||
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) {
|
||||
assertNotNull(params, "mapChunkLoadError: params cannot be NULL");
|
||||
void mapChunkLoadError(assetentry_t *entry, void *user) {
|
||||
assertNotNull(entry, "mapChunkLoadError: entry cannot be NULL");
|
||||
assertNotNull(user, "mapChunkLoadError: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dcfEntry != entry) return;
|
||||
consolePrint(
|
||||
@@ -397,8 +403,10 @@ void mapChunkLoadError(void *params, void *user) {
|
||||
(int32_t)chunk->position.y,
|
||||
(int32_t)chunk->position.z
|
||||
);
|
||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
||||
entry->onLoaded = NULL;
|
||||
entry->onLoadedUser = NULL;
|
||||
entry->onError = NULL;
|
||||
entry->onErrorUser = NULL;
|
||||
assetUnlockEntry(chunk->dcfEntry);
|
||||
chunk->dcfEntry = NULL;
|
||||
memorySet(chunk->tiles, 0x00, sizeof(chunk->tiles));
|
||||
@@ -407,10 +415,9 @@ void mapChunkLoadError(void *params, void *user) {
|
||||
mapChunkLoadNext();
|
||||
}
|
||||
|
||||
void mapChunkLoaded(void *params, void *user) {
|
||||
assertNotNull(params, "mapChunkLoaded: params cannot be NULL");
|
||||
void mapChunkLoaded(assetentry_t *entry, void *user) {
|
||||
assertNotNull(entry, "mapChunkLoaded: entry cannot be NULL");
|
||||
assertNotNull(user, "mapChunkLoaded: user cannot be NULL");
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
chunk_t *chunk = (chunk_t *)user;
|
||||
if(chunk->dcfEntry != entry) return;
|
||||
// consolePrint(
|
||||
@@ -457,8 +464,10 @@ void mapChunkLoaded(void *params, void *user) {
|
||||
// modelEntries must still be intact for that next reuse to copy from.
|
||||
chunk->modelEntries[m] = entry->data.chunk.modelEntries[m];
|
||||
}
|
||||
eventUnsubscribe(&entry->onLoaded, mapChunkLoaded);
|
||||
eventUnsubscribe(&entry->onError, mapChunkLoadError);
|
||||
entry->onLoaded = NULL;
|
||||
entry->onLoadedUser = NULL;
|
||||
entry->onError = NULL;
|
||||
entry->onErrorUser = NULL;
|
||||
// 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
|
||||
// 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.
|
||||
* 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.
|
||||
*/
|
||||
void mapChunkLoadError(void *params, void *user);
|
||||
void mapChunkLoadError(assetentry_t *entry, void *user);
|
||||
|
||||
/**
|
||||
* Callback invoked when a chunk DCF asset finishes loading.
|
||||
* 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.
|
||||
*/
|
||||
void mapChunkLoaded(void *params, void *user);
|
||||
void mapChunkLoaded(assetentry_t *entry, void *user);
|
||||
|
||||
/**
|
||||
* Rebuilds chunkOrder from the loaded chunks that fall within the
|
||||
|
||||
+4
-25
@@ -17,11 +17,8 @@
|
||||
#include "rpgcamera.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "save/save.h"
|
||||
#include "save/autosave.h"
|
||||
#include "error/error.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
#include "ui/rpg/uiemoji.h"
|
||||
#include "rpg/story/storyflag.h"
|
||||
|
||||
@@ -29,27 +26,14 @@ errorret_t rpgInit(void) {
|
||||
memoryZero(ENTITIES, sizeof(ENTITIES));
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
|
||||
saveslot_t *saveSlot = saveGetSlot(SAVE_ACTIVE_SLOT);
|
||||
// saveInit() eagerly loads every slot from disk, but there's no
|
||||
// continue-game flow yet - every boot is a fresh game regardless of
|
||||
// what was found on disk, so force this false rather than let a stale
|
||||
// "exists" from a real save skip storyFlagInitDefaults() below while
|
||||
// everything else here still hardcodes new-game state.
|
||||
saveSlot->exists = false;
|
||||
|
||||
// Must run before any code reads a story flag - stamps CSV-defined
|
||||
// defaults onto the active save slot since it's now forced to look
|
||||
// unloaded.
|
||||
storyFlagInitDefaults(saveSlot);
|
||||
|
||||
backpackInit();
|
||||
partyInit();
|
||||
cutsceneSystemInit();
|
||||
|
||||
errorChain(mapInit());
|
||||
|
||||
rpgCameraInit();
|
||||
// Init world
|
||||
|
||||
|
||||
// Init test world
|
||||
errorChain(mapPositionSet((chunkpos_t){ 0, 0, 0 }));
|
||||
|
||||
// The player is the one entity that isn't sourced from map/chunk data -
|
||||
@@ -68,6 +52,7 @@ errorret_t rpgInit(void) {
|
||||
backpackAdd(ITEM_ID_POTATO, 3);
|
||||
backpackAdd(ITEM_ID_APPLE, 8);
|
||||
|
||||
|
||||
// All Good!
|
||||
errorOk();
|
||||
}
|
||||
@@ -79,12 +64,6 @@ errorret_t rpgUpdate(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
// A failed autosave forces a no-save-device prompt (see autosave.h) -
|
||||
// freeze the world entirely until the player resolves it, rather than
|
||||
// letting NPCs/cutscenes/camera keep running behind a modal that's
|
||||
// supposed to be blocking.
|
||||
if(autoSaveIsBlocking()) errorOk();
|
||||
|
||||
// TODO: Do not update if the scene is not the map scene?
|
||||
errorChain(mapUpdate());
|
||||
|
||||
|
||||
@@ -7,20 +7,17 @@
|
||||
|
||||
#include "storyflag.h"
|
||||
#include "assert/assert.h"
|
||||
#include "console/console.h"
|
||||
|
||||
storyflagvalue_t storyFlagGet(const storyflag_t flag) {
|
||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||
consolePrint("Story flag get without save implementation");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value) {
|
||||
assertTrue(flag > STORY_FLAG_NULL && flag < STORY_FLAG_COUNT, "Bad Flag");
|
||||
saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[flag] = value;
|
||||
}
|
||||
|
||||
void storyFlagInitDefaults(saveslot_t *file) {
|
||||
assertNotNull(file, "Save slot cannot be NULL");
|
||||
if(file->exists) return;
|
||||
assertTrue(
|
||||
STORY_FLAG_COUNT <= SAVE_STORY_FLAG_COUNT_MAX,
|
||||
"Too many story flags for the save format - bump SAVE_STORY_FLAG_COUNT_MAX"
|
||||
);
|
||||
for(storyflag_t i = 0; i < STORY_FLAG_COUNT; i++) {
|
||||
file->storyFlags[i] = STORY_FLAG_DEFAULTS[i];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Dirty savefile
|
||||
consolePrint("Story flag set without save implementation");
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#pragma once
|
||||
#include "rpg/story/storyflagvalue.h"
|
||||
#include "save/save.h"
|
||||
|
||||
/**
|
||||
* Gets the value of a story flag. Reads directly from the active save
|
||||
@@ -16,25 +15,12 @@
|
||||
* @param flag The story flag to get.
|
||||
* @return The value of the story flag.
|
||||
*/
|
||||
#define storyFlagGet(flag) (saveGetSlot(SAVE_ACTIVE_SLOT)->storyFlags[(flag)])
|
||||
storyflagvalue_t storyFlagGet(const storyflag_t flag);
|
||||
|
||||
/**
|
||||
* Sets the value of a story flag, directly in the active save slot (see
|
||||
* SAVE_ACTIVE_SLOT). Does not itself write the save to disk - call
|
||||
* saveWriteSlot() separately once ready to persist it.
|
||||
* Sets the value of a story flag. Will dirty the savefile.
|
||||
*
|
||||
* @param flag The story flag to set.
|
||||
* @param value The value to set the story flag to.
|
||||
*/
|
||||
void storyFlagSet(const storyflag_t flag, const storyflagvalue_t value);
|
||||
|
||||
/**
|
||||
* Stamps each story flag's CSV-defined default (STORY_FLAG_DEFAULTS) onto
|
||||
* the given save slot, but only if it hasn't actually been loaded from
|
||||
* disk yet (file->exists is false) - otherwise leaves already-played
|
||||
* progress alone. Call once, e.g. during rpgInit(), before any gameplay
|
||||
* code reads a story flag.
|
||||
*
|
||||
* @param file The save slot to stamp defaults onto.
|
||||
*/
|
||||
void storyFlagInitDefaults(saveslot_t *file);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
save.c
|
||||
savestream.c
|
||||
autosave.c
|
||||
savedevice.c
|
||||
saveslot.c
|
||||
savesettings.c
|
||||
)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "autosave.h"
|
||||
#include "save.h"
|
||||
#include "error/error.h"
|
||||
#include "ui/frame/initial/uiinitialnocard.h"
|
||||
|
||||
autosave_t AUTO_SAVE;
|
||||
|
||||
static void autoSaveNoCardResult(const bool_t retry, void *user) {
|
||||
AUTO_SAVE.blocking = false;
|
||||
|
||||
// "Retry" just re-queues the write for the next tick - there's no way
|
||||
// to force a re-probe of the hardware mid-session (see saveInit()'s doc
|
||||
// comment), so this only actually helps for a transient failure.
|
||||
if(retry) {
|
||||
AUTO_SAVE.pending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// The player explicitly acknowledged there's no save device and chose
|
||||
// to proceed anyway - sticks for the rest of the session, so future
|
||||
// autoSaveUpdate() calls silently skip instead of prompting again.
|
||||
saveMarkTemporary();
|
||||
}
|
||||
|
||||
static void autoSaveWriteComplete(errorret_t result, void *user) {
|
||||
AUTO_SAVE.saving = false;
|
||||
if(errorIsOk(result)) return;
|
||||
|
||||
errorCatch(errorPrint(result));
|
||||
|
||||
// The write failed - most likely no save medium is present right now
|
||||
// (a GameCube with no memory card inserted, for example). Pause world
|
||||
// simulation and force the same prompt the initial boot scene uses so
|
||||
// the player can insert a card and retry, or explicitly accept a
|
||||
// temporary, unsaved session.
|
||||
AUTO_SAVE.blocking = true;
|
||||
uiInitialNoCardOpen(autoSaveNoCardResult, NULL);
|
||||
}
|
||||
|
||||
void autoSaveQueue(void) {
|
||||
AUTO_SAVE.pending = true;
|
||||
}
|
||||
|
||||
void autoSaveUpdate(void) {
|
||||
if(AUTO_SAVE.blocking) return;
|
||||
if(!AUTO_SAVE.pending) return;
|
||||
if(saveIsBusy()) return;
|
||||
|
||||
AUTO_SAVE.pending = false;
|
||||
|
||||
// Already opted out of saving for this session - nothing to do, and
|
||||
// definitely don't reprompt every time an autosave is queued.
|
||||
if(saveIsTemporary()) return;
|
||||
|
||||
AUTO_SAVE.saving = true;
|
||||
saveWriteSlot(SAVE_ACTIVE_SLOT, autoSaveWriteComplete, NULL);
|
||||
}
|
||||
|
||||
bool_t autoSaveIsSaving(void) {
|
||||
return AUTO_SAVE.saving;
|
||||
}
|
||||
|
||||
bool_t autoSaveIsBlocking(void) {
|
||||
return AUTO_SAVE.blocking;
|
||||
}
|
||||
@@ -1,57 +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"
|
||||
|
||||
typedef struct {
|
||||
/** True from autoSaveQueue() until the queued write starts. */
|
||||
bool_t pending;
|
||||
/** True while the queued write is actually in flight. */
|
||||
bool_t saving;
|
||||
/**
|
||||
* True while the forced no-save-device prompt is up after a queued
|
||||
* write failed - see autoSaveUpdate(). World simulation must pause
|
||||
* while this is true (see rpgUpdate()).
|
||||
*/
|
||||
bool_t blocking;
|
||||
} autosave_t;
|
||||
|
||||
extern autosave_t AUTO_SAVE;
|
||||
|
||||
/**
|
||||
* Queues an autosave to run on a future engine tick. Safe to call as
|
||||
* often as needed (e.g. after every map transition or story event) -
|
||||
* repeated calls before the queued save starts just collapse into one.
|
||||
*/
|
||||
void autoSaveQueue(void);
|
||||
|
||||
/**
|
||||
* Pumps the queued autosave: starts the write once nothing else is
|
||||
* using the save system, and if that write fails (e.g. a GameCube with
|
||||
* no memory card inserted), forces the same no-save-device prompt the
|
||||
* initial boot scene uses and pauses world simulation until the player
|
||||
* resolves it. Must be called every engine frame, after saveUpdate().
|
||||
*/
|
||||
void autoSaveUpdate(void);
|
||||
|
||||
/**
|
||||
* True while a queued autosave's write is actually in progress. Intended
|
||||
* for UI to show a "Saving" indicator.
|
||||
*
|
||||
* @return true if an autosave write is currently in flight.
|
||||
*/
|
||||
bool_t autoSaveIsSaving(void);
|
||||
|
||||
/**
|
||||
* True while a failed autosave is forcing the no-save-device prompt and
|
||||
* waiting on the player's decision. Intended for the world simulation to
|
||||
* pause while this is true.
|
||||
*
|
||||
* @return true if an autosave is currently blocking on player input.
|
||||
*/
|
||||
bool_t autoSaveIsBlocking(void);
|
||||
+204
-147
@@ -1,188 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/save.h"
|
||||
#include "save/savestream.h"
|
||||
#include "save.h"
|
||||
#include "util/memory.h"
|
||||
#include "assert/assert.h"
|
||||
#include "error/error.h"
|
||||
|
||||
save_t SAVE;
|
||||
|
||||
const saveslot_t SAVE_DEFAULT = {
|
||||
.header = {
|
||||
SAVE_SLOT_HEADER[0], SAVE_SLOT_HEADER[1], SAVE_SLOT_HEADER[2]
|
||||
},
|
||||
.version = SAVE_SLOT_VERSION,
|
||||
.exists = true,
|
||||
.playerName = "Player"
|
||||
// globalItemCollected/storyFlags are left at their zero default.
|
||||
};
|
||||
|
||||
static void _saveEagerLoadComplete(errorret_t result, void *user) {
|
||||
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||
}
|
||||
|
||||
errorret_t saveInit(void) {
|
||||
errorret_t saveInit() {
|
||||
memoryZero(&SAVE, sizeof(save_t));
|
||||
SAVE.meta.deadzone = SAVE_META_DEADZONE_DEFAULT;
|
||||
SAVE.meta.language = SAVE_META_LANGUAGE_DEFAULT;
|
||||
|
||||
#ifdef saveInitPlatform
|
||||
// A missing/unreachable save medium is expected, recoverable state,
|
||||
// not a reason to fail booting the whole game - log it and carry on
|
||||
// with SAVE.available false instead of chaining the error upward.
|
||||
errorret_t result = saveInitPlatform();
|
||||
SAVE.available = errorIsOk(result);
|
||||
if(!SAVE.available) errorCatch(errorPrint(result));
|
||||
#else
|
||||
SAVE.available = false;
|
||||
#endif
|
||||
SAVE.deviceCurrent = 0xFF;// No current device.
|
||||
SAVE.slotCurrent = 0xFF;// No current slot.
|
||||
|
||||
// Eagerly pull meta + every slot into memory up front - cheap and
|
||||
// harmless (nothing consumes loaded slot data automatically; there's no
|
||||
// continue-game flow yet). PSP opts out entirely (see
|
||||
// saveSkipEagerLoadPlatform) since its only read path is now the native
|
||||
// savedata dialog, and running that on every boot would defeat the whole
|
||||
// point of folding meta into it instead of a separate instant-write file.
|
||||
#ifndef saveSkipEagerLoadPlatform
|
||||
if(SAVE.available) {
|
||||
saveLoadMeta(_saveEagerLoadComplete, NULL);
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT_MAX; i++) {
|
||||
saveLoadSlot(i, _saveEagerLoadComplete, NULL);
|
||||
// Initialize the slots and settings
|
||||
saveSettingsInit(&SAVE.settings);
|
||||
saveSlotInit(&SAVE.slot);
|
||||
|
||||
// Update caches to match default data.
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
SAVE.caches[i] = SAVE.slot.cachedData;
|
||||
}
|
||||
|
||||
// Start by initializing each of the save devices.
|
||||
savedevice_t *device = &SAVE.devices[0];
|
||||
do {
|
||||
errorChain(saveDeviceInit(device));
|
||||
} while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveUpdate() {
|
||||
assertIsMainThread("Invalid thread");
|
||||
|
||||
// Start by updating each device
|
||||
savedevice_t *device = &SAVE.devices[0];
|
||||
do {
|
||||
saveDeviceUpdate(device);
|
||||
} while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
// We may be waiting for a device to become available
|
||||
if(SAVE.findingAvailableDevice) {
|
||||
assertNotNull(
|
||||
SAVE.findAvailableCallback,
|
||||
"Callback cannot be null while looking for devices"
|
||||
);
|
||||
|
||||
// Yes, we have a device available, fire the callback.
|
||||
if(SAVE.deviceCurrent == 0xFF) {
|
||||
// Are we still looking or did we fail to find anything?
|
||||
if(SAVE.noAvailableDeviceFound) {
|
||||
SAVE.findingAvailableDevice = false;
|
||||
SAVE.findAvailableCallback(
|
||||
NULL,
|
||||
SAVE.findAvailableUser
|
||||
);
|
||||
}
|
||||
// Still searching
|
||||
} else {
|
||||
// Device was found, cache in the data.
|
||||
errorChain(saveLoadSettings());
|
||||
errorChain(saveLoadAllSlots());
|
||||
|
||||
// Reset slot, implying no slot has been selected.
|
||||
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.findAvailableCallback(
|
||||
&SAVE.devices[SAVE.deviceCurrent],
|
||||
SAVE.findAvailableUser
|
||||
);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t saveIsAvailable(void) {
|
||||
return SAVE.available && !SAVE.temporary;
|
||||
void saveFindAvailableDevice(
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
assertIsMainThread("Invalid thread");
|
||||
assertNotNull(callback, "Callback cannot be null");
|
||||
assertFalse(
|
||||
SAVE.findingAvailableDevice,
|
||||
"Already finding an available device"
|
||||
);
|
||||
|
||||
SAVE.findingAvailableDevice = true;
|
||||
SAVE.findAvailableCallback = callback;
|
||||
SAVE.findAvailableUser = user;
|
||||
SAVE.noAvailableDeviceFound = false;
|
||||
|
||||
// Is there an available device marked already?
|
||||
if(SAVE.deviceCurrent != 0xFF) {
|
||||
// Yes, fire the callback next tick.
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we have an available device not yet marked as current?
|
||||
savedevice_t *device = &SAVE.devices[0];
|
||||
do {
|
||||
if(device->state != SAVE_DEVICE_STATE_AVAILABLE) continue;
|
||||
// Yes this device is available, set as current device.
|
||||
SAVE.deviceCurrent = (uint8_t)(device - &SAVE.devices[0]);
|
||||
return;// Next tick will fire the callback.
|
||||
} while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
// No currently available device, check them IN ORDER, starting at 0
|
||||
savedevice_t *deviceToCheck = &SAVE.devices[0];
|
||||
assertNotNull(deviceToCheck, "Device to check cannot be null");
|
||||
saveDeviceCheckAvailability(
|
||||
deviceToCheck,
|
||||
saveOnDeviceAvailabilityChecked,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
void saveMarkTemporary(void) {
|
||||
SAVE.temporary = true;
|
||||
void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user) {
|
||||
assertNotNull(device, "Device cannot be null");
|
||||
assertTrue(
|
||||
SAVE.findingAvailableDevice,
|
||||
"Not currently finding an available device"
|
||||
);
|
||||
|
||||
// Did we already find a device?
|
||||
if(SAVE.deviceCurrent != 0xFF) return;
|
||||
|
||||
// Is this device available?
|
||||
if(device->state != SAVE_DEVICE_STATE_AVAILABLE) {
|
||||
// Since it's unavailable we should tell the next device to check its
|
||||
// availability. If there is no next device we give up.
|
||||
savedevice_t *nextDevice = device + 1;
|
||||
if(nextDevice >= &SAVE.devices[SAVE_DEVICE_COUNT]) {
|
||||
// No more devices to check, we give up.
|
||||
SAVE.noAvailableDeviceFound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the next device's availability.
|
||||
saveDeviceCheckAvailability(
|
||||
nextDevice,
|
||||
saveOnDeviceAvailabilityChecked,
|
||||
NULL
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Yes this device is available, set as current device and next tick will
|
||||
// invoke callback
|
||||
SAVE.deviceCurrent = (uint8_t)(device - &SAVE.devices[0]);
|
||||
}
|
||||
|
||||
bool_t saveIsTemporary(void) {
|
||||
return SAVE.temporary;
|
||||
}
|
||||
errorret_t saveSaveSettings() {
|
||||
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||
|
||||
errorret_t saveDispose(void) {
|
||||
#ifdef saveDisposePlatform
|
||||
errorChain(saveDisposePlatform());
|
||||
#endif
|
||||
if(!SAVE.settingsDirty) errorOk();
|
||||
|
||||
errorChain(saveDeviceSettingsWrite(
|
||||
&SAVE.devices[SAVE.deviceCurrent],
|
||||
&SAVE.settings
|
||||
));
|
||||
|
||||
SAVE.settingsDirty = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveUpdate(void) {
|
||||
#ifdef savePlatformUpdate
|
||||
errorChain(savePlatformUpdate());
|
||||
#endif
|
||||
errorret_t saveLoadSettings() {
|
||||
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||
|
||||
errorChain(saveDeviceSettingsRead(
|
||||
&SAVE.devices[SAVE.deviceCurrent],
|
||||
&SAVE.settings
|
||||
));
|
||||
|
||||
SAVE.settingsDirty = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t saveIsBusy(void) {
|
||||
#ifdef saveIsBusyPlatform
|
||||
return saveIsBusyPlatform();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
errorret_t saveSaveSlot() {
|
||||
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||
assertTrue(SAVE.slotCurrent < SAVE_SLOT_COUNT, "Invalid slot index");
|
||||
|
||||
errorChain(saveDeviceSlotWrite(
|
||||
&SAVE.devices[SAVE.deviceCurrent],
|
||||
&SAVE.slot,
|
||||
SAVE.slotCurrent
|
||||
));
|
||||
|
||||
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
// Update cache
|
||||
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
|
||||
SAVE.slotDirty = false;
|
||||
|
||||
SAVE.slots[slot].exists = false;
|
||||
|
||||
// Some platforms (PSP's native save dialog) can't complete within this
|
||||
// call - they take over entirely and invoke onComplete later, from
|
||||
// saveUpdate(), once their own multi-frame flow finishes. Those
|
||||
// platforms never define the sync saveSlotLoadPlatform() at all, so the
|
||||
// fallback below must live in the #else, not just after an early return.
|
||||
#ifdef saveSlotAsyncLoadPlatform
|
||||
saveSlotAsyncLoadPlatform(slot, onComplete, user);
|
||||
#else
|
||||
errorret_t ret = saveSlotLoadPlatform(slot, &SAVE.slots[slot]);
|
||||
SAVE.available = errorIsOk(ret);
|
||||
onComplete(ret, user);
|
||||
#endif
|
||||
}
|
||||
|
||||
void saveLoadDefault(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||
SAVE.slots[slot] = SAVE_DEFAULT;
|
||||
}
|
||||
|
||||
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user) {
|
||||
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
|
||||
// See saveLoadSlot() - some platforms take over and complete later.
|
||||
#ifdef saveSlotAsyncWritePlatform
|
||||
saveSlotAsyncWritePlatform(slot, onComplete, user);
|
||||
#else
|
||||
errorret_t ret = saveSlotWritePlatform(slot, &SAVE.slots[slot]);
|
||||
SAVE.available = errorIsOk(ret);
|
||||
onComplete(ret, user);
|
||||
#endif
|
||||
}
|
||||
|
||||
errorret_t saveDeleteSlot(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||
|
||||
#ifdef saveSlotDeletePlatform
|
||||
errorret_t deleteRet = saveSlotDeletePlatform(slot);
|
||||
SAVE.available = errorIsOk(deleteRet);
|
||||
errorChain(deleteRet);
|
||||
#endif
|
||||
|
||||
SAVE.slots[slot].exists = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t saveSlotExists(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||
return SAVE.slots[slot].exists;
|
||||
errorret_t saveLoadSlot() {
|
||||
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||
assertTrue(SAVE.slotCurrent < SAVE_SLOT_COUNT, "Invalid slot index");
|
||||
|
||||
errorChain(saveDeviceSlotRead(
|
||||
&SAVE.devices[SAVE.deviceCurrent],
|
||||
&SAVE.slot,
|
||||
SAVE.slotCurrent
|
||||
));
|
||||
|
||||
// Update cache
|
||||
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
|
||||
SAVE.slotDirty = false;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
saveslot_t * saveGetSlot(const uint8_t slot) {
|
||||
assertTrue(slot < SAVE_SLOT_COUNT_MAX, "slot exceeds SAVE_SLOT_COUNT_MAX");
|
||||
return &SAVE.slots[slot];
|
||||
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();
|
||||
}
|
||||
|
||||
void saveLoadMeta(savecallback_t onComplete, void *user) {
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
errorret_t saveDispose() {
|
||||
// Dispose each device.
|
||||
savedevice_t *device = &SAVE.devices[0];
|
||||
do {
|
||||
saveDeviceDispose(device);
|
||||
} while(device++ < &SAVE.devices[SAVE_DEVICE_COUNT - 1]);
|
||||
|
||||
SAVE.meta.exists = false;
|
||||
|
||||
#ifdef saveMetaAsyncLoadPlatform
|
||||
saveMetaAsyncLoadPlatform(onComplete, user);
|
||||
#else
|
||||
errorret_t ret = saveMetaLoadPlatform(&SAVE.meta);
|
||||
SAVE.available = errorIsOk(ret);
|
||||
onComplete(ret, user);
|
||||
#endif
|
||||
}
|
||||
|
||||
void saveWriteMeta(savecallback_t onComplete, void *user) {
|
||||
assertNotNull(onComplete, "onComplete cannot be NULL");
|
||||
|
||||
#ifdef saveMetaAsyncWritePlatform
|
||||
saveMetaAsyncWritePlatform(onComplete, user);
|
||||
#else
|
||||
errorret_t ret = saveMetaWritePlatform(&SAVE.meta);
|
||||
SAVE.available = errorIsOk(ret);
|
||||
onComplete(ret, user);
|
||||
#endif
|
||||
}
|
||||
|
||||
savemeta_t * saveGetMeta(void) {
|
||||
return &SAVE.meta;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
+81
-179
@@ -1,210 +1,112 @@
|
||||
/**
|
||||
* 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 "savedevice.h"
|
||||
#include "saveslot.h"
|
||||
#include "savemeta.h"
|
||||
#include "save/saveplatform.h"
|
||||
#include "savesettings.h"
|
||||
|
||||
typedef struct {
|
||||
/** Per-slot save data; indexed 0 to SAVE_SLOT_COUNT_MAX - 1. */
|
||||
saveslot_t slots[SAVE_SLOT_COUNT_MAX];
|
||||
/** Device-wide preferences - see savemeta.h. */
|
||||
savemeta_t meta;
|
||||
/** Platform-specific save system state (paths, card handles, etc.). */
|
||||
saveplatform_t platform;
|
||||
/**
|
||||
* True if the save medium (memory card/stick/disk) was reachable the
|
||||
* last time it was checked - at saveInit(), and refreshed by every
|
||||
* subsequent load/write attempt. Starting the game with no card/stick
|
||||
* inserted, or one being removed mid-session, are both expected
|
||||
* conditions here, not fatal errors - see saveIsAvailable().
|
||||
*/
|
||||
bool_t available;
|
||||
/**
|
||||
* True once the player has explicitly acknowledged there's no save
|
||||
* device and chosen to continue anyway (see the initial scene's "no
|
||||
* card" prompt) - sticky for the rest of the session, folded into
|
||||
* saveIsAvailable() so every existing/future call site that already
|
||||
* gates on that automatically refuses to save/load from here on. See
|
||||
* saveMarkTemporary()/saveIsTemporary().
|
||||
*/
|
||||
bool_t temporary;
|
||||
/**
|
||||
* Scratch error state used by platforms whose save/load completes
|
||||
* asynchronously (see saveIsBusy()) to construct a result to hand to a
|
||||
* savecallback_t from inside saveUpdate(), rather than from a direct
|
||||
* errorThrow() return - mirrors network_t.errorState for the same reason.
|
||||
*/
|
||||
errorstate_t errorState;
|
||||
// File state
|
||||
savesettings_t settings;
|
||||
saveslotcache_t caches[SAVE_SLOT_COUNT];
|
||||
saveslot_t slot;
|
||||
|
||||
uint8_t slotCurrent;
|
||||
bool_t settingsDirty;
|
||||
bool_t slotDirty;
|
||||
|
||||
// Device state
|
||||
savedevice_t devices[SAVE_DEVICE_COUNT];
|
||||
uint8_t deviceCurrent;
|
||||
bool_t findingAvailableDevice;
|
||||
bool_t noAvailableDeviceFound;
|
||||
savedevicestatecallback_t findAvailableCallback;
|
||||
void *findAvailableUser;
|
||||
} save_t;
|
||||
|
||||
extern save_t SAVE;
|
||||
|
||||
/**
|
||||
* Initializes the save system. Never fails the way saveWriteSlot()/
|
||||
* saveWriteMeta() can - if the platform's save medium isn't reachable
|
||||
* (e.g. no memory card/stick inserted), that's logged and reflected in
|
||||
* saveIsAvailable() rather than treated as fatal, since the game should
|
||||
* still be playable without save support.
|
||||
*
|
||||
* On most platforms, this also eagerly loads meta and every slot from
|
||||
* disk immediately - cheap and harmless, since nothing consumes the
|
||||
* loaded slot data automatically (there's no "continue game" flow yet;
|
||||
* rpgInit() always hardcodes a fresh game regardless of what's loaded).
|
||||
* PSP skips this (see saveSkipEagerLoadPlatform) - its only read path is
|
||||
* the native sceUtilitySavedata dialog now that meta lives inside the
|
||||
* same payload as the save slot, and running that multi-frame dialog on
|
||||
* every single boot would reintroduce the exact UX problem a lightweight
|
||||
* settings-only file used to avoid.
|
||||
*
|
||||
* @return An error code only for unexpected platform failures.
|
||||
* Initializes the save system, this does not do anything related to mounting
|
||||
* files, memory cards, etc, this is entirely prepping for that capability.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveInit(void);
|
||||
errorret_t saveInit();
|
||||
|
||||
/**
|
||||
* Checks whether the save medium was reachable as of the last load/write
|
||||
* attempt (or saveInit(), if none has been attempted yet), AND the
|
||||
* session hasn't been marked temporary (see saveMarkTemporary()).
|
||||
* Intended for UI to decide whether to offer saving/loading at all, or to
|
||||
* explain why it isn't available right now - e.g. "No memory card
|
||||
* inserted".
|
||||
*
|
||||
* @return true if saving/loading can be attempted right now.
|
||||
* Updates the save system.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
bool_t saveIsAvailable(void);
|
||||
errorret_t saveUpdate();
|
||||
|
||||
/**
|
||||
* Marks this session as temporary - the player explicitly acknowledged
|
||||
* there's no save device and chose to continue anyway. One-way: nothing
|
||||
* currently re-probes the save medium mid-session (see saveInit()'s doc
|
||||
* comment), so there's no legitimate way to un-stick this once set short
|
||||
* of restarting the game.
|
||||
* Requests the save system to try and find an available device. This will fire
|
||||
* the callback whenever an avaialble device is found, or will fire with a NULL
|
||||
* save device if no available save devices are found.
|
||||
*
|
||||
* @param callback The callback to fire when an available device is found.
|
||||
* @param user The user data to pass to the callback.
|
||||
*/
|
||||
void saveMarkTemporary(void);
|
||||
void saveFindAvailableDevice(
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* True once saveMarkTemporary() has been called this session. Distinct
|
||||
* from saveIsAvailable() so UI can give a more specific message (e.g.
|
||||
* "this session is temporary" rather than the generic "no save device
|
||||
* found") once the player has already made that choice.
|
||||
* Internal method to fire the save callback, does it at the appropriate time.
|
||||
*
|
||||
* @return true if this session has been marked temporary.
|
||||
* @param device The save device to fire the callback for.
|
||||
* @param user Unused, present to match savedevicestatecallback_t.
|
||||
*/
|
||||
bool_t saveIsTemporary(void);
|
||||
void saveOnDeviceAvailabilityChecked(savedevice_t *device, void *user);
|
||||
|
||||
/**
|
||||
* Saves the current settings to the current device.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveSaveSettings();
|
||||
|
||||
/**
|
||||
* Loads the current settings from the current device.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveLoadSettings();
|
||||
|
||||
/**
|
||||
* Saves the current slot to the current device, at the current slot index.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveSaveSlot();
|
||||
|
||||
/**
|
||||
* Loads the current slot from the current device, at the current slot index.
|
||||
*
|
||||
* @return Error state if any.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return An error code if disposal fails.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDispose(void);
|
||||
|
||||
/**
|
||||
* Updates the save manager, pumping any in-progress async save/load and
|
||||
* dispatching its callback once complete. No-op on platforms where every
|
||||
* operation always completes synchronously (see saveIsBusy()). Must be
|
||||
* called every engine frame for platforms that need it (PSP's native save
|
||||
* dialog spans multiple frames).
|
||||
*
|
||||
* @return An error code indicating success or failure.
|
||||
*/
|
||||
errorret_t saveUpdate(void);
|
||||
|
||||
/**
|
||||
* True while an async save/load is in progress (e.g. PSP's native save
|
||||
* dialog is open). Calling any save/load function again while this is true
|
||||
* is undefined behavior - wait for the previous call's callback first.
|
||||
*
|
||||
* @return True if a save/load request is currently in progress.
|
||||
*/
|
||||
bool_t saveIsBusy(void);
|
||||
|
||||
/**
|
||||
* Loads the save slot for a given index from persistent storage. Slow/
|
||||
* async on some platforms (PSP's native save dialog spans multiple
|
||||
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
|
||||
* call returns. See saveIsBusy().
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||
* @param onComplete Callback invoked with the result once loading finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void saveLoadSlot(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||
|
||||
/**
|
||||
* Resets a save slot in memory to SAVE_DEFAULT, for starting a new game
|
||||
* without reading anything from persistent storage. Synchronous - no
|
||||
* disk I/O is involved, so there's no callback to wait on.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1) to reset.
|
||||
*/
|
||||
void saveLoadDefault(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Writes the save slot for a given index to persistent storage. Slow/
|
||||
* async on some platforms (PSP's native save dialog spans multiple
|
||||
* frames) - on others (Linux, Dolphin) onComplete is invoked before this
|
||||
* call returns. See saveIsBusy().
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||
* @param onComplete Callback invoked with the result once writing finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void saveWriteSlot(const uint8_t slot, savecallback_t onComplete, void *user);
|
||||
|
||||
/**
|
||||
* Deletes the save slot for a given index from persistent storage.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||
* @return An error code if the delete fails.
|
||||
*/
|
||||
errorret_t saveDeleteSlot(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Checks whether a save slot has data for a given index.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||
* @return true if the slot has data, false otherwise.
|
||||
*/
|
||||
bool_t saveSlotExists(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Gets a pointer to the save slot data for a given index.
|
||||
*
|
||||
* @param slot The save slot index (0 to SAVE_SLOT_COUNT_MAX - 1).
|
||||
* @return A pointer to the saveslot_t for the given slot.
|
||||
*/
|
||||
saveslot_t * saveGetSlot(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Loads device-wide meta (preferences) from persistent storage. Callback-
|
||||
* based on every platform, same as saveLoadSlot() - on PSP specifically,
|
||||
* loading meta means loading the same combined savedata payload as the
|
||||
* active slot, which is unavoidably async there.
|
||||
*
|
||||
* @param onComplete Callback invoked with the result once loading finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void saveLoadMeta(savecallback_t onComplete, void *user);
|
||||
|
||||
/**
|
||||
* Writes device-wide meta (preferences) to persistent storage.
|
||||
*
|
||||
* @param onComplete Callback invoked with the result once writing finishes.
|
||||
* @param user User data passed through to onComplete.
|
||||
*/
|
||||
void saveWriteMeta(savecallback_t onComplete, void *user);
|
||||
|
||||
/**
|
||||
* Gets a pointer to the live meta data. Modify fields directly, then call
|
||||
* saveWriteMeta() to persist them.
|
||||
*
|
||||
* @return A pointer to the meta.
|
||||
*/
|
||||
savemeta_t * saveGetMeta(void);
|
||||
errorret_t saveDispose();
|
||||
@@ -0,0 +1,571 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savedevice.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
#include "yyjson.h"
|
||||
#include "save/savejson.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/endian.h"
|
||||
#include <zlib.h>
|
||||
#endif
|
||||
|
||||
errorret_t saveDeviceInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
|
||||
memoryZero(device, sizeof(savedevice_t));
|
||||
device->state = 0xFF;// We set this because the platform must define it.
|
||||
errorChain(saveDevicePlatformInit(device));
|
||||
|
||||
// The platform must put the device into a state we can work with.
|
||||
assertTrue(
|
||||
device->state == SAVE_DEVICE_STATE_UNKNOWN ||
|
||||
device->state == SAVE_DEVICE_STATE_UNAVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_AVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_ERRORED,
|
||||
"Save device must be in a state that allows checking availability"
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceUpdate(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertIsMainThread("Invalid thread");
|
||||
|
||||
// Perform a device update.
|
||||
errorChain(saveDevicePlatformUpdate(device));
|
||||
|
||||
// Fire the callback if desired.
|
||||
if(device->fireCallback) {
|
||||
device->fireCallback = false;
|
||||
if(device->stateCallback) device->stateCallback(device, device->user);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveDeviceFireCallback(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertIsMainThread("Invalid thread");
|
||||
assertFalse(device->fireCallback, "Device callback already fired?");
|
||||
device->fireCallback = true;
|
||||
}
|
||||
|
||||
void saveDeviceCheckAvailability(
|
||||
savedevice_t *device,
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(callback, "callback cannot be null");
|
||||
assertIsMainThread("Invalid thread");
|
||||
|
||||
// The device must be in a state that we allow checking the availability.
|
||||
assertTrue(
|
||||
device->state == SAVE_DEVICE_STATE_UNKNOWN ||
|
||||
device->state == SAVE_DEVICE_STATE_UNAVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_AVAILABLE ||
|
||||
device->state == SAVE_DEVICE_STATE_ERRORED,
|
||||
"Save device must be in a state that allows checking availability"
|
||||
);
|
||||
|
||||
// Set state data and callback.
|
||||
device->state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY;
|
||||
device->stateCallback = callback;
|
||||
device->user = user;
|
||||
|
||||
// Handoff to the platform to do its checks, it can opt to do this either
|
||||
// synchronously or asynchronously.
|
||||
saveDeviceCheckAvailabilityPlatform(device);
|
||||
}
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
|
||||
// Bumped to 2 for the addition of `generation` below.
|
||||
#define SAVE_DEVICE_RAW_VERSION ((uint32_t)2)
|
||||
// Sanity cap against a corrupted/hostile header driving a bogus allocation.
|
||||
#define SAVE_DEVICE_RAW_MAX_SIZE ((uint32_t)(256 * 1024))
|
||||
|
||||
static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
char_t magic[4];
|
||||
uint32_t version;
|
||||
uint32_t uncompressedSize;// size of the logical blob, pre-compression
|
||||
uint32_t compressedSize;// size of the payload following this header
|
||||
uint32_t checksum;// cryptCRC32() over the compressed payload
|
||||
// Monotonically increasing with every store. Unused by single-file
|
||||
// platforms (rename already guarantees the current file is the latest),
|
||||
// but load-bearing for platforms with no rename primitive and multiple
|
||||
// physical copies - e.g. GameCube memory cards ping-ponging between two
|
||||
// fixed files, where this is how saveDeviceRawIsValid() picks the newest
|
||||
// valid copy.
|
||||
uint32_t generation;
|
||||
} savedevicerawheader_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *ptr;
|
||||
uint32_t len;
|
||||
} savedevicerawspan_t;
|
||||
|
||||
typedef struct {
|
||||
savedevicerawspan_t settings;
|
||||
savedevicerawspan_t slots[SAVE_SLOT_COUNT];
|
||||
} savedevicerawspans_t;
|
||||
|
||||
// A single settings/slot item's JSON bytes, either borrowed from an existing
|
||||
// decompressed blob (owned == NULL) or freshly serialized just now via
|
||||
// yyjson_mut_write (owned != NULL, must be freed with plain free()).
|
||||
typedef struct {
|
||||
const uint8_t *ptr;
|
||||
uint32_t len;
|
||||
char_t *owned;
|
||||
} savedevicerawitem_t;
|
||||
|
||||
// Parses the length-prefixed span table at the front of a decompressed
|
||||
// logical blob. Spans point directly into `logical`, nothing is copied.
|
||||
static void saveDeviceRawParseSpans(
|
||||
const uint8_t *logical,
|
||||
savedevicerawspans_t *spans
|
||||
) {
|
||||
size_t offset = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT);
|
||||
|
||||
spans->settings.len = endianLittleToHost32(*(const uint32_t *)(logical + 0));
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
spans->slots[i].len = endianLittleToHost32(
|
||||
*(const uint32_t *)(logical + sizeof(uint32_t) * (1 + i))
|
||||
);
|
||||
}
|
||||
|
||||
spans->settings.ptr = logical + offset;
|
||||
offset += spans->settings.len;
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
spans->slots[i].ptr = logical + offset;
|
||||
offset += spans->slots[i].len;
|
||||
}
|
||||
}
|
||||
|
||||
// Cheaply checks whether `raw` is a well-formed, checksum-valid raw save
|
||||
// blob, without decompressing it - just header + checksum validation. Used
|
||||
// both by saveDeviceRawInflate below and, publicly (see savedevice.h), by
|
||||
// platforms that keep more than one physical copy and need to pick the
|
||||
// newest valid one (e.g. GameCube memory cards - see `generation` above).
|
||||
bool_t saveDeviceRawIsValid(
|
||||
const uint8_t *raw,
|
||||
const size_t rawSize,
|
||||
uint32_t *outGeneration
|
||||
) {
|
||||
if(raw == NULL || rawSize < sizeof(savedevicerawheader_t)) return false;
|
||||
|
||||
const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw;
|
||||
if(memoryCompare(header->magic, SAVE_DEVICE_RAW_MAGIC, 4) != 0) {
|
||||
return false;
|
||||
}
|
||||
if(endianLittleToHost32(header->version) != SAVE_DEVICE_RAW_VERSION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize);
|
||||
uint32_t compressedSize = endianLittleToHost32(header->compressedSize);
|
||||
if(
|
||||
uncompressedSize > SAVE_DEVICE_RAW_MAX_SIZE ||
|
||||
compressedSize > SAVE_DEVICE_RAW_MAX_SIZE ||
|
||||
// Trailing bytes beyond the declared payload are tolerated as padding -
|
||||
// e.g. a GameCube memory card file rounded up to a whole sector.
|
||||
rawSize < sizeof(savedevicerawheader_t) + compressedSize
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t checksum = cryptCRC32(raw + sizeof(savedevicerawheader_t), compressedSize);
|
||||
if(checksum != endianLittleToHost32(header->checksum)) return false;
|
||||
|
||||
if(outGeneration != NULL) {
|
||||
*outGeneration = endianLittleToHost32(header->generation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validates the header and inflates the compressed payload that follows it.
|
||||
// On success, *outLogical is a memoryAllocate'd buffer the caller must free.
|
||||
static errorret_t saveDeviceRawInflate(
|
||||
const uint8_t *raw,
|
||||
const size_t rawSize,
|
||||
uint8_t **outLogical,
|
||||
size_t *outLogicalSize,
|
||||
uint32_t *outGeneration
|
||||
) {
|
||||
if(!saveDeviceRawIsValid(raw, rawSize, outGeneration)) {
|
||||
errorThrow("Save data failed validation (magic/version/size/checksum)");
|
||||
}
|
||||
|
||||
const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw;
|
||||
uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize);
|
||||
uint32_t compressedSize = endianLittleToHost32(header->compressedSize);
|
||||
const uint8_t *compressed = raw + sizeof(savedevicerawheader_t);
|
||||
|
||||
uint8_t *logical = memoryAllocate(uncompressedSize);
|
||||
uLongf destLen = (uLongf)uncompressedSize;
|
||||
int result = uncompress(
|
||||
logical, &destLen, compressed, (uLong)compressedSize
|
||||
);
|
||||
if(result != Z_OK || destLen != uncompressedSize) {
|
||||
memoryFree(logical);
|
||||
errorThrow("Failed to decompress save data (zlib error %d)", result);
|
||||
}
|
||||
|
||||
*outLogical = logical;
|
||||
*outLogicalSize = uncompressedSize;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Serializes `settings` to a freshly malloc'd JSON string via yyjson - the
|
||||
// result must be freed with plain free(), not memoryFree().
|
||||
static errorret_t saveDeviceRawSerializeSettings(
|
||||
savesettings_t *settings,
|
||||
char_t **outJson,
|
||||
size_t *outLen
|
||||
) {
|
||||
writeInit();
|
||||
|
||||
errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object);
|
||||
if(errorIsNotOk(writeResult)) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
errorChain(writeResult);
|
||||
}
|
||||
|
||||
*outJson = yyjson_mut_write(doc, 0, outLen);
|
||||
yyjson_mut_doc_free(doc);
|
||||
if(*outJson == NULL) errorThrow("Failed to write settings JSON");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Serializes `slot` to a freshly malloc'd JSON string via yyjson - the
|
||||
// result must be freed with plain free(), not memoryFree().
|
||||
static errorret_t saveDeviceRawSerializeSlot(
|
||||
saveslot_t *slot,
|
||||
char_t **outJson,
|
||||
size_t *outLen
|
||||
) {
|
||||
writeInit();
|
||||
|
||||
errorret_t writeResult = saveSlotWriteJSON(slot, doc, object);
|
||||
if(errorIsNotOk(writeResult)) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
errorChain(writeResult);
|
||||
}
|
||||
|
||||
*outJson = yyjson_mut_write(doc, 0, outLen);
|
||||
yyjson_mut_doc_free(doc);
|
||||
if(*outJson == NULL) errorThrow("Failed to write slot JSON");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Frees whatever's been collected so far - safe to call at any point since
|
||||
// unset items are zero-initialized (owned == NULL is a no-op skip).
|
||||
static void saveDeviceRawCleanupStore(
|
||||
uint8_t *oldRaw,
|
||||
uint8_t *oldLogical,
|
||||
savedevicerawitem_t *settingsItem,
|
||||
savedevicerawitem_t *slotItems
|
||||
) {
|
||||
if(oldRaw != NULL) memoryFree(oldRaw);
|
||||
if(oldLogical != NULL) memoryFree(oldLogical);
|
||||
if(settingsItem->owned != NULL) free(settingsItem->owned);
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
if(slotItems[i].owned != NULL) free(slotItems[i].owned);
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the existing combined blob (if any), replaces exactly one item -
|
||||
// `settings`, or the slot at `slotIndex` when `slot` is given, exactly one
|
||||
// of the two must be non-NULL - and rewrites the whole blob. Every other
|
||||
// item's JSON is carried through byte-for-byte from what was already
|
||||
// stored (or freshly defaulted if nothing was stored yet), so a save never
|
||||
// reconstructs data it wasn't given.
|
||||
static errorret_t saveDeviceRawStoreItem(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
) {
|
||||
uint8_t *oldRaw = NULL;
|
||||
size_t oldRawSize = 0;
|
||||
errorChain(saveDeviceDataReadPlatform(device, &oldRaw, &oldRawSize));
|
||||
|
||||
uint8_t *oldLogical = NULL;
|
||||
size_t oldLogicalSize = 0;
|
||||
uint32_t oldGeneration = 0;
|
||||
savedevicerawspans_t oldSpans;
|
||||
bool_t haveOld = oldRaw != NULL;
|
||||
if(haveOld) {
|
||||
errorret_t inflateResult = saveDeviceRawInflate(
|
||||
oldRaw, oldRawSize, &oldLogical, &oldLogicalSize, &oldGeneration
|
||||
);
|
||||
if(errorIsNotOk(inflateResult)) {
|
||||
memoryFree(oldRaw);
|
||||
errorChain(inflateResult);
|
||||
}
|
||||
saveDeviceRawParseSpans(oldLogical, &oldSpans);
|
||||
}
|
||||
|
||||
savedevicerawitem_t settingsItem = {0};
|
||||
savedevicerawitem_t slotItems[SAVE_SLOT_COUNT] = {0};
|
||||
|
||||
if(settings != NULL) {
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSettings(settings, &json, &len);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
settingsItem = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
} else if(haveOld) {
|
||||
settingsItem.ptr = oldSpans.settings.ptr;
|
||||
settingsItem.len = oldSpans.settings.len;
|
||||
} else {
|
||||
savesettings_t defaultSettings;
|
||||
saveSettingsInit(&defaultSettings);
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSettings(
|
||||
&defaultSettings, &json, &len
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
settingsItem = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
if(slot != NULL && i == slotIndex) {
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSlot(slot, &json, &len);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
slotItems[i] = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
} else if(haveOld) {
|
||||
slotItems[i].ptr = oldSpans.slots[i].ptr;
|
||||
slotItems[i].len = oldSpans.slots[i].len;
|
||||
} else {
|
||||
saveslot_t defaultSlot;
|
||||
saveSlotInit(&defaultSlot);
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSlot(&defaultSlot, &json, &len);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
slotItems[i] = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble the new logical blob: length table, then each item's bytes.
|
||||
size_t tableSize = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT);
|
||||
size_t logicalSize = tableSize + settingsItem.len;
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) logicalSize += slotItems[i].len;
|
||||
|
||||
uint8_t *newLogical = memoryAllocate(logicalSize);
|
||||
*(uint32_t *)(newLogical + 0) = endianLittleToHost32(settingsItem.len);
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
*(uint32_t *)(newLogical + sizeof(uint32_t) * (1 + i)) =
|
||||
endianLittleToHost32(slotItems[i].len);
|
||||
}
|
||||
|
||||
size_t offset = tableSize;
|
||||
memoryCopy(newLogical + offset, settingsItem.ptr, settingsItem.len);
|
||||
offset += settingsItem.len;
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
memoryCopy(newLogical + offset, slotItems[i].ptr, slotItems[i].len);
|
||||
offset += slotItems[i].len;
|
||||
}
|
||||
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
|
||||
// Compress and frame with the header.
|
||||
uLongf compressedCap = compressBound((uLong)logicalSize);
|
||||
uint8_t *compressedBuf = memoryAllocate(compressedCap);
|
||||
uLongf compressedLen = compressedCap;
|
||||
int compressResult = compress2(
|
||||
compressedBuf, &compressedLen, newLogical, (uLong)logicalSize,
|
||||
Z_DEFAULT_COMPRESSION
|
||||
);
|
||||
memoryFree(newLogical);
|
||||
if(compressResult != Z_OK) {
|
||||
memoryFree(compressedBuf);
|
||||
errorThrow("Failed to compress save data (zlib error %d)", compressResult);
|
||||
}
|
||||
|
||||
savedevicerawheader_t header;
|
||||
memoryCopy(header.magic, SAVE_DEVICE_RAW_MAGIC, 4);
|
||||
header.version = endianLittleToHost32(SAVE_DEVICE_RAW_VERSION);
|
||||
header.uncompressedSize = endianLittleToHost32((uint32_t)logicalSize);
|
||||
header.compressedSize = endianLittleToHost32((uint32_t)compressedLen);
|
||||
header.checksum = endianLittleToHost32(cryptCRC32(compressedBuf, compressedLen));
|
||||
header.generation = endianLittleToHost32(haveOld ? oldGeneration + 1 : 1);
|
||||
|
||||
size_t finalSize = sizeof(header) + compressedLen;
|
||||
uint8_t *finalBuf = memoryAllocate(finalSize);
|
||||
memoryCopy(finalBuf, &header, sizeof(header));
|
||||
memoryCopy(finalBuf + sizeof(header), compressedBuf, compressedLen);
|
||||
memoryFree(compressedBuf);
|
||||
|
||||
errorret_t writeResult = saveDeviceDataWritePlatform(
|
||||
device, finalBuf, finalSize
|
||||
);
|
||||
memoryFree(finalBuf);
|
||||
errorChain(writeResult);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Reads the combined blob and populates exactly one item - `settings`, or
|
||||
// the slot at `slotIndex` when `slot` is given, exactly one of the two must
|
||||
// be non-NULL. Leaves the destination untouched if nothing's been saved yet.
|
||||
static errorret_t saveDeviceRawFetchItem(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
) {
|
||||
uint8_t *raw = NULL;
|
||||
size_t rawSize = 0;
|
||||
errorChain(saveDeviceDataReadPlatform(device, &raw, &rawSize));
|
||||
if(raw == NULL) errorOk();
|
||||
|
||||
uint8_t *logical = NULL;
|
||||
size_t logicalSize = 0;
|
||||
errorret_t inflateResult = saveDeviceRawInflate(
|
||||
raw, rawSize, &logical, &logicalSize, NULL
|
||||
);
|
||||
memoryFree(raw);
|
||||
errorChain(inflateResult);
|
||||
|
||||
savedevicerawspans_t spans;
|
||||
saveDeviceRawParseSpans(logical, &spans);
|
||||
const savedevicerawspan_t *span = settings != NULL ?
|
||||
&spans.settings : &spans.slots[slotIndex];
|
||||
|
||||
yyjson_doc *jsonDoc = yyjson_read((const char_t *)span->ptr, span->len, 0);
|
||||
if(jsonDoc == NULL) {
|
||||
memoryFree(logical);
|
||||
errorThrow("Failed to parse save data item JSON");
|
||||
}
|
||||
|
||||
yyjson_val *object = yyjson_doc_get_root(jsonDoc);
|
||||
if(object == NULL) {
|
||||
yyjson_doc_free(jsonDoc);
|
||||
memoryFree(logical);
|
||||
errorThrow("Save data item JSON missing root object");
|
||||
}
|
||||
|
||||
errorret_t readResult = settings != NULL ?
|
||||
saveSettingsReadJSON(settings, object) : saveSlotReadJSON(slot, object);
|
||||
yyjson_doc_free(jsonDoc);
|
||||
memoryFree(logical);
|
||||
errorChain(readResult);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
#endif// defined(SAVE_DEVICE_DATA_RAW)
|
||||
|
||||
errorret_t saveDeviceSlotWrite(
|
||||
savedevice_t *device,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(slot, "slot cannot be null");
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawStoreItem(device, NULL, slot, slotIndex));
|
||||
#elif defined(saveDeviceSlotWritePlatform)
|
||||
errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex));
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceSlotRead(
|
||||
savedevice_t *device,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(slot, "slot cannot be null");
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawFetchItem(device, NULL, slot, slotIndex));
|
||||
#elif defined(saveDeviceSlotReadPlatform)
|
||||
errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex));
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceSettingsWrite(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(settings, "settings cannot be null");
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawStoreItem(device, settings, NULL, 0));
|
||||
#elif defined(saveDeviceSettingsWritePlatform)
|
||||
errorChain(saveDeviceSettingsWritePlatform(device, settings));
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceSettingsRead(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(settings, "settings cannot be null");
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawFetchItem(device, settings, NULL, 0));
|
||||
#elif defined(saveDeviceSettingsReadPlatform)
|
||||
errorChain(saveDeviceSettingsReadPlatform(device, settings));
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDispose(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
errorChain(saveDevicePlatformDispose(device));
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "save/savedeviceplatform.h"
|
||||
|
||||
// Save device count e.g. Memory Cards count.
|
||||
#ifndef SAVE_DEVICE_COUNT
|
||||
#error "SAVE_DEVICE_COUNT must be defined"
|
||||
#endif
|
||||
|
||||
// Platform opt-in: define this in a platform's savedeviceplatform.h (instead
|
||||
// of the four saveDevice{Slot,Settings}{Write,Read}Platform macros) for
|
||||
// devices without free-form filesystem access - e.g. a memory card/stick
|
||||
// that suits one combined save blob better than N+1 separate files. When
|
||||
// defined, the platform must instead provide these two:
|
||||
//
|
||||
// errorret_t saveDeviceDataWritePlatform(
|
||||
// savedevice_t *device, const uint8_t *buffer, size_t size
|
||||
// );
|
||||
// errorret_t saveDeviceDataReadPlatform(
|
||||
// savedevice_t *device, uint8_t **outBuffer, size_t *outSize
|
||||
// );
|
||||
//
|
||||
// savedevice.c compresses settings + all save slots' JSON into one
|
||||
// magic/version/checksum-framed blob and read/writes it as a single raw
|
||||
// buffer through these two hooks instead. saveDeviceDataReadPlatform must
|
||||
// allocate its output with memoryAllocate (ownership passes to the caller,
|
||||
// who frees it with memoryFree), and set *outBuffer = NULL/*outSize = 0
|
||||
// (not an error) when nothing has been saved yet - same convention as the
|
||||
// "file not found" case in the non-raw four-hook mode.
|
||||
// #define SAVE_DEVICE_DATA_RAW
|
||||
//
|
||||
// saveDeviceDataReadPlatform must always resolve to "the one current blob"
|
||||
// even if the platform keeps more than one physical copy under the hood -
|
||||
// e.g. a platform with no rename primitive (GameCube memory cards) that
|
||||
// ping-pongs between two fixed files instead. saveDeviceRawIsValid() below
|
||||
// is what such a platform uses to tell which of its copies is newest.
|
||||
|
||||
typedef struct savedevice_s savedevice_t;
|
||||
typedef struct savesettings_s savesettings_t;
|
||||
typedef struct saveslot_s saveslot_t;
|
||||
|
||||
typedef void (*savedevicestatecallback_t)(savedevice_t *device, void *user);
|
||||
|
||||
typedef enum {
|
||||
SAVE_DEVICE_STATE_UNKNOWN,
|
||||
SAVE_DEVICE_STATE_CHECKING_AVAILABILITY,
|
||||
SAVE_DEVICE_STATE_UNAVAILABLE,
|
||||
SAVE_DEVICE_STATE_AVAILABLE,
|
||||
SAVE_DEVICE_STATE_ERRORED
|
||||
} savedevicestate_t;
|
||||
|
||||
typedef struct savedevice_s {
|
||||
savedevicestate_t state;
|
||||
savedeviceplatform_t platform;
|
||||
const char_t *reasonKey;
|
||||
bool_t fireCallback;
|
||||
void *user;
|
||||
savedevicestatecallback_t stateCallback;
|
||||
} savedevice_t;
|
||||
|
||||
/**
|
||||
* Initializes the save device.
|
||||
*
|
||||
* @param device The save device to initialize.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceInit(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Updates the save device.
|
||||
*
|
||||
* @param device The save device to update.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceUpdate(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Internal method to fire the save callback, does it at the appropriate time.
|
||||
*
|
||||
* @param device The save device to fire the callback for.
|
||||
*/
|
||||
void saveDeviceFireCallback(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Requests the device to check its availability, this will call the callback
|
||||
* whence completed.
|
||||
*
|
||||
* @param device The save device to check availability.
|
||||
* @param callback The callback to call when the availability check is complete.
|
||||
* @param user User data to pass to the callback.
|
||||
*/
|
||||
void saveDeviceCheckAvailability(
|
||||
savedevice_t *device,
|
||||
savedevicestatecallback_t callback,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes the given save slot to the device at the given slot index.
|
||||
*
|
||||
* @param device The save device to write to.
|
||||
* @param slot The save slot to write.
|
||||
* @param slotIndex The slot index to write to.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceSlotWrite(
|
||||
savedevice_t *device,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a save slot from the device at the given slot index.
|
||||
*
|
||||
* @param device The save device to read from.
|
||||
* @param slot The save slot to read into.
|
||||
* @param slotIndex The slot index to read from.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceSlotRead(
|
||||
savedevice_t *device,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes the given save settings to the device.
|
||||
*
|
||||
* @param device The save device to write to.
|
||||
* @param settings The save settings to write.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceSettingsWrite(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the save settings from the device.
|
||||
*
|
||||
* @param device The save device to read from.
|
||||
* @param settings The save settings to read into.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceSettingsRead(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings
|
||||
);
|
||||
|
||||
/**
|
||||
* Disposes of the save device.
|
||||
*
|
||||
* @param device The save device to dispose.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDispose(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Only relevant to SAVE_DEVICE_DATA_RAW platforms that keep more than one
|
||||
* physical copy of the save blob (see the note above) - cheaply checks
|
||||
* whether `raw` is a well-formed, checksum-valid raw save blob without
|
||||
* decompressing it, and if so reports its generation counter so the caller
|
||||
* can tell which of several copies is newest.
|
||||
*
|
||||
* @param raw The raw bytes to validate.
|
||||
* @param rawSize The number of bytes in raw.
|
||||
* @param outGeneration Receives the blob's generation counter if valid, may
|
||||
* be NULL if the caller doesn't need it.
|
||||
* @return true if raw is a valid, checksum-passing save blob.
|
||||
*/
|
||||
bool_t saveDeviceRawIsValid(
|
||||
const uint8_t *raw,
|
||||
const size_t rawSize,
|
||||
uint32_t *outGeneration
|
||||
);
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "yyjson.h"
|
||||
#include "error/error.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/string.h"
|
||||
#include "time/timeepoch.h"
|
||||
|
||||
#define SAVE_JSON_STRING_BUFFER_SIZE 256
|
||||
|
||||
/**
|
||||
* Creates a new mutable JSON document with an empty root object, ready for
|
||||
* the writeX macros below to populate. Declares `doc` and `object` in the
|
||||
* calling scope.
|
||||
*/
|
||||
#define writeInit() \
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); \
|
||||
assertNotNull(doc, "Failed to create JSON document"); \
|
||||
yyjson_mut_val *object = yyjson_mut_obj(doc); \
|
||||
yyjson_mut_doc_set_root(doc, object)
|
||||
|
||||
/**
|
||||
* Parses the given JSON text and exposes its root object for the readX
|
||||
* macros below to consume. Declares `jsonDoc` (keep it around to dispose
|
||||
* with yyjson_doc_free once reading is done) and `object` in the calling
|
||||
* scope. Errors if the text fails to parse or has no object root.
|
||||
*
|
||||
* @param jsonText The JSON text to parse.
|
||||
* @param jsonLength The length of the JSON text, in bytes.
|
||||
*/
|
||||
#define readInit(jsonText, jsonLength) \
|
||||
yyjson_doc *jsonDoc = yyjson_read((jsonText), (jsonLength), 0); \
|
||||
if(jsonDoc == NULL) errorThrow("Failed to parse save JSON"); \
|
||||
yyjson_val *object = yyjson_doc_get_root(jsonDoc); \
|
||||
if(object == NULL) errorThrow("Save JSON missing root object")
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasInt32(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireInt32(key) \
|
||||
if(!hasInt32(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes an int32_t to the current JSON object.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeInt32(key, value) \
|
||||
yyjson_mut_obj_add_int(doc, object, key, (int64_t)(value))
|
||||
|
||||
/**
|
||||
* Reads an int32_t from the current JSON object, falling back to the given
|
||||
* default if the key is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination variable to assign to.
|
||||
* @param def The default value to use if the key is missing.
|
||||
*/
|
||||
#define readInt32(key, dest, def) \
|
||||
(dest) = hasInt32(key) ? \
|
||||
(int32_t)yyjson_get_int(yyjson_obj_get(object, key)) : (int32_t)(def)
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasUInt32(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireUInt32(key) \
|
||||
if(!hasUInt32(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes a uint32_t to the current JSON object.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeUInt32(key, value) \
|
||||
yyjson_mut_obj_add_uint(doc, object, key, (uint64_t)(value))
|
||||
|
||||
/**
|
||||
* Reads a uint32_t from the current JSON object, falling back to the given
|
||||
* default if the key is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination variable to assign to.
|
||||
* @param def The default value to use if the key is missing.
|
||||
*/
|
||||
#define readUInt32(key, dest, def) \
|
||||
(dest) = hasUInt32(key) ? \
|
||||
(uint32_t)yyjson_get_uint(yyjson_obj_get(object, key)) : (uint32_t)(def)
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasUInt8(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireUInt8(key) \
|
||||
if(!hasUInt8(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes a uint8_t to the current JSON object.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeUInt8(key, value) \
|
||||
yyjson_mut_obj_add_uint(doc, object, key, (uint64_t)(value))
|
||||
|
||||
/**
|
||||
* Reads a uint8_t from the current JSON object, falling back to the given
|
||||
* default if the key is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination variable to assign to.
|
||||
* @param def The default value to use if the key is missing.
|
||||
*/
|
||||
#define readUInt8(key, dest, def) \
|
||||
(dest) = hasUInt8(key) ? \
|
||||
(uint8_t)yyjson_get_uint(yyjson_obj_get(object, key)) : (uint8_t)(def)
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasInt64(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireInt64(key) \
|
||||
if(!hasInt64(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes an int64_t to the current JSON object.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeInt64(key, value) \
|
||||
yyjson_mut_obj_add_sint(doc, object, key, (int64_t)(value))
|
||||
|
||||
/**
|
||||
* Reads an int64_t from the current JSON object, falling back to the given
|
||||
* default if the key is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination variable to assign to.
|
||||
* @param def The default value to use if the key is missing.
|
||||
*/
|
||||
#define readInt64(key, dest, def) \
|
||||
(dest) = hasInt64(key) ? \
|
||||
yyjson_get_sint(yyjson_obj_get(object, key)) : (int64_t)(def)
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasUInt64(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireUInt64(key) \
|
||||
if(!hasUInt64(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes a uint64_t to the current JSON object.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeUInt64(key, value) \
|
||||
yyjson_mut_obj_add_uint(doc, object, key, (uint64_t)(value))
|
||||
|
||||
/**
|
||||
* Reads a uint64_t from the current JSON object, falling back to the given
|
||||
* default if the key is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination variable to assign to.
|
||||
* @param def The default value to use if the key is missing.
|
||||
*/
|
||||
#define readUInt64(key, dest, def) \
|
||||
(dest) = hasUInt64(key) ? \
|
||||
yyjson_get_uint(yyjson_obj_get(object, key)) : (uint64_t)(def)
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasFloat(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireFloat(key) \
|
||||
if(!hasFloat(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes a float_t to the current JSON object.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeFloat(key, value) \
|
||||
yyjson_mut_obj_add_real(doc, object, key, (double)(value))
|
||||
|
||||
/**
|
||||
* Reads a float_t from the current JSON object, falling back to the given
|
||||
* default if the key is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination variable to assign to.
|
||||
* @param def The default value to use if the key is missing.
|
||||
*/
|
||||
#define readFloat(key, dest, def) \
|
||||
(dest) = hasFloat(key) ? \
|
||||
(float_t)yyjson_get_real(yyjson_obj_get(object, key)) : (float_t)(def)
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasString(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireString(key) \
|
||||
if(!hasString(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes a string to the current JSON object. The string is copied, so it
|
||||
* does not need to outlive the JSON document.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The value to write.
|
||||
*/
|
||||
#define writeString(key, value) \
|
||||
yyjson_mut_obj_add_strcpy(doc, object, key, value)
|
||||
|
||||
/**
|
||||
* Reads a string from the current JSON object into dest, falling back to
|
||||
* the given default if the key is missing. Errors if the JSON string is
|
||||
* longer than maxLength. Uses the caller's `saveJsonStringBuffer` local as
|
||||
* scratch space.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination buffer to copy into.
|
||||
* @param def The default value to use if the key is missing.
|
||||
* @param maxLength The maximum length of dest, excluding the null terminator.
|
||||
*/
|
||||
#define readString(key, dest, def, maxLength) { \
|
||||
yyjson_val *saveJsonStrVal = yyjson_obj_get(object, key); \
|
||||
if(saveJsonStrVal != NULL) { \
|
||||
if(yyjson_get_len(saveJsonStrVal) > (size_t)(maxLength)) { \
|
||||
errorThrow( \
|
||||
"Save JSON string '%s' exceeds max length of %d", \
|
||||
key, (int)(maxLength) \
|
||||
); \
|
||||
} \
|
||||
stringCopy(saveJsonStringBuffer, yyjson_get_str(saveJsonStrVal), (maxLength)); \
|
||||
} else { \
|
||||
stringCopy(saveJsonStringBuffer, (def), (maxLength)); \
|
||||
} \
|
||||
stringCopy((dest), saveJsonStringBuffer, (maxLength)); \
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given key exists on the current JSON object.
|
||||
*
|
||||
* @param key The key to check for.
|
||||
*/
|
||||
#define hasTime(key) (yyjson_obj_get(object, key) != NULL)
|
||||
|
||||
/**
|
||||
* Errors if the given key does not exist on the current JSON object.
|
||||
*
|
||||
* @param key The key that must exist.
|
||||
*/
|
||||
#define requireTime(key) \
|
||||
if(!hasTime(key)) errorThrow("Save JSON missing '%s' key", key)
|
||||
|
||||
/**
|
||||
* Writes a dusktimeepoch_t to the current JSON object as a nested object of
|
||||
* its time/timeZone/offsetTime fields.
|
||||
*
|
||||
* @param key The key to write to.
|
||||
* @param value The dusktimeepoch_t value to write.
|
||||
*/
|
||||
#define writeTime(key, value) { \
|
||||
yyjson_mut_val *saveJsonTimeObj = yyjson_mut_obj_add_obj(doc, object, key); \
|
||||
yyjson_mut_obj_add_real(doc, saveJsonTimeObj, "time", (value).time); \
|
||||
yyjson_mut_obj_add_real(doc, saveJsonTimeObj, "timeZone", (value).timeZone); \
|
||||
yyjson_mut_obj_add_real(doc, saveJsonTimeObj, "offsetTime", (value).offsetTime); \
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a dusktimeepoch_t from the current JSON object's nested time/
|
||||
* timeZone/offsetTime fields. Errors if the key or any sub-field is missing.
|
||||
*
|
||||
* @param key The key to read from.
|
||||
* @param dest The destination dusktimeepoch_t to assign to.
|
||||
*/
|
||||
#define readTime(key, dest) { \
|
||||
yyjson_val *saveJsonTimeObj = yyjson_obj_get(object, key); \
|
||||
if(saveJsonTimeObj == NULL) errorThrow("Save JSON missing '%s' key", key); \
|
||||
yyjson_val *saveJsonTimeTime = yyjson_obj_get(saveJsonTimeObj, "time"); \
|
||||
yyjson_val *saveJsonTimeZone = yyjson_obj_get(saveJsonTimeObj, "timeZone"); \
|
||||
yyjson_val *saveJsonTimeOffset = yyjson_obj_get(saveJsonTimeObj, "offsetTime"); \
|
||||
if(saveJsonTimeTime == NULL) errorThrow("Save JSON missing '%s.time' key", key); \
|
||||
if(saveJsonTimeZone == NULL) { \
|
||||
errorThrow("Save JSON missing '%s.timeZone' key", key); \
|
||||
} \
|
||||
if(saveJsonTimeOffset == NULL) { \
|
||||
errorThrow("Save JSON missing '%s.offsetTime' key", key); \
|
||||
} \
|
||||
(dest).time = yyjson_get_real(saveJsonTimeTime); \
|
||||
(dest).timeZone = yyjson_get_real(saveJsonTimeZone); \
|
||||
(dest).offsetTime = yyjson_get_real(saveJsonTimeOffset); \
|
||||
}
|
||||
@@ -1,63 +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"
|
||||
|
||||
/** Save meta format version. Increment on breaking change. */
|
||||
#define SAVE_META_VERSION 1
|
||||
|
||||
/** Magic bytes that identify a Dusk save meta blob. */
|
||||
#define SAVE_META_HEADER "DSM"
|
||||
|
||||
/** Byte length of the magic header (excludes the null terminator). */
|
||||
#define SAVE_META_HEADER_SIZE (sizeof(SAVE_META_HEADER) - 1)
|
||||
|
||||
/**
|
||||
* Default gamepad deadzone for meta that's never actually been loaded from
|
||||
* disk yet (see saveInit(), which stamps this on first boot) - the save
|
||||
* meta is the single source of truth for this value (see
|
||||
* savemeta_t.deadzone); nothing else stores or defaults it.
|
||||
*/
|
||||
#define SAVE_META_DEADZONE_DEFAULT 0.1f
|
||||
|
||||
/**
|
||||
* Default language index for meta that's never actually been loaded from
|
||||
* disk yet - index 0 into LOCALE_LIST (see locale/localemanager.h), i.e.
|
||||
* LOCALE_EN_US.
|
||||
*/
|
||||
#define SAVE_META_LANGUAGE_DEFAULT 0
|
||||
|
||||
/**
|
||||
* Device/user-wide preferences, independent of any individual game save
|
||||
* slot (see saveslot.h) - there's exactly one of these, not one per slot,
|
||||
* since a setting like gamepad deadzone shouldn't reset or diverge just
|
||||
* because the player started a new game in a different slot.
|
||||
*/
|
||||
typedef struct {
|
||||
/** Magic header bytes read from the blob; must equal SAVE_META_HEADER. */
|
||||
char_t header[SAVE_META_HEADER_SIZE];
|
||||
/** Format version read from the blob; used to branch on older layouts. */
|
||||
uint32_t version;
|
||||
/** Runtime flag - true if meta was successfully loaded or written. */
|
||||
bool_t exists;
|
||||
/**
|
||||
* User-configured gamepad deadzone (0.0f-1.0f) - the save meta is the
|
||||
* only place this lives; read it directly via saveGetMeta()->deadzone
|
||||
* rather than caching it anywhere else.
|
||||
*/
|
||||
float_t deadzone;
|
||||
/**
|
||||
* Index into LOCALE_LIST (see locale/localemanager.h) for the player's
|
||||
* preferred UI language - the save meta is the only place this lives;
|
||||
* read it directly via saveGetMeta()->language rather than caching it
|
||||
* anywhere else. Never reorder/remove entries from LOCALE_LIST, only
|
||||
* append, since this index must keep meaning the same locale across
|
||||
* versions.
|
||||
*/
|
||||
uint8_t language;
|
||||
} savemeta_t;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
|
||||
// Custom error codes for the save system, these start at 2 since ERROR_OK
|
||||
// and ERROR_NOT_OK occupy 0 and 1 respectively.
|
||||
#define SAVE_ERROR_NO_CURRENT_DEVICE ((errorcode_t)2)
|
||||
#define SAVE_ERROR_NO_CURRENT_SLOT ((errorcode_t)3)
|
||||
#define SAVE_ERROR_DEVICE_NOT_AVAILABLE ((errorcode_t)4)
|
||||
#define SAVE_ERROR_SLOT_NOT_IN_USE ((errorcode_t)5)
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "savesettings.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "savejson.h"
|
||||
|
||||
void saveSettingsInit(savesettings_t *settings) {
|
||||
assertNotNull(settings, "Settings cannot be null");
|
||||
|
||||
memorySet(settings, 0, sizeof(savesettings_t));
|
||||
}
|
||||
|
||||
errorret_t saveSettingsWriteJSON(
|
||||
savesettings_t *settings,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *object
|
||||
) {
|
||||
assertNotNull(settings, "Settings cannot be null");
|
||||
assertNotNull(doc, "Doc cannot be null");
|
||||
assertNotNull(object, "Object cannot be null");
|
||||
|
||||
writeInt32("someSetting", settings->someSetting);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object) {
|
||||
assertNotNull(settings, "Settings cannot be null");
|
||||
assertNotNull(object, "Object cannot be null");
|
||||
|
||||
readInt32("someSetting", settings->someSetting, 0);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -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 "time/timeepoch.h"
|
||||
#include "savedevice.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
typedef struct savesettings_s {
|
||||
int32_t someSetting;
|
||||
} savesettings_t;
|
||||
|
||||
/**
|
||||
* Inits the save settings with the default state, this is functionally "new
|
||||
* game" but will not set the player name, as that is what we use to determine
|
||||
* if a save slot is "in use" or not.
|
||||
*
|
||||
* @param settings The save settings to init.
|
||||
*/
|
||||
void saveSettingsInit(savesettings_t *settings);
|
||||
|
||||
/**
|
||||
* Writes the given save settings out to the given JSON object.
|
||||
*
|
||||
* @param settings The save settings to write.
|
||||
* @param doc The mutable JSON document that owns the object.
|
||||
* @param object The mutable JSON object to write into.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveSettingsWriteJSON(
|
||||
savesettings_t *settings,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *object
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the given save settings in from the given JSON object.
|
||||
*
|
||||
* @param settings The save settings to read into.
|
||||
* @param object The JSON object to read from.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object);
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "saveslot.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "savejson.h"
|
||||
#include "time/time.h"
|
||||
|
||||
void saveSlotInit(saveslot_t *slot) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
|
||||
memorySet(slot, 0, sizeof(saveslot_t));
|
||||
slot->version = 1;
|
||||
}
|
||||
|
||||
bool_t saveSlotInUse(saveslotcache_t *slot) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
return slot->name[0] != '\0';
|
||||
}
|
||||
|
||||
bool_t saveSlotHasSaved(saveslotcache_t *slot) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
return slot->time.time != 0;
|
||||
}
|
||||
|
||||
errorret_t saveSlotWriteJSON(
|
||||
saveslot_t *slot,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *object
|
||||
) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
assertNotNull(doc, "Doc cannot be null");
|
||||
assertNotNull(object, "Object cannot be null");
|
||||
|
||||
// Update time
|
||||
slot->cachedData.time = timeGetEpoch();
|
||||
|
||||
writeString("name", slot->cachedData.name);
|
||||
writeTime("time", slot->cachedData.time);
|
||||
writeInt32("playerLevel", slot->cachedData.playerLevel);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
assertNotNull(object, "Object cannot be null");
|
||||
|
||||
char_t saveJsonStringBuffer[SAVE_JSON_STRING_BUFFER_SIZE];
|
||||
|
||||
readString("name", slot->cachedData.name, "", SAVE_SLOT_NAME_LENGTH);
|
||||
readTime("time", slot->cachedData.time);
|
||||
readInt32("playerLevel", slot->cachedData.playerLevel, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
+60
-82
@@ -1,100 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
#include "time/timeepoch.h"
|
||||
#include "savedevice.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
/** Save slot format version. Increment on breaking change. */
|
||||
#define SAVE_SLOT_VERSION 1
|
||||
|
||||
/** Magic bytes that identify a Dusk save slot. */
|
||||
#define SAVE_SLOT_HEADER "DSK"
|
||||
|
||||
/** Byte length of the magic header (excludes the null terminator). */
|
||||
#define SAVE_SLOT_HEADER_SIZE (sizeof(SAVE_SLOT_HEADER) - 1)
|
||||
|
||||
/**
|
||||
* Maximum number of independent save slots supported. Platform-overridable
|
||||
* via a compiler define (not a header #ifndef alone, since this header is
|
||||
* included before any platform header gets a chance to react) - see PSP's
|
||||
* CMakeLists.txt, which overrides this to 1.
|
||||
*/
|
||||
#ifndef SAVE_SLOT_COUNT_MAX
|
||||
#define SAVE_SLOT_COUNT_MAX 3
|
||||
#define SAVE_SLOT_NAME_LENGTH 8
|
||||
#ifndef SAVE_SLOT_COUNT
|
||||
#define SAVE_SLOT_COUNT 3
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The save slot actually used for gameplay right now - there's no slot
|
||||
* select/multi-save UX yet (SAVE_SLOT_COUNT_MAX > 1 exists for later), so
|
||||
* every part of the game that needs "the" save slot (the game menu's Save
|
||||
* button, etc.) reads/writes this one slot.
|
||||
*/
|
||||
#define SAVE_ACTIVE_SLOT 0
|
||||
|
||||
/** Maximum length of a saved player name, including the null terminator. */
|
||||
#define SAVE_PLAYER_NAME_MAX 32
|
||||
|
||||
/**
|
||||
* Maximum number of global entities whose "collected" state can be
|
||||
* tracked - see rpg/entity/global/globalitemstore.h. Bounded/fixed here
|
||||
* rather than tied to ENTITY_GLOBAL_LIST_COUNT, since saveslot.h is a
|
||||
* leaf header with no dependency on the entity system (and no reason to
|
||||
* take one just for a size constant).
|
||||
*/
|
||||
#define SAVE_GLOBAL_ITEM_COUNT_MAX 64
|
||||
|
||||
/**
|
||||
* Maximum number of story flags the save format can hold - see
|
||||
* rpg/story/storyflag.h. Bounded/fixed here (with real headroom over the
|
||||
* current flag count) rather than tied to STORY_FLAG_COUNT, since
|
||||
* saveslot.h is a leaf header with no dependency on generated story
|
||||
* content, matching SAVE_GLOBAL_ITEM_COUNT_MAX's reasoning.
|
||||
*/
|
||||
#define SAVE_STORY_FLAG_COUNT_MAX 128
|
||||
|
||||
/** Per-slot game progress - the state a "save file" traditionally means. */
|
||||
typedef struct {
|
||||
/** Magic header bytes read from the slot; must equal SAVE_SLOT_HEADER. */
|
||||
char_t header[SAVE_SLOT_HEADER_SIZE];
|
||||
/** Format version read from the slot; used to branch on older layouts. */
|
||||
uint32_t version;
|
||||
/** Runtime flag - true if this slot was successfully loaded or written. */
|
||||
bool_t exists;
|
||||
/** The player's saved name. */
|
||||
char_t playerName[SAVE_PLAYER_NAME_MAX];
|
||||
/** Per-global-ID "already collected" flags - see globalitemstore.h. */
|
||||
bool_t globalItemCollected[SAVE_GLOBAL_ITEM_COUNT_MAX];
|
||||
/**
|
||||
* Story flag values, indexed by storyflag_t - the save slot is the only
|
||||
* place these live; read/write via storyFlagGet()/storyFlagSet() (see
|
||||
* rpg/story/storyflag.h), not directly.
|
||||
*/
|
||||
uint8_t storyFlags[SAVE_STORY_FLAG_COUNT_MAX];
|
||||
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
||||
dusktimeepoch_t time;
|
||||
int32_t playerLevel;
|
||||
} saveslotcache_t;
|
||||
|
||||
typedef struct saveslot_s {
|
||||
uint8_t version;
|
||||
uint8_t dataType;
|
||||
|
||||
saveslotcache_t cachedData;
|
||||
} saveslot_t;
|
||||
|
||||
/**
|
||||
* Callback invoked when an async saveWriteSlot()/saveLoadSlot()/
|
||||
* saveWriteMeta()/saveLoadMeta() request completes. Declared here (rather
|
||||
* than save.h) so platform save headers - which save.h's platform
|
||||
* indirection pulls in before save.h finishes defining anything else - can
|
||||
* reference it without a circular include.
|
||||
*
|
||||
* @param result Whether the request succeeded.
|
||||
* @param user User data passed through from the original call.
|
||||
* Inits the save slot with the default state, this is functionally "new game"
|
||||
* but will not set the player name, as that is what we use to determine if a
|
||||
* save slot is "in use" or not.
|
||||
*
|
||||
* @param slot The save slot to init.
|
||||
*/
|
||||
typedef void (*savecallback_t)(errorret_t result, void *user);
|
||||
void saveSlotInit(saveslot_t *slot);
|
||||
|
||||
/**
|
||||
* The blank template used to (re)initialize a save slot for a brand new
|
||||
* game via saveLoadDefault(), rather than reading one from persistent
|
||||
* storage. Deliberately generic at this layer (empty collected-item
|
||||
* flags, no story flags set) - anything that needs CSV-defined story flag
|
||||
* defaults layers that on top afterward (see storyFlagInitDefaults()),
|
||||
* since saveslot.h stays a leaf header with no dependency on generated
|
||||
* story content (see SAVE_STORY_FLAG_COUNT_MAX's doc comment above).
|
||||
* Checks if the save slot is in use, this is determined by checking if the
|
||||
* player name is set or not.
|
||||
*
|
||||
* @param slot The save slot to check.
|
||||
* @return True if the save slot is in use, false otherwise.
|
||||
*/
|
||||
extern const saveslot_t SAVE_DEFAULT;
|
||||
bool_t saveSlotInUse(saveslotcache_t *slot);
|
||||
|
||||
/**
|
||||
* Returns whether or not the given save slot has ever saved (has an epoc non 0)
|
||||
*
|
||||
* @param slot The save slot to check.
|
||||
* @return True if the save slot has ever saved, false otherwise.
|
||||
*/
|
||||
bool_t saveSlotHasSaved(saveslotcache_t *slot);
|
||||
|
||||
/**
|
||||
* Writes the given save slot out to the given JSON object.
|
||||
*
|
||||
* @param slot The save slot to write.
|
||||
* @param doc The mutable JSON document that owns the object.
|
||||
* @param object The mutable JSON object to write into.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveSlotWriteJSON(
|
||||
saveslot_t *slot,
|
||||
yyjson_mut_doc *doc,
|
||||
yyjson_mut_val *object
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the given save slot in from the given JSON object.
|
||||
*
|
||||
* @param slot The save slot to read into.
|
||||
* @param object The JSON object to read from.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object);
|
||||
@@ -1,403 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savestream.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/endian.h"
|
||||
#include "util/string.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t saveStreamReadBytesRawImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
) {
|
||||
#ifdef saveStreamReadBytesPlatform
|
||||
errorChain(saveStreamReadBytesPlatform(stream, buf, len));
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesRawImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
) {
|
||||
#ifdef saveStreamWriteBytesPlatform
|
||||
errorChain(saveStreamWriteBytesPlatform(stream, buf, len));
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBytesImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
) {
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, buf, len));
|
||||
cryptCRC32Update(&stream->checksum, buf, len);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
) {
|
||||
cryptCRC32Update(&stream->checksum, buf, len);
|
||||
errorChain(saveStreamWriteBytesRawImpl(stream, buf, len));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out) {
|
||||
#ifdef saveStreamTellPlatform
|
||||
errorChain(saveStreamTellPlatform(stream, out));
|
||||
#else
|
||||
*out = 0;
|
||||
#endif
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamFinalizeWriteImpl(
|
||||
savestream_t *stream, const size_t headerPosition, const size_t headerSize
|
||||
) {
|
||||
uint32_t finalCRC = cryptCRC32End(stream->checksum);
|
||||
uint32_t leChecksum = endianLittleToHost32(finalCRC);
|
||||
|
||||
#ifdef saveStreamSeekPlatform
|
||||
errorChain(saveStreamSeekPlatform(stream, headerPosition + headerSize));
|
||||
#endif
|
||||
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, &leChecksum, sizeof(uint32_t)
|
||||
));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamVerifyChecksumImpl(
|
||||
savestream_t *stream, const char_t *sectionLabel
|
||||
) {
|
||||
uint32_t computed = cryptCRC32End(stream->checksum);
|
||||
if(computed != stream->expectedChecksum) {
|
||||
errorThrow("%s has invalid checksum", sectionLabel);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadHeaderImpl(
|
||||
savestream_t *stream, char_t *header, const char_t *expectedHeader,
|
||||
const size_t headerSize
|
||||
) {
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, header, headerSize));
|
||||
|
||||
for(size_t i = 0; i < headerSize; i++) {
|
||||
if(header[i] != expectedHeader[i]) {
|
||||
errorThrow("Save data has invalid header");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t leChecksum;
|
||||
errorChain(saveStreamReadBytesRawImpl(stream, &leChecksum, sizeof(uint32_t)));
|
||||
stream->expectedChecksum = endianLittleToHost32(leChecksum);
|
||||
stream->checksum = cryptCRC32Begin();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteHeaderImpl(
|
||||
savestream_t *stream, const char_t *header, const size_t headerSize
|
||||
) {
|
||||
errorChain(saveStreamWriteBytesRawImpl(stream, header, headerSize));
|
||||
|
||||
uint32_t placeholder = 0;
|
||||
errorChain(saveStreamWriteBytesRawImpl(
|
||||
stream, &placeholder, sizeof(uint32_t)
|
||||
));
|
||||
stream->checksum = cryptCRC32Begin();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
*out = endianLittleToHost32(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteVersionImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
) {
|
||||
uint32_t raw = endianLittleToHost32(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out) {
|
||||
uint8_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint8_t)));
|
||||
*out = (bool_t)(raw != 0);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input) {
|
||||
uint8_t raw = *input ? 1 : 0;
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(int8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input) {
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(int8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt8Impl(
|
||||
savestream_t *stream, const uint8_t *input
|
||||
) {
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(uint8_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out) {
|
||||
uint16_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
uint16_t host = endianLittleToHost16(raw);
|
||||
memoryCopy(out, &host, sizeof(int16_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt16Impl(
|
||||
savestream_t *stream, const int16_t *input
|
||||
) {
|
||||
uint16_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int16_t));
|
||||
raw = endianLittleToHost16(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out) {
|
||||
uint16_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
*out = endianLittleToHost16(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt16Impl(
|
||||
savestream_t *stream, const uint16_t *input
|
||||
) {
|
||||
uint16_t raw = endianLittleToHost16(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
uint32_t host = endianLittleToHost32(raw);
|
||||
memoryCopy(out, &host, sizeof(int32_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt32Impl(
|
||||
savestream_t *stream, const int32_t *input
|
||||
) {
|
||||
uint32_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int32_t));
|
||||
raw = endianLittleToHost32(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out) {
|
||||
uint32_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
*out = endianLittleToHost32(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt32Impl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
) {
|
||||
uint32_t raw = endianLittleToHost32(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out) {
|
||||
uint64_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
uint64_t host = endianLittleToHost64(raw);
|
||||
memoryCopy(out, &host, sizeof(int64_t));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteInt64Impl(
|
||||
savestream_t *stream, const int64_t *input
|
||||
) {
|
||||
uint64_t raw;
|
||||
memoryCopy(&raw, input, sizeof(int64_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out) {
|
||||
uint64_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
*out = endianLittleToHost64(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteUInt64Impl(
|
||||
savestream_t *stream, const uint64_t *input
|
||||
) {
|
||||
uint64_t raw = endianLittleToHost64(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out) {
|
||||
float_t raw;
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(float_t)));
|
||||
*out = endianLittleToHostFloat(raw);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteFloatImpl(
|
||||
savestream_t *stream, const float_t *input
|
||||
) {
|
||||
float_t raw = endianLittleToHostFloat(*input);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(float_t)));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadStringImpl(
|
||||
savestream_t *stream, char_t *out, const size_t maxLen
|
||||
) {
|
||||
for(size_t i = 0; i < maxLen; i++) {
|
||||
errorChain(saveStreamReadBytesImpl(stream, &out[i], sizeof(char_t)));
|
||||
if(out[i] == '\0') errorOk();
|
||||
}
|
||||
out[maxLen - 1] = '\0';
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteStringImpl(
|
||||
savestream_t *stream, const char_t *input, const size_t maxLen
|
||||
) {
|
||||
size_t len = strlen(input);
|
||||
if(len >= maxLen) len = maxLen - 1;
|
||||
errorChain(saveStreamWriteBytesImpl(stream, input, len + 1));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadDateImpl(savestream_t *stream, dusktimeepoch_t *out) {
|
||||
uint64_t raw;
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->time, &raw, sizeof(double_t));
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->timeZone, &raw, sizeof(double_t));
|
||||
|
||||
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
raw = endianLittleToHost64(raw);
|
||||
memoryCopy(&out->offsetTime, &raw, sizeof(double_t));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteDateImpl(
|
||||
savestream_t *stream, const dusktimeepoch_t *input
|
||||
) {
|
||||
uint64_t raw;
|
||||
|
||||
memoryCopy(&raw, &input->time, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
memoryCopy(&raw, &input->timeZone, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
memoryCopy(&raw, &input->offsetTime, sizeof(double_t));
|
||||
raw = endianLittleToHost64(raw);
|
||||
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta) {
|
||||
saveFileReadHeader(
|
||||
stream, meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE
|
||||
);
|
||||
saveFileReadVersion(stream, &meta->version);
|
||||
saveFileReadFloat(stream, &meta->deadzone);
|
||||
saveFileReadUInt8(stream, &meta->language);
|
||||
errorChain(saveStreamVerifyChecksumImpl(stream, "Save meta"));
|
||||
meta->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta) {
|
||||
memoryCopy(meta->header, SAVE_META_HEADER, SAVE_META_HEADER_SIZE);
|
||||
meta->version = SAVE_META_VERSION;
|
||||
|
||||
size_t headerPosition;
|
||||
errorChain(saveStreamTellImpl(stream, &headerPosition));
|
||||
saveFileWriteHeader(stream, meta->header, SAVE_META_HEADER_SIZE);
|
||||
saveFileWriteVersion(stream, &meta->version);
|
||||
saveFileWriteFloat(stream, &meta->deadzone);
|
||||
saveFileWriteUInt8(stream, &meta->language);
|
||||
errorChain(saveStreamFinalizeWriteImpl(
|
||||
stream, headerPosition, SAVE_META_HEADER_SIZE
|
||||
));
|
||||
meta->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot) {
|
||||
saveFileReadHeader(
|
||||
stream, slot->header, SAVE_SLOT_HEADER, SAVE_SLOT_HEADER_SIZE
|
||||
);
|
||||
saveFileReadVersion(stream, &slot->version);
|
||||
saveFileReadString(stream, slot->playerName, SAVE_PLAYER_NAME_MAX);
|
||||
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
||||
saveFileReadBool(stream, &slot->globalItemCollected[i]);
|
||||
}
|
||||
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
||||
saveFileReadUInt8(stream, &slot->storyFlags[i]);
|
||||
}
|
||||
errorChain(saveStreamVerifyChecksumImpl(stream, "Save slot"));
|
||||
slot->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot) {
|
||||
memoryCopy(slot->header, SAVE_SLOT_HEADER, SAVE_SLOT_HEADER_SIZE);
|
||||
slot->version = SAVE_SLOT_VERSION;
|
||||
|
||||
size_t headerPosition;
|
||||
errorChain(saveStreamTellImpl(stream, &headerPosition));
|
||||
saveFileWriteHeader(stream, slot->header, SAVE_SLOT_HEADER_SIZE);
|
||||
saveFileWriteVersion(stream, &slot->version);
|
||||
saveFileWriteString(stream, slot->playerName, SAVE_PLAYER_NAME_MAX);
|
||||
for(size_t i = 0; i < SAVE_GLOBAL_ITEM_COUNT_MAX; i++) {
|
||||
saveFileWriteBool(stream, &slot->globalItemCollected[i]);
|
||||
}
|
||||
for(size_t i = 0; i < SAVE_STORY_FLAG_COUNT_MAX; i++) {
|
||||
saveFileWriteUInt8(stream, &slot->storyFlags[i]);
|
||||
}
|
||||
errorChain(saveStreamFinalizeWriteImpl(
|
||||
stream, headerPosition, SAVE_SLOT_HEADER_SIZE
|
||||
));
|
||||
slot->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,511 +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 "saveslot.h"
|
||||
#include "savemeta.h"
|
||||
#include "save/saveplatform.h"
|
||||
#include "time/timeepoch.h"
|
||||
|
||||
typedef struct {
|
||||
bool_t found;
|
||||
uint32_t checksum;
|
||||
uint32_t expectedChecksum;
|
||||
saveplatformstream_t platform;
|
||||
} savestream_t;
|
||||
|
||||
/**
|
||||
* Reads bytes from the platform stream without updating the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesRawImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes bytes to the platform stream without updating the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesRawImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads bytes from the platform stream and accumulates them into the CRC.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesImpl(
|
||||
savestream_t *stream, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the CRC then writes bytes to the platform stream.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesImpl(
|
||||
savestream_t *stream, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the current read/write position within the stream. Used to capture
|
||||
* a section's start position before writing its header, so its checksum
|
||||
* can be backfilled at the right offset once the section's body is known -
|
||||
* required now that a single stream can hold multiple self-contained
|
||||
* sections back-to-back (meta + N save slots), not just one.
|
||||
*
|
||||
* @param stream Active stream.
|
||||
* @param out Receives the current position.
|
||||
* @return An error if the platform can't report a position.
|
||||
*/
|
||||
errorret_t saveStreamTellImpl(savestream_t *stream, size_t *out);
|
||||
|
||||
/**
|
||||
* Finalizes a write stream: computes the final CRC32, seeks back to the
|
||||
* checksum field just after this section's header, and writes it in
|
||||
* little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param headerPosition Byte offset where this section's header started
|
||||
* (see saveStreamTellImpl), captured before writing the header.
|
||||
* @param headerSize Byte length of this section's magic header.
|
||||
* @return An error if the seek or write fails.
|
||||
*/
|
||||
errorret_t saveStreamFinalizeWriteImpl(
|
||||
savestream_t *stream, const size_t headerPosition, const size_t headerSize
|
||||
);
|
||||
|
||||
/**
|
||||
* Verifies that the CRC32 accumulated during loading matches the value
|
||||
* stored in this section's header.
|
||||
*
|
||||
* @param stream Active read stream (loading must be complete).
|
||||
* @param sectionLabel Human-readable label used in the error message on
|
||||
* mismatch (e.g. "save meta", "save slot").
|
||||
* @return An error if the checksum does not match.
|
||||
*/
|
||||
errorret_t saveStreamVerifyChecksumImpl(
|
||||
savestream_t *stream, const char_t *sectionLabel
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads and validates a section's magic header, then reads its stored
|
||||
* CRC32 and resets the running accumulator.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param header Buffer of headerSize bytes to receive the header.
|
||||
* @param expectedHeader The magic bytes this section must match.
|
||||
* @param headerSize Byte length of the magic header.
|
||||
* @return An error if the header is missing or invalid.
|
||||
*/
|
||||
errorret_t saveStreamReadHeaderImpl(
|
||||
savestream_t *stream, char_t *header, const char_t *expectedHeader,
|
||||
const size_t headerSize
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a section's magic header and a zero CRC32 placeholder, then
|
||||
* resets the running accumulator.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param header Buffer of headerSize bytes to write.
|
||||
* @param headerSize Byte length of the magic header.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteHeaderImpl(
|
||||
savestream_t *stream, const char_t *header, const size_t headerSize
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian uint32 version field from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a uint32 version field to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteVersionImpl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a single byte as a boolean (0 = false, non-zero = true).
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the boolean value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out);
|
||||
|
||||
/**
|
||||
* Writes a boolean as a single byte (true = 1, false = 0).
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input);
|
||||
|
||||
/**
|
||||
* Reads a signed 8-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 8-bit integer to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input);
|
||||
|
||||
/**
|
||||
* Reads an unsigned 8-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 8-bit integer to the stream.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt8Impl(savestream_t *stream, const uint8_t *input);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 16-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 16-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt16Impl(
|
||||
savestream_t *stream, const int16_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 16-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 16-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt16Impl(
|
||||
savestream_t *stream, const uint16_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 32-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 32-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt32Impl(
|
||||
savestream_t *stream, const int32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 32-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 32-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt32Impl(
|
||||
savestream_t *stream, const uint32_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed 64-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out);
|
||||
|
||||
/**
|
||||
* Writes a signed 64-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteInt64Impl(
|
||||
savestream_t *stream, const int64_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian unsigned 64-bit integer from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out);
|
||||
|
||||
/**
|
||||
* Writes an unsigned 64-bit integer to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteUInt64Impl(
|
||||
savestream_t *stream, const uint64_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a little-endian float from the stream.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the host-order value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out);
|
||||
|
||||
/**
|
||||
* Writes a float to the stream in little-endian order.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteFloatImpl(
|
||||
savestream_t *stream, const float_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a null-terminated string from the stream up to maxLen bytes
|
||||
* (including the terminator). Always null-terminates the output buffer.
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Destination buffer of at least maxLen bytes.
|
||||
* @param maxLen Maximum bytes to read, including the null terminator.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadStringImpl(
|
||||
savestream_t *stream, char_t *out, const size_t maxLen
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a null-terminated string to the stream, truncating to maxLen-1
|
||||
* characters and always appending a null terminator.
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Source string.
|
||||
* @param maxLen Maximum bytes to write, including the null terminator.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteStringImpl(
|
||||
savestream_t *stream, const char_t *input, const size_t maxLen
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
|
||||
* (time, timeZone, offsetTime).
|
||||
*
|
||||
* @param stream Active read stream.
|
||||
* @param out Receives the epoch value.
|
||||
* @return An error if the read fails.
|
||||
*/
|
||||
errorret_t saveStreamReadDateImpl(
|
||||
savestream_t *stream, dusktimeepoch_t *out
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
|
||||
* (time, timeZone, offsetTime).
|
||||
*
|
||||
* @param stream Active write stream.
|
||||
* @param input Epoch value to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteDateImpl(
|
||||
savestream_t *stream, const dusktimeepoch_t *input
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads a self-contained save meta section (header, version, fields,
|
||||
* checksum verification) from the stream.
|
||||
*
|
||||
* @param stream Active read stream, positioned at the section's start.
|
||||
* @param meta Meta struct to populate.
|
||||
* @return An error code if loading fails.
|
||||
*/
|
||||
errorret_t saveMetaSerializeRead(savestream_t *stream, savemeta_t *meta);
|
||||
|
||||
/**
|
||||
* Writes a self-contained save meta section (header, version, fields,
|
||||
* checksum) to the stream.
|
||||
*
|
||||
* @param stream Active write stream, positioned at the section's start.
|
||||
* @param meta Meta struct to serialize.
|
||||
* @return An error code if writing fails.
|
||||
*/
|
||||
errorret_t saveMetaSerializeWrite(savestream_t *stream, savemeta_t *meta);
|
||||
|
||||
/**
|
||||
* Reads a self-contained save slot section (header, version, fields,
|
||||
* checksum verification) from the stream.
|
||||
*
|
||||
* @param stream Active read stream, positioned at the section's start.
|
||||
* @param slot Slot struct to populate.
|
||||
* @return An error code if loading fails.
|
||||
*/
|
||||
errorret_t saveSlotSerializeRead(savestream_t *stream, saveslot_t *slot);
|
||||
|
||||
/**
|
||||
* Writes a self-contained save slot section (header, version, fields,
|
||||
* checksum) to the stream.
|
||||
*
|
||||
* @param stream Active write stream, positioned at the section's start.
|
||||
* @param slot Slot struct to serialize.
|
||||
* @return An error code if writing fails.
|
||||
*/
|
||||
errorret_t saveSlotSerializeWrite(savestream_t *stream, saveslot_t *slot);
|
||||
|
||||
#define saveFileReadHeader(stream, header, expected, size) \
|
||||
errorChain(saveStreamReadHeaderImpl(stream, header, expected, size))
|
||||
#define saveFileWriteHeader(stream, header, size) \
|
||||
errorChain(saveStreamWriteHeaderImpl(stream, header, size))
|
||||
|
||||
#define saveFileReadVersion(stream, out) \
|
||||
errorChain(saveStreamReadVersionImpl(stream, out))
|
||||
#define saveFileWriteVersion(stream, input) \
|
||||
errorChain(saveStreamWriteVersionImpl(stream, input))
|
||||
|
||||
#define saveFileReadBool(stream, out) \
|
||||
errorChain(saveStreamReadBoolImpl(stream, out))
|
||||
#define saveFileWriteBool(stream, input) \
|
||||
errorChain(saveStreamWriteBoolImpl(stream, input))
|
||||
|
||||
#define saveFileReadInt8(stream, out) \
|
||||
errorChain(saveStreamReadInt8Impl(stream, out))
|
||||
#define saveFileWriteInt8(stream, input) \
|
||||
errorChain(saveStreamWriteInt8Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt8(stream, out) \
|
||||
errorChain(saveStreamReadUInt8Impl(stream, out))
|
||||
#define saveFileWriteUInt8(stream, input) \
|
||||
errorChain(saveStreamWriteUInt8Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt16(stream, out) \
|
||||
errorChain(saveStreamReadInt16Impl(stream, out))
|
||||
#define saveFileWriteInt16(stream, input) \
|
||||
errorChain(saveStreamWriteInt16Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt16(stream, out) \
|
||||
errorChain(saveStreamReadUInt16Impl(stream, out))
|
||||
#define saveFileWriteUInt16(stream, input) \
|
||||
errorChain(saveStreamWriteUInt16Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt32(stream, out) \
|
||||
errorChain(saveStreamReadInt32Impl(stream, out))
|
||||
#define saveFileWriteInt32(stream, input) \
|
||||
errorChain(saveStreamWriteInt32Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt32(stream, out) \
|
||||
errorChain(saveStreamReadUInt32Impl(stream, out))
|
||||
#define saveFileWriteUInt32(stream, input) \
|
||||
errorChain(saveStreamWriteUInt32Impl(stream, input))
|
||||
|
||||
#define saveFileReadInt64(stream, out) \
|
||||
errorChain(saveStreamReadInt64Impl(stream, out))
|
||||
#define saveFileWriteInt64(stream, input) \
|
||||
errorChain(saveStreamWriteInt64Impl(stream, input))
|
||||
|
||||
#define saveFileReadUInt64(stream, out) \
|
||||
errorChain(saveStreamReadUInt64Impl(stream, out))
|
||||
#define saveFileWriteUInt64(stream, input) \
|
||||
errorChain(saveStreamWriteUInt64Impl(stream, input))
|
||||
|
||||
#define saveFileReadFloat(stream, out) \
|
||||
errorChain(saveStreamReadFloatImpl(stream, out))
|
||||
#define saveFileWriteFloat(stream, input) \
|
||||
errorChain(saveStreamWriteFloatImpl(stream, input))
|
||||
|
||||
#define saveFileReadString(stream, out, maxLen) \
|
||||
errorChain(saveStreamReadStringImpl(stream, out, maxLen))
|
||||
#define saveFileWriteString(stream, input, maxLen) \
|
||||
errorChain(saveStreamWriteStringImpl(stream, input, maxLen))
|
||||
|
||||
#define saveFileReadDate(stream, out) \
|
||||
errorChain(saveStreamReadDateImpl(stream, out))
|
||||
#define saveFileWriteDate(stream, input) \
|
||||
errorChain(saveStreamWriteDateImpl(stream, input))
|
||||
@@ -9,70 +9,40 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "error/error.h"
|
||||
#include "save/save.h"
|
||||
#include "ui/frame/initial/uiinitialnocard.h"
|
||||
#include "ui/frame/initial/uiinitialcreatesave.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "console/console.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
static void sceneInitialCheckSave(void);
|
||||
|
||||
static void sceneInitialCreateSaveWriteComplete(errorret_t result, void *user) {
|
||||
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
|
||||
sceneSet(SCENE_TYPE_MAIN_MENU);
|
||||
}
|
||||
|
||||
static void sceneInitialCreateSaveResult(const bool_t create, void *user) {
|
||||
if(!create) {
|
||||
sceneSet(SCENE_TYPE_MAIN_MENU);
|
||||
return;
|
||||
}
|
||||
|
||||
saveWriteSlot(SAVE_ACTIVE_SLOT, sceneInitialCreateSaveWriteComplete, NULL);
|
||||
}
|
||||
|
||||
static void sceneInitialNoCardResult(const bool_t retry, void *user) {
|
||||
if(retry) {
|
||||
sceneInitialCheckSave();
|
||||
return;
|
||||
}
|
||||
|
||||
// The player explicitly acknowledged there's no save device and chose
|
||||
// to proceed anyway - stick with that for the rest of the session (see
|
||||
// saveMarkTemporary()'s doc comment for why this can't be un-set later).
|
||||
saveMarkTemporary();
|
||||
sceneSet(SCENE_TYPE_MAIN_MENU);
|
||||
}
|
||||
|
||||
static void sceneInitialLoadComplete(errorret_t result, void *user) {
|
||||
if(errorIsNotOk(result) || !saveIsAvailable()) {
|
||||
errorCatch(result);
|
||||
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
|
||||
sceneSet(SCENE_TYPE_MAIN_MENU);
|
||||
return;
|
||||
}
|
||||
|
||||
uiInitialCreateSaveOpen(sceneInitialCreateSaveResult, NULL);
|
||||
}
|
||||
|
||||
static void sceneInitialCheckSave(void) {
|
||||
if(!saveIsAvailable()) {
|
||||
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if(saveIsBusy()) return;
|
||||
|
||||
saveLoadSlot(SAVE_ACTIVE_SLOT, sceneInitialLoadComplete, NULL);
|
||||
}
|
||||
// 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
|
||||
// lifetime convention as e.g. LOCALE.entry).
|
||||
static assetentry_t *INITIAL_CUTSCENE_ENTRY = NULL;
|
||||
|
||||
errorret_t sceneInitialInit(scenedata_t *sceneData) {
|
||||
assertNotNull(sceneData, "Scene data cannot be null");
|
||||
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
|
||||
|
||||
sceneInitialCheckSave();
|
||||
// Set background color to black for the initial scene
|
||||
SCREEN.background = COLOR_BLACK;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -8,11 +8,9 @@
|
||||
#pragma once
|
||||
#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 {
|
||||
uint8_t reserved;
|
||||
// uint32_t callbackState;
|
||||
void *nothing;
|
||||
} sceneinitial_t;
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user