diff --git a/assets/cutscenes/main_menu.jsonc b/assets/cutscenes/main_menu.jsonc index a46c60db..bc17e12c 100644 --- a/assets/cutscenes/main_menu.jsonc +++ b/assets/cutscenes/main_menu.jsonc @@ -1,9 +1,20 @@ { "items": [ + { + "type": "MARKER", + "name": "MAIN_MENU_IDLE" + }, { "type": "UI_SHOW", "name": "main_menu" }, + + // Options button goes here - no options screen yet, so this is just + // an alias landing right back on the idle loop below. + { + "type": "MARKER", + "name": "OPTIONS" + }, { "type": "IDLE" }, @@ -88,6 +99,35 @@ { "type": "MARKER", "name": "LOADED" + }, + + // Quit Game button goes here. Cancelling jumps back to + // MAIN_MENU_IDLE; confirming falls through to QUIT_GAME, which + // ends the process directly. + { + "type": "MARKER", + "name": "QUIT" + }, + { + "type": "MODAL_OPTIONS_MARKERS", + "title": "main_menu.quit_confirm", + "options": [ + { + "text": "ui.confirm.confirm", + "marker": "QUIT_CONFIRMED" + }, + { + "text": "ui.confirm.cancel", + "marker": "MAIN_MENU_IDLE" + } + ] + }, + { + "type": "MARKER", + "name": "QUIT_CONFIRMED" + }, + { + "type": "QUIT_GAME" } ] } diff --git a/src/dusk/asset/loader/locale/assetlocaleloader.c b/src/dusk/asset/loader/locale/assetlocaleloader.c index ef01bbb1..839e55b8 100644 --- a/src/dusk/asset/loader/locale/assetlocaleloader.c +++ b/src/dusk/asset/loader/locale/assetlocaleloader.c @@ -30,8 +30,8 @@ errorret_t assetLocaleLoaderAsync(assetloading_t *loading) { assetLoaderErrorChain(loading, assetFileOpen(&localeFile->file)); char_t buffer[1024]; - assetLoaderErrorChain(loading, assetLocaleGetString( - localeFile, "", 0, buffer, sizeof(buffer) + assetLoaderErrorChain(loading, assetLocaleGetHeader( + localeFile, buffer, sizeof(buffer) )); assetLoaderErrorChain(loading, assetLocaleParseHeader( localeFile, buffer, sizeof(buffer) @@ -487,6 +487,7 @@ errorret_t assetLocaleCacheFind( assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(messageId, "Message ID cannot be NULL."); assertNotNull(outHit, "outHit cannot be NULL."); + assertStrLenMin(messageId, 1, "Message ID cannot be empty."); *outHit = false; if(file->cache == NULL) errorOk(); @@ -527,6 +528,7 @@ void assetLocaleCacheInsert( assertNotNull(file, "Asset file cannot be NULL."); assertNotNull(messageId, "Message ID cannot be NULL."); assertNotNull(value, "Value cannot be NULL."); + assertStrLenMin(messageId, 1, "Message ID cannot be empty."); // Caching is a pure optimization - a message ID or value too long for a // fixed cache slot is just never cached, not an error. @@ -555,7 +557,13 @@ void assetLocaleCacheInsert( stringCopy(entry->value, value, ASSET_LOCALE_CACHE_VALUE_MAX); } -errorret_t assetLocaleGetString( +// Shared by assetLocaleGetString (external callers, messageId required +// non-empty) and assetLocaleGetHeader (messageId "", the PO format's own +// sentinel for its header entry - see assetLocaleGetHeader). Caching is +// skipped entirely for an empty messageId: assetLocaleCacheFind/Insert +// both reject one outright (same guard as assetLocaleGetString's own), +// and it's a one-shot boot-time read anyway, not worth caching. +errorret_t assetLocaleGetStringLookup( assetlocalefile_t *file, const char_t *messageId, const int32_t pluralCount, @@ -569,10 +577,12 @@ errorret_t assetLocaleGetString( assertTrue(stringBufferSize > 0, "String buffer size must be > 0"); bool_t cacheHit = false; - errorChain(assetLocaleCacheFind( - file, messageId, pluralCount, stringBuffer, stringBufferSize, &cacheHit - )); - if(cacheHit) errorOk(); + if(messageId[0] != '\0') { + errorChain(assetLocaleCacheFind( + file, messageId, pluralCount, stringBuffer, stringBufferSize, &cacheHit + )); + if(cacheHit) errorOk(); + } assetfilelinereader_t reader; @@ -717,11 +727,38 @@ errorret_t assetLocaleGetString( errorThrow("Failed to find msgstr for message ID: %s", messageId); } - assetLocaleCacheInsert(file, messageId, pluralCount, stringBuffer); + if(messageId[0] != '\0') { + assetLocaleCacheInsert(file, messageId, pluralCount, stringBuffer); + } errorOk(); } +errorret_t assetLocaleGetString( + assetlocalefile_t *file, + const char_t *messageId, + const int32_t pluralCount, + char_t *stringBuffer, + const size_t stringBufferSize +) { + assertNotNull(messageId, "Message ID cannot be NULL."); + assertStrLenMin(messageId, 1, "Message ID cannot be empty."); + + return assetLocaleGetStringLookup( + file, messageId, pluralCount, stringBuffer, stringBufferSize + ); +} + +errorret_t assetLocaleGetHeader( + assetlocalefile_t *file, + char_t *stringBuffer, + const size_t stringBufferSize +) { + return assetLocaleGetStringLookup( + file, "", 0, stringBuffer, stringBufferSize + ); +} + errorret_t assetLocaleGetStringWithVA( assetlocalefile_t *file, const char_t *messageId, diff --git a/src/dusk/asset/loader/locale/assetlocaleloader.h b/src/dusk/asset/loader/locale/assetlocaleloader.h index 44d1fcce..fefcc557 100644 --- a/src/dusk/asset/loader/locale/assetlocaleloader.h +++ b/src/dusk/asset/loader/locale/assetlocaleloader.h @@ -299,16 +299,42 @@ void assetLocaleCacheInsert( ); /** - * Looks up a translated string by message ID from the open locale file. + * Shared lookup core behind assetLocaleGetString and assetLocaleGetHeader: + * checks the file's cache first (see @ref assetLocaleCacheFind; skipped + * entirely for an empty messageId, since the cache rejects one), on a + * miss rewinds the file and scans from the beginning, then caches the + * result (see @ref assetLocaleCacheInsert, same empty-messageId skip) + * before returning. For plural entries (`msgid_plural`) the + * `pluralCount` is evaluated against the loaded plural rules to select + * the correct `msgstr[N]` form. * - * Checks the file's cache first (see @ref assetLocaleCacheFind); on a miss, - * rewinds the file and scans from the beginning, then caches the result - * (see @ref assetLocaleCacheInsert) before returning. For plural entries - * (`msgid_plural`) the `pluralCount` is evaluated against the loaded plural - * rules to select the correct `msgstr[N]` form. + * Unlike assetLocaleGetString, messageId may be `""` here (see + * assetLocaleGetHeader) - this is the shared implementation both of + * them delegate to, not a public entry point of its own in practice. * * @param file Locale file to search. Must be open. - * @param messageId PO message ID to find (`""` retrieves the header entry). + * @param messageId PO message ID to find, or `""` for the header entry. + * @param pluralCount Count used to select the plural form (ignored for + * singular entries). + * @param stringBuffer Buffer to receive the translated string. + * @param stringBufferSize Capacity of `stringBuffer` in bytes. + * @return OK on success, error if the message ID is not found or I/O fails. + */ +errorret_t assetLocaleGetStringLookup( + assetlocalefile_t *file, + const char_t *messageId, + const int32_t pluralCount, + char_t *stringBuffer, + const size_t stringBufferSize +); + +/** + * Looks up a translated string by message ID from the open locale file. + * See @ref assetLocaleGetStringLookup for the lookup/caching mechanics. + * + * @param file Locale file to search. Must be open. + * @param messageId PO message ID to find. Must be non-empty - use + * @ref assetLocaleGetHeader to read the header entry. * @param pluralCount Count used to select the plural form (ignored for * singular entries). * @param stringBuffer Buffer to receive the translated string. @@ -323,6 +349,27 @@ errorret_t assetLocaleGetString( const size_t stringBufferSize ); +/** + * Reads the PO file's header entry (the `msgid ""` block every PO file + * starts with, holding metadata like the plural-forms rule) - see @ref + * assetLocaleParseHeader, its caller. A dedicated function rather than + * calling assetLocaleGetString(file, "", ...) since messageId "" is a + * PO-format-internal sentinel, not a real (untrusted) caller-supplied + * message ID, so it doesn't go through assetLocaleGetString's normal + * non-empty validation. + * + * @param file Locale file to search. Must be open. + * @param stringBuffer Buffer to receive the raw header block. + * @param stringBufferSize Capacity of `stringBuffer` in bytes. + * @return OK on success, error if the header entry is missing or I/O + * fails. + */ +errorret_t assetLocaleGetHeader( + assetlocalefile_t *file, + char_t *stringBuffer, + const size_t stringBufferSize +); + /** * Looks up a translated string and formats it with a `printf`-style variadic * argument list. diff --git a/src/dusk/locale/localemanager.h b/src/dusk/locale/localemanager.h index 4f5e9a1a..5951336d 100644 --- a/src/dusk/locale/localemanager.h +++ b/src/dusk/locale/localemanager.h @@ -34,13 +34,43 @@ errorret_t localeManagerInit(); errorret_t localeManagerSetLocale(const localeinfo_t *locale); /** - * Get a localized string for the given message ID. - * + * Get a localized string for the given message ID verbatim, with no + * printf-style formatting applied - use this (rather than + * localeManagerGetText with no variadic args) whenever the raw string + * is displayed as-is, or cached to be formatted later (e.g. a + * "_format" message ID whose %-specifiers are filled in by the caller + * itself via stringFormat once the actual values are known). Passing a + * "_format" string through localeManagerGetText/GetTextArgs instead + * would consume it immediately as a format string with zero args, + * which is wrong for this use, and would also mishandle any literal + * '%' in a plain display string. + * * @param id The message ID to retrieve. * @param buffer Buffer to write the retrieved string to. * @param bufferSize Size of the buffer. * @param plural Plural index to retrieve. - * @param ... Additional arguments for formatting the string. + * @return An error code if a failure occurs. + */ +#define localeManagerGetString(id, buffer, bufferSize, plural) \ + assetLocaleGetString( \ + &LOCALE.entry->data.locale, \ + id, \ + plural, \ + buffer, \ + bufferSize \ + ) + +/** + * Get a localized string for the given message ID, formatted + * printf-style with the given variadic arguments. + * + * @param id The message ID to retrieve. + * @param buffer Buffer to write the retrieved string to. + * @param bufferSize Size of the buffer. + * @param plural Plural index to retrieve. + * @param ... Arguments for formatting the string - must not be empty, + * since the retrieved string is always run through vsnprintf; see + * localeManagerGetString for a message ID with no format args at all. * @return An error code if a failure occurs. */ #define localeManagerGetText(id, buffer, bufferSize, plural, ...) \ diff --git a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt b/src/dusk/rpg/cutscene/item/control/CMakeLists.txt index c422b90a..43f8e9dd 100644 --- a/src/dusk/rpg/cutscene/item/control/CMakeLists.txt +++ b/src/dusk/rpg/cutscene/item/control/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} cutsceneconcurrent.c cutscenemarker.c cutscenerestart.c + cutscenequitgame.c cutscenescene.c cutsceneidle.c ) diff --git a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c new file mode 100644 index 00000000..b07a5e8e --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.c @@ -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 "engine/engine.h" + +void cutsceneQuitGameStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + ENGINE.running = false; +} + +bool_t cutsceneQuitGameUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +) { + return false; +} diff --git a/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h new file mode 100644 index 00000000..9848a84f --- /dev/null +++ b/src/dusk/rpg/cutscene/item/control/cutscenequitgame.h @@ -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 "rpg/cutscene/item/cutsceneitembase.h" + +/** + * Starts a quit-game item (sets ENGINE.running to false, ending the + * process's main loop). + * + * @param item The cutscene item. + * @param data Runtime data storage. + */ +void cutsceneQuitGameStart( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +/** + * Updates a quit-game item. The engine is shutting down by the time this + * would run, so this always reports incomplete (mirrors + * cutsceneRestartUpdate). + * + * @param item The cutscene item. + * @param data Runtime data storage. + * @returns false always. + */ +bool_t cutsceneQuitGameUpdate( + const cutsceneitem_t *item, + cutsceneitemdata_t *data +); + +// QUIT_GAME has no fields of its own to parse - see cutsceneItemLoadNoop +// (cutsceneitembase.h), used directly as its .load callback. diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.c b/src/dusk/rpg/cutscene/item/cutsceneitem.c index 2192e74a..c556e6fa 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.c +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.c @@ -220,6 +220,13 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = { .load = cutsceneItemLoadNoop }, + [CUTSCENE_ITEM_TYPE_QUIT_GAME] = { + .init = cutsceneQuitGameStart, + .update = cutsceneQuitGameUpdate, + .jsonName = "QUIT_GAME", + .load = cutsceneItemLoadNoop + }, + [CUTSCENE_ITEM_TYPE_SCENE] = { .init = cutsceneSceneStart, .update = cutsceneSceneUpdate, diff --git a/src/dusk/rpg/cutscene/item/cutsceneitem.h b/src/dusk/rpg/cutscene/item/cutsceneitem.h index b93c66bd..f73572e0 100644 --- a/src/dusk/rpg/cutscene/item/cutsceneitem.h +++ b/src/dusk/rpg/cutscene/item/cutsceneitem.h @@ -15,6 +15,7 @@ #include "control/cutsceneconcurrent.h" #include "control/cutscenemarker.h" #include "control/cutscenerestart.h" +#include "control/cutscenequitgame.h" #include "control/cutscenescene.h" #include "control/cutsceneidle.h" #include "entity/cutsceneentityteleport.h" @@ -87,6 +88,7 @@ typedef enum { CUTSCENE_ITEM_TYPE_PRINT, CUTSCENE_ITEM_TYPE_MARKER, CUTSCENE_ITEM_TYPE_RESTART, + CUTSCENE_ITEM_TYPE_QUIT_GAME, CUTSCENE_ITEM_TYPE_SCENE, CUTSCENE_ITEM_TYPE_SAVE_DEVICE_CHECK, CUTSCENE_ITEM_TYPE_SAVE_LOAD_ALL_SLOTS, diff --git a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c b/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c index 48e95cab..41c1e76e 100644 --- a/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c +++ b/src/dusk/rpg/cutscene/item/ui/cutscenemodaloptionsmarkers.c @@ -61,11 +61,11 @@ errorret_t cutsceneModalOptionsMarkersLoad( CUTSCENE_MODAL_TITLE_MAX_CHARS ); if(!cutsceneJsonCopyString( - itemObj, "message", NULL, item->modalOptionsMarkers.message, + itemObj, "message", "", item->modalOptionsMarkers.message, CUTSCENE_MODAL_MESSAGE_MAX_CHARS )) { errorThrow( - "Cutscene item (MODAL_OPTIONS_MARKERS): missing/too-long 'message'" + "Cutscene item (MODAL_OPTIONS_MARKERS): 'message' too long" ); } diff --git a/src/dusk/rpg/item/item.c b/src/dusk/rpg/item/item.c index d80a6b67..010b6845 100644 --- a/src/dusk/rpg/item/item.c +++ b/src/dusk/rpg/item/item.c @@ -18,13 +18,7 @@ errorret_t itemGetName( assertTrue(item > ITEM_ID_NULL, "Item ID must not be null"); assertTrue(item < ITEM_ID_COUNT, "Item ID out of range"); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, - ITEMS[item].name, - 0, - buffer, - bufferSize - )); + errorChain(localeManagerGetString(ITEMS[item].name, buffer, bufferSize, 0)); errorOk(); } diff --git a/src/dusk/ui/dialog/save/uisaveslot.c b/src/dusk/ui/dialog/save/uisaveslot.c index 11267e4a..674c87f6 100644 --- a/src/dusk/ui/dialog/save/uisaveslot.c +++ b/src/dusk/ui/dialog/save/uisaveslot.c @@ -29,9 +29,9 @@ void uiSaveSlotInit( &saveSlot->numberLabel, saveSlot->numberText, saveSlot->numberSprites, UI_SAVE_SLOT_NUMBER_SPRITES_MAX ); - errorret_t result = assetLocaleGetStringWithVA( - &LOCALE.entry->data.locale, "ui.save_slot.number_format", 0, - saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX, + errorret_t result = localeManagerGetText( + "ui.save_slot.number_format", + saveSlot->numberText, UI_SAVE_SLOT_NUMBER_TEXT_MAX, 0, slotIndex + 1 ); if(errorIsNotOk(result)) errorCatch(result); diff --git a/src/dusk/ui/dialog/save/uiselectsave.c b/src/dusk/ui/dialog/save/uiselectsave.c index ada89625..fc8bbc9b 100644 --- a/src/dusk/ui/dialog/save/uiselectsave.c +++ b/src/dusk/ui/dialog/save/uiselectsave.c @@ -32,9 +32,8 @@ void uiSelectSaveFormatSlot( saveslotcache_t *cache = &SAVE.caches[index]; if(!saveSlotInUse(cache)) { - errorret_t result = assetLocaleGetString( - &LOCALE.entry->data.locale, "ui.select_save.empty", 0, - buffer, bufferSize + errorret_t result = localeManagerGetString( + "ui.select_save.empty", buffer, bufferSize, 0 ); if(errorIsNotOk(result)) errorCatch(result); return; @@ -43,9 +42,8 @@ void uiSelectSaveFormatSlot( char_t dateText[32]; timeEpochFormat(cache->time, "%Y-%m-%d %H:%M", dateText, sizeof(dateText)); - errorret_t result = assetLocaleGetStringWithVA( - &LOCALE.entry->data.locale, "ui.select_save.slot_format", 0, - buffer, bufferSize, + errorret_t result = localeManagerGetText( + "ui.select_save.slot_format", buffer, bufferSize, 0, cache->name, cache->playerLevel, dateText ); if(errorIsNotOk(result)) errorCatch(result); @@ -130,9 +128,8 @@ void uiSelectSaveNameEmptySlot(const uint8_t index) { UI_SELECT_SAVE.pendingNameIndex = index; char_t title[UI_KEYBOARD_TITLE_TEXT_MAX]; - errorret_t result = assetLocaleGetString( - &LOCALE.entry->data.locale, "ui.select_save.name_title", 0, - title, sizeof(title) + errorret_t result = localeManagerGetString( + "ui.select_save.name_title", title, sizeof(title), 0 ); if(errorIsNotOk(result)) errorCatch(result); @@ -151,9 +148,8 @@ void uiSelectSaveSetActionLabel(const uiselectsavetype_t type) { const char_t *labelKey = type == UI_SELECT_SAVE_TYPE_LOAD ? "ui.select_save.delete_mode" : "ui.confirm.cancel"; - errorret_t result = assetLocaleGetString( - &LOCALE.entry->data.locale, labelKey, 0, - UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX + errorret_t result = localeManagerGetString( + labelKey, UI_SELECT_SAVE.actionText, UI_SELECT_SAVE_ACTION_LABEL_MAX, 0 ); if(errorIsNotOk(result)) errorCatch(result); @@ -235,9 +231,11 @@ errorret_t uiSelectSaveInit(void) { &UI_SELECT_SAVE.titleLabel, UI_SELECT_SAVE.titleText, UI_SELECT_SAVE.titleSprites, UI_SELECT_SAVE_TITLE_SPRITES_MAX ); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, "ui.select_save.title", 0, - UI_SELECT_SAVE.titleText, UI_SELECT_SAVE_TITLE_TEXT_MAX + errorChain(localeManagerGetString( + "ui.select_save.title", + UI_SELECT_SAVE.titleText, + UI_SELECT_SAVE_TITLE_TEXT_MAX, + 0 )); UI_SELECT_SAVE.titleLabel.dirty = true; uiLabelRebuffer(&UI_SELECT_SAVE.titleLabel); diff --git a/src/dusk/ui/overlay/uiloading.c b/src/dusk/ui/overlay/uiloading.c index ad52345a..70927eac 100644 --- a/src/dusk/ui/overlay/uiloading.c +++ b/src/dusk/ui/overlay/uiloading.c @@ -21,12 +21,8 @@ uiloading_t UI_LOADING; errorret_t uiLoadingInit(void) { memoryZero(&UI_LOADING, sizeof(uiloading_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, - "ui.loading.text", - 0, - UI_LOADING.text, - UI_LOADING_TEXT_MAX + errorChain(localeManagerGetString( + "ui.loading.text", UI_LOADING.text, UI_LOADING_TEXT_MAX, 0 )); errorOk(); diff --git a/src/dusk/ui/screen/backpack/uibackpack.c b/src/dusk/ui/screen/backpack/uibackpack.c index 2f054125..cc37e27a 100644 --- a/src/dusk/ui/screen/backpack/uibackpack.c +++ b/src/dusk/ui/screen/backpack/uibackpack.c @@ -43,12 +43,11 @@ void uiBackpackTabSelected( errorret_t uiBackpackInit(void) { memoryZero(&UI_BACKPACK, sizeof(uibackpack_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.backpack.category_format", - 0, UI_BACKPACK.categoryFormat, - UI_BACKPACK_CATEGORY_FORMAT_MAX + UI_BACKPACK_CATEGORY_FORMAT_MAX, + 0 )); MENU_BEGIN( diff --git a/src/dusk/ui/screen/battle/uibattlehud.c b/src/dusk/ui/screen/battle/uibattlehud.c index c196075c..e4c7b54f 100644 --- a/src/dusk/ui/screen/battle/uibattlehud.c +++ b/src/dusk/ui/screen/battle/uibattlehud.c @@ -25,19 +25,17 @@ uibattlehud_t UI_BATTLE_HUD; errorret_t uiBattleHudInit(void) { memoryZero(&UI_BATTLE_HUD, sizeof(uibattlehud_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.battle.hud.hp_format", - 0, UI_BATTLE_HUD.hpFormat, - UI_BATTLE_HUD_FORMAT_MAX + UI_BATTLE_HUD_FORMAT_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.battle.hud.mp_format", - 0, UI_BATTLE_HUD.mpFormat, - UI_BATTLE_HUD_FORMAT_MAX + UI_BATTLE_HUD_FORMAT_MAX, + 0 )); errorOk(); diff --git a/src/dusk/ui/screen/battle/uibattlemenu.c b/src/dusk/ui/screen/battle/uibattlemenu.c index a0265b14..a71f33c1 100644 --- a/src/dusk/ui/screen/battle/uibattlemenu.c +++ b/src/dusk/ui/screen/battle/uibattlemenu.c @@ -83,26 +83,23 @@ void uiBattleMenuActionSelected( errorret_t uiBattleMenuInit(void) { memoryZero(&UI_BATTLE_MENU, sizeof(uibattlemenu_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.battle.menu.attack", - 0, UI_BATTLE_MENU.attackLabel, - UI_BATTLE_MENU_ACTION_LABEL_MAX + UI_BATTLE_MENU_ACTION_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.battle.menu.flee", - 0, UI_BATTLE_MENU.fleeLabel, - UI_BATTLE_MENU_ACTION_LABEL_MAX + UI_BATTLE_MENU_ACTION_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.battle.menu.target_format", - 0, UI_BATTLE_MENU.targetFormat, - UI_BATTLE_MENU_TARGET_FORMAT_MAX + UI_BATTLE_MENU_TARGET_FORMAT_MAX, + 0 )); MENU_BEGIN( diff --git a/src/dusk/ui/screen/game/uigamemenu.c b/src/dusk/ui/screen/game/uigamemenu.c index a0284375..dbe26561 100644 --- a/src/dusk/ui/screen/game/uigamemenu.c +++ b/src/dusk/ui/screen/game/uigamemenu.c @@ -41,75 +41,65 @@ void uiGameMenuSelected( errorret_t uiGameMenuInit(void) { memoryZero(&UI_GAME_MENU, sizeof(uigamemenu_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.characters", - 0, UI_GAME_MENU.charactersLabel, - UI_GAME_MENU_LABEL_MAX + UI_GAME_MENU_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.items", - 0, UI_GAME_MENU.itemsLabel, - UI_GAME_MENU_LABEL_MAX + UI_GAME_MENU_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.settings", - 0, UI_GAME_MENU.settingsLabel, - UI_GAME_MENU_LABEL_MAX + UI_GAME_MENU_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_success", - 0, UI_GAME_MENU.saveSuccessText, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_cancelled", - 0, UI_GAME_MENU.saveCancelledText, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_unavailable", - 0, UI_GAME_MENU.saveUnavailableText, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_temporary", - 0, UI_GAME_MENU.saveTemporaryText, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_create_confirm", - 0, UI_GAME_MENU.saveCreateConfirmText, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_failed_format", - 0, UI_GAME_MENU.saveFailedFormat, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "ui.game_menu.save_check_failed_format", - 0, UI_GAME_MENU.saveCheckFailedFormat, - UI_GAME_MENU_MESSAGE_MAX + UI_GAME_MENU_MESSAGE_MAX, + 0 )); MENU_BEGIN( diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c index 29e06cce..7b75ccb7 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.c @@ -7,7 +7,6 @@ #include "uimainmenu.h" #include "ui/widget/uiframe.h" -#include "engine/engine.h" #include "rpg/battle/testbattle/testbattle.h" #include "util/memory.h" #include "display/spritebatch/spritebatch.h" @@ -15,7 +14,6 @@ #include "assert/assert.h" #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" -#include "ui/dialog/uiconfirm.h" #include "ui/dialog/save/uiselectsave.h" #include "rpg/cutscene/cutscenesystem.h" #include "scene/scene.h" @@ -28,10 +26,6 @@ uimainmenu_t UI_MAIN_MENU; assetentry_t *MAIN_MENU_CUTSCENE_ENTRY = NULL; -void uiMainMenuQuitConfirmed(const bool_t result, void *user) { - if(result) ENGINE.running = false; -} - cutscene_t *uiMainMenuLoadCutscene(void) { if(MAIN_MENU_CUTSCENE_ENTRY == NULL) { MAIN_MENU_CUTSCENE_ENTRY = assetLock( @@ -75,6 +69,24 @@ void uiMainMenuStartGame(void) { cutsceneSystemSetOnComplete(uiMainMenuOpenSelectSave); } +void uiMainMenuOptions(void) { + if(CUTSCENE_SYSTEM.scene == NULL) { + cutsceneSystemStartCutsceneAndGoToMarker( + uiMainMenuLoadCutscene(), "OPTIONS" + ); + } else { + cutsceneGoTo("OPTIONS"); + } +} + +void uiMainMenuQuit(void) { + if(CUTSCENE_SYSTEM.scene == NULL) { + cutsceneSystemStartCutsceneAndGoToMarker(uiMainMenuLoadCutscene(), "QUIT"); + } else { + cutsceneGoTo("QUIT"); + } +} + void uiMainMenuSelected( const uimenu_t *menu, const uint8_t index, @@ -88,10 +100,12 @@ void uiMainMenuSelected( break; case UI_MAIN_MENU_INDEX_OPTIONS: + uiMainMenuOptions(); break; case UI_MAIN_MENU_INDEX_QUIT: - uiConfirmOpen("main_menu.quit_confirm", uiMainMenuQuitConfirmed, NULL); + uiMenuClose(&UI_MAIN_MENU.menu); + uiMainMenuQuit(); break; default: @@ -102,26 +116,23 @@ void uiMainMenuSelected( errorret_t uiMainMenuInit(void) { memoryZero(&UI_MAIN_MENU, sizeof(uimainmenu_t)); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "main_menu.start_game", - 0, UI_MAIN_MENU.startGameLabel, - UI_MAIN_MENU_LABEL_MAX + UI_MAIN_MENU_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "main_menu.options", - 0, UI_MAIN_MENU.optionsLabel, - UI_MAIN_MENU_LABEL_MAX + UI_MAIN_MENU_LABEL_MAX, + 0 )); - errorChain(assetLocaleGetString( - &LOCALE.entry->data.locale, + errorChain(localeManagerGetString( "main_menu.quit", - 0, UI_MAIN_MENU.quitLabel, - UI_MAIN_MENU_LABEL_MAX + UI_MAIN_MENU_LABEL_MAX, + 0 )); MENU_BEGIN( diff --git a/src/dusk/ui/widget/uimodal.c b/src/dusk/ui/widget/uimodal.c index e0a80428..37e0eac6 100644 --- a/src/dusk/ui/widget/uimodal.c +++ b/src/dusk/ui/widget/uimodal.c @@ -131,9 +131,12 @@ void uiModalLocalize( char_t *buffer, const size_t bufferSize ) { - errorret_t result = assetLocaleGetString( - &LOCALE.entry->data.locale, key, 0, buffer, bufferSize - ); + if(key[0] == '\0') { + buffer[0] = '\0'; + return; + } + + errorret_t result = localeManagerGetString(key, buffer, bufferSize, 0); if(errorIsNotOk(result)) { errorCatch(result); stringCopy(buffer, key, bufferSize - 1); diff --git a/src/dusk/ui/widget/uimodal.h b/src/dusk/ui/widget/uimodal.h index 66ebe222..2a10b13f 100644 --- a/src/dusk/ui/widget/uimodal.h +++ b/src/dusk/ui/widget/uimodal.h @@ -159,7 +159,11 @@ errorret_t uiModalDispose(void); /** * Resolves display text for uiModalOpen: if key matches a locale * message ID in the active locale file, buffer is filled with its - * translated string, otherwise key is copied into buffer verbatim. + * translated string, otherwise key is copied into buffer verbatim. An + * empty key (e.g. an omitted JSON title/message, which defaults to "" + * rather than NULL - see cutscenemodal_t/cutscenemodaloptionsmarkers_t) + * is left as an empty buffer rather than looked up, since + * assetLocaleGetString rejects an empty message ID outright. * * @param key Display string or locale message ID. * @param buffer Destination buffer.