Wire main menu Options/Quit through the cutscene, fix locale crashes

Options and Quit now route through main_menu.jsonc markers the same
way New Game does, with a new QUIT_GAME cutscene item type ending the
process directly once quit is confirmed via MODAL_OPTIONS_MARKERS
(whose "message" field is now optional, defaulting to "" like "title").

Fixes two crashes surfaced along the way:
- uiModalLocalize asserted on an empty locale key instead of treating
  it as "nothing to show" - hit as soon as a modal omitted a title or
  message.
- assetLocaleGetString's new empty-message-ID guard broke the PO
  format's own header-read convention (messageId "") used internally
  by assetLocaleLoaderAsync, crashing on every boot. Extracted the
  shared lookup into assetLocaleGetStringLookup and added a dedicated
  assetLocaleGetHeader for that internal read, leaving
  assetLocaleGetString's validation untouched for every real caller.

Also replaced every raw assetLocaleGetString/WithVA call across the UI
with the locale/localemanager.h helpers (adding localeManagerGetString
for the plain, non-formatting case), so app code no longer bypasses
them.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-06 11:47:34 -05:00
co-authored by Claude Sonnet 5
parent 5b523c68a7
commit f63653c307
21 changed files with 353 additions and 138 deletions
+40
View File
@@ -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"
}
]
}
@@ -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,
@@ -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.
+33 -3
View File
@@ -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, ...) \
@@ -10,6 +10,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
cutsceneconcurrent.c
cutscenemarker.c
cutscenerestart.c
cutscenequitgame.c
cutscenescene.c
cutsceneidle.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;
}
@@ -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.
@@ -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,
@@ -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,
@@ -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"
);
}
+1 -7
View File
@@ -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();
}
+3 -3
View File
@@ -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);
+13 -15
View File
@@ -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);
+2 -6
View File
@@ -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();
+3 -4
View File
@@ -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(
+6 -8
View File
@@ -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();
+9 -12
View File
@@ -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(
+30 -40
View File
@@ -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(
+30 -19
View File
@@ -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(
+6 -3
View File
@@ -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);
+5 -1
View File
@@ -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.