diff --git a/assets/locale/es_MX.po b/assets/locale/es_MX.po index 6d5ef44a..f01e1e41 100644 --- a/assets/locale/es_MX.po +++ b/assets/locale/es_MX.po @@ -27,12 +27,16 @@ msgid "ui.settings.tabs.audio" msgstr "Audio" msgid "ui.settings.input.deadzone" -msgstr "Zona muerta" +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" diff --git a/assets/locale/jp_JP.po b/assets/locale/jp_JP.po index 1b23bcfd..04e1bfb2 100644 --- a/assets/locale/jp_JP.po +++ b/assets/locale/jp_JP.po @@ -33,6 +33,10 @@ msgstr "デッドゾーン" 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 "適用" diff --git a/src/dusk/rpg/CMakeLists.txt b/src/dusk/rpg/CMakeLists.txt index 0652db73..51db8064 100644 --- a/src/dusk/rpg/CMakeLists.txt +++ b/src/dusk/rpg/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} ) # Subdirs +add_subdirectory(battle) add_subdirectory(cutscene) add_subdirectory(entity) add_subdirectory(overworld) diff --git a/src/dusk/rpg/battle/CMakeLists.txt b/src/dusk/rpg/battle/CMakeLists.txt new file mode 100644 index 00000000..5b0a546c --- /dev/null +++ b/src/dusk/rpg/battle/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + battle.c + battlefighter.c + party.c +) diff --git a/src/dusk/rpg/battle/battle.c b/src/dusk/rpg/battle/battle.c new file mode 100644 index 00000000..813cbc19 --- /dev/null +++ b/src/dusk/rpg/battle/battle.c @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battle.h" +#include "util/memory.h" + +battle_t BATTLE; + +void battleInit(void) { + memoryZero(&BATTLE, sizeof(battle_t)); + for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { + BATTLE.fighters[i].id = i; + } +} + +uint8_t battleGetAvailableFighter(void) { + for(uint8_t i = 0; i < BATTLE_FIGHTER_COUNT_MAX; i++) { + if(BATTLE.fighters[i].status == BATTLE_FIGHTER_STATUS_NULL) return i; + } + + return 0xFF; +} + +battlefighter_t *battleAddFighter( + const battlefighterteam_t team, + const battlefightercontroller_t controller, + const battlefighterstats_t stats, + const uint16_t healthMax, + const uint16_t mpMax +) { + const uint8_t index = battleGetAvailableFighter(); + if(index == 0xFF) return NULL; + + battlefighter_t *fighter = &BATTLE.fighters[index]; + battleFighterInit(fighter, team, controller, stats, healthMax, mpMax); + return fighter; +} + +void battleStart(void) { + BATTLE.active = true; +} + +void battleDispose(void) { + battleInit(); +} diff --git a/src/dusk/rpg/battle/battle.h b/src/dusk/rpg/battle/battle.h new file mode 100644 index 00000000..7d52c173 --- /dev/null +++ b/src/dusk/rpg/battle/battle.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "battlefighter.h" + +#define BATTLE_FIGHTER_COUNT_MAX 8 + +typedef struct { + bool_t active; + battlefighter_t fighters[BATTLE_FIGHTER_COUNT_MAX]; +} battle_t; + +extern battle_t BATTLE; + +/** + * Initializes the battle system. Marks it as inactive with no fighters. + */ +void battleInit(void); + +/** + * Gets an available (unused) fighter slot index. + * + * @return The index of an available fighter slot, or 0xFF if none are + * available. + */ +uint8_t battleGetAvailableFighter(void); + +/** + * Adds a fighter to the battle in the next available slot. + * + * @param team The team the fighter belongs to. + * @param controller Who makes decisions for the fighter. + * @param stats The fighter's base combat stats. + * @param healthMax The fighter's maximum health. + * @param mpMax The fighter's maximum mp. + * @return Pointer to the newly added fighter, or NULL if the battle is + * already full. + */ +battlefighter_t *battleAddFighter( + const battlefighterteam_t team, + const battlefightercontroller_t controller, + const battlefighterstats_t stats, + const uint16_t healthMax, + const uint16_t mpMax +); + +/** + * Starts a battle, marking it active. Any fighters already added via + * battleAddFighter remain in place. + */ +void battleStart(void); + +/** + * Disposes of the battle, clearing all fighters and marking it inactive. + */ +void battleDispose(void); diff --git a/src/dusk/rpg/battle/battlefighter.c b/src/dusk/rpg/battle/battlefighter.c new file mode 100644 index 00000000..8d72d552 --- /dev/null +++ b/src/dusk/rpg/battle/battlefighter.c @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "battlefighter.h" +#include "assert/assert.h" +#include "util/memory.h" + +void battleFighterInit( + battlefighter_t *fighter, + const battlefighterteam_t team, + const battlefightercontroller_t controller, + const battlefighterstats_t stats, + const uint16_t healthMax, + const uint16_t mpMax +) { + assertNotNull(fighter, "Fighter pointer cannot be NULL"); + assertTrue(team < BATTLE_FIGHTER_TEAM_COUNT, "Invalid fighter team"); + assertTrue( + controller < BATTLE_FIGHTER_CONTROLLER_COUNT, + "Invalid fighter controller" + ); + + const uint8_t id = fighter->id; + memoryZero(fighter, sizeof(battlefighter_t)); + fighter->id = id; + fighter->status = BATTLE_FIGHTER_STATUS_NORMAL; + fighter->team = team; + fighter->controller = controller; + fighter->stats = stats; + fighter->healthMax = healthMax; + fighter->health = healthMax; + fighter->mpMax = mpMax; + fighter->mp = mpMax; +} + +bool_t battleFighterIsAlive(const battlefighter_t *fighter) { + assertNotNull(fighter, "Fighter pointer cannot be NULL"); + return fighter->status == BATTLE_FIGHTER_STATUS_NORMAL; +} diff --git a/src/dusk/rpg/battle/battlefighter.h b/src/dusk/rpg/battle/battlefighter.h new file mode 100644 index 00000000..af032331 --- /dev/null +++ b/src/dusk/rpg/battle/battlefighter.h @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +// An empty status means the slot in BATTLE.fighters is unused. +typedef enum { + BATTLE_FIGHTER_STATUS_NULL, + + BATTLE_FIGHTER_STATUS_NORMAL, + BATTLE_FIGHTER_STATUS_DEAD, + + BATTLE_FIGHTER_STATUS_COUNT +} battlefighterstatus_t; + +typedef enum { + BATTLE_FIGHTER_TEAM_ALLY, + BATTLE_FIGHTER_TEAM_ENEMY, + + BATTLE_FIGHTER_TEAM_COUNT +} battlefighterteam_t; + +typedef enum { + BATTLE_FIGHTER_CONTROLLER_PLAYER, + BATTLE_FIGHTER_CONTROLLER_AI, + + BATTLE_FIGHTER_CONTROLLER_COUNT +} battlefightercontroller_t; + +// Base combat stats, kept separate from the resource pools (health/mp) on +// battlefighter_t so that equipment/buffs can later modify them without +// touching current health/mp state. +typedef struct { + uint16_t attack; + uint16_t defense; + uint16_t magic; + uint16_t speed; + uint16_t luck; +} battlefighterstats_t; + +typedef struct { + uint8_t id; + battlefighterstatus_t status; + battlefighterteam_t team; + battlefightercontroller_t controller; + + uint16_t health; + uint16_t healthMax; + uint16_t mp; + uint16_t mpMax; + + battlefighterstats_t stats; +} battlefighter_t; + +/** + * Initializes a battle fighter in place, filling health/mp to their maximum + * values and setting its status to normal. + * + * @param fighter Pointer to the fighter to initialize. + * @param team The team the fighter belongs to. + * @param controller Who makes decisions for the fighter. + * @param stats The fighter's base combat stats. + * @param healthMax The fighter's maximum health. + * @param mpMax The fighter's maximum mp. + */ +void battleFighterInit( + battlefighter_t *fighter, + const battlefighterteam_t team, + const battlefightercontroller_t controller, + const battlefighterstats_t stats, + const uint16_t healthMax, + const uint16_t mpMax +); + +/** + * Returns true if the fighter is in a state where it can still act (i.e. + * is not dead). + * + * @param fighter Pointer to the fighter to check. + * @returns True if the fighter can act. + */ +bool_t battleFighterIsAlive(const battlefighter_t *fighter); diff --git a/src/dusk/rpg/battle/party.c b/src/dusk/rpg/battle/party.c new file mode 100644 index 00000000..0350949d --- /dev/null +++ b/src/dusk/rpg/battle/party.c @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "party.h" +#include "assert/assert.h" +#include "util/memory.h" + +party_t PARTY; + +void partyInit(void) { + memoryZero(&PARTY, sizeof(party_t)); + for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) { + PARTY.members[i].id = i; + } + for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { + PARTY.order[i] = PARTY_ORDER_EMPTY; + } +} + +uint8_t partyGetAvailableMember(void) { + for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) { + if(PARTY.members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i; + } + + return 0xFF; +} + +battlefighter_t *partyAddMember( + const battlefighterstats_t stats, + const uint16_t healthMax, + const uint16_t mpMax +) { + const uint8_t index = partyGetAvailableMember(); + if(index == 0xFF) return NULL; + + battlefighter_t *member = &PARTY.members[index]; + battleFighterInit( + member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, + stats, healthMax, mpMax + ); + + for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) { + if(PARTY.order[i] != PARTY_ORDER_EMPTY) continue; + PARTY.order[i] = index; + break; + } + + return member; +} + +battlefighter_t *partyGetOrderMember(const uint8_t slot) { + assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot"); + + const uint8_t index = PARTY.order[slot]; + if(index == PARTY_ORDER_EMPTY) return NULL; + return &PARTY.members[index]; +} + +void partySetOrder(const uint8_t slot, const uint8_t memberIndex) { + assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot"); + assertTrue( + memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX, + "Invalid party member index" + ); + + PARTY.order[slot] = memberIndex; +} diff --git a/src/dusk/rpg/battle/party.h b/src/dusk/rpg/battle/party.h new file mode 100644 index 00000000..5f958169 --- /dev/null +++ b/src/dusk/rpg/battle/party.h @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "battlefighter.h" + +#define PARTY_MEMBER_COUNT_MAX 4 +#define PARTY_ACTIVE_SIZE_MAX 3 +#define PARTY_ORDER_EMPTY 0xFF + +typedef struct { + battlefighter_t members[PARTY_MEMBER_COUNT_MAX]; + + // Maps an active battle slot to the roster member filling it, or + // PARTY_ORDER_EMPTY if the slot is unfilled. Only the first + // PARTY_ACTIVE_SIZE_MAX of the PARTY_MEMBER_COUNT_MAX roster members + // can be in the active lineup at once. + uint8_t order[PARTY_ACTIVE_SIZE_MAX]; +} party_t; + +extern party_t PARTY; + +/** + * Initializes the party system with an empty roster and order. + */ +void partyInit(void); + +/** + * Gets an available (unused) party member slot index. + * + * @return The index of an available slot, or 0xFF if the party is full. + */ +uint8_t partyGetAvailableMember(void); + +/** + * Adds a member to the party roster in the next available slot. Party + * members are always allies controlled by the player. If there is a + * free active order slot, the new member is placed into it. + * + * @param stats The member's base combat stats. + * @param healthMax The member's maximum health. + * @param mpMax The member's maximum mp. + * @return Pointer to the newly added party member, or NULL if the party is + * already full. + */ +battlefighter_t *partyAddMember( + const battlefighterstats_t stats, + const uint16_t healthMax, + const uint16_t mpMax +); + +/** + * Gets the roster member currently occupying an active order slot. + * + * @param slot The active order slot to query. + * @return Pointer to the member in that slot, or NULL if the slot is + * empty. + */ +battlefighter_t *partyGetOrderMember(const uint8_t slot); + +/** + * Assigns a roster member to an active order slot, replacing whatever + * was there. Use PARTY_ORDER_EMPTY to clear a slot. + * + * @param slot The active order slot to assign. + * @param memberIndex The roster member index to place there, or + * PARTY_ORDER_EMPTY to clear the slot. + */ +void partySetOrder(const uint8_t slot, const uint8_t memberIndex); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index 351b4a6b..b6b8f99d 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -14,6 +14,7 @@ #include "rpg/cutscene/cutscenesystem.h" #include "rpg/cutscene/scene/testcutscene.h" #include "rpg/item/backpack.h" +#include "rpg/battle/party.h" #include "time/time.h" #include "rpgcamera.h" #include "util/memory.h" @@ -30,6 +31,7 @@ errorret_t rpgInit(void) { memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); backpackInit(); + partyInit(); cutsceneSystemInit(); errorChain(mapInit()); diff --git a/src/dusk/ui/frame/settings/uisettings.c b/src/dusk/ui/frame/settings/uisettings.c index 73d65b6d..23b496e5 100644 --- a/src/dusk/ui/frame/settings/uisettings.c +++ b/src/dusk/ui/frame/settings/uisettings.c @@ -26,7 +26,7 @@ uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = { { .localeId = "ui.settings.tabs.general", .init = uiSettingsGeneralInit, - .menu = &UI_SETTINGS_GENERAL.menu, + .menu = (uimenu_t *)&UI_SETTINGS.data, .load = uiSettingsGeneralLoad, .apply = uiSettingsGeneralApply, .hasChanges = uiSettingsGeneralHasChanges @@ -35,7 +35,7 @@ uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = { { .localeId = "ui.settings.tabs.input", .init = uiSettingsInputInit, - .menu = &UI_SETTINGS_INPUT.menu, + .menu = (uimenu_t *)&UI_SETTINGS.data, .load = uiSettingsInputLoad, .apply = uiSettingsInputApply, .hasChanges = uiSettingsInputHasChanges @@ -44,7 +44,7 @@ uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = { { .localeId = "ui.settings.tabs.display", .init = uiSettingsDisplayInit, - .menu = &UI_SETTINGS_DISPLAY.menu, + .menu = (uimenu_t *)&UI_SETTINGS.data, .load = uiSettingsDisplayLoad, .apply = uiSettingsDisplayApply, .hasChanges = uiSettingsDisplayHasChanges @@ -53,7 +53,7 @@ uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = { { .localeId = "ui.settings.tabs.audio", .init = uiSettingsAudioInit, - .menu = &UI_SETTINGS_AUDIO.menu, + .menu = (uimenu_t *)&UI_SETTINGS.data, .load = uiSettingsAudioLoad, .apply = uiSettingsAudioApply, .hasChanges = uiSettingsAudioHasChanges @@ -67,6 +67,27 @@ static uisettingstabdef_t UI_SETTINGS_TOP_DEF = { }; static uisettingstabdef_t *UI_SETTINGS_PENDING_DISCARD = NULL; +static uisettingstabdef_t *UI_SETTINGS_ACTIVE_TAB = NULL; + +errorret_t uiSettingsActivateTab(uisettingstabdef_t *def) { + if(def == UI_SETTINGS_ACTIVE_TAB) errorOk(); + + errorChain(def->init(&UI_SETTINGS.data)); + def->menu->user = def; + def->load(); + UI_SETTINGS_ACTIVE_TAB = def; + + errorOk(); +} + +void uiSettingsTabChanged( + const uimenu_t *menu, + const uint8_t index, + const uimenuitem_t *item +) { + if(index >= UI_SETTINGS_TAB_COUNT) return; + errorCatch(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[index])); +} void uiSettingsTabSelected( const uimenu_t *menu, @@ -74,11 +95,15 @@ void uiSettingsTabSelected( const uimenuitem_t *item ) { if(index >= UI_SETTINGS_TAB_COUNT) return; - uiMenuOpen(UI_SETTINGS_TAB_DEFS[index].menu); + + uisettingstabdef_t *def = &UI_SETTINGS_TAB_DEFS[index]; + errorCatch(uiSettingsActivateTab(def)); + uiMenuOpen(def->menu); } errorret_t uiSettingsInit(void) { memoryZero(&UI_SETTINGS, sizeof(uisettings_t)); + UI_SETTINGS_ACTIVE_TAB = NULL; for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { errorChain(assetLocaleGetString( @@ -92,7 +117,7 @@ errorret_t uiSettingsInit(void) { MENU_BEGIN( &UI_SETTINGS.tabsMenu, UI_SETTINGS.tabs, uiSettingsTabSelected, - uiSettingsDiscardMenuClosed, NULL + uiSettingsDiscardMenuClosed, uiSettingsTabChanged ); for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { MENU_TAB(UI_SETTINGS.tabLabels[i]); @@ -109,10 +134,7 @@ errorret_t uiSettingsInit(void) { UI_SETTINGS_APPLY_LABEL_MAX )); - for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { - errorChain(UI_SETTINGS_TAB_DEFS[i].init()); - UI_SETTINGS_TAB_DEFS[i].menu->user = &UI_SETTINGS_TAB_DEFS[i]; - } + errorChain(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[0])); errorOk(); } @@ -146,12 +168,10 @@ errorret_t uiSettingsDraw(void) { const float_t pageY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y; const float_t pageHeight = contentHeight - tabsRowHeight - UI_FRAME_PADDING_Y; - for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { - if(!uiTabIsActive(&UI_SETTINGS.tabs[i].tab)) continue; + if(UI_SETTINGS_ACTIVE_TAB != NULL) { errorChain(uiMenuDraw( - UI_SETTINGS_TAB_DEFS[i].menu, contentX, pageY, contentWidth, pageHeight + UI_SETTINGS_ACTIVE_TAB->menu, contentX, pageY, contentWidth, pageHeight )); - break; } errorChain(spriteBatchFlush()); @@ -185,22 +205,15 @@ errorret_t uiSettingsDispose(void) { } void uiSettingsLoad(void) { - for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { - UI_SETTINGS_TAB_DEFS[i].load(); - } + if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->load(); } void uiSettingsApply(void) { - for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { - UI_SETTINGS_TAB_DEFS[i].apply(); - } + if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->apply(); } bool_t uiSettingsHasChanges(void) { - for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) { - if(UI_SETTINGS_TAB_DEFS[i].hasChanges()) return true; - } - return false; + return UI_SETTINGS_ACTIVE_TAB != NULL && UI_SETTINGS_ACTIVE_TAB->hasChanges(); } void uiSettingsDiscardMenuClosed(const uimenu_t *menu) { diff --git a/src/dusk/ui/frame/settings/uisettings.h b/src/dusk/ui/frame/settings/uisettings.h index 244e25ac..6b41f568 100644 --- a/src/dusk/ui/frame/settings/uisettings.h +++ b/src/dusk/ui/frame/settings/uisettings.h @@ -8,27 +8,29 @@ #pragma once #include "error/error.h" #include "ui/widget/uimenu.h" +#include "uisettingsdata.h" #define UI_SETTINGS_TAB_COUNT 4 #define UI_SETTINGS_TAB_LABEL_MAX 32 #define UI_SETTINGS_APPLY_LABEL_MAX 32 -typedef struct { - uimenu_t tabsMenu; - uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT]; - char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX]; - char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX]; -} uisettings_t; - typedef struct { const char_t *localeId; - errorret_t (*init)(void); + errorret_t (*init)(uisettingsdata_t *data); uimenu_t *menu; void (*load)(void); void (*apply)(void); bool_t (*hasChanges)(void); } uisettingstabdef_t; +typedef struct { + uimenu_t tabsMenu; + uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT]; + char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX]; + char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX]; + uisettingsdata_t data; +} uisettings_t; + extern uisettings_t UI_SETTINGS; extern uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT]; diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.c b/src/dusk/ui/frame/settings/uisettingsaudio.c index ec524ddf..0d968cc1 100644 --- a/src/dusk/ui/frame/settings/uisettingsaudio.c +++ b/src/dusk/ui/frame/settings/uisettingsaudio.c @@ -6,11 +6,10 @@ */ #include "uisettingsaudio.h" +#include "uisettings.h" #include "assert/assert.h" #include "util/memory.h" -uisettingsaudio_t UI_SETTINGS_AUDIO; - void uiSettingsAudioSelected( const uimenu_t *menu, const uint8_t index, @@ -20,17 +19,18 @@ void uiSettingsAudioSelected( uiSettingsAudioApply(); } -errorret_t uiSettingsAudioInit(void) { - memoryZero(&UI_SETTINGS_AUDIO, sizeof(uisettingsaudio_t)); +errorret_t uiSettingsAudioInit(uisettingsdata_t *data) { + uisettingsaudio_t *audio = &data->audio; + memoryZero(audio, sizeof(uisettingsaudio_t)); MENU_BEGIN( - &UI_SETTINGS_AUDIO.menu, UI_SETTINGS_AUDIO.items, + &audio->menu, audio->items, uiSettingsAudioSelected, uiSettingsDiscardMenuClosed, NULL ); MENU_LABEL("No audio settings yet"); MENU_BUTTON(UI_SETTINGS.applyLabel); - MENU_END(UI_SETTINGS_AUDIO.items, 1); + MENU_END(audio->items, 1); errorOk(); } @@ -39,7 +39,7 @@ void uiSettingsAudioLoad(void) { } void uiSettingsAudioApply(void) { - uiMenuClose(&UI_SETTINGS_AUDIO.menu); + uiMenuClose(&UI_SETTINGS.data.audio.menu); } bool_t uiSettingsAudioHasChanges(void) { diff --git a/src/dusk/ui/frame/settings/uisettingsaudio.h b/src/dusk/ui/frame/settings/uisettingsaudio.h index 8d504f38..04f8a605 100644 --- a/src/dusk/ui/frame/settings/uisettingsaudio.h +++ b/src/dusk/ui/frame/settings/uisettingsaudio.h @@ -7,25 +7,16 @@ #pragma once #include "error/error.h" -#include "ui/widget/uimenu.h" -#include "uisettings.h" - -#define UI_SETTINGS_AUDIO_ITEM_COUNT 2 -#define UI_SETTINGS_AUDIO_INDEX_APPLY 1 - -typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT]; -} uisettingsaudio_t; - -extern uisettingsaudio_t UI_SETTINGS_AUDIO; +#include "uisettingsdata.h" /** - * Initializes the audio settings page. + * Initializes the audio settings page into the shared settings data + * union. * + * @param data The shared settings data union to initialize into. * @return Any error that occurs. */ -errorret_t uiSettingsAudioInit(void); +errorret_t uiSettingsAudioInit(uisettingsdata_t *data); /** * Loads the page's widgets from the current engine audio settings. diff --git a/src/dusk/ui/frame/settings/uisettingsdata.h b/src/dusk/ui/frame/settings/uisettingsdata.h new file mode 100644 index 00000000..511b0f04 --- /dev/null +++ b/src/dusk/ui/frame/settings/uisettingsdata.h @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "ui/widget/uimenu.h" + +#define UI_SETTINGS_GENERAL_ITEM_COUNT 3 +#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0 +#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1 +#define UI_SETTINGS_GENERAL_INDEX_APPLY 2 +#define UI_SETTINGS_GENERAL_LOCALE_COUNT 3 +#define UI_SETTINGS_GENERAL_LABEL_MAX 32 +#define UI_SETTINGS_GENERAL_INFO_MAX 64 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT]; + const char_t *localeNames[UI_SETTINGS_GENERAL_LOCALE_COUNT]; + char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX]; + char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX]; +} uisettingsgeneral_t; + +#define UI_SETTINGS_INPUT_ITEM_COUNT 2 +#define UI_SETTINGS_INPUT_LABEL_MAX 32 +#define UI_SETTINGS_INPUT_INDEX_DEADZONE 0 +#define UI_SETTINGS_INPUT_INDEX_APPLY 1 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT]; + char_t deadzoneLabel[UI_SETTINGS_INPUT_LABEL_MAX]; +} uisettingsinput_t; + +#define UI_SETTINGS_DISPLAY_ITEM_COUNT 2 +#define UI_SETTINGS_DISPLAY_INDEX_APPLY 1 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT]; +} uisettingsdisplay_t; + +#define UI_SETTINGS_AUDIO_ITEM_COUNT 2 +#define UI_SETTINGS_AUDIO_INDEX_APPLY 1 + +typedef struct { + uimenu_t menu; + uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT]; +} uisettingsaudio_t; + +/** + * Shared, overlapping storage for every settings tab's page data. Only + * one member is ever live at a time -- the active tab's Init writes + * into it, overwriting whatever the previously active tab left there. + * This trades simultaneous access to every tab for a memory footprint + * of only the single largest page. + */ +typedef union { + uisettingsgeneral_t general; + uisettingsinput_t input; + uisettingsdisplay_t display; + uisettingsaudio_t audio; +} uisettingsdata_t; diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.c b/src/dusk/ui/frame/settings/uisettingsdisplay.c index d2a018d6..75e60d8b 100644 --- a/src/dusk/ui/frame/settings/uisettingsdisplay.c +++ b/src/dusk/ui/frame/settings/uisettingsdisplay.c @@ -6,11 +6,10 @@ */ #include "uisettingsdisplay.h" +#include "uisettings.h" #include "assert/assert.h" #include "util/memory.h" -uisettingsdisplay_t UI_SETTINGS_DISPLAY; - void uiSettingsDisplaySelected( const uimenu_t *menu, const uint8_t index, @@ -20,17 +19,18 @@ void uiSettingsDisplaySelected( uiSettingsDisplayApply(); } -errorret_t uiSettingsDisplayInit(void) { - memoryZero(&UI_SETTINGS_DISPLAY, sizeof(uisettingsdisplay_t)); +errorret_t uiSettingsDisplayInit(uisettingsdata_t *data) { + uisettingsdisplay_t *display = &data->display; + memoryZero(display, sizeof(uisettingsdisplay_t)); MENU_BEGIN( - &UI_SETTINGS_DISPLAY.menu, UI_SETTINGS_DISPLAY.items, + &display->menu, display->items, uiSettingsDisplaySelected, uiSettingsDiscardMenuClosed, NULL ); MENU_LABEL("No display settings yet"); MENU_BUTTON(UI_SETTINGS.applyLabel); - MENU_END(UI_SETTINGS_DISPLAY.items, 1); + MENU_END(display->items, 1); errorOk(); } @@ -39,7 +39,7 @@ void uiSettingsDisplayLoad(void) { } void uiSettingsDisplayApply(void) { - uiMenuClose(&UI_SETTINGS_DISPLAY.menu); + uiMenuClose(&UI_SETTINGS.data.display.menu); } bool_t uiSettingsDisplayHasChanges(void) { diff --git a/src/dusk/ui/frame/settings/uisettingsdisplay.h b/src/dusk/ui/frame/settings/uisettingsdisplay.h index 80654328..5811db0f 100644 --- a/src/dusk/ui/frame/settings/uisettingsdisplay.h +++ b/src/dusk/ui/frame/settings/uisettingsdisplay.h @@ -7,25 +7,16 @@ #pragma once #include "error/error.h" -#include "ui/widget/uimenu.h" -#include "uisettings.h" - -#define UI_SETTINGS_DISPLAY_ITEM_COUNT 2 -#define UI_SETTINGS_DISPLAY_INDEX_APPLY 1 - -typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT]; -} uisettingsdisplay_t; - -extern uisettingsdisplay_t UI_SETTINGS_DISPLAY; +#include "uisettingsdata.h" /** - * Initializes the display settings page. + * Initializes the display settings page into the shared settings data + * union. * + * @param data The shared settings data union to initialize into. * @return Any error that occurs. */ -errorret_t uiSettingsDisplayInit(void); +errorret_t uiSettingsDisplayInit(uisettingsdata_t *data); /** * Loads the page's widgets from the current engine display settings. diff --git a/src/dusk/ui/frame/settings/uisettingsgeneral.c b/src/dusk/ui/frame/settings/uisettingsgeneral.c index d0e989e9..d4c44594 100644 --- a/src/dusk/ui/frame/settings/uisettingsgeneral.c +++ b/src/dusk/ui/frame/settings/uisettingsgeneral.c @@ -6,6 +6,7 @@ */ #include "uisettingsgeneral.h" +#include "uisettings.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" @@ -21,8 +22,6 @@ static const localeinfo_t *const UI_SETTINGS_GENERAL_LOCALES[ &LOCALE_ES_MX }; -uisettingsgeneral_t UI_SETTINGS_GENERAL; - void uiSettingsGeneralSelected( const uimenu_t *menu, const uint8_t index, @@ -38,46 +37,49 @@ void uiSettingsGeneralSelected( } } -errorret_t uiSettingsGeneralInit(void) { - memoryZero(&UI_SETTINGS_GENERAL, sizeof(uisettingsgeneral_t)); +errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) { + uisettingsgeneral_t *general = &data->general; + memoryZero(general, sizeof(uisettingsgeneral_t)); for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) { - UI_SETTINGS_GENERAL.localeNames[i] = UI_SETTINGS_GENERAL_LOCALES[i]->name; + general->localeNames[i] = UI_SETTINGS_GENERAL_LOCALES[i]->name; } errorChain(assetLocaleGetString( &LOCALE.entry->data.locale, "ui.settings.general.language", 0, - UI_SETTINGS_GENERAL.languageLabel, + general->languageLabel, UI_SETTINGS_GENERAL_LABEL_MAX )); errorChain(assetLocaleGetString( &LOCALE.entry->data.locale, "ui.settings.general.language_detail", 0, - UI_SETTINGS_GENERAL.labelInfo, + general->labelInfo, UI_SETTINGS_GENERAL_INFO_MAX )); MENU_BEGIN( - &UI_SETTINGS_GENERAL.menu, UI_SETTINGS_GENERAL.items, + &general->menu, general->items, uiSettingsGeneralSelected, uiSettingsDiscardMenuClosed, NULL ); MENU_DROPDOWN( - UI_SETTINGS_GENERAL.languageLabel, UI_SETTINGS_GENERAL.localeNames, + general->languageLabel, general->localeNames, UI_SETTINGS_GENERAL_LOCALE_COUNT, 0 ); - MENU_LABEL(UI_SETTINGS_GENERAL.labelInfo); + MENU_LABEL(general->labelInfo); MENU_BUTTON(UI_SETTINGS.applyLabel); - MENU_END(UI_SETTINGS_GENERAL.items, 1); + MENU_END(general->items, 1); errorOk(); } void uiSettingsGeneralLoad(void) { + uisettingsgeneral_t *general = &UI_SETTINGS.data.general; + uint8_t index = 0; for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) { if(stringCompare(LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[i]->file) != 0) { @@ -88,23 +90,26 @@ void uiSettingsGeneralLoad(void) { } uiDropdownSetSelectedIndex( - &UI_SETTINGS_GENERAL.items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, - index + &general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, index ); } void uiSettingsGeneralApply(void) { + uisettingsgeneral_t *general = &UI_SETTINGS.data.general; + uint8_t index = uiDropdownGetSelectedIndex( - &UI_SETTINGS_GENERAL.items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown + &general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown ); errorCatch(localeManagerSetLocale(UI_SETTINGS_GENERAL_LOCALES[index])); - uiMenuClose(&UI_SETTINGS_GENERAL.menu); + uiMenuClose(&general->menu); } bool_t uiSettingsGeneralHasChanges(void) { + uisettingsgeneral_t *general = &UI_SETTINGS.data.general; + uint8_t index = uiDropdownGetSelectedIndex( - &UI_SETTINGS_GENERAL.items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown + &general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown ); return stringCompare( LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[index]->file diff --git a/src/dusk/ui/frame/settings/uisettingsgeneral.h b/src/dusk/ui/frame/settings/uisettingsgeneral.h index 16c5dfeb..47dbf8ee 100644 --- a/src/dusk/ui/frame/settings/uisettingsgeneral.h +++ b/src/dusk/ui/frame/settings/uisettingsgeneral.h @@ -7,35 +7,16 @@ #pragma once #include "error/error.h" -#include "ui/widget/uimenu.h" -#include "uisettings.h" - -#define UI_SETTINGS_GENERAL_ITEM_COUNT 3 - -#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0 -#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1 -#define UI_SETTINGS_GENERAL_INDEX_APPLY 2 - -#define UI_SETTINGS_GENERAL_LOCALE_COUNT 3 -#define UI_SETTINGS_GENERAL_LABEL_MAX 32 -#define UI_SETTINGS_GENERAL_INFO_MAX 64 - -typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT]; - const char_t *localeNames[UI_SETTINGS_GENERAL_LOCALE_COUNT]; - char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX]; - char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX]; -} uisettingsgeneral_t; - -extern uisettingsgeneral_t UI_SETTINGS_GENERAL; +#include "uisettingsdata.h" /** - * Initializes the general settings page. + * Initializes the general settings page into the shared settings data + * union. * + * @param data The shared settings data union to initialize into. * @return Any error that occurs. */ -errorret_t uiSettingsGeneralInit(void); +errorret_t uiSettingsGeneralInit(uisettingsdata_t *data); /** * Loads the page's widgets from the current engine general settings. diff --git a/src/dusk/ui/frame/settings/uisettingsinput.c b/src/dusk/ui/frame/settings/uisettingsinput.c index 1a6a1983..287329cd 100644 --- a/src/dusk/ui/frame/settings/uisettingsinput.c +++ b/src/dusk/ui/frame/settings/uisettingsinput.c @@ -6,14 +6,13 @@ */ #include "uisettingsinput.h" +#include "uisettings.h" #include "assert/assert.h" #include "util/memory.h" #include "locale/localemanager.h" #include "asset/loader/locale/assetlocaleloader.h" #include "input/input.h" -uisettingsinput_t UI_SETTINGS_INPUT; - void uiSettingsInputSelected( const uimenu_t *menu, const uint8_t index, @@ -23,11 +22,12 @@ void uiSettingsInputSelected( uiSettingsInputApply(); } -errorret_t uiSettingsInputInit(void) { - memoryZero(&UI_SETTINGS_INPUT, sizeof(uisettingsinput_t)); +errorret_t uiSettingsInputInit(uisettingsdata_t *data) { + uisettingsinput_t *input = &data->input; + memoryZero(input, sizeof(uisettingsinput_t)); MENU_BEGIN( - &UI_SETTINGS_INPUT.menu, UI_SETTINGS_INPUT.items, + &input->menu, input->items, uiSettingsInputSelected, uiSettingsDiscardMenuClosed, NULL ); #ifdef DUSK_INPUT_GAMEPAD @@ -35,18 +35,18 @@ errorret_t uiSettingsInputInit(void) { &LOCALE.entry->data.locale, "ui.settings.input.deadzone", 0, - UI_SETTINGS_INPUT.deadzoneLabel, + input->deadzoneLabel, UI_SETTINGS_INPUT_LABEL_MAX )); MENU_SLIDER_FLOAT( - UI_SETTINGS_INPUT.deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f + input->deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f ); #else MENU_LABEL("No input settings yet"); #endif MENU_BUTTON(UI_SETTINGS.applyLabel); - MENU_END(UI_SETTINGS_INPUT.items, 1); + MENU_END(input->items, 1); errorOk(); } @@ -54,7 +54,7 @@ errorret_t uiSettingsInputInit(void) { void uiSettingsInputLoad(void) { #ifdef DUSK_INPUT_GAMEPAD uiSliderSetFloat( - &UI_SETTINGS_INPUT.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider, + &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider, INPUT.deadzone ); #endif @@ -63,16 +63,16 @@ void uiSettingsInputLoad(void) { void uiSettingsInputApply(void) { #ifdef DUSK_INPUT_GAMEPAD INPUT.deadzone = uiSliderGetFloat( - &UI_SETTINGS_INPUT.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider + &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider ); #endif - uiMenuClose(&UI_SETTINGS_INPUT.menu); + uiMenuClose(&UI_SETTINGS.data.input.menu); } bool_t uiSettingsInputHasChanges(void) { #ifdef DUSK_INPUT_GAMEPAD if(uiSliderGetFloat( - &UI_SETTINGS_INPUT.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider + &UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider ) != INPUT.deadzone) return true; #endif diff --git a/src/dusk/ui/frame/settings/uisettingsinput.h b/src/dusk/ui/frame/settings/uisettingsinput.h index 38f3d07e..3fad214e 100644 --- a/src/dusk/ui/frame/settings/uisettingsinput.h +++ b/src/dusk/ui/frame/settings/uisettingsinput.h @@ -7,28 +7,16 @@ #pragma once #include "error/error.h" -#include "ui/widget/uimenu.h" -#include "uisettings.h" - -#define UI_SETTINGS_INPUT_ITEM_COUNT 2 -#define UI_SETTINGS_INPUT_LABEL_MAX 32 -#define UI_SETTINGS_INPUT_INDEX_DEADZONE 0 -#define UI_SETTINGS_INPUT_INDEX_APPLY 1 - -typedef struct { - uimenu_t menu; - uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT]; - char_t deadzoneLabel[UI_SETTINGS_INPUT_LABEL_MAX]; -} uisettingsinput_t; - -extern uisettingsinput_t UI_SETTINGS_INPUT; +#include "uisettingsdata.h" /** - * Initializes the input settings page. + * Initializes the input settings page into the shared settings data + * union. * + * @param data The shared settings data union to initialize into. * @return Any error that occurs. */ -errorret_t uiSettingsInputInit(void); +errorret_t uiSettingsInputInit(uisettingsdata_t *data); /** * Loads the page's widgets from the current engine input settings.