Compare commits
62 Commits
a84137b5ff
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a858cc424 | |||
| c0292842a5 | |||
| 717902462b | |||
| 1d73b9d224 | |||
| 51f262efa0 | |||
| a6e4e3f71f | |||
| 4387d223b9 | |||
| 00bfaf6360 | |||
| 709bd7be52 | |||
| 80f4348e21 | |||
| e630827b34 | |||
| 106d9b0fc0 | |||
| 4a26f79945 | |||
| 1a199f6ce3 | |||
| ec59e77867 | |||
| cb28d2b611 | |||
| 27b6ddf5cb | |||
| 475c865e33 | |||
| 0b4ba062bb | |||
| 28f5e66662 | |||
| e225a076f0 | |||
| 774c8ad0f8 | |||
| 52d1e7414d | |||
| af4cb53e5f | |||
| c019271e12 | |||
| 82ae2bce9d | |||
| 092e259a06 | |||
| 08b4bbfe91 | |||
| 560c51cf27 | |||
| 674f86b18a | |||
| e3f10e0926 | |||
| d7223d7387 | |||
| 45331c2a60 | |||
| 33f50a2c69 | |||
| 6c8e4d5cbd | |||
| 3b7215876a | |||
| c74f5890bd | |||
| fbd3c71ba7 | |||
| 128f9ab9d4 | |||
| 36fb359aa2 | |||
| 1bd73d69fe | |||
| fb48285143 | |||
| 3de50b8370 | |||
| e61cbe25b7 | |||
| 8395830be6 | |||
| 6e4ec2b9d8 | |||
| f501bb8e28 | |||
| d07cd3397d | |||
| e008fb108a | |||
| 9aaffff7a8 | |||
| 7357b4a5df | |||
| 1ddc298a74 | |||
| e2a9442aa6 | |||
| aa0180571e | |||
| 7f7be39230 | |||
| 4d95415232 | |||
| 2cbd80a004 | |||
| 9abf8101da | |||
| 24badd06a5 | |||
| 7a03ef8eaf | |||
| f3ea507313 | |||
| 4b0388a0e1 |
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||
|
||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||
option(DUSK_NETWORK "Enable network support" ON)
|
||||
|
||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
||||
DUSK_VERSION="${DUSK_VERSION}"
|
||||
)
|
||||
|
||||
if(DUSK_NETWORK)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_NETWORK
|
||||
)
|
||||
endif()
|
||||
|
||||
# Toolchains
|
||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,11 +5,107 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||
|
||||
# Initial Scene
|
||||
msgid "initial.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "initial.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "initial.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "initial.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "initial.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "initial.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
|
||||
# Main Menu Scene
|
||||
msgid "main_menu.start_game"
|
||||
msgstr "Start Game"
|
||||
|
||||
msgid "main_menu.options"
|
||||
msgstr "Options"
|
||||
|
||||
msgid "main_menu.quit"
|
||||
msgstr "Quit Game"
|
||||
|
||||
msgid "main_menu.quit_confirm"
|
||||
msgstr "Are you sure you want to quit?"
|
||||
|
||||
msgid "main_menu.checking_save.title"
|
||||
msgstr "Checking for save data"
|
||||
|
||||
msgid "main_menu.checking_save.message"
|
||||
msgstr "Please wait..."
|
||||
|
||||
msgid "main_menu.no_device.title"
|
||||
msgstr "No Save Device Found"
|
||||
|
||||
msgid "main_menu.no_device.message"
|
||||
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||
|
||||
msgid "main_menu.no_device.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
msgid "main_menu.no_device.continue"
|
||||
msgstr "Continue without saving"
|
||||
|
||||
msgid "main_menu.save_load_error.title"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "main_menu.save_load_error.message"
|
||||
msgstr "Failed to load save data. Please try again."
|
||||
|
||||
msgid "main_menu.save_load_error.retry"
|
||||
msgstr "Try Again"
|
||||
|
||||
|
||||
# Select Save Screen
|
||||
msgid "ui.select_save.title"
|
||||
msgstr "Select Save"
|
||||
|
||||
msgid "ui.select_save.empty"
|
||||
msgstr "Empty Slot"
|
||||
|
||||
msgid "ui.select_save.slot_format"
|
||||
msgstr "%s Lv.%d %s"
|
||||
|
||||
msgid "ui.select_save.delete_mode"
|
||||
msgstr "Delete a Save"
|
||||
|
||||
msgid "ui.select_save.delete_confirm"
|
||||
msgstr "Are you sure you want to delete this save?"
|
||||
|
||||
msgid "ui.select_save.name_title"
|
||||
msgstr "Enter game save file name"
|
||||
|
||||
msgid "ui.save_slot.number_format"
|
||||
msgstr "Slot %d"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Welcome"
|
||||
|
||||
msgid "save.linux.mkdirp_failed"
|
||||
msgstr "Failed to create save directory, check the disk is not full or write-protected."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
@@ -56,6 +152,114 @@ msgstr "Items"
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save"
|
||||
msgstr "Save"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_success"
|
||||
msgstr "Game saved."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_cancelled"
|
||||
msgstr "Save cancelled."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_unavailable"
|
||||
msgstr "Can't save - no save device found."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_temporary"
|
||||
msgstr "This session is temporary - no save device was found, so saving is disabled."
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_create_confirm"
|
||||
msgstr "No save data found. Create a new save?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_failed_format"
|
||||
msgstr "Save failed: %s"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.save_check_failed_format"
|
||||
msgstr "Can't save: %s"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.message"
|
||||
msgstr "No save device found. You can continue, but\nprogress will not be saved."
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.retry"
|
||||
msgstr "Retry"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialnocard.c
|
||||
msgid "ui.initial.no_card.continue"
|
||||
msgstr "Continue Anyway"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.message"
|
||||
msgstr "No save data found. Create a new save?"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.yes"
|
||||
msgstr "Yes"
|
||||
|
||||
#: src/dusk/ui/frame/initial/uiinitialcreatesave.c
|
||||
msgid "ui.initial.create_save.no"
|
||||
msgstr "No"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.confirm"
|
||||
msgstr "Confirm"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.attack"
|
||||
msgstr "Attack"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.flee"
|
||||
msgstr "Flee"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlemenu.c
|
||||
msgid "ui.battle.menu.target_format"
|
||||
msgstr "Enemy %u (%u/%u HP)"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.hp_format"
|
||||
msgstr "HP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/battle/uibattlehud.c
|
||||
msgid "ui.battle.hud.mp_format"
|
||||
msgstr "MP %u/%u"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsaudio.c
|
||||
msgid "ui.settings.audio.placeholder"
|
||||
msgstr "No audio settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsdisplay.c
|
||||
msgid "ui.settings.display.placeholder"
|
||||
msgstr "No display settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsinput.c
|
||||
msgid "ui.settings.input.placeholder"
|
||||
msgstr "No input settings yet"
|
||||
|
||||
#: src/dusk/ui/frame/backpack/uibackpack.c
|
||||
msgid "ui.backpack.category_format"
|
||||
msgstr "Category %u"
|
||||
|
||||
#: src/dusk/ui/overlay/uiloading.c
|
||||
msgid "ui.loading.text"
|
||||
msgstr "loading"
|
||||
|
||||
#: src/dusk/ui/overlay/uiautosave.c
|
||||
msgid "ui.autosave.saving"
|
||||
msgstr "SAVING"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: es\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Bienvenido"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "Pantalla"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "Deadzone"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "Se aplica después de reiniciar la aplicación."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "¿Descartar los cambios no guardados?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Personajes"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Objetos"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Configuración"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "Poción"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "Papa"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "Manzana"
|
||||
@@ -1,70 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: ja\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"歓迎"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "一般"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "入力"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "表示"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "オーディオ"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "デッドゾーン"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "言語"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "アプリケーションを再起動すると適用されます。"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "適用"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "未保存の変更を破棄しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "キャラクター"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "アイテム"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "ポーション"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "ジャガイモ"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "リンゴ"
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -2179,5 +2179,27 @@
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"entities": [
|
||||
{
|
||||
"type": "global",
|
||||
"globalId": 3,
|
||||
"pos": [8, 8, 1]
|
||||
},
|
||||
{
|
||||
"type": "item",
|
||||
"itemId": 1,
|
||||
"quantity": 1,
|
||||
"pos": [12, 2, 0]
|
||||
}
|
||||
],
|
||||
"areas": [
|
||||
{
|
||||
"min": [11, 3, 0],
|
||||
"max": [16, 9, 10],
|
||||
"callbackId": 1,
|
||||
"notify": 3,
|
||||
"trigger": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"items": [
|
||||
// Boot check: make sure a save device is available before handing off
|
||||
// to the main menu.
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "initial.checking_save.title",
|
||||
"message": "initial.checking_save.message"
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 0.2
|
||||
},
|
||||
{
|
||||
"type": "SAVE_DEVICE_CHECK",
|
||||
"successMarker": "CONTINUE",
|
||||
"failureMarker": "NO_DEVICE"
|
||||
},
|
||||
|
||||
// No save device found - offer to retry or continue without saving.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "NO_DEVICE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_OPTIONS_MARKERS",
|
||||
"title": "initial.no_device.title",
|
||||
"message": "initial.no_device.message",
|
||||
"options": [
|
||||
{
|
||||
"text": "initial.no_device.retry",
|
||||
"marker": "RETRY"
|
||||
},
|
||||
{
|
||||
"text": "initial.no_device.continue",
|
||||
"marker": "CONTINUE"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "RETRY"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "RESTART"
|
||||
},
|
||||
|
||||
// Save device found (or continuing without one) - hand off to the
|
||||
// main menu scene.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "CONTINUE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "SCENE",
|
||||
"sceneType": "MAIN_MENU"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"type": "MODAL",
|
||||
"title": "main_menu.checking_save.title",
|
||||
"message": "main_menu.checking_save.message"
|
||||
},
|
||||
{
|
||||
"type": "WAIT",
|
||||
"seconds": 0.2
|
||||
},
|
||||
{
|
||||
"type": "SAVE_DEVICE_CHECK",
|
||||
"successMarker": "CONTINUE",
|
||||
"failureMarker": "NO_DEVICE"
|
||||
},
|
||||
|
||||
// No save device found - offer to retry or continue without saving.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "NO_DEVICE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_OPTIONS_MARKERS",
|
||||
"title": "main_menu.no_device.title",
|
||||
"message": "main_menu.no_device.message",
|
||||
"options": [
|
||||
{
|
||||
"text": "main_menu.no_device.retry",
|
||||
"marker": "RETRY"
|
||||
},
|
||||
{
|
||||
"text": "main_menu.no_device.continue",
|
||||
"marker": "CONTINUE"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Retry
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "RETRY"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "RESTART"
|
||||
},
|
||||
|
||||
// Save device found - attempt to load all save slots.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "CONTINUE"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_CLOSE"
|
||||
},
|
||||
{
|
||||
"type": "SAVE_LOAD_ALL_SLOTS",
|
||||
"successMarker": "LOADED",
|
||||
"failureMarker": "LOAD_ERROR"
|
||||
},
|
||||
|
||||
// Save data failed to load (e.g. corrupt/unreadable) - only option is
|
||||
// to retry, no "continue without saving" here since we already know a
|
||||
// device is present.
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "LOAD_ERROR"
|
||||
},
|
||||
{
|
||||
"type": "MODAL_OPTIONS_MARKERS",
|
||||
"title": "main_menu.save_load_error.title",
|
||||
"message": "main_menu.save_load_error.message",
|
||||
"options": [
|
||||
{
|
||||
"text": "main_menu.save_load_error.retry",
|
||||
"marker": "RETRY"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "MARKER",
|
||||
"name": "LOADED"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
easing.c
|
||||
animation.c
|
||||
keyframe.c
|
||||
)
|
||||
|
||||
+109
-29
@@ -11,42 +11,122 @@
|
||||
void animationInit(
|
||||
animation_t *anim,
|
||||
keyframe_t *keyframes,
|
||||
uint16_t keyframeCount
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||
assertNotNull(keyframeCounts, "Keyframe counts pointer cannot be null.");
|
||||
assertTrue(layerCount > 0, "Layer count must be greater than zero.");
|
||||
|
||||
memoryZero(anim, sizeof(animation_t));
|
||||
anim->keyframes = keyframes;
|
||||
anim->keyframeCount = keyframeCount;
|
||||
anim->keyframeCounts = keyframeCounts;
|
||||
anim->layerCount = layerCount;
|
||||
|
||||
// Determine duration
|
||||
float_t duration = 0.0f;
|
||||
for(uint16_t layer = 0; layer < layerCount; layer++) {
|
||||
uint16_t keyframeCount = keyframeCounts[layer];
|
||||
assertTrue(keyframeCount > 0, "Keyframe count invalid.");
|
||||
keyframe_t *layerKeyframes = keyframes + layer * keyframeCount;
|
||||
|
||||
#ifdef DUSK_ASSERTIONS
|
||||
// Check that the keyframes are sorted by time.
|
||||
for(uint16_t i = 1; i < keyframeCount; i++) {
|
||||
assertTrue(
|
||||
layerKeyframes[i].time >= layerKeyframes[i - 1].time,
|
||||
"Keyframes must be sorted by time."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
keyframe_t *lastKeyframe = layerKeyframes + keyframeCount - 1;
|
||||
duration = mathMax(duration, lastKeyframe->time);
|
||||
}
|
||||
assertTrue(duration > 0, "Animation duration must be greater than 0.");
|
||||
anim->duration = duration;
|
||||
}
|
||||
|
||||
float_t animationGetValue(animation_t *anim, const float_t time) {
|
||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertNotNull(anim->keyframes, "Keyframes pointer cannot be null.");
|
||||
assertTrue(anim->keyframeCount > 0, "Keyframe count invalid.");
|
||||
assertTrue(time >= 0, "Time must be non-negative.");
|
||||
assertTrue(layer < anim->layerCount, "Layer index out of bounds.");
|
||||
|
||||
keyframe_t *start;
|
||||
keyframe_t *end;
|
||||
keyframe_t *last = anim->keyframes + anim->keyframeCount - 1;
|
||||
keyframe_t *current = anim->keyframes;
|
||||
start = current;
|
||||
|
||||
do {
|
||||
if(current->time > time) {
|
||||
end = current;
|
||||
break;
|
||||
}
|
||||
start = current;
|
||||
current++;
|
||||
|
||||
if(current > last) {
|
||||
end = start;
|
||||
break;
|
||||
}
|
||||
} while(true);
|
||||
|
||||
float_t t = (time - start->time) / (end->time - start->time);
|
||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||
uint16_t keyframeCount = anim->keyframeCounts[layer];
|
||||
keyframe_t *layerKeyframes = anim->keyframes + layer * keyframeCount;
|
||||
return keyframeGetValue(layerKeyframes, keyframeCount, anim->time);
|
||||
}
|
||||
|
||||
void animationUpdate(
|
||||
animation_t *anim,
|
||||
const float_t deltaTime
|
||||
) {
|
||||
assertNotNull(anim, "Animation pointer cannot be null.");
|
||||
assertTrue(deltaTime >= 0, "Delta time must be non-negative.");
|
||||
|
||||
bool_t justCompleted = false;
|
||||
|
||||
if(!(anim->flags & ANIMATION_FLAG_INTERNAL_COMPLETED)) {
|
||||
bool_t loop = (anim->flags & ANIMATION_FLAG_LOOP) != 0;
|
||||
bool_t pingpong = (anim->flags & ANIMATION_FLAG_PINGPONG) != 0;
|
||||
assertFalse(
|
||||
loop && pingpong,
|
||||
"Cannot set both ANIMATION_FLAG_LOOP and ANIMATION_FLAG_PINGPONG."
|
||||
);
|
||||
|
||||
bool_t backward;
|
||||
if(pingpong) {
|
||||
backward = (anim->flags & ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD) != 0;
|
||||
} else {
|
||||
backward = (anim->flags & ANIMATION_FLAG_REVERSE) != 0;
|
||||
}
|
||||
|
||||
// Resolve boundary crossings one at a time, so a single large deltaTime
|
||||
// can correctly loop/pingpong across multiple boundaries in one call.
|
||||
float_t remaining = deltaTime;
|
||||
while(remaining > 0.0f) {
|
||||
float_t toBoundary = (
|
||||
backward ? anim->time : (anim->duration - anim->time)
|
||||
);
|
||||
if(remaining < toBoundary) {
|
||||
anim->time += backward ? -remaining : remaining;
|
||||
break;
|
||||
}
|
||||
|
||||
remaining -= toBoundary;
|
||||
anim->time = backward ? 0.0f : anim->duration;
|
||||
|
||||
bool_t stopHere;
|
||||
if(backward) {
|
||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_BEGINNING) != 0;
|
||||
} else {
|
||||
stopHere = (anim->flags & ANIMATION_FLAG_STOP_END) != 0;
|
||||
}
|
||||
|
||||
if(stopHere) {
|
||||
justCompleted = true;
|
||||
break;
|
||||
} else if(pingpong) {
|
||||
backward = !backward;
|
||||
if(backward) anim->flags |= ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
else anim->flags &= ~ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD;
|
||||
} else if(loop) {
|
||||
anim->time = backward ? anim->duration : 0.0f;
|
||||
if(anim->onLoop) anim->onLoop(anim->user);
|
||||
} else {
|
||||
justCompleted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(justCompleted) anim->flags |= ANIMATION_FLAG_INTERNAL_COMPLETED;
|
||||
}
|
||||
|
||||
// Call onUpdate for each layer.
|
||||
for(uint16_t layer = 0; layer < anim->layerCount; layer++) {
|
||||
float_t value = animationGetLayerValue(anim, layer);
|
||||
if(anim->onUpdate) anim->onUpdate(layer, value, anim->user);
|
||||
}
|
||||
|
||||
if(justCompleted && anim->onComplete) anim->onComplete(anim->user);
|
||||
}
|
||||
@@ -6,29 +6,89 @@
|
||||
#pragma once
|
||||
#include "keyframe.h"
|
||||
|
||||
#define ANIMATION_FLAG_LOOP (1 << 0)
|
||||
#define ANIMATION_FLAG_REVERSE (1 << 1)
|
||||
#define ANIMATION_FLAG_PINGPONG (1 << 2)
|
||||
#define ANIMATION_FLAG_STOP_BEGINNING (1 << 3)
|
||||
#define ANIMATION_FLAG_STOP_END (1 << 4)
|
||||
|
||||
// Internal - tracks which direction a pingponging animation is currently
|
||||
// travelling. Do not set this manually, it is managed by animationUpdate().
|
||||
#define ANIMATION_FLAG_INTERNAL_PINGPONG_BACKWARD (1 << 7)
|
||||
|
||||
// Internal - set once the animation has stopped advancing (see
|
||||
// animationUpdate()). Do not set this manually. There is currently no way to
|
||||
// restart a completed animation short of clearing this bit and resetting
|
||||
// anim->time by hand.
|
||||
#define ANIMATION_FLAG_INTERNAL_COMPLETED (1 << 6)
|
||||
|
||||
typedef struct {
|
||||
keyframe_t *keyframes;
|
||||
uint16_t keyframeCount;
|
||||
uint16_t *keyframeCounts;
|
||||
uint16_t layerCount;
|
||||
float_t time;
|
||||
float_t duration;
|
||||
uint8_t flags;
|
||||
|
||||
void *user;
|
||||
void (*onUpdate)(const uint16_t layer, const float_t value, void *user);
|
||||
void (*onComplete)(void *user);
|
||||
void (*onLoop)(void *user);
|
||||
} animation_t;
|
||||
|
||||
/**
|
||||
* Initializes an animation.
|
||||
* Initializes an animation with the given keyframes and layer count.
|
||||
*
|
||||
* @param anim The animation to initialize.
|
||||
* @param keyframes The keyframes to use for the animation.
|
||||
* @param keyframeCount The number of keyframes in the animation.
|
||||
* @param anim Pointer to the animation to initialize.
|
||||
* @param keyframes Pointer to the array of keyframes for each layer.
|
||||
* @param keyframeCount Number of keyframes in each layer.
|
||||
* @param layerCount Number of layers in the animation.
|
||||
*/
|
||||
void animationInit(
|
||||
animation_t *anim,
|
||||
keyframe_t *keyframes,
|
||||
uint16_t keyframeCount
|
||||
uint16_t *keyframeCounts,
|
||||
const uint16_t layerCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the value of the animation at a given time.
|
||||
* Sets the current time of the animation, clamping it to the valid range.
|
||||
* This will call the onUpdate callback but none of the other callbacks.
|
||||
*
|
||||
* @param anim The animation to get the value from.
|
||||
* @param time The time at which to get the value, in seconds.
|
||||
* @return The value of the animation at the given time.
|
||||
* @param anim Pointer to the animation to set the time for.
|
||||
* @param time The new time to set for the animation.
|
||||
*/
|
||||
float_t animationGetValue(animation_t *anim, const float_t time);
|
||||
void animationSetTime(animation_t *anim, const float_t time);
|
||||
|
||||
/**
|
||||
* Gets the current value of a specific layer in the animation based on the
|
||||
* current animation time.
|
||||
*/
|
||||
float_t animationGetLayerValue(const animation_t *anim, const uint16_t layer);
|
||||
|
||||
/**
|
||||
* Updates the animation state based on the elapsed time. Advances anim->time
|
||||
* by deltaTime (or against it, if ANIMATION_FLAG_REVERSE is set), then
|
||||
* resolves whatever happens when it reaches the 0 or duration boundary:
|
||||
*
|
||||
* - ANIMATION_FLAG_PINGPONG: reflects off the boundary and continues playing
|
||||
* in the opposite direction, forever, unless stopped (see below).
|
||||
* - ANIMATION_FLAG_LOOP: wraps back around to the other boundary and keeps
|
||||
* playing in the same direction, forever, unless stopped (see below).
|
||||
* - ANIMATION_FLAG_STOP_BEGINNING / ANIMATION_FLAG_STOP_END: when the
|
||||
* animation reaches that specific boundary, it clamps there and stops
|
||||
* (firing onComplete) instead of looping/pingponging past it.
|
||||
* - If none of the above apply at a boundary, the animation clamps there and
|
||||
* stops, firing onComplete.
|
||||
*
|
||||
* onUpdate is called for every layer on every call. onLoop is called each
|
||||
* time a loop wraps around. onComplete is called at most once, the moment
|
||||
* the animation stops advancing.
|
||||
*
|
||||
* @param anim Pointer to the animation to update.
|
||||
* @param deltaTime Time elapsed since the last update (in seconds).
|
||||
*/
|
||||
void animationUpdate(
|
||||
animation_t *anim,
|
||||
const float_t deltaTime
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "keyframe.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
float_t keyframeGetValue(
|
||||
const keyframe_t *keyframes,
|
||||
const uint32_t keyframeCount,
|
||||
const float_t time
|
||||
) {
|
||||
assertNotNull(keyframes, "Keyframes pointer cannot be null.");
|
||||
assertTrue(keyframeCount > 0, "Keyframe count must be more than 0.");
|
||||
assertTrue(time >= 0, "Time must be non-negative.");
|
||||
#ifdef DUSK_ASSERTIONS
|
||||
// Checks that the keyframes are sorted by time.
|
||||
for(uint32_t i = 1; i < keyframeCount; i++) {
|
||||
assertTrue(
|
||||
keyframes[i].time >= keyframes[i - 1].time,
|
||||
"Keyframes must be sorted by time."
|
||||
);
|
||||
}
|
||||
#endif
|
||||
|
||||
keyframe_t *last = (keyframe_t *)(keyframes + keyframeCount - 1);
|
||||
if(time >= last->time) return last->value;
|
||||
|
||||
// Since time < last->time (checked above), current is guaranteed to stop
|
||||
// at or before reaching last, so no separate end-of-array check is needed.
|
||||
keyframe_t *current = (keyframe_t *)keyframes;
|
||||
keyframe_t *start = current;
|
||||
while(current->time <= time) {
|
||||
start = current;
|
||||
current++;
|
||||
}
|
||||
keyframe_t *end = current;
|
||||
|
||||
float_t t = (time - start->time) / (end->time - start->time);
|
||||
return mathLerp(start->value, end->value, easingApply(start->easing, t));
|
||||
}
|
||||
@@ -11,3 +11,17 @@ typedef struct {
|
||||
float_t value;
|
||||
easingtype_t easing;
|
||||
} keyframe_t;
|
||||
|
||||
/**
|
||||
* Gets the value of a keyframe at a given time.
|
||||
*
|
||||
* @param keyframes The keyframes to get the value from.
|
||||
* @param keyframeCount The number of keyframes in the array.
|
||||
* @param time The time at which to get the value, in seconds.
|
||||
* @return The value of the keyframe at the given time.
|
||||
*/
|
||||
float_t keyframeGetValue(
|
||||
const keyframe_t *keyframes,
|
||||
const uint32_t keyframeCount,
|
||||
const float_t time
|
||||
);
|
||||
@@ -22,6 +22,8 @@
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_ASSERTIONS_FAKED
|
||||
#define DUSK_ASSERTIONS 1
|
||||
|
||||
/**
|
||||
* Initializes the assert system. Must be the very first call in engine
|
||||
* startup.
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
asset.c
|
||||
assetbatch.c
|
||||
assetfile.c
|
||||
)
|
||||
|
||||
|
||||
@@ -322,7 +322,9 @@ errorret_t assetUpdate(void) {
|
||||
} else if(loading->entry->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
assetentry_t *loadedEntry = loading->entry;
|
||||
loading->entry = NULL;
|
||||
eventInvoke(&loadedEntry->onLoaded, loadedEntry);
|
||||
if(loadedEntry->onLoaded) {
|
||||
loadedEntry->onLoaded(loadedEntry, loadedEntry->onLoadedUser);
|
||||
}
|
||||
}
|
||||
|
||||
loading++;
|
||||
@@ -346,8 +348,8 @@ errorret_t assetUpdate(void) {
|
||||
assetentry_t *errEntry = loading->entry;
|
||||
loading->entry = NULL;
|
||||
threadMutexUnlock(&loading->mutex);
|
||||
eventInvoke(&errEntry->onError, errEntry);
|
||||
errorThrow("Failed to load asset asynchronously.");
|
||||
if(errEntry->onError) errEntry->onError(errEntry, errEntry->onErrorUser);
|
||||
loading++;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "assetbatch.h"
|
||||
#include "asset.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include <unistd.h>
|
||||
|
||||
void assetBatchInit(
|
||||
assetbatch_t *batch,
|
||||
const uint16_t count,
|
||||
const assetbatchdesc_t *descs
|
||||
) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
assertNotNull(descs, "Descs cannot be NULL.");
|
||||
assertTrue(count > 0, "Count must be greater than 0.");
|
||||
assertTrue(
|
||||
count <= ASSET_BATCH_COUNT_MAX, "Count exceeds ASSET_BATCH_COUNT_MAX."
|
||||
);
|
||||
|
||||
memoryZero(batch, sizeof(assetbatch_t));
|
||||
batch->count = count;
|
||||
|
||||
eventInit(
|
||||
&batch->onLoaded,
|
||||
batch->onLoadedCallbacks, batch->onLoadedUsers, ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&batch->onEntryLoaded,
|
||||
batch->onEntryLoadedCallbacks,
|
||||
batch->onEntryLoadedUsers,
|
||||
ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&batch->onError,
|
||||
batch->onErrorCallbacks, batch->onErrorUsers, ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
eventInit(
|
||||
&batch->onEntryError,
|
||||
batch->onEntryErrorCallbacks,
|
||||
batch->onEntryErrorUsers,
|
||||
ASSET_BATCH_EVENT_MAX
|
||||
);
|
||||
|
||||
for(uint16_t i = 0; i < count; i++) {
|
||||
batch->inputs[i] = descs[i].input;
|
||||
batch->entries[i] = assetLock(
|
||||
descs[i].path, descs[i].type, &batch->inputs[i]
|
||||
);
|
||||
|
||||
if(batch->entries[i]->state == ASSET_ENTRY_STATE_LOADED) {
|
||||
// Already loaded (cached) - count it now, no subscription needed.
|
||||
batch->loadedCount++;
|
||||
} else if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) {
|
||||
batch->errorCount++;
|
||||
} else {
|
||||
eventSubscribe(
|
||||
&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb, batch
|
||||
);
|
||||
eventSubscribe(
|
||||
&batch->entries[i]->onError, assetBatchEntryOnErrorCb, batch
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void assetBatchLock(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
assetEntryLock(batch->entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void assetBatchUnlock(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
assetEntryUnlock(batch->entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool_t assetBatchIsLoaded(const assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
if(batch->entries[i]->state != ASSET_ENTRY_STATE_LOADED) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t assetBatchHasError(const assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
if(batch->entries[i]->state == ASSET_ENTRY_STATE_ERROR) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
errorret_t assetBatchRequireLoaded(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
|
||||
bool_t allDone;
|
||||
do {
|
||||
allDone = true;
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
const assetentrystate_t state = batch->entries[i]->state;
|
||||
if(state == ASSET_ENTRY_STATE_ERROR) {
|
||||
errorThrow("Asset '%s' failed to load.", batch->entries[i]->name);
|
||||
}
|
||||
if(state != ASSET_ENTRY_STATE_LOADED) {
|
||||
allDone = false;
|
||||
}
|
||||
}
|
||||
if(!allDone) {
|
||||
usleep(1000);
|
||||
errorChain(assetUpdate());
|
||||
}
|
||||
} while(!allDone);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void assetBatchDispose(assetbatch_t *batch) {
|
||||
assertNotNull(batch, "Batch cannot be NULL.");
|
||||
for(uint16_t i = 0; i < batch->count; i++) {
|
||||
if(batch->entries[i]) {
|
||||
// Unsubscribe while we still hold a lock so the entry is live.
|
||||
eventUnsubscribe(&batch->entries[i]->onLoaded, assetBatchEntryOnLoadedCb);
|
||||
eventUnsubscribe(&batch->entries[i]->onError, assetBatchEntryOnErrorCb);
|
||||
assetUnlockEntry(batch->entries[i]);
|
||||
}
|
||||
}
|
||||
memoryZero(batch, sizeof(assetbatch_t));
|
||||
}
|
||||
|
||||
void assetBatchEntryOnLoadedCb(void *params, void *user) {
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
assetbatch_t *batch = (assetbatch_t *)user;
|
||||
|
||||
batch->loadedCount++;
|
||||
eventInvoke(&batch->onEntryLoaded, entry);
|
||||
|
||||
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
||||
if(batch->errorCount == 0) {
|
||||
eventInvoke(&batch->onLoaded, batch);
|
||||
} else {
|
||||
eventInvoke(&batch->onError, batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void assetBatchEntryOnErrorCb(void *params, void *user) {
|
||||
assetentry_t *entry = (assetentry_t *)params;
|
||||
assetbatch_t *batch = (assetbatch_t *)user;
|
||||
|
||||
batch->errorCount++;
|
||||
eventInvoke(&batch->onEntryError, entry);
|
||||
|
||||
if((uint16_t)(batch->loadedCount + batch->errorCount) >= batch->count) {
|
||||
eventInvoke(&batch->onError, batch);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#define ASSET_BATCH_COUNT_MAX 64
|
||||
#define ASSET_BATCH_EVENT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
const char_t *path;
|
||||
assetloadertype_t type;
|
||||
assetloaderinput_t input;
|
||||
} assetbatchdesc_t;
|
||||
|
||||
typedef struct {
|
||||
assetentry_t *entries[ASSET_BATCH_COUNT_MAX];
|
||||
assetloaderinput_t inputs[ASSET_BATCH_COUNT_MAX];
|
||||
uint16_t count;
|
||||
uint16_t loadedCount;
|
||||
uint16_t errorCount;
|
||||
|
||||
/** Fires once when every entry loaded. params = assetbatch_t * */
|
||||
event_t onLoaded;
|
||||
eventcallback_t onLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
||||
|
||||
/** Fires each time a single entry loads. params = assetentry_t * */
|
||||
event_t onEntryLoaded;
|
||||
eventcallback_t onEntryLoadedCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onEntryLoadedUsers[ASSET_BATCH_EVENT_MAX];
|
||||
|
||||
/** Fires when all entries finish (any with errors). params: assetbatch_t * */
|
||||
event_t onError;
|
||||
eventcallback_t onErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onErrorUsers[ASSET_BATCH_EVENT_MAX];
|
||||
|
||||
/** Fires each time a single entry errors. params = assetentry_t * */
|
||||
event_t onEntryError;
|
||||
eventcallback_t onEntryErrorCallbacks[ASSET_BATCH_EVENT_MAX];
|
||||
void *onEntryErrorUsers[ASSET_BATCH_EVENT_MAX];
|
||||
} assetbatch_t;
|
||||
|
||||
/**
|
||||
* Initialises the batch from an array of descriptors. Each entry is locked
|
||||
* and queued for loading immediately.
|
||||
*
|
||||
* @param batch Batch to initialise.
|
||||
* @param descs Array of entry descriptors (need not outlive this call).
|
||||
* @param count Number of descriptors (must be <= ASSET_BATCH_COUNT_MAX).
|
||||
*/
|
||||
void assetBatchInit(
|
||||
assetbatch_t *batch,
|
||||
uint16_t count,
|
||||
const assetbatchdesc_t *descs
|
||||
);
|
||||
|
||||
/**
|
||||
* Acquires one additional lock on every entry in the batch.
|
||||
*
|
||||
* @param batch Batch to lock.
|
||||
*/
|
||||
void assetBatchLock(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Releases one lock from every entry in the batch. When an entry's lock
|
||||
* count reaches zero it will be reaped on the next assetUpdate.
|
||||
*
|
||||
* @param batch Batch to unlock.
|
||||
*/
|
||||
void assetBatchUnlock(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Returns true if every entry in the batch has finished loading.
|
||||
*
|
||||
* @param batch Batch to query.
|
||||
*/
|
||||
bool_t assetBatchIsLoaded(const assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Returns true if any entry in the batch is in an error state.
|
||||
*
|
||||
* @param batch Batch to query.
|
||||
*/
|
||||
bool_t assetBatchHasError(const assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Blocks until every entry is loaded. Returns an error if any entry fails.
|
||||
*
|
||||
* @param batch Batch to wait on.
|
||||
*/
|
||||
errorret_t assetBatchRequireLoaded(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Releases the batch's lock on every entry and clears the batch. After this
|
||||
* call the batch struct may be reused with assetBatchInit.
|
||||
*
|
||||
* @param batch Batch to dispose.
|
||||
*/
|
||||
void assetBatchDispose(assetbatch_t *batch);
|
||||
|
||||
/**
|
||||
* Event trampoline invoked when a batch entry finishes loading.
|
||||
* Increments the loaded counter and fires batch-level events.
|
||||
*
|
||||
* @param params The loaded assetentry_t pointer.
|
||||
* @param user The owning assetbatch_t pointer.
|
||||
*/
|
||||
void assetBatchEntryOnLoadedCb(void *params, void *user);
|
||||
|
||||
/**
|
||||
* Event trampoline invoked when a batch entry fails to load.
|
||||
* Increments the error counter and fires batch-level events.
|
||||
*
|
||||
* @param params The errored assetentry_t pointer.
|
||||
* @param user The owning assetbatch_t pointer.
|
||||
*/
|
||||
void assetBatchEntryOnErrorCb(void *params, void *user);
|
||||
@@ -87,13 +87,30 @@ errorret_t assetFileRead(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// I assume zip_fread takes buffer NULL for skipping?
|
||||
zip_int64_t bytesRead = zip_fread(file->zipFile, buffer, bufferSize);
|
||||
if(bytesRead < 0) {
|
||||
errorThrow("Failed to read from asset file: %s", file->filename);
|
||||
// Some zip_fread() implementations (seen on PSP) reject a single call
|
||||
// asking for the entire (potentially large) file at once with EINVAL;
|
||||
// the line reader above only ever asks for up to 1024 bytes per call and
|
||||
// works fine, so read in bounded chunks here too.
|
||||
size_t totalRead = 0;
|
||||
uint8_t *dest = (uint8_t *)buffer;
|
||||
while(totalRead < bufferSize) {
|
||||
size_t chunkSize = mathMin(
|
||||
bufferSize - totalRead, ASSET_FILE_READ_CHUNK_MAX
|
||||
);
|
||||
zip_int64_t bytesRead = zip_fread(
|
||||
file->zipFile, dest + totalRead, chunkSize
|
||||
);
|
||||
if(bytesRead < 0) {
|
||||
errorThrow(
|
||||
"Failed to read from asset file: %s (%s)",
|
||||
file->filename, zip_file_strerror(file->zipFile)
|
||||
);
|
||||
}
|
||||
if(bytesRead == 0) break;
|
||||
totalRead += (size_t)bytesRead;
|
||||
}
|
||||
file->position += bytesRead;
|
||||
file->lastRead = bytesRead;
|
||||
file->position += totalRead;
|
||||
file->lastRead = totalRead;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
#define ASSET_FILE_NAME_MAX 48
|
||||
|
||||
// Max bytes requested per zip_fread() call in assetFileRead(). Some
|
||||
// zip_fread() implementations (seen on PSP) reject a single call asking
|
||||
// for very large amounts of data at once; the locale line reader has
|
||||
// always used 1024-byte reads successfully, so that's the proven-safe cap.
|
||||
#define ASSET_FILE_READ_CHUNK_MAX 1024
|
||||
|
||||
typedef struct assetfile_s assetfile_t;
|
||||
|
||||
typedef errorret_t (*assetfileloader_t)(assetfile_t *file);
|
||||
|
||||
@@ -17,3 +17,4 @@ add_subdirectory(locale)
|
||||
add_subdirectory(json)
|
||||
add_subdirectory(chunk)
|
||||
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;
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/asset.h"
|
||||
|
||||
// Reads a little-endian int16 from a potentially-unaligned offset into a
|
||||
// worldunit_t, advancing *offset past it.
|
||||
static worldunit_t assetChunkReadWorldUnit(
|
||||
const uint8_t *data,
|
||||
size_t *offset
|
||||
) {
|
||||
int16_t value;
|
||||
memoryCopy(&value, data + *offset, sizeof(int16_t));
|
||||
*offset += sizeof(int16_t);
|
||||
return (worldunit_t)endianLittleToHost16((uint16_t)value);
|
||||
}
|
||||
|
||||
static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) {
|
||||
worldpos_t pos;
|
||||
pos.x = assetChunkReadWorldUnit(data, offset);
|
||||
pos.y = assetChunkReadWorldUnit(data, offset);
|
||||
pos.z = assetChunkReadWorldUnit(data, offset);
|
||||
return pos;
|
||||
}
|
||||
|
||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||
assertNotNull(loading, "Loading cannot be NULL");
|
||||
assertNotMainThread("Should be called from an async thread.");
|
||||
@@ -146,6 +166,62 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
||||
out->meshOffsets[m][2] = endianLittleToHostFloat(out->meshOffsets[m][2]);
|
||||
}
|
||||
|
||||
out->entitySpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->entitySpawnCount <= CHUNK_ENTITY_SPAWN_COUNT_MAX,
|
||||
"Chunk entity spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->entitySpawnCount; s++) {
|
||||
chunkentityspawn_t *spawn = &out->entitySpawns[s];
|
||||
spawn->kind = (chunkentityspawnkind_t)data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
uint16_t a;
|
||||
memoryCopy(&a, data + offset, sizeof(uint16_t));
|
||||
a = endianLittleToHost16(a);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
uint8_t b = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
|
||||
if(spawn->kind == CHUNK_ENTITY_SPAWN_KIND_ITEM) {
|
||||
spawn->globalId = 0;
|
||||
spawn->itemId = a;
|
||||
spawn->itemQuantity = b;
|
||||
} else {
|
||||
spawn->globalId = a;
|
||||
spawn->itemId = 0;
|
||||
spawn->itemQuantity = 0;
|
||||
}
|
||||
|
||||
spawn->position = assetChunkReadWorldPos(data, &offset);
|
||||
}
|
||||
|
||||
out->areaSpawnCount = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
assertTrue(
|
||||
out->areaSpawnCount <= CHUNK_AREA_COUNT_MAX,
|
||||
"Chunk area spawn count exceeds maximum."
|
||||
);
|
||||
|
||||
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||
area->min = assetChunkReadWorldPos(data, &offset);
|
||||
area->max = assetChunkReadWorldPos(data, &offset);
|
||||
|
||||
uint16_t callbackId;
|
||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||
area->callbackId = endianLittleToHost16(callbackId);
|
||||
offset += sizeof(uint16_t);
|
||||
|
||||
area->notify = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
area->trigger = data[offset];
|
||||
offset += sizeof(uint8_t);
|
||||
}
|
||||
|
||||
memoryFree(data);
|
||||
loading->loading.chunk.data = NULL;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "asset/assetfile.h"
|
||||
#include "rpg/overworld/chunk.h"
|
||||
|
||||
#define ASSET_CHUNK_FILE_VERSION 4
|
||||
#define ASSET_CHUNK_FILE_VERSION 5
|
||||
|
||||
typedef struct assetloading_s assetloading_t;
|
||||
typedef struct assetentry_s assetentry_t;
|
||||
@@ -33,12 +33,39 @@ typedef struct {
|
||||
uint8_t modelIndex;
|
||||
} assetchunkloaderloading_t;
|
||||
|
||||
typedef enum {
|
||||
CHUNK_ENTITY_SPAWN_KIND_GLOBAL,
|
||||
CHUNK_ENTITY_SPAWN_KIND_ITEM
|
||||
} chunkentityspawnkind_t;
|
||||
|
||||
typedef struct {
|
||||
chunkentityspawnkind_t kind;
|
||||
uint16_t globalId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_GLOBAL.
|
||||
uint16_t itemId; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||
uint8_t itemQuantity; // Valid when kind == CHUNK_ENTITY_SPAWN_KIND_ITEM.
|
||||
worldpos_t position;
|
||||
} chunkentityspawn_t;
|
||||
|
||||
typedef struct {
|
||||
worldpos_t min;
|
||||
worldpos_t max;
|
||||
uint16_t callbackId; // Index into MAP_AREA_CALLBACK_LIST.
|
||||
uint8_t notify;
|
||||
uint8_t trigger;
|
||||
} chunkareaspawn_t;
|
||||
|
||||
typedef struct {
|
||||
tile_t *tiles;
|
||||
uint8_t meshCount;
|
||||
char_t modelNames[CHUNK_MESH_COUNT_MAX][CHUNK_MESH_NAME_MAX];
|
||||
vec3 meshOffsets[CHUNK_MESH_COUNT_MAX];
|
||||
assetentry_t *modelEntries[CHUNK_MESH_COUNT_MAX];
|
||||
|
||||
uint8_t entitySpawnCount;
|
||||
chunkentityspawn_t entitySpawns[CHUNK_ENTITY_SPAWN_COUNT_MAX];
|
||||
|
||||
uint8_t areaSpawnCount;
|
||||
chunkareaspawn_t areaSpawns[CHUNK_AREA_COUNT_MAX];
|
||||
} assetchunkoutput_t;
|
||||
|
||||
/**
|
||||
|
||||
+1
-5
@@ -5,9 +5,5 @@
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uisettings.c
|
||||
uisettingsgeneral.c
|
||||
uisettingsinput.c
|
||||
uisettingsdisplay.c
|
||||
uisettingsaudio.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,11 +17,9 @@ console_t CONSOLE;
|
||||
|
||||
void consoleInit(void) {
|
||||
memoryZero(&CONSOLE, sizeof(console_t));
|
||||
CONSOLE.visible = false;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
#endif
|
||||
// CONSOLE.visible = false;
|
||||
CONSOLE.visible = true;
|
||||
threadMutexInit(&CONSOLE.printMutex);
|
||||
}
|
||||
|
||||
void consolePrint(const char_t *message, ...) {
|
||||
@@ -32,20 +30,14 @@ void consolePrint(const char_t *message, ...) {
|
||||
int32_t len = stringFormatVA(buffer, CONSOLE_LINE_MAX, message, args);
|
||||
va_end(args);
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexLock(&CONSOLE.printMutex);
|
||||
#endif
|
||||
|
||||
threadMutexLock(&CONSOLE.printMutex);
|
||||
memoryMove(
|
||||
CONSOLE.line[0],
|
||||
CONSOLE.line[1],
|
||||
(CONSOLE_HISTORY_MAX - 1) * CONSOLE_LINE_MAX
|
||||
);
|
||||
memoryCopy(CONSOLE.line[CONSOLE_HISTORY_MAX - 1], buffer, len + 1);
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexUnlock(&CONSOLE.printMutex);
|
||||
#endif
|
||||
threadMutexUnlock(&CONSOLE.printMutex);
|
||||
|
||||
logDebug("%s\n", buffer);
|
||||
}
|
||||
@@ -61,7 +53,5 @@ void consoleUpdate(void) {
|
||||
}
|
||||
|
||||
void consoleDispose(void) {
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadMutexDispose(&CONSOLE.printMutex);
|
||||
#endif
|
||||
threadMutexDispose(&CONSOLE.printMutex);
|
||||
}
|
||||
@@ -6,24 +6,18 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "consoledefs.h"
|
||||
#include "error/error.h"
|
||||
#include "dusk.h"
|
||||
#include "thread/thread.h"
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
#include "thread/thread.h"
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#define CONSOLE_POSIX_POLL_RATE 75
|
||||
#endif
|
||||
#define CONSOLE_LINE_MAX 128
|
||||
#define CONSOLE_HISTORY_MAX 16
|
||||
#define CONSOLE_EXEC_BUFFER_MAX 32
|
||||
|
||||
typedef struct {
|
||||
char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX];
|
||||
bool_t visible;
|
||||
|
||||
#ifdef DUSK_CONSOLE_POSIX
|
||||
threadmutex_t printMutex;
|
||||
#endif
|
||||
threadmutex_t printMutex;
|
||||
} console_t;
|
||||
|
||||
extern console_t CONSOLE;
|
||||
|
||||
@@ -33,13 +33,17 @@ errorret_t displayInit(void) {
|
||||
#ifdef displayPlatformInit
|
||||
errorChain(displayPlatformInit());
|
||||
#endif
|
||||
|
||||
// Set initial state
|
||||
errorChain(displaySetState((displaystate_t){ .flags = 0 }));
|
||||
|
||||
// Init the fixed textures
|
||||
errorChain(textureInit(
|
||||
&TEXTURE_WHITE, 4, 4,
|
||||
&TEXTURE_WHITE, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_WHITE_PIXELS }
|
||||
));
|
||||
errorChain(textureInit(
|
||||
&TEXTURE_TEST, 4, 4,
|
||||
&TEXTURE_TEST, TEXTURE_FIXED_WIDTH, TEXTURE_FIXED_HEIGHT,
|
||||
TEXTURE_FORMAT_RGBA, (texturedata_t){ .rgbaColors = TEXTURE_TEST_PIXELS }
|
||||
));
|
||||
|
||||
@@ -51,13 +55,13 @@ errorret_t displayInit(void) {
|
||||
errorChain(capsuleInit());
|
||||
errorChain(triPrismInit());
|
||||
|
||||
// Init the subsystems
|
||||
errorChain(frameBufferInitBackBuffer());
|
||||
errorChain(spriteBatchInit());
|
||||
errorChain(textInit());
|
||||
errorChain(screenInit());
|
||||
|
||||
// Setup initial shader with default values
|
||||
|
||||
errorChain(shaderListInit());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
text.c
|
||||
font.c
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "font.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include "display/color.h"
|
||||
|
||||
font_t FONT_DEFAULT;
|
||||
static texture_t FONT_DEFAULT_TEXTURE;
|
||||
static tileset_t FONT_DEFAULT_TILESET;
|
||||
|
||||
const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
FONT_DEFAULT_TILE_HEIGHT
|
||||
] = {
|
||||
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00 }, // !
|
||||
{ 0x00, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // "
|
||||
{ 0x00, 0x14, 0x14, 0x3E, 0x14, 0x3E, 0x14, 0x14, 0x00, 0x00 }, // #
|
||||
{ 0x00, 0x08, 0x1E, 0x28, 0x1C, 0x0A, 0x3C, 0x08, 0x00, 0x00 }, // $
|
||||
{ 0x00, 0x00, 0x22, 0x24, 0x08, 0x12, 0x22, 0x00, 0x00, 0x00 }, // %
|
||||
{ 0x00, 0x08, 0x14, 0x14, 0x1A, 0x24, 0x24, 0x1A, 0x00, 0x00 }, // &
|
||||
{ 0x00, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // '
|
||||
{ 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04, 0x00, 0x00 }, // (
|
||||
{ 0x00, 0x10, 0x08, 0x08, 0x08, 0x08, 0x08, 0x10, 0x00, 0x00 }, // )
|
||||
{ 0x00, 0x00, 0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, 0x00, 0x00 }, // *
|
||||
{ 0x00, 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x00, 0x00 }, // +
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x00 }, // ,
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00 }, // -
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00 }, // .
|
||||
{ 0x00, 0x04, 0x04, 0x08, 0x08, 0x08, 0x10, 0x10, 0x00, 0x00 }, // /
|
||||
{ 0x00, 0x1C, 0x22, 0x26, 0x2A, 0x32, 0x22, 0x1C, 0x00, 0x00 }, // 0
|
||||
{ 0x00, 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // 1
|
||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // 2
|
||||
{ 0x00, 0x1C, 0x22, 0x02, 0x0C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 3
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x02, 0x02, 0x02, 0x00, 0x00 }, // 4
|
||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 5
|
||||
{ 0x00, 0x1C, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 6
|
||||
{ 0x00, 0x3E, 0x02, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00 }, // 7
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1C, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // 8
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // 9
|
||||
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00 }, // :
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ;
|
||||
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00 }, // <
|
||||
{ 0x00, 0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00 }, // =
|
||||
{ 0x00, 0x10, 0x08, 0x04, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00 }, // >
|
||||
{ 0x00, 0x1C, 0x22, 0x02, 0x04, 0x08, 0x00, 0x08, 0x00, 0x00 }, // ?
|
||||
{ 0x00, 0x1C, 0x26, 0x2A, 0x2A, 0x26, 0x20, 0x1C, 0x00, 0x00 }, // @
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x00, 0x00 }, // A
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // B
|
||||
{ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // C
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // D
|
||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // E
|
||||
{ 0x00, 0x3E, 0x20, 0x20, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // F
|
||||
{ 0x00, 0x1C, 0x22, 0x20, 0x2E, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // G
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x22, 0x00, 0x00 }, // H
|
||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // I
|
||||
{ 0x00, 0x02, 0x02, 0x02, 0x02, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // J
|
||||
{ 0x00, 0x22, 0x24, 0x28, 0x30, 0x28, 0x24, 0x22, 0x00, 0x00 }, // K
|
||||
{ 0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x00, 0x00 }, // L
|
||||
{ 0x00, 0x22, 0x36, 0x2A, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // M
|
||||
{ 0x00, 0x22, 0x22, 0x32, 0x2A, 0x26, 0x22, 0x22, 0x00, 0x00 }, // N
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // O
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x20, 0x20, 0x20, 0x00, 0x00 }, // P
|
||||
{ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x06, 0x00 }, // Q
|
||||
{ 0x00, 0x3C, 0x22, 0x22, 0x3C, 0x22, 0x22, 0x22, 0x00, 0x00 }, // R
|
||||
{ 0x00, 0x1C, 0x22, 0x20, 0x1C, 0x02, 0x22, 0x1C, 0x00, 0x00 }, // S
|
||||
{ 0x00, 0x3E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // T
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // U
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x14, 0x14, 0x08, 0x00, 0x00 }, // V
|
||||
{ 0x00, 0x22, 0x22, 0x22, 0x22, 0x2A, 0x36, 0x22, 0x00, 0x00 }, // W
|
||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x14, 0x22, 0x22, 0x00, 0x00 }, // X
|
||||
{ 0x00, 0x22, 0x22, 0x14, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00 }, // Y
|
||||
{ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E, 0x00, 0x00 }, // Z
|
||||
{ 0x00, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0C, 0x00, 0x00 }, // [
|
||||
// Custom icon glyph, not a real backslash - a backspace symbol: Caps
|
||||
// Lock's up arrow (see FONT_ICON_CAPSLOCK) rotated 270 degrees to
|
||||
// point left instead, adapted to this font's fixed 6x10 tile (each of
|
||||
// Caps Lock's row widths becomes a column height here, centered
|
||||
// vertically). Backslash was never drawn anyway, and isn't a key this
|
||||
// virtual keyboard can type - see FONT_ICON_BACKSPACE.
|
||||
{ 0x00, 0x00, 0x08, 0x1F, 0x3F, 0x3F, 0x1F, 0x08, 0x00, 0x00 }, // FONT_ICON_BACKSPACE
|
||||
{ 0x00, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x00, 0x00 }, // ]
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // ^ (not drawn in source font)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E }, // _
|
||||
// Custom icon glyph, not a real backtick - an up arrow for Shift, same
|
||||
// head as Caps Lock's (see FONT_ICON_CAPSLOCK) but with a shaft 2px
|
||||
// thinner, plus one empty row near the bottom of it, so it reads as a
|
||||
// lighter/broken version of Caps Lock's thicker uninterrupted one.
|
||||
// Backtick was never drawn anyway, and isn't a key this virtual
|
||||
// keyboard can type - see FONT_ICON_SHIFT.
|
||||
{ 0x0C, 0x1E, 0x3F, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x0C, 0x00 }, // FONT_ICON_SHIFT
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // a
|
||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x00, 0x00 }, // b
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x20, 0x22, 0x1C, 0x00, 0x00 }, // c
|
||||
{ 0x00, 0x02, 0x02, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // d
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x3E, 0x20, 0x1C, 0x00, 0x00 }, // e
|
||||
{ 0x00, 0x0C, 0x12, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x00, 0x00 }, // f
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // g
|
||||
{ 0x00, 0x20, 0x20, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // h
|
||||
{ 0x00, 0x08, 0x00, 0x18, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00 }, // i
|
||||
{ 0x00, 0x02, 0x00, 0x06, 0x02, 0x02, 0x02, 0x02, 0x22, 0x1C }, // j
|
||||
{ 0x00, 0x20, 0x20, 0x22, 0x24, 0x38, 0x24, 0x22, 0x00, 0x00 }, // k
|
||||
{ 0x00, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // l
|
||||
{ 0x00, 0x00, 0x00, 0x3C, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00 }, // m
|
||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00 }, // n
|
||||
{ 0x00, 0x00, 0x00, 0x1C, 0x22, 0x22, 0x22, 0x1C, 0x00, 0x00 }, // o
|
||||
{ 0x00, 0x00, 0x00, 0x3C, 0x22, 0x22, 0x22, 0x3C, 0x20, 0x20 }, // p
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x02 }, // q
|
||||
{ 0x00, 0x00, 0x00, 0x2C, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00 }, // r
|
||||
{ 0x00, 0x00, 0x00, 0x1E, 0x20, 0x1C, 0x02, 0x3C, 0x00, 0x00 }, // s
|
||||
{ 0x00, 0x10, 0x10, 0x3C, 0x10, 0x10, 0x10, 0x0E, 0x00, 0x00 }, // t
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x00, 0x00 }, // u
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x14, 0x08, 0x00, 0x00 }, // v
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x2A, 0x2A, 0x14, 0x00, 0x00 }, // w
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x14, 0x08, 0x14, 0x22, 0x00, 0x00 }, // x
|
||||
{ 0x00, 0x00, 0x00, 0x22, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x1C }, // y
|
||||
{ 0x00, 0x00, 0x00, 0x3E, 0x04, 0x08, 0x10, 0x3E, 0x00, 0x00 }, // z
|
||||
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00 }, // {
|
||||
// Custom icon glyph, not a real pipe - a "return" arrow for the
|
||||
// keyboard's newline key: a vertical riser down the right side that
|
||||
// hooks left into a leftward-pointing arrowhead, i.e. "<-|" rotated
|
||||
// into an L. Pipe was never drawn anyway, and isn't a key this
|
||||
// virtual keyboard can type - see FONT_ICON_NEWLINE.
|
||||
{ 0x02, 0x02, 0x02, 0x02, 0x0E, 0x1E, 0x08, 0x00, 0x00, 0x00 }, // FONT_ICON_NEWLINE
|
||||
{ 0x00, 0x10, 0x08, 0x08, 0x04, 0x08, 0x08, 0x10, 0x00, 0x00 }, // }
|
||||
// Custom icon glyph, not a real tilde - a spacebar symbol for the
|
||||
// keyboard's space key: an underscore with a tick at each end, like
|
||||
// "|___|". Tilde was never drawn anyway, and was explicitly dropped
|
||||
// from this virtual keyboard's own key set - see FONT_ICON_SPACE.
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x3F }, // FONT_ICON_SPACE
|
||||
// Custom icon glyph, not a real ASCII character - a thick, solid,
|
||||
// uninterrupted up arrow for Caps Lock (contrast FONT_ICON_SHIFT's
|
||||
// same head but a thinner, gapped shaft). Assigned to char code 127
|
||||
// (DEL) since that codepoint is never legitimately typed text and
|
||||
// (unlike 128+) is still a positive value regardless of whether this
|
||||
// platform's plain `char` is signed - see FONT_ICON_CAPSLOCK.
|
||||
{ 0x0C, 0x1E, 0x3F, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x00 }, // FONT_ICON_CAPSLOCK
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // unused trailing tile
|
||||
};
|
||||
|
||||
errorret_t fontDefaultInit(void) {
|
||||
const int32_t width = (int32_t)mathNextPowTwo(
|
||||
FONT_DEFAULT_COLUMNS * FONT_DEFAULT_TILE_WIDTH
|
||||
);
|
||||
const int32_t height = (int32_t)mathNextPowTwo(
|
||||
FONT_DEFAULT_ROWS * FONT_DEFAULT_TILE_HEIGHT
|
||||
);
|
||||
|
||||
color_t *pixels = memoryAllocate(sizeof(color_t) * width * height);
|
||||
memoryZero(pixels, sizeof(color_t) * width * height);
|
||||
|
||||
for(uint16_t i = 0; i < FONT_DEFAULT_TILE_COUNT; i++) {
|
||||
const uint16_t tileX = (i % FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_WIDTH;
|
||||
const uint16_t tileY = (i / FONT_DEFAULT_COLUMNS) * FONT_DEFAULT_TILE_HEIGHT;
|
||||
|
||||
for(uint8_t row = 0; row < FONT_DEFAULT_TILE_HEIGHT; row++) {
|
||||
const uint8_t bits = FONT_DEFAULT_GLYPHS[i][row];
|
||||
|
||||
for(uint8_t col = 0; col < FONT_DEFAULT_TILE_WIDTH; col++) {
|
||||
if(!((bits >> (FONT_DEFAULT_TILE_WIDTH - 1 - col)) & 1)) continue;
|
||||
pixels[((tileY + row) * width) + (tileX + col)] = COLOR_WHITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FONT_DEFAULT_TILESET.tileWidth = FONT_DEFAULT_TILE_WIDTH;
|
||||
FONT_DEFAULT_TILESET.tileHeight = FONT_DEFAULT_TILE_HEIGHT;
|
||||
FONT_DEFAULT_TILESET.columns = FONT_DEFAULT_COLUMNS;
|
||||
FONT_DEFAULT_TILESET.rows = FONT_DEFAULT_ROWS;
|
||||
FONT_DEFAULT_TILESET.tileCount = FONT_DEFAULT_TILE_COUNT;
|
||||
FONT_DEFAULT_TILESET.uv[0] = (float_t)FONT_DEFAULT_TILE_WIDTH / (float_t)width;
|
||||
FONT_DEFAULT_TILESET.uv[1] = (float_t)FONT_DEFAULT_TILE_HEIGHT / (float_t)height;
|
||||
|
||||
const texturedata_t data = { .rgbaColors = pixels };
|
||||
errorret_t textureResult = textureInit(
|
||||
&FONT_DEFAULT_TEXTURE, width, height, TEXTURE_FORMAT_RGBA, data
|
||||
);
|
||||
memoryFree(pixels);
|
||||
errorChain(textureResult);
|
||||
|
||||
FONT_DEFAULT.texture = &FONT_DEFAULT_TEXTURE;
|
||||
FONT_DEFAULT.tileset = &FONT_DEFAULT_TILESET;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t fontDefaultDispose(void) {
|
||||
errorChain(textureDispose(&FONT_DEFAULT_TEXTURE));
|
||||
FONT_DEFAULT.texture = NULL;
|
||||
FONT_DEFAULT.tileset = NULL;
|
||||
errorOk();
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/texture/tileset.h"
|
||||
|
||||
@@ -13,3 +14,86 @@ typedef struct {
|
||||
texture_t *texture;
|
||||
tileset_t *tileset;
|
||||
} font_t;
|
||||
|
||||
/**
|
||||
* Pixel width/height of a single default-font glyph tile.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_WIDTH 6
|
||||
#define FONT_DEFAULT_TILE_HEIGHT 10
|
||||
|
||||
/** Grid layout of the generated default-font texture, in tiles. */
|
||||
#define FONT_DEFAULT_COLUMNS 16
|
||||
#define FONT_DEFAULT_ROWS 6
|
||||
|
||||
/**
|
||||
* Number of glyphs defined in FONT_DEFAULT_GLYPHS (FONT_DEFAULT_COLUMNS *
|
||||
* FONT_DEFAULT_ROWS), covering the printable ASCII range starting at
|
||||
* TEXT_CHAR_START ('!'), one custom icon glyph in a trailing unused tile
|
||||
* (see FONT_ICON_CAPSLOCK), plus one more genuinely unused trailing tile.
|
||||
* FONT_ICON_SHIFT/FONT_ICON_NEWLINE reuse existing-but-blank slots within
|
||||
* the printable range instead of more trailing tiles - see their own
|
||||
* comments.
|
||||
*/
|
||||
#define FONT_DEFAULT_TILE_COUNT (FONT_DEFAULT_COLUMNS * FONT_DEFAULT_ROWS)
|
||||
|
||||
/**
|
||||
* Custom (non-ASCII-meaning) icon glyphs baked into FONT_DEFAULT_GLYPHS
|
||||
* at otherwise-unused codepoints within the range this font covers -
|
||||
* safe to use anywhere a char_t string is expected, e.g. a uibutton_t
|
||||
* label. Kept below 128: char_t is a plain `char`, whose signedness
|
||||
* varies by platform, so a codepoint of 128 or above isn't safely
|
||||
* representable everywhere this engine targets.
|
||||
*/
|
||||
// Thick, solid, uninterrupted up arrow - Caps Lock. A trailing tile
|
||||
// (char code 127/DEL) that was never a real character to begin with.
|
||||
#define FONT_ICON_CAPSLOCK "\x7F"
|
||||
// Same up arrow, but with a thinner and gapped shaft - Shift. Reuses
|
||||
// the backtick's glyph slot: backtick was never drawn by this font
|
||||
// anyway, and isn't a key this engine's virtual keyboard can type.
|
||||
#define FONT_ICON_SHIFT "`"
|
||||
// A "return" arrow (down then left, with a leftward arrowhead) - the
|
||||
// keyboard's newline key. Reuses the pipe's glyph slot, for the same
|
||||
// reason as FONT_ICON_SHIFT.
|
||||
#define FONT_ICON_NEWLINE "|"
|
||||
// An underscore with a tick at each end ("|___|") - the keyboard's
|
||||
// space key. Reuses the tilde's glyph slot: tilde was never drawn by
|
||||
// this font, and was explicitly dropped from this engine's virtual
|
||||
// keyboard's own key set.
|
||||
#define FONT_ICON_SPACE "~"
|
||||
// Caps Lock's arrow rotated to point left - the keyboard's backspace
|
||||
// key. Reuses the backslash's glyph slot, for the same reason as
|
||||
// FONT_ICON_SHIFT/FONT_ICON_NEWLINE/FONT_ICON_SPACE. Last free reused
|
||||
// slot below 128 - one more custom icon after this needs
|
||||
// FONT_DEFAULT_COLUMNS/ROWS grown to make room.
|
||||
#define FONT_ICON_BACKSPACE "\\"
|
||||
|
||||
extern font_t FONT_DEFAULT;
|
||||
|
||||
/**
|
||||
* Hard coded bitmap data for the built-in default font. Indexed
|
||||
* [glyph][row], where glyph 0 corresponds to TEXT_CHAR_START ('!') and
|
||||
* glyphs run consecutively through the printable ASCII range. Each row
|
||||
* byte holds FONT_DEFAULT_TILE_WIDTH bit flags, one per pixel column:
|
||||
* bit (FONT_DEFAULT_TILE_WIDTH - 1) is the leftmost pixel and bit 0 is
|
||||
* the rightmost; 1 means the pixel is set, 0 means it is not.
|
||||
*/
|
||||
extern const uint8_t FONT_DEFAULT_GLYPHS[FONT_DEFAULT_TILE_COUNT][
|
||||
FONT_DEFAULT_TILE_HEIGHT
|
||||
];
|
||||
|
||||
/**
|
||||
* Builds the default font's texture + tileset directly from
|
||||
* FONT_DEFAULT_GLYPHS, without going through the asset system - so the
|
||||
* engine always has a usable font to render with regardless of whether
|
||||
* asset loading (e.g. the packed .dsk archive) succeeds.
|
||||
*
|
||||
* @return Either an error or success result.
|
||||
*/
|
||||
errorret_t fontDefaultInit(void);
|
||||
|
||||
/**
|
||||
* Disposes of the default font created by fontDefaultInit().
|
||||
*
|
||||
* @return Either an error or success result.
|
||||
*/
|
||||
errorret_t fontDefaultDispose(void);
|
||||
|
||||
+114
-41
@@ -9,34 +9,15 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/display/assettextureloader.h"
|
||||
#include "asset/loader/display/assettilesetloader.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
|
||||
font_t FONT_DEFAULT;
|
||||
|
||||
errorret_t textInit(void) {
|
||||
assetloaderinput_t input = { .texture = TEXTURE_FORMAT_RGBA };
|
||||
assetentry_t *entryTexture = assetLock(
|
||||
"ui/minogram.png", ASSET_LOADER_TYPE_TEXTURE, &input
|
||||
);
|
||||
assetentry_t *entryTileset = assetLock(
|
||||
"ui/minogram.dtf", ASSET_LOADER_TYPE_TILESET, NULL
|
||||
);
|
||||
errorChain(assetRequireLoaded(entryTexture));
|
||||
errorChain(assetRequireLoaded(entryTileset));
|
||||
|
||||
FONT_DEFAULT.texture = &entryTexture->data.texture;
|
||||
FONT_DEFAULT.tileset = &entryTileset->data.tileset;
|
||||
errorChain(fontDefaultInit());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t textDispose(void) {
|
||||
FONT_DEFAULT.texture = NULL;
|
||||
FONT_DEFAULT.tileset = NULL;
|
||||
assetUnlock("ui/minogram.png");
|
||||
assetUnlock("ui/minogram.dtf");
|
||||
errorChain(fontDefaultDispose());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -73,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,
|
||||
@@ -81,10 +118,11 @@ errorret_t textDraw(
|
||||
font_t *font
|
||||
) {
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
int32_t length = strlen(text);
|
||||
if(length == 0) errorOk();
|
||||
|
||||
if(font == NULL) font = &FONT_DEFAULT;
|
||||
|
||||
spritebatchsprite_t sprite;
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = color,
|
||||
@@ -92,31 +130,25 @@ errorret_t textDraw(
|
||||
}
|
||||
};
|
||||
|
||||
spritebatchsprite_t sprites[32];
|
||||
float_t posX = x;
|
||||
float_t posY = y;
|
||||
int32_t buffered = 0;
|
||||
int32_t charIndex = 0;
|
||||
do {
|
||||
buffered = textBuffer(
|
||||
x, y, text, font,
|
||||
sprites,
|
||||
sizeof(sprites) / sizeof(spritebatchsprite_t),
|
||||
&charIndex, &posX, &posY
|
||||
);
|
||||
errorChain(spriteBatchBuffer(sprites, buffered, &SHADER_UNLIT, material));
|
||||
} while(charIndex < length);
|
||||
|
||||
char_t c;
|
||||
int32_t i = 0;
|
||||
while((c = text[i++]) != '\0') {
|
||||
if(c == '\n') {
|
||||
posX = x;
|
||||
posY += font->tileset->tileHeight;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(c == ' ') {
|
||||
posX += font->tileset->tileWidth;
|
||||
continue;
|
||||
}
|
||||
|
||||
sprite = textGetSprite((vec2){posX, posY}, c, font);
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
posX += font->tileset->tileWidth;
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void textMeasure(
|
||||
int32_t textMeasure(
|
||||
const char_t *text,
|
||||
const font_t *font,
|
||||
int32_t *outWidth,
|
||||
@@ -129,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;
|
||||
@@ -141,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
#define TEXT_CHAR_START '!'
|
||||
|
||||
extern font_t FONT_DEFAULT;
|
||||
|
||||
/**
|
||||
* Initializes the text system.
|
||||
*
|
||||
@@ -42,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.
|
||||
*
|
||||
@@ -67,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);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "display/display.h"
|
||||
|
||||
texture_t TEXTURE_WHITE;
|
||||
color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||
color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE,
|
||||
@@ -20,7 +20,7 @@ color_t TEXTURE_WHITE_PIXELS[4*4] = {
|
||||
};
|
||||
|
||||
texture_t TEXTURE_TEST;
|
||||
color_t TEXTURE_TEST_PIXELS[4*4] = {
|
||||
color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT] = {
|
||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||
COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK,
|
||||
COLOR_BLACK, COLOR_MAGENTA, COLOR_BLACK, COLOR_MAGENTA,
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#error "textureDisposePlatform should not be defined."
|
||||
#endif
|
||||
|
||||
#define TEXTURE_FIXED_WIDTH 4
|
||||
#define TEXTURE_FIXED_HEIGHT 4
|
||||
|
||||
typedef textureformatplatform_t textureformat_t;
|
||||
typedef textureplatform_t texture_t;
|
||||
|
||||
@@ -29,9 +32,9 @@ typedef union texturedata_u {
|
||||
} texturedata_t;
|
||||
|
||||
extern texture_t TEXTURE_WHITE;
|
||||
extern color_t TEXTURE_WHITE_PIXELS[4*4];
|
||||
extern color_t TEXTURE_WHITE_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
||||
extern texture_t TEXTURE_TEST;
|
||||
extern color_t TEXTURE_TEST_PIXELS[4*4];
|
||||
extern color_t TEXTURE_TEST_PIXELS[TEXTURE_FIXED_WIDTH * TEXTURE_FIXED_HEIGHT];
|
||||
|
||||
/**
|
||||
* Initializes a texture.
|
||||
|
||||
@@ -16,10 +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/save.h"\
|
||||
|
||||
engine_t ENGINE;
|
||||
|
||||
@@ -37,12 +39,14 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
errorChain(systemInit());
|
||||
errorChain(inputInit());
|
||||
errorChain(assetInit());
|
||||
// errorChain(saveInit());
|
||||
errorChain(saveInit());
|
||||
errorChain(localeManagerInit());
|
||||
errorChain(displayInit());
|
||||
errorChain(uiInit());
|
||||
errorChain(rpgInit());
|
||||
errorChain(networkInit());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkInit());
|
||||
#endif
|
||||
errorChain(sceneInit());
|
||||
|
||||
consolePrint("Engine initialized");
|
||||
@@ -53,15 +57,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
||||
consolePrint("Assertions real");
|
||||
#endif
|
||||
|
||||
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||
|
||||
sceneSet(SCENE_TYPE_INITIAL);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t engineUpdate(void) {
|
||||
// Order here is important.
|
||||
errorChain(networkUpdate());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkUpdate());
|
||||
#endif
|
||||
errorChain(saveUpdate());
|
||||
timeUpdate();
|
||||
inputUpdate();
|
||||
consoleUpdate();
|
||||
@@ -82,13 +88,15 @@ void engineExit(void) {
|
||||
|
||||
errorret_t engineDispose(void) {
|
||||
errorChain(sceneDispose());
|
||||
errorChain(networkDispose());
|
||||
#ifdef DUSK_NETWORK
|
||||
errorChain(networkDispose());
|
||||
#endif
|
||||
errorChain(rpgDispose());
|
||||
localeManagerDispose();
|
||||
errorChain(uiDispose());
|
||||
consoleDispose();
|
||||
errorChain(displayDispose());
|
||||
// errorChain(saveDispose());
|
||||
errorChain(saveDispose());
|
||||
errorChain(assetDispose());
|
||||
|
||||
errorOk();
|
||||
|
||||
@@ -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,32 +11,16 @@
|
||||
#include "util/string.h"
|
||||
#include "util/math.h"
|
||||
#include "time/time.h"
|
||||
#include "event/event.h"
|
||||
|
||||
input_t INPUT;
|
||||
|
||||
errorret_t inputInit(void) {
|
||||
memoryZero(&INPUT, sizeof(input_t));
|
||||
INPUT.deadzone = INPUT_DEADZONE_DEFAULT;
|
||||
|
||||
for(uint8_t i = 0; i < INPUT_ACTION_COUNT; i++) {
|
||||
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
|
||||
@@ -107,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,17 +10,9 @@
|
||||
#include "inputbutton.h"
|
||||
#include "inputaction.h"
|
||||
|
||||
#define INPUT_LISTENER_PRESSED_MAX 16
|
||||
#define INPUT_LISTENER_RELEASED_MAX INPUT_LISTENER_PRESSED_MAX
|
||||
#define INPUT_DEADZONE_DEFAULT 0.1f
|
||||
|
||||
typedef struct {
|
||||
inputactiondata_t actions[INPUT_ACTION_COUNT];
|
||||
|
||||
inputplatform_t platform;
|
||||
|
||||
/** User-configured gamepad axis deadzone (0.0f to 1.0f). */
|
||||
float_t deadzone;
|
||||
} input_t;
|
||||
|
||||
extern input_t INPUT;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,13 +7,22 @@
|
||||
|
||||
#include "localemanager.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "assert/assert.h"
|
||||
#include "ui/ui.h"
|
||||
#include "system/system.h"
|
||||
#include "console/console.h"
|
||||
|
||||
localemanager_t LOCALE;
|
||||
|
||||
errorret_t localeManagerInit() {
|
||||
memoryZero(&LOCALE, sizeof(localemanager_t));
|
||||
errorChain(localeManagerSetLocale(&LOCALE_EN_US));
|
||||
|
||||
// TODO: Set locale based on system locale.
|
||||
const localeinfo_t *locale = systemGetLocale();
|
||||
errorChain(localeManagerSetLocale(locale));
|
||||
consolePrint("Locale set to: %s", locale->name);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -30,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"
|
||||
|
||||
|
||||
@@ -10,3 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
battlefighter.c
|
||||
party.c
|
||||
)
|
||||
|
||||
# Subdirs
|
||||
add_subdirectory(testbattle)
|
||||
|
||||
+161
-58
@@ -8,6 +8,7 @@
|
||||
#include "battle.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
|
||||
battle_t BATTLE;
|
||||
|
||||
@@ -51,10 +52,8 @@ void battleStart(
|
||||
BATTLE.fleeAvailable = fleeAvailable;
|
||||
BATTLE.result = BATTLE_RESULT_NONE;
|
||||
BATTLE.round = 1;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(true);
|
||||
|
||||
BATTLE.active = true;
|
||||
battleSetState(BATTLE_STATE_OPENING);
|
||||
}
|
||||
|
||||
void battleDispose(void) {
|
||||
@@ -62,9 +61,9 @@ void battleDispose(void) {
|
||||
}
|
||||
|
||||
battlefighter_t *battleGetCurrentFighter(void) {
|
||||
if(!BATTLE.active) return NULL;
|
||||
if(BATTLE.turnIndex >= BATTLE.turnCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.turnOrder[BATTLE.turnIndex]];
|
||||
if(BATTLE.state != BATTLE_STATE_PLAYER_SELECTION) return NULL;
|
||||
if(BATTLE.selectionIndex >= BATTLE.executionCount) return NULL;
|
||||
return &BATTLE.fighters[BATTLE.executionOrder[BATTLE.selectionIndex]];
|
||||
}
|
||||
|
||||
uint8_t battleGetAliveCount(const battlefighterteam_t team) {
|
||||
@@ -92,91 +91,105 @@ void battleResolveAttack(
|
||||
if(defender->health == 0) defender->status = BATTLE_FIGHTER_STATUS_DEAD;
|
||||
}
|
||||
|
||||
void battleNextTurn(void) {
|
||||
BATTLE.turnIndex++;
|
||||
if(BATTLE.turnIndex < BATTLE.turnCount) return;
|
||||
|
||||
BATTLE.round++;
|
||||
BATTLE.turnIndex = 0;
|
||||
battleBuildTurnOrder(false);
|
||||
}
|
||||
|
||||
battleresult_t battleCheckResult(void) {
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return BATTLE.result;
|
||||
|
||||
if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ALLY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_LOSS;
|
||||
battleSetResult(BATTLE_RESULT_LOSS);
|
||||
} else if(battleGetAliveCount(BATTLE_FIGHTER_TEAM_ENEMY) == 0) {
|
||||
BATTLE.result = BATTLE_RESULT_WIN;
|
||||
battleSetResult(BATTLE_RESULT_WIN);
|
||||
}
|
||||
|
||||
return BATTLE.result;
|
||||
}
|
||||
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
) {
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
action->type = type;
|
||||
action->targetIndex = targetIndex;
|
||||
|
||||
if(BATTLE.onActionDecided != NULL) {
|
||||
BATTLE.onActionDecided(&BATTLE.fighters[fighterIndex], action);
|
||||
}
|
||||
}
|
||||
|
||||
void battlePlayerAttack(const uint8_t targetIndex) {
|
||||
battlefighter_t *attacker = battleGetCurrentFighter();
|
||||
if(attacker == NULL) return;
|
||||
if(attacker->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(targetIndex >= BATTLE_FIGHTER_COUNT_MAX) return;
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[targetIndex])) return;
|
||||
|
||||
battlefighter_t *defender = &BATTLE.fighters[targetIndex];
|
||||
if(!battleFighterIsAlive(defender)) return;
|
||||
|
||||
battleResolveAttack(attacker, defender);
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
battleQueueAction(fighter->id, BATTLE_ACTION_ATTACK, targetIndex);
|
||||
BATTLE.selectionIndex++;
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battlePlayerFlee(void) {
|
||||
battlefighter_t *fighter = battleGetCurrentFighter();
|
||||
if(fighter == NULL) return;
|
||||
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) return;
|
||||
if(!BATTLE.fleeAvailable) return;
|
||||
|
||||
BATTLE.result = BATTLE_RESULT_FLED;
|
||||
battleSetResult(BATTLE_RESULT_FLED);
|
||||
}
|
||||
|
||||
void battleUpdate(void) {
|
||||
if(!BATTLE.active) return;
|
||||
if(BATTLE.result != BATTLE_RESULT_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_NONE) return;
|
||||
if(BATTLE.state == BATTLE_STATE_ENDED) return;
|
||||
if(CUTSCENE_SYSTEM.pause & CUTSCENE_PAUSE_BATTLE) return;
|
||||
|
||||
battlefighter_t *current = battleGetCurrentFighter();
|
||||
if(current == NULL) return;
|
||||
switch(BATTLE.state) {
|
||||
case BATTLE_STATE_OPENING:
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
break;
|
||||
|
||||
if(!battleFighterIsAlive(current)) {
|
||||
battleNextTurn();
|
||||
return;
|
||||
case BATTLE_STATE_PRE_ROUND:
|
||||
battleUpdatePreRound();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_AI_SELECTION:
|
||||
battleUpdateAiSelection();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_MOVES_EXECUTING:
|
||||
battleUpdateMovesExecuting();
|
||||
break;
|
||||
|
||||
case BATTLE_STATE_POST_ROUND:
|
||||
battleUpdatePostRound();
|
||||
break;
|
||||
|
||||
default:
|
||||
// BATTLE_STATE_PLAYER_SELECTION: waits on battlePlayerAttack/Flee.
|
||||
// BATTLE_STATE_NONE/ENDED: handled above.
|
||||
break;
|
||||
}
|
||||
|
||||
if(current->controller != BATTLE_FIGHTER_CONTROLLER_AI) return;
|
||||
|
||||
battlefighter_t *target = battleAIChooseTarget(current);
|
||||
if(target != NULL) battleResolveAttack(current, target);
|
||||
|
||||
battleCheckResult();
|
||||
if(BATTLE.result == BATTLE_RESULT_NONE) battleNextTurn();
|
||||
}
|
||||
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.turnCount = 0;
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias) {
|
||||
BATTLE.executionCount = 0;
|
||||
for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) {
|
||||
if(!battleFighterIsAlive(&BATTLE.fighters[i])) continue;
|
||||
BATTLE.turnOrder[BATTLE.turnCount++] = i;
|
||||
BATTLE.executionOrder[BATTLE.executionCount++] = i;
|
||||
}
|
||||
|
||||
// Insertion sort by speed descending -- fine for BATTLE_FIGHTER_COUNT_MAX.
|
||||
for(uint8_t i = 1; i < BATTLE.turnCount; i++) {
|
||||
const uint8_t key = BATTLE.turnOrder[i];
|
||||
for(uint8_t i = 1; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t key = BATTLE.executionOrder[i];
|
||||
const uint16_t keySpeed = BATTLE.fighters[key].stats.speed;
|
||||
|
||||
int8_t j = (int8_t)i - 1;
|
||||
while(
|
||||
j >= 0 && BATTLE.fighters[BATTLE.turnOrder[j]].stats.speed < keySpeed
|
||||
j >= 0 &&
|
||||
BATTLE.fighters[BATTLE.executionOrder[j]].stats.speed < keySpeed
|
||||
) {
|
||||
BATTLE.turnOrder[j + 1] = BATTLE.turnOrder[j];
|
||||
BATTLE.executionOrder[j + 1] = BATTLE.executionOrder[j];
|
||||
j--;
|
||||
}
|
||||
BATTLE.turnOrder[j + 1] = key;
|
||||
BATTLE.executionOrder[j + 1] = key;
|
||||
}
|
||||
|
||||
if(!applyEncounterBias) return;
|
||||
@@ -192,16 +205,16 @@ void battleMoveTeamFirst(const battlefighterteam_t team) {
|
||||
uint8_t sorted[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t count = 0;
|
||||
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team != team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
for(uint8_t i = 0; i < BATTLE.turnCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.turnOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.turnOrder[i];
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
if(BATTLE.fighters[BATTLE.executionOrder[i]].team == team) continue;
|
||||
sorted[count++] = BATTLE.executionOrder[i];
|
||||
}
|
||||
|
||||
memoryCopy(BATTLE.turnOrder, sorted, sizeof(uint8_t) * BATTLE.turnCount);
|
||||
memoryCopy(BATTLE.executionOrder, sorted, sizeof(uint8_t) * BATTLE.executionCount);
|
||||
}
|
||||
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
@@ -221,3 +234,93 @@ battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter) {
|
||||
|
||||
return weakest;
|
||||
}
|
||||
|
||||
void battleSetState(const battlestate_t next) {
|
||||
const battlestate_t previous = BATTLE.state;
|
||||
BATTLE.state = next;
|
||||
if(BATTLE.onStateChanged != NULL) BATTLE.onStateChanged(previous, next);
|
||||
}
|
||||
|
||||
void battleSetResult(const battleresult_t result) {
|
||||
BATTLE.result = result;
|
||||
battleSetState(BATTLE_STATE_ENDED);
|
||||
}
|
||||
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
) {
|
||||
return battleFighterIsAlive(&BATTLE.fighters[fighterIndex])
|
||||
&& BATTLE.fighters[fighterIndex].controller == controller
|
||||
&& BATTLE.actions[fighterIndex].type == BATTLE_ACTION_NONE;
|
||||
}
|
||||
|
||||
void battleAdvanceSelection(void) {
|
||||
while(BATTLE.selectionIndex < BATTLE.executionCount) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.selectionIndex];
|
||||
if(
|
||||
battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_PLAYER)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
BATTLE.selectionIndex++;
|
||||
}
|
||||
|
||||
battleSetState(BATTLE_STATE_AI_SELECTION);
|
||||
}
|
||||
|
||||
void battleUpdatePreRound(void) {
|
||||
// No need to clear BATTLE.actions here: every living fighter's action is
|
||||
// unconditionally reset to BATTLE_ACTION_NONE as it's processed in
|
||||
// battleUpdateMovesExecuting, and round 1 starts pre-zeroed by
|
||||
// battleInit(). Clearing it here would also wipe out any action a
|
||||
// cutscene force-queued while parked at BATTLE_STATE_PRE_ROUND.
|
||||
battleBuildExecutionOrder(BATTLE.round == 1);
|
||||
|
||||
BATTLE.selectionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_PLAYER_SELECTION);
|
||||
battleAdvanceSelection();
|
||||
}
|
||||
|
||||
void battleUpdateAiSelection(void) {
|
||||
for(uint8_t i = 0; i < BATTLE.executionCount; i++) {
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[i];
|
||||
if(
|
||||
!battleFighterNeedsDecision(fighterIndex, BATTLE_FIGHTER_CONTROLLER_AI)
|
||||
) continue;
|
||||
|
||||
battlefighter_t *target =
|
||||
battleAIChooseTarget(&BATTLE.fighters[fighterIndex]);
|
||||
if(target == NULL) continue;
|
||||
|
||||
battleQueueAction(fighterIndex, BATTLE_ACTION_ATTACK, target->id);
|
||||
}
|
||||
|
||||
BATTLE.executionIndex = 0;
|
||||
battleSetState(BATTLE_STATE_MOVES_EXECUTING);
|
||||
}
|
||||
|
||||
void battleUpdateMovesExecuting(void) {
|
||||
if(BATTLE.executionIndex >= BATTLE.executionCount) {
|
||||
battleSetState(BATTLE_STATE_POST_ROUND);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t fighterIndex = BATTLE.executionOrder[BATTLE.executionIndex++];
|
||||
battlefighter_t *fighter = &BATTLE.fighters[fighterIndex];
|
||||
if(!battleFighterIsAlive(fighter)) return;
|
||||
|
||||
battleaction_t *action = &BATTLE.actions[fighterIndex];
|
||||
if(action->type == BATTLE_ACTION_ATTACK) {
|
||||
battlefighter_t *target = &BATTLE.fighters[action->targetIndex];
|
||||
if(battleFighterIsAlive(target)) battleResolveAttack(fighter, target);
|
||||
}
|
||||
action->type = BATTLE_ACTION_NONE;
|
||||
|
||||
battleCheckResult();
|
||||
}
|
||||
|
||||
void battleUpdatePostRound(void) {
|
||||
BATTLE.round++;
|
||||
battleSetState(BATTLE_STATE_PRE_ROUND);
|
||||
}
|
||||
|
||||
+155
-36
@@ -27,19 +27,68 @@ typedef enum {
|
||||
BATTLE_RESULT_COUNT
|
||||
} battleresult_t;
|
||||
|
||||
// Where BATTLE currently is within a round. A cutscene can pause progression
|
||||
// (CUTSCENE_PAUSE_BATTLE) and use CUTSCENE_BATTLE_WAIT_STATE to synchronize
|
||||
// with any of these, or CUTSCENE_BATTLE_FORCE_ACTION to decide a fighter's
|
||||
// action ahead of PLAYER_SELECTION/AI_SELECTION reaching them.
|
||||
typedef enum {
|
||||
BATTLE_STATE_NONE, // Battle inactive.
|
||||
|
||||
BATTLE_STATE_OPENING, // Entered once by battleStart().
|
||||
BATTLE_STATE_PRE_ROUND, // Rebuilds execution order, clears the action queue.
|
||||
BATTLE_STATE_PLAYER_SELECTION, // Waits on battlePlayerAttack/Flee.
|
||||
BATTLE_STATE_AI_SELECTION, // Auto-queues every undecided AI fighter.
|
||||
BATTLE_STATE_MOVES_EXECUTING, // Resolves one queued action per update.
|
||||
BATTLE_STATE_POST_ROUND, // Round wrap-up; loops back to PRE_ROUND.
|
||||
|
||||
BATTLE_STATE_ENDED, // Terminal for WIN/LOSS/FLED alike -- see BATTLE.result.
|
||||
|
||||
BATTLE_STATE_COUNT
|
||||
} battlestate_t;
|
||||
|
||||
typedef enum {
|
||||
BATTLE_ACTION_NONE, // No action decided yet for this fighter this round.
|
||||
BATTLE_ACTION_ATTACK,
|
||||
|
||||
BATTLE_ACTION_COUNT
|
||||
} battleactiontype_t;
|
||||
|
||||
typedef struct {
|
||||
bool_t active;
|
||||
battleactiontype_t type;
|
||||
uint8_t targetIndex; // Meaningful for BATTLE_ACTION_ATTACK.
|
||||
} battleaction_t;
|
||||
|
||||
typedef void (*battlestatechangedcallback_t)(
|
||||
const battlestate_t previous,
|
||||
const battlestate_t next
|
||||
);
|
||||
|
||||
typedef void (*battleactiondecidedcallback_t)(
|
||||
const battlefighter_t *fighter,
|
||||
const battleaction_t *action
|
||||
);
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX];
|
||||
battleaction_t actions[BATTLE_FIGHTER_COUNT_MAX];
|
||||
|
||||
battleencountertype_t encounterType;
|
||||
bool_t fleeAvailable;
|
||||
battleresult_t result;
|
||||
|
||||
// Fighter indices (into fighters[]), sorted for the current round.
|
||||
uint8_t turnOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t turnCount;
|
||||
uint8_t turnIndex;
|
||||
uint8_t executionOrder[BATTLE_FIGHTER_COUNT_MAX];
|
||||
uint8_t executionCount;
|
||||
uint8_t executionIndex;
|
||||
uint16_t round;
|
||||
|
||||
// Cursor into executionOrder used by BATTLE_STATE_PLAYER_SELECTION to find
|
||||
// the next player-controlled fighter that still needs a decision.
|
||||
uint8_t selectionIndex;
|
||||
|
||||
battlestatechangedcallback_t onStateChanged;
|
||||
battleactiondecidedcallback_t onActionDecided;
|
||||
} battle_t;
|
||||
|
||||
extern battle_t BATTLE;
|
||||
@@ -77,11 +126,10 @@ battlefighter_t *battleAddFighter(
|
||||
);
|
||||
|
||||
/**
|
||||
* Starts the battle: builds the opening turn order (biased by
|
||||
* encounterType for the first round only) and marks the battle active.
|
||||
* Call once every fighter has been added via battleAddFighter.
|
||||
* Starts the battle: enters BATTLE_STATE_OPENING and marks the battle
|
||||
* active. Call once every fighter has been added via battleAddFighter.
|
||||
*
|
||||
* @param encounterType Determines the opening round's turn order.
|
||||
* @param encounterType Determines the opening round's execution order.
|
||||
* @param fleeAvailable Whether the party may attempt to flee this battle.
|
||||
*/
|
||||
void battleStart(
|
||||
@@ -95,10 +143,11 @@ void battleStart(
|
||||
void battleDispose(void);
|
||||
|
||||
/**
|
||||
* Returns the fighter whose turn it currently is.
|
||||
* Returns the fighter currently awaiting a player decision.
|
||||
*
|
||||
* @return Pointer to the active fighter, or NULL if the battle isn't
|
||||
* active or has no living fighters left to act.
|
||||
* @return Pointer to the fighter awaiting a decision, or NULL if the battle
|
||||
* isn't in BATTLE_STATE_PLAYER_SELECTION or every player-controlled
|
||||
* fighter has already decided.
|
||||
*/
|
||||
battlefighter_t *battleGetCurrentFighter(void);
|
||||
|
||||
@@ -125,61 +174,68 @@ void battleResolveAttack(
|
||||
);
|
||||
|
||||
/**
|
||||
* Ends the current fighter's turn and advances to the next fighter in
|
||||
* the turn order, starting a new round (rebuilding turn order purely by
|
||||
* speed) once every fighter in the current round has acted.
|
||||
*/
|
||||
void battleNextTurn(void);
|
||||
|
||||
/**
|
||||
* Checks whether the battle has been won or lost, updating and
|
||||
* returning BATTLE.result. Does nothing if a result has already been
|
||||
* set (e.g. by a successful flee).
|
||||
* Checks whether the battle has been won or lost, transitioning to
|
||||
* BATTLE_STATE_ENDED and updating BATTLE.result if so. Does nothing if a
|
||||
* result has already been set (e.g. by a successful flee).
|
||||
*
|
||||
* @return The battle's current result.
|
||||
*/
|
||||
battleresult_t battleCheckResult(void);
|
||||
|
||||
/**
|
||||
* Submits the current fighter's attack against a target, if it is
|
||||
* currently a player-controlled fighter's turn. Resolves the attack,
|
||||
* checks for a battle result, and advances the turn.
|
||||
* Queues an action for a fighter to perform once BATTLE_STATE_MOVES_EXECUTING
|
||||
* reaches them this round, overwriting any action already queued for that
|
||||
* fighter. Fires BATTLE.onActionDecided.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters of the deciding fighter.
|
||||
* @param type The type of action to perform.
|
||||
* @param targetIndex Index into BATTLE.fighters of the target, meaningful
|
||||
* for BATTLE_ACTION_ATTACK.
|
||||
*/
|
||||
void battleQueueAction(
|
||||
const uint8_t fighterIndex,
|
||||
const battleactiontype_t type,
|
||||
const uint8_t targetIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Submits the currently-selecting fighter's attack against a target, if the
|
||||
* battle is in BATTLE_STATE_PLAYER_SELECTION and awaiting a decision.
|
||||
* Queues the action and advances the selection cursor.
|
||||
*
|
||||
* @param targetIndex Index into BATTLE.fighters of the target.
|
||||
*/
|
||||
void battlePlayerAttack(const uint8_t targetIndex);
|
||||
|
||||
/**
|
||||
* Submits a flee attempt for the current fighter's turn, if it is
|
||||
* currently a player-controlled fighter's turn and fleeing is
|
||||
* available for this battle. Always succeeds, ending the battle with
|
||||
* BATTLE_RESULT_FLED.
|
||||
* Submits a flee attempt for the currently-selecting fighter, if the battle
|
||||
* is in BATTLE_STATE_PLAYER_SELECTION and fleeing is available for this
|
||||
* battle. Always succeeds, ending the battle with BATTLE_RESULT_FLED.
|
||||
*/
|
||||
void battlePlayerFlee(void);
|
||||
|
||||
/**
|
||||
* Updates the battle simulation for one frame: resolves the current
|
||||
* fighter's turn automatically if AI-controlled, otherwise waits for a
|
||||
* player action via battlePlayerAttack/battlePlayerFlee. No-op if the
|
||||
* battle isn't active or already has a result.
|
||||
* Updates the battle simulation for one frame, dispatching on BATTLE.state.
|
||||
* No-op if the battle isn't active, has already ended, or
|
||||
* CUTSCENE_PAUSE_BATTLE is set.
|
||||
*/
|
||||
void battleUpdate(void);
|
||||
|
||||
/**
|
||||
* Rebuilds BATTLE.turnOrder/turnCount from every currently living
|
||||
* Rebuilds BATTLE.executionOrder/executionCount from every currently living
|
||||
* fighter, sorted by speed descending.
|
||||
*
|
||||
* @param applyEncounterBias If true, reorders the freshly speed-sorted
|
||||
* queue so BATTLE.encounterType's favoured team goes first (used only
|
||||
* for the opening round).
|
||||
*/
|
||||
void battleBuildTurnOrder(const bool_t applyEncounterBias);
|
||||
void battleBuildExecutionOrder(const bool_t applyEncounterBias);
|
||||
|
||||
/**
|
||||
* Stably partitions BATTLE.turnOrder so every fighter on the given team
|
||||
* Stably partitions BATTLE.executionOrder so every fighter on the given team
|
||||
* comes first, preserving each side's relative (speed-sorted) order.
|
||||
*
|
||||
* @param team The team to move to the front of the turn order.
|
||||
* @param team The team to move to the front of the execution order.
|
||||
*/
|
||||
void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
|
||||
@@ -192,3 +248,66 @@ void battleMoveTeamFirst(const battlefighterteam_t team);
|
||||
* living fighters.
|
||||
*/
|
||||
battlefighter_t *battleAIChooseTarget(const battlefighter_t *fighter);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.state and fires BATTLE.onStateChanged with the previous and
|
||||
* new state.
|
||||
*
|
||||
* @param next The state to transition to.
|
||||
*/
|
||||
void battleSetState(const battlestate_t next);
|
||||
|
||||
/**
|
||||
* Sets BATTLE.result and transitions to BATTLE_STATE_ENDED.
|
||||
*
|
||||
* @param result The result to end the battle with.
|
||||
*/
|
||||
void battleSetResult(const battleresult_t result);
|
||||
|
||||
/**
|
||||
* Checks whether a fighter is a live, undecided candidate for the given
|
||||
* controller -- i.e. whether PLAYER_SELECTION or AI_SELECTION should still
|
||||
* be deciding an action for it this round.
|
||||
*
|
||||
* @param fighterIndex Index into BATTLE.fighters to check.
|
||||
* @param controller The controller PLAYER_SELECTION/AI_SELECTION is
|
||||
* currently deciding for.
|
||||
* @return True if the fighter is alive, matches controller, and has no
|
||||
* action queued yet.
|
||||
*/
|
||||
bool_t battleFighterNeedsDecision(
|
||||
const uint8_t fighterIndex,
|
||||
const battlefightercontroller_t controller
|
||||
);
|
||||
|
||||
/**
|
||||
* Advances BATTLE.selectionIndex to the next player-controlled fighter that
|
||||
* still needs a decision, or transitions to BATTLE_STATE_AI_SELECTION once
|
||||
* none remain.
|
||||
*/
|
||||
void battleAdvanceSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_PRE_ROUND: rebuilds the execution order and moves on
|
||||
* to BATTLE_STATE_PLAYER_SELECTION, positioning the selection cursor.
|
||||
*/
|
||||
void battleUpdatePreRound(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_AI_SELECTION: queues an attack for every undecided
|
||||
* AI-controlled fighter, then moves on to BATTLE_STATE_MOVES_EXECUTING.
|
||||
*/
|
||||
void battleUpdateAiSelection(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_MOVES_EXECUTING: resolves one queued action from
|
||||
* BATTLE.executionOrder per call, or transitions to BATTLE_STATE_POST_ROUND
|
||||
* once every fighter this round has been processed.
|
||||
*/
|
||||
void battleUpdateMovesExecuting(void);
|
||||
|
||||
/**
|
||||
* Handles BATTLE_STATE_POST_ROUND: advances BATTLE.round and transitions
|
||||
* back to BATTLE_STATE_PRE_ROUND.
|
||||
*/
|
||||
void battleUpdatePostRound(void);
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
# Sources
|
||||
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, \
|
||||
@@ -162,6 +299,28 @@ typedef struct cutscene_s {
|
||||
.shake = { .amount = AMOUNT, .duration = DURATION } \
|
||||
}
|
||||
|
||||
// Waits until BATTLE.state reaches STATE. Put this BEFORE
|
||||
// CUTSCENE_SET_PAUSE(CUTSCENE_PAUSE_BATTLE), not after -- pausing first
|
||||
// freezes BATTLE.state wherever it already is, so it would never reach
|
||||
// STATE on its own to satisfy the wait. Waiting unpaused, then pausing the
|
||||
// moment it's satisfied, catches the battle right at STATE before it can
|
||||
// advance further.
|
||||
#define CUTSCENE_BATTLE_WAIT_STATE(STATE) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE, \
|
||||
.battleWaitState = { .state = STATE } \
|
||||
}
|
||||
|
||||
// Immediately queues an attack for FIGHTER_INDEX against TARGET_INDEX,
|
||||
// bypassing normal player/AI selection for that fighter this round.
|
||||
#define CUTSCENE_BATTLE_FORCE_ACTION(FIGHTER_INDEX, TARGET_INDEX) \
|
||||
{ \
|
||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION, \
|
||||
.battleForceAction = { \
|
||||
.fighterIndex = FIGHTER_INDEX, .targetIndex = TARGET_INDEX \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CUTSCENE_SET_PAUSE(FLAGS) \
|
||||
{ .type = CUTSCENE_ITEM_TYPE_SET_PAUSE, .setPause = (FLAGS) }
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ typedef uint8_t cutscenepause_t;
|
||||
#define CUTSCENE_PAUSE_NPC ((cutscenepause_t)(1 << 0))
|
||||
#define CUTSCENE_PAUSE_PLAYER ((cutscenepause_t)(1 << 1))
|
||||
#define CUTSCENE_PAUSE_WORLD ((cutscenepause_t)(1 << 2))
|
||||
#define CUTSCENE_PAUSE_BATTLE ((cutscenepause_t)(1 << 3))
|
||||
|
||||
#define CUTSCENE_PAUSE_DEFAULT ((cutscenepause_t)( \
|
||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER \
|
||||
))
|
||||
|
||||
#define CUTSCENE_PAUSE_ALL ((cutscenepause_t)( \
|
||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD \
|
||||
CUTSCENE_PAUSE_NPC | CUTSCENE_PAUSE_PLAYER | CUTSCENE_PAUSE_WORLD | \
|
||||
CUTSCENE_PAUSE_BATTLE \
|
||||
))
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "cutscenesystem.h"
|
||||
#include "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)
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenestartbattle.c
|
||||
cutscenebattlewaitstate.c
|
||||
cutscenebattleforceaction.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
battleQueueAction(
|
||||
item->battleForceAction.fighterIndex, BATTLE_ACTION_ATTACK,
|
||||
item->battleForceAction.targetIndex
|
||||
);
|
||||
}
|
||||
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t fighterIndex;
|
||||
uint8_t targetIndex;
|
||||
} cutscenebattleforceaction_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
/**
|
||||
* Starts a battle force-action step: immediately queues an attack for the
|
||||
* given fighter against the given target, bypassing normal player/AI
|
||||
* selection for that fighter this round.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneBattleForceActionStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a battle force-action step (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutsceneBattleForceActionUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
|
||||
bool_t cutsceneBattleWaitStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return BATTLE.state == item->battleWaitState.state;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "rpg/battle/battle.h"
|
||||
|
||||
typedef struct {
|
||||
battlestate_t state;
|
||||
} cutscenebattlewaitstate_t;
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
/**
|
||||
* Updates a battle wait-state step, completing once BATTLE.state reaches
|
||||
* the watched state. Has no Start callback -- there's nothing to do until
|
||||
* the state is actually reached.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true once BATTLE.state equals the watched state.
|
||||
*/
|
||||
bool_t cutsceneBattleWaitStateUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -8,4 +8,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenewait.c
|
||||
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
|
||||
);
|
||||
@@ -118,6 +118,65 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
||||
[CUTSCENE_ITEM_TYPE_SHAKE] = {
|
||||
.init = cutsceneShakeStart,
|
||||
.update = cutsceneShakeUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_WAIT_STATE] = {
|
||||
.update = cutsceneBattleWaitStateUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_BATTLE_FORCE_ACTION] = {
|
||||
.init = cutsceneBattleForceActionStart,
|
||||
.update = cutsceneBattleForceActionUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL] = {
|
||||
.init = cutsceneModalStart,
|
||||
.update = cutsceneModalUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS] = {
|
||||
.init = cutsceneModalOptionsMarkersStart,
|
||||
.update = cutsceneModalOptionsMarkersUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MODAL_CLOSE] = {
|
||||
.init = cutsceneModalCloseStart,
|
||||
.update = cutsceneModalCloseUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_PRINT] = {
|
||||
.init = cutscenePrintStart,
|
||||
.update = cutscenePrintUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_MARKER] = {
|
||||
.init = cutsceneMarkerStart,
|
||||
.update = cutsceneMarkerUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_RESTART] = {
|
||||
.init = cutsceneRestartStart,
|
||||
.update = cutsceneRestartUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SCENE] = {
|
||||
.init = cutsceneSceneStart,
|
||||
.update = cutsceneSceneUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK] = {
|
||||
.init = cutsceneSaveDeviceCheckStart,
|
||||
.update = cutsceneSaveDeviceCheckUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS] = {
|
||||
.init = cutsceneSaveLoadAllSlotsStart,
|
||||
.update = cutsceneSaveLoadAllSlotsUpdate
|
||||
},
|
||||
|
||||
[CUTSCENE_ITEM_TYPE_KEYBOARD] = {
|
||||
.init = cutsceneKeyboardStart,
|
||||
.update = cutsceneKeyboardUpdate
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
|
||||
#pragma once
|
||||
#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,11 +26,18 @@
|
||||
#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"
|
||||
#include "maparea/cutscenemapareawait.h"
|
||||
#include "battle/cutscenestartbattle.h"
|
||||
#include "battle/cutscenebattlewaitstate.h"
|
||||
#include "battle/cutscenebattleforceaction.h"
|
||||
#include "save/cutscenesavedevicecheck.h"
|
||||
#include "save/cutscenesaveloadallslots.h"
|
||||
|
||||
typedef struct cutscene_s cutscene_t;
|
||||
|
||||
@@ -55,6 +66,18 @@ typedef enum {
|
||||
CUTSCENE_ITEM_TYPE_START_BATTLE,
|
||||
CUTSCENE_ITEM_TYPE_EMOJI,
|
||||
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;
|
||||
@@ -85,6 +108,16 @@ struct cutsceneitem_s {
|
||||
cutscenestartbattle_t startBattle;
|
||||
cutsceneemoji_t emoji;
|
||||
cutsceneshake_t shake;
|
||||
cutscenebattlewaitstate_t battleWaitState;
|
||||
cutscenebattleforceaction_t battleForceAction;
|
||||
cutscenemodal_t modal;
|
||||
cutscenemodaloptionsmarkers_t modalOptionsMarkers;
|
||||
cutsceneprint_t print;
|
||||
cutscenemarker_t marker;
|
||||
cutscenescene_t sceneChange;
|
||||
cutscenesavedevicecheck_t saveDeviceCheck;
|
||||
cutscenesaveloadallslots_t saveLoadAllSlots;
|
||||
cutscenekeyboard_t keyboard;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "cutsceneitem.h"
|
||||
#include "console/console.h"
|
||||
|
||||
void cutscenePrintStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
consolePrint("%s", item->print.text);
|
||||
}
|
||||
|
||||
bool_t cutscenePrintUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
#define CUTSCENE_PRINT_MAX_CHARS 128
|
||||
|
||||
typedef struct {
|
||||
char_t text[CUTSCENE_PRINT_MAX_CHARS];
|
||||
} cutsceneprint_t;
|
||||
|
||||
/**
|
||||
* Starts a print item (prints the item's text to the console).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutscenePrintStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a print item (always completes immediately).
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns true always.
|
||||
*/
|
||||
bool_t cutscenePrintUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
cutscenesavedevicecheck.c
|
||||
cutscenesaveloadallslots.c
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "save/save.h"
|
||||
|
||||
void cutsceneSaveDeviceCheckCallback(savedevice_t *device, void *user) {
|
||||
const cutsceneitem_t *item = cutsceneSystemGetCurrentItem();
|
||||
cutsceneGoTo(
|
||||
device != NULL ?
|
||||
item->saveDeviceCheck.successMarker :
|
||||
item->saveDeviceCheck.failureMarker
|
||||
);
|
||||
}
|
||||
|
||||
void cutsceneSaveDeviceCheckStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
saveFindAvailableDevice(cutsceneSaveDeviceCheckCallback, NULL);
|
||||
}
|
||||
|
||||
bool_t cutsceneSaveDeviceCheckUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
const char_t *successMarker;
|
||||
const char_t *failureMarker;
|
||||
} cutscenesavedevicecheck_t;
|
||||
|
||||
/**
|
||||
* Starts a save-device-check item: (re)requests an available save
|
||||
* device via saveFindAvailableDevice. Never completes on its own - see
|
||||
* cutsceneSaveDeviceCheckUpdate.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneSaveDeviceCheckStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a save-device-check item. Like a CUTSCENE_MODAL_OPTIONS item,
|
||||
* this never completes on its own - once saveFindAvailableDevice's
|
||||
* callback fires, it jumps straight to the item's successMarker or
|
||||
* failureMarker via cutsceneGoTo.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns false always.
|
||||
*/
|
||||
bool_t cutsceneSaveDeviceCheckUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||
#include "rpg/cutscene/cutscenesystem.h"
|
||||
#include "save/save.h"
|
||||
|
||||
void cutsceneSaveLoadAllSlotsStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
errorret_t result = saveLoadAllSlots();
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
cutsceneGoTo(item->saveLoadAllSlots.failureMarker);
|
||||
return;
|
||||
}
|
||||
|
||||
cutsceneGoTo(item->saveLoadAllSlots.successMarker);
|
||||
}
|
||||
|
||||
bool_t cutsceneSaveLoadAllSlotsUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct cutsceneitem_s cutsceneitem_t;
|
||||
typedef union cutsceneitemdata_u cutsceneitemdata_t;
|
||||
|
||||
typedef struct {
|
||||
const char_t *successMarker;
|
||||
const char_t *failureMarker;
|
||||
} cutscenesaveloadallslots_t;
|
||||
|
||||
/**
|
||||
* Starts a save-load-all-slots item: calls saveLoadAllSlots() and jumps
|
||||
* straight to the item's successMarker or failureMarker via
|
||||
* cutsceneGoTo, depending on the result.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
*/
|
||||
void cutsceneSaveLoadAllSlotsStart(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates a save-load-all-slots item. Never completes on its own -
|
||||
* Start already jumps to whichever marker applies before this would
|
||||
* ever run.
|
||||
*
|
||||
* @param item The cutscene item.
|
||||
* @param data Runtime data storage.
|
||||
* @returns false always.
|
||||
*/
|
||||
bool_t cutsceneSaveLoadAllSlotsUpdate(
|
||||
const cutsceneitem_t *item,
|
||||
cutsceneitemdata_t *data
|
||||
);
|
||||
@@ -11,4 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
cutscenefade.c
|
||||
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
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user