diff --git a/docs/modding.md b/docs/modding.md index 6fadaf55f1..b4508d46d4 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -636,83 +636,78 @@ first in-game frame. Projection matrices match the renderer's WebGPU clip conven Camera operators allow overriding the main camera. When an operator callback returns true, its values replace the camera state for the current frame. Register and unregister using `register_camera_operator` / `unregister_camera_operator`. -### GameModeService (`mods/svc/gamemode.h`) +### GameModeService (`mods/svc/game_mode.h`) Allows a mod to register a game mode with callbacks for key gameplay and save lifecycle events. Registered game modes -appear in the prelaunch menu. Game modes may use a unique set of saves by configuring `saveName`; leave it empty to use +appear in the prelaunch menu. Game modes may use a unique set of saves by configuring `save_name`; leave it empty to use the vanilla `gczelda2` save. ```cpp -// An example that shows registering a gamemode with function hooks that are scoped to the gamemode being active IMPORT_SERVICE(LogService, svc_log); IMPORT_SERVICE(HookService, svc_hook); -IMPORT_SERVICE(GameModeService, svc_gamemode); +IMPORT_SERVICE(GameModeService, svc_game_mode); DEFINE_HOOK(fopAcM_createItem, CreateItem); #define MY_GAME_MODE_ID "game-mode-id" -static HookAction myFunctionHook(ModContext *ctx, void *args, void *, void *) { +static HookAction my_function_hook(ModContext* ctx, void* args, void*, void*) { // If we wish to have this hook only run while the gamemode is registered, we need to hook the function from the // gamemode's onActivatedFunction, and uninstall the hook during the onDeactivatedFunction. Example below. - // Alternatively, check with `svc_gamemode->is_active(mod_ctx, MY_GAME_MODE_ID, &active) == MOD_OK && active`. - + // Alternatively, check with `svc_game_mode->is_active(mod_ctx, MY_GAME_MODE_ID, &active) == MOD_OK && active`. return HOOK_CONTINUE; } -void onGameModeActivated() { +ModResult on_game_mode_activated(void*, ModError* outError) { // Setup the gamemode, Add any hooks that are gamemode specific // Overlay any files that are gamemode specific - ModResult result = mods::hook_add_pre(svc_hook, myFunctionHook); + ModResult result = mods::hook_add_pre(svc_hook, my_function_hook); if (result != MOD_OK) { - svc_log->error(mod_ctx, "failed to install hook to fopAcM_createItem"); - } + return mods::set_error(outError, result, "failed to install fopAcM_createItem hook"); + } + return MOD_OK; } -void onGameModeDeactivated() { +ModResult on_game_mode_deactivated(void*, ModError* outError) { // Uninstall any hooks that are gamemode specific // Remove any file overlays that are gamemode specific ModResult result = mods::hook_uninstall(); if (result != MOD_OK) { - svc_log->error(mod_ctx, "failed to uninstall CreateItem hook"); - } + return mods::set_error(outError, result, "failed to uninstall fopAcM_createItem hook"); + } + return MOD_OK; } -void onSaveLoaded() { +ModResult on_save_loaded(void*, ModError*) { // This function will be invoked by the game as a save is loaded + return MOD_OK; } -// Register the game mode when the mod is initialized. const GameModeDesc gameModeDesc = { - .gameModeId = MY_GAME_MODE_ID, - .fullName = "My Game Mode", - .saveName = "my-unique-save", // Custom save names should be unique, max 31 chars - .onActivatedFunction = onGameModeActivated, // Called when the game mode is selected or launched - .onDeactivatedFunction = onGameModeDeactivated, // Called when it is deselected or the mod is disabled - .onPlayFunction = nullptr, // Called when "Play" is pressed on the prelaunch menu - .onSaveLoadedFunction = onSaveLoaded, // Called when a save is loaded - .onNewSaveFunction = nullptr, // Called after a new savefile is created - .onNewSaveSelectFunction = nullptr, // Called during the flow before the file name select is ran (see below) - .onGameResetFunction = nullptr, // Called when the game is reset - .onTickFunction = nullptr, // Called every game tick while the game mode is active + .struct_size = sizeof(GameModeDesc), + .game_mode_id = MY_GAME_MODE_ID, + .full_name = "My Game Mode", + .save_name = "my-unique-save", + .user_data = nullptr, + .on_activated = on_game_mode_activated, + .on_deactivated = on_game_mode_deactivated, + .on_save_loaded = on_save_loaded, }; -svc_gamemode->register_game_mode(mod_ctx, &gameModeDesc); +svc_game_mode->register_game_mode(mod_ctx, &gameModeDesc); ``` -A game mode can also open UI for per-save settings when creating a new file. +A game mode can also open UI for per-save settings when creating a new file. The state begins as +`GAME_MODE_STATE_PENDING` and remains valid until the mod selects `PROCEED` or `RETURN`. ```cpp -IMPORT_SERVICE(GameModeService, svc_gamemode); +IMPORT_SERVICE(GameModeService, svc_game_mode); IMPORT_SERVICE(UiService, svc_ui); -void onNewSaveSelect(bool *out_proceedToNameSelect, bool *out_returnToFileSelect) { - static bool *proceedToNameSelect; - static bool *returnToFileSelect; +ModResult on_new_save_select(void*, GameModeNewSaveState* state, ModError* outError) { + static GameModeNewSaveState* newSaveState; static UiWindowHandle windowHandle; - // Used within callbacks as needed - proceedToNameSelect = out_proceedToNameSelect; - returnToFileSelect = out_returnToFileSelect; + newSaveState = state; UiTabDesc tabs[1]{}; @@ -724,7 +719,7 @@ void onNewSaveSelect(bool *out_proceedToNameSelect, bool *out_returnToFileSelect desc.label = "Play"; desc.help_rml = "Play Button"; desc.on_pressed = [](ModContext* ctx, void* userdata) { - *proceedToNameSelect = true; + *newSaveState = GAME_MODE_STATE_PROCEED; svc_ui->window_close(ctx, *static_cast(userdata)); }; desc.user_data = &windowHandle; @@ -737,21 +732,26 @@ void onNewSaveSelect(bool *out_proceedToNameSelect, bool *out_returnToFileSelect desc.tab_count = 1; desc.on_closed = [](ModContext *, UiWindowHandle, void *userdata) { // If closing the window through backing out, return to file select - if (*proceedToNameSelect == false) { - *returnToFileSelect = true; + if (*newSaveState == GAME_MODE_STATE_PENDING) { + *newSaveState = GAME_MODE_STATE_RETURN; } }; - svc_ui->window_push(mod_ctx, &desc, &windowHandle); + ModResult result = svc_ui->window_push(mod_ctx, &desc, &windowHandle); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to open new-save settings"); + } + return MOD_OK; } const GameModeDesc gameModeDesc = { - .gameModeId = "my-game-mode-id", - .fullName = "My Game Mode", - .saveName = "my-unique-save", - .onNewSaveSelectFunction = onNewSaveSelect, + .struct_size = sizeof(GameModeDesc), + .game_mode_id = "my-game-mode-id", + .full_name = "My Game Mode", + .save_name = "my-unique-save", + .on_new_save_select = on_new_save_select, }; -svc_gamemode->register_game_mode(mod_ctx, &gameModeDesc); +svc_game_mode->register_game_mode(mod_ctx, &gameModeDesc); ``` diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index 1e5d0c8671..41349acac3 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -9,6 +9,10 @@ #include "JSystem/J3DGraphLoader/J3DModelLoader.h" #include "JSystem/J3DGraphLoader/J3DAnmLoader.h" +#if TARGET_PC +#include "mods/svc/game_mode.h" +#endif + class dFile_info_c; class J2DPicture; @@ -741,8 +745,7 @@ public: #ifdef TARGET_PC dDlst_FileSelFade_c mFadeDlst; bool mGameModeSaveStartBuildUi = true; - bool mGameModeProceedToNameSelect = false; - bool mGameModeReturnToFileSelect = false; + GameModeNewSaveState mGameModeNewSaveState = GAME_MODE_STATE_PENDING; #endif #if PLATFORM_WII || PLATFORM_SHIELD diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 206d118976..6cc5da65ae 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -143,6 +143,74 @@ eyebrow span { transition: decorator color opacity 0.1s linear-in-out; } +#menu-list button.game-mode-button { + position: relative; + padding: 0; + overflow: visible; +} + +#menu-list game-mode-label-viewport { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 8dp; + pointer-events: none; + z-index: 0; +} + +#menu-list game-mode-label { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 8dp 16dp; + opacity: 0; + text-overflow: ellipsis; + white-space: nowrap; + transition: opacity 0.24s cubic-in-out; +} + +#menu-list game-mode-label.active { + opacity: 1; +} + +#menu-list game-mode-previous, +#menu-list game-mode-next { + display: none; + position: absolute; + top: 0; + width: 48dp; + height: 54dp; + align-items: center; + justify-content: center; + color: #FFFFFF; + font-family: "Material Symbols Rounded"; + font-weight: normal; + font-size: 30dp; + z-index: 1; +} + +#menu-list game-mode-previous { + left: -48dp; + decorator: text("" center center); +} + +#menu-list game-mode-next { + right: -48dp; + decorator: text("" center center); +} + +#menu-list button.game-mode-button.can-cycle game-mode-previous, +#menu-list button.game-mode-button.can-cycle game-mode-next { + display: flex; +} + #menu-list button:hover, #menu-list button:focus-visible { color: black; @@ -366,34 +434,6 @@ body.animate-in .intro-item { transition: opacity transform 0.3s 0.7s cubic-in-out; } -/* Locks the menu element to not go below the disc status element */ -@media (max-height: 900dp) { - menu { - top: auto; - bottom: 150dp; - transform: none; - gap: 24dp; - } - - hero { - gap: 2dp; - max-height: 220dp; - } - - hero img { - width: 80%; - max-height: 180dp; - } -} - -@media (max-height: 750dp) { - #menu-list button { - font-size: 26dp; - height: 45dp; - text-align: right; - } -} - /* Mobile layout */ @media (max-height: 640dp) { .gradient { @@ -447,8 +487,6 @@ body.animate-in .intro-item { #menu-list button { width: 100%; max-width: 100%; - font-size: 23dp; - height: 40dp; text-align: right; } diff --git a/sdk/include/mods/svc/game_mode.h b/sdk/include/mods/svc/game_mode.h index c497836049..8ccc5d64f1 100644 --- a/sdk/include/mods/svc/game_mode.h +++ b/sdk/include/mods/svc/game_mode.h @@ -3,40 +3,48 @@ #include #include -#define GAMEMODE_SERVICE_ID "dev.twilitrealm.dusklight.gamemode" -#define GAMEMODE_SERVICE_MAJOR 1u -#define GAMEMODE_SERVICE_MINOR 0u +#define GAME_MODE_SERVICE_ID "dev.twilitrealm.dusklight.gamemode" +#define GAME_MODE_SERVICE_MAJOR 1u +#define GAME_MODE_SERVICE_MINOR 0u + +/* MOD_OK on success; failures disable the mod. */ +typedef ModResult (*GameModeCallback)(void* user_data, ModError* out_error); + +typedef enum GameModeNewSaveState { + GAME_MODE_STATE_PENDING = 0, + GAME_MODE_STATE_PROCEED, + GAME_MODE_STATE_RETURN, +} GameModeNewSaveState; + +/* Set state to PROCEED after custom UI completes, or RETURN to cancel. + * State pointer is valid until selection completes. */ +typedef ModResult (*GameModeNewSaveSelectCallback)( + void* user_data, GameModeNewSaveState* state, ModError* out_error); typedef struct { - const char* gameModeId; - const char* fullName; - const char saveName[32]; // Empty uses default (gczelda2); max 31 chars - void (*onActivatedFunction)(); // Called when the game mode is selected - void (*onDeactivatedFunction)(); // Called when the game mode is deselected - void (*onPlayFunction)(); // Called when play is pressed on the prelaunch menu - void (*onSaveLoadedFunction)(); // Called whenever a save file is loaded - void (*onNewSaveFunction)(); // Called when a new save is created - void (*onNewSaveSelectFunction)(bool* out_proceedToNameSelect, - bool* out_returnToFileSelect); // Set out_proceedToNameSelect to true once any UI flows are - // completed - void (*onGameResetFunction)(); // Called when the game is reset - void (*onTickFunction)(); // Called on every tick + uint32_t struct_size; + const char* game_mode_id; + const char* full_name; + const char save_name[32]; // Empty uses default (gczelda2); max 31 chars + void* user_data; // Pointer will be passed to all callbacks + GameModeCallback on_activated; // Called when the game mode is selected + GameModeCallback on_deactivated; // Called when the game mode is deselected + GameModeCallback on_play; // Called when play is pressed + GameModeCallback on_save_loaded; // Called whenever a save file is loaded + GameModeCallback on_new_save; // Called when a new save is created + GameModeNewSaveSelectCallback on_new_save_select; + GameModeCallback on_game_reset; // Called when the game is reset + GameModeCallback on_tick; // Called on every game tick while active } GameModeDesc; +#define GAME_MODE_DESC_INIT {sizeof(GameModeDesc)} + typedef struct GameModeService { ServiceHeader header; ModResult (*register_game_mode)(ModContext* ctx, const GameModeDesc* desc); ModResult (*unregister_game_mode)(ModContext* ctx, const char* id); - ModResult (*is_active)(ModContext* ctx, const char* gameModeId, bool* out_active); + ModResult (*is_active)(ModContext* ctx, const char* game_mode_id, bool* out_active); } GameModeService; -#ifdef __cplusplus -#include "mods/service.hpp" - -template <> -struct mods::ServiceTraits { - static constexpr const char* id = GAMEMODE_SERVICE_ID; - static constexpr uint16_t major_version = GAMEMODE_SERVICE_MAJOR; - static constexpr uint16_t minor_version = GAMEMODE_SERVICE_MINOR; -}; -#endif +MOD_DECLARE_SERVICE(GameModeService, svc_game_mode, GAME_MODE_SERVICE_ID, GAME_MODE_SERVICE_MAJOR, + GAME_MODE_SERVICE_MINOR); diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 258aea3b90..e075e2b8a6 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -1320,21 +1320,19 @@ void dFile_select_c::selectDataNameMove() { if (gameMode) { if (isHeaderTxtChange == true && isFileRecScale == true && isModoruTxtDisp == true) { if (mGameModeSaveStartBuildUi) { - gameMode->invokeOnNewSaveSelectFunction( - &mGameModeProceedToNameSelect, &mGameModeReturnToFileSelect); + gameMode->invokeOnNewSaveSelectFunction(&mGameModeNewSaveState); mGameModeSaveStartBuildUi = false; } - if (mGameModeReturnToFileSelect) { + if (mGameModeNewSaveState == GAME_MODE_STATE_RETURN) { backToDataSelectMove(); mGameModeSaveStartBuildUi = true; - mGameModeProceedToNameSelect = false; - mGameModeReturnToFileSelect = false; + mGameModeNewSaveState = GAME_MODE_STATE_PENDING; return; } - if (!mGameModeProceedToNameSelect) { + if (mGameModeNewSaveState != GAME_MODE_STATE_PROCEED) { return; } - }else { + } else { return; } } @@ -1347,8 +1345,7 @@ void dFile_select_c::selectDataNameMove() { { #ifdef TARGET_PC mGameModeSaveStartBuildUi = true; - mGameModeProceedToNameSelect = false; - mGameModeReturnToFileSelect = false; + mGameModeNewSaveState = GAME_MODE_STATE_PENDING; #endif mDataSelProc = DATASELPROC_NAME_INPUT_WAIT; } diff --git a/src/dusk/game_mode.cpp b/src/dusk/game_mode.cpp index 2e97ea4c23..8fdb3106f3 100644 --- a/src/dusk/game_mode.cpp +++ b/src/dusk/game_mode.cpp @@ -62,27 +62,36 @@ void GameModeManager::unregisterGameMode(const GameModeId& gameModeId) { ui::Prelaunch::refresh_menu_buttons(); } -void GameModeManager::setCurrentGameMode(const GameModeId& id) { +bool GameModeManager::setCurrentGameMode(const GameModeId& id) { if (mCurrentGameModeId == id) { - return; + return true; } if (!mRegisteredGameModes.contains(id)) { Log.warn("Attempting to configure unknown game mode {}", id); - return; + return false; } const GameMode* currentGameMode = getCurrentGameMode(); if (currentGameMode) { currentGameMode->invokeOnDeactivatedFunction(); } mCurrentGameModeId = id; - getSettings().game.lastSelectedGameModeId.setValue(id); - config::save(); currentGameMode = getCurrentGameMode(); if (currentGameMode) { mDoMemCd_SetFileName(currentGameMode->getSaveName()); - currentGameMode->invokeOnActivatedFunction(); + if (!currentGameMode->invokeOnActivatedFunction()) { + mCurrentGameModeId = kVanillaGameModeId; + currentGameMode = getCurrentGameMode(); + mDoMemCd_SetFileName(currentGameMode->getSaveName()); + currentGameMode->invokeOnActivatedFunction(); + getSettings().game.lastSelectedGameModeId.setValue(kVanillaGameModeId); + config::save(); + return false; + } } + getSettings().game.lastSelectedGameModeId.setValue(id); + config::save(); + return true; } } // namespace dusk::gamemode diff --git a/src/dusk/game_mode.hpp b/src/dusk/game_mode.hpp index b60c6498e8..3c2b63a484 100644 --- a/src/dusk/game_mode.hpp +++ b/src/dusk/game_mode.hpp @@ -1,6 +1,7 @@ #pragma once #include "d/d_file_select.h" +#include "mods/svc/game_mode.h" #include #include @@ -16,6 +17,9 @@ constexpr const char* kDefaultGameModeSaveName = "gczelda2"; // Holds a game mode definition and its lifecycle callbacks. class GameMode { public: + using Callback = std::function; + using NewSaveSelectCallback = std::function; + GameMode(GameModeId id, std::string fullName, std::string saveName = {}) : mId{std::move(id)}, mFullName{std::move(fullName)}, mSaveName{saveName.empty() ? kDefaultGameModeSaveName : std::move(saveName)} {} @@ -27,66 +31,76 @@ public: std::string mFullName; std::string mSaveName; - void invokeOnActivatedFunction() const { + bool invokeOnActivatedFunction() const { if (mOnActivatedFunction) { - mOnActivatedFunction(); + return mOnActivatedFunction(); } + return true; } - void invokeOnDeactivatedFunction() const { + bool invokeOnDeactivatedFunction() const { if (mOnDeactivatedFunction) { - mOnDeactivatedFunction(); + return mOnDeactivatedFunction(); } + return true; } - void invokeOnPlayFunction() const { + bool invokeOnPlayFunction() const { if (mOnPlayFunction) { - mOnPlayFunction(); + return mOnPlayFunction(); } + return true; } - void invokeOnSaveLoadedFunction() const { + bool invokeOnSaveLoadedFunction() const { if (mOnSaveLoadedFunction) { - mOnSaveLoadedFunction(); + return mOnSaveLoadedFunction(); } + return true; } - void invokeOnNewSaveFunction() const { + bool invokeOnNewSaveFunction() const { if (mOnNewSaveFunction) { - mOnNewSaveFunction(); + return mOnNewSaveFunction(); } + return true; } - void invokeOnNewSaveSelectFunction( - bool* out_proceedToNameSelect, bool* out_returnToFileSelect) const { + bool invokeOnNewSaveSelectFunction(GameModeNewSaveState* state) const { + *state = GAME_MODE_STATE_PENDING; if (mOnNewSaveSelectFunction) { - mOnNewSaveSelectFunction(out_proceedToNameSelect, out_returnToFileSelect); - } else { - *out_proceedToNameSelect = true; + if (mOnNewSaveSelectFunction(state)) { + return true; + } + *state = GAME_MODE_STATE_RETURN; + return false; } + *state = GAME_MODE_STATE_PROCEED; + return true; } - void invokeOnGameResetFunction() const { + bool invokeOnGameResetFunction() const { if (mOnGameResetFunction) { - mOnGameResetFunction(); + return mOnGameResetFunction(); } + return true; } - void invokeOnTickFunction() const { + bool invokeOnTickFunction() const { if (mOnTickFunction) { - mOnTickFunction(); + return mOnTickFunction(); } + return true; } - std::function mOnActivatedFunction; - std::function mOnDeactivatedFunction; - std::function mOnPlayFunction; - std::function mOnSaveLoadedFunction; - std::function mOnNewSaveFunction; - std::function - mOnNewSaveSelectFunction; - std::function mOnGameResetFunction; - std::function mOnTickFunction; + Callback mOnActivatedFunction; + Callback mOnDeactivatedFunction; + Callback mOnPlayFunction; + Callback mOnSaveLoadedFunction; + Callback mOnNewSaveFunction; + NewSaveSelectCallback mOnNewSaveSelectFunction; + Callback mOnGameResetFunction; + Callback mOnTickFunction; }; class GameModeManager { @@ -107,7 +121,7 @@ public: } return false; } - void setCurrentGameMode(const GameModeId& id); + bool setCurrentGameMode(const GameModeId& id); void setGameModeToPrevious(); const std::map& getRegisteredGameModes() const { diff --git a/src/dusk/mods/svc/game_mode.cpp b/src/dusk/mods/svc/game_mode.cpp index 089d2799af..cfa655baf8 100644 --- a/src/dusk/mods/svc/game_mode.cpp +++ b/src/dusk/mods/svc/game_mode.cpp @@ -7,9 +7,12 @@ #include "aurora/lib/logging.hpp" #include "dusk/mod_loader.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "fmt/format.h" #include #include +#include #include #include #include @@ -19,9 +22,52 @@ namespace { aurora::Module Log("dusk::mods::game_mode"); -// Track which gamemodes are registered by which mods, allowing us to automatically unregister them +// Track ownership of mod ID to game modes std::unordered_map> s_gameModesByMod; +template +bool invoke_mod_callback(LoadedMod& mod, const char* what, Fn&& fn) { + if (!mod.active) { + return false; + } + + ModError error = MOD_ERROR_INIT; + ModResult result = MOD_OK; + try { + result = fn(&error); + } catch (const std::exception& exception) { + fail_mod(mod, MOD_ERROR, fmt::format("exception in {}: {}", what, exception.what())); + return false; + } catch (...) { + fail_mod(mod, MOD_ERROR, fmt::format("unknown exception in {}", what)); + return false; + } + + if (result != MOD_OK && mod.active) { + fail_mod(mod, result, + error.message[0] != '\0' ? + error.message : + fmt::format("{} failed with result {}", what, static_cast(result))); + } + return result == MOD_OK && mod.active; +} + +gamemode::GameMode::Callback wrap_callback( + LoadedMod& mod, GameModeCallback callback, void* userData, const char* what) { + return [&mod, callback, userData, what] { + return invoke_mod_callback( + mod, what, [callback, userData](ModError* error) { return callback(userData, error); }); + }; +} + +gamemode::GameMode::NewSaveSelectCallback wrap_new_save_select_callback( + LoadedMod& mod, GameModeNewSaveSelectCallback callback, void* userData, const char* what) { + return [&mod, callback, userData, what](GameModeNewSaveState* state) { + return invoke_mod_callback( + mod, what, [=](ModError* error) { return callback(userData, state, error); }); + }; +} + std::string get_mod_game_mode_id(ModContext* ctx, const std::string& id) { // Include the mod ID to prevent clashes and normalize to lowercase std::string fullId = id + "_" + ctx->mod->metadata.id; @@ -42,12 +88,17 @@ void game_mode_remove_mod(LoadedMod& mod) { } // namespace ModResult register_game_mode(ModContext* ctx, const GameModeDesc* desc) { + auto* owner = mod_from_context(ctx); + if (owner == nullptr || desc == nullptr || desc->struct_size < sizeof(GameModeDesc)) { + return MOD_INVALID_ARGUMENT; + } + std::string id; - if (!desc->gameModeId) { + if (!desc->game_mode_id) { Log.error("Attempted to register a game mode with a null ID"); return MOD_ERROR; } - id = desc->gameModeId; + id = desc->game_mode_id; if (id.empty()) { Log.error("Attempted to register a game mode with an empty ID"); return MOD_ERROR; @@ -55,41 +106,49 @@ ModResult register_game_mode(ModContext* ctx, const GameModeDesc* desc) { id = get_mod_game_mode_id(ctx, id); std::string fullName; - if (!desc->fullName) { + if (!desc->full_name) { Log.warn("Game mode {} has no display name; using its ID", id); fullName = id; } else { - fullName = desc->fullName; + fullName = desc->full_name; if (fullName.empty()) { Log.warn("Game mode {} has an empty display name; using its ID", id); fullName = id; } } - gamemode::GameMode mode{id, fullName, desc->saveName}; - if (desc->onActivatedFunction) { - mode.mOnActivatedFunction = desc->onActivatedFunction; + gamemode::GameMode mode{id, fullName, desc->save_name}; + if (desc->on_activated) { + mode.mOnActivatedFunction = wrap_callback( + *owner, desc->on_activated, desc->user_data, "game mode activation callback"); } - if (desc->onDeactivatedFunction) { - mode.mOnDeactivatedFunction = desc->onDeactivatedFunction; + if (desc->on_deactivated) { + mode.mOnDeactivatedFunction = wrap_callback( + *owner, desc->on_deactivated, desc->user_data, "game mode deactivation callback"); } - if (desc->onPlayFunction) { - mode.mOnPlayFunction = desc->onPlayFunction; + if (desc->on_play) { + mode.mOnPlayFunction = + wrap_callback(*owner, desc->on_play, desc->user_data, "game mode play callback"); } - if (desc->onSaveLoadedFunction) { - mode.mOnSaveLoadedFunction = desc->onSaveLoadedFunction; + if (desc->on_save_loaded) { + mode.mOnSaveLoadedFunction = wrap_callback( + *owner, desc->on_save_loaded, desc->user_data, "game mode save-loaded callback"); } - if (desc->onNewSaveFunction) { - mode.mOnNewSaveFunction = desc->onNewSaveFunction; + if (desc->on_new_save) { + mode.mOnNewSaveFunction = wrap_callback( + *owner, desc->on_new_save, desc->user_data, "game mode new-save callback"); } - if (desc->onNewSaveSelectFunction) { - mode.mOnNewSaveSelectFunction = desc->onNewSaveSelectFunction; + if (desc->on_new_save_select) { + mode.mOnNewSaveSelectFunction = wrap_new_save_select_callback(*owner, + desc->on_new_save_select, desc->user_data, "game mode new-save selection callback"); } - if (desc->onGameResetFunction) { - mode.mOnGameResetFunction = desc->onGameResetFunction; + if (desc->on_game_reset) { + mode.mOnGameResetFunction = + wrap_callback(*owner, desc->on_game_reset, desc->user_data, "game mode reset callback"); } - if (desc->onTickFunction) { - mode.mOnTickFunction = desc->onTickFunction; + if (desc->on_tick) { + mode.mOnTickFunction = + wrap_callback(*owner, desc->on_tick, desc->user_data, "game mode tick callback"); } gamemode::getGameModeManager().registerGameMode(mode); @@ -101,7 +160,7 @@ ModResult unregister_game_mode(ModContext* ctx, const char* id) { std::string fullId = get_mod_game_mode_id(ctx, id); gamemode::getGameModeManager().unregisterGameMode(fullId); - // Remove the game mode from the service registered game modes map + // Remove the game mode from the ownership map auto it = s_gameModesByMod.find(ctx->mod->metadata.id); if (it != s_gameModesByMod.end()) { std::erase(it->second, fullId); @@ -121,7 +180,7 @@ namespace dusk::mods::svc { namespace { constexpr GameModeService s_gamemodeService{ - .header = SERVICE_HEADER(GameModeService, GAMEMODE_SERVICE_MAJOR, GAMEMODE_SERVICE_MINOR), + .header = SERVICE_HEADER(GameModeService, GAME_MODE_SERVICE_MAJOR, GAME_MODE_SERVICE_MINOR), .register_game_mode = game_mode_impl::register_game_mode, .unregister_game_mode = game_mode_impl::unregister_game_mode, .is_active = game_mode_impl::is_active, @@ -130,9 +189,9 @@ constexpr GameModeService s_gamemodeService{ } // namespace constinit const ServiceModule g_gamemodeModule{ - .id = GAMEMODE_SERVICE_ID, - .majorVersion = GAMEMODE_SERVICE_MAJOR, - .minorVersion = GAMEMODE_SERVICE_MINOR, + .id = GAME_MODE_SERVICE_ID, + .majorVersion = GAME_MODE_SERVICE_MAJOR, + .minorVersion = GAME_MODE_SERVICE_MINOR, .service = &s_gamemodeService, .modDeactivating = game_mode_impl::game_mode_remove_mod, }; diff --git a/src/dusk/speedrun.cpp b/src/dusk/speedrun.cpp index cf6b4068e6..8527f9510a 100644 --- a/src/dusk/speedrun.cpp +++ b/src/dusk/speedrun.cpp @@ -22,11 +22,24 @@ static void onSpeedrunModeDeactive() { } void registerSpeedrunGameMode() { - dusk::gamemode::GameMode speedrunGameMode(kSpeedrunGameModeId,"Speedrun","gczelda2-speedrun"); - speedrunGameMode.mOnSaveLoadedFunction = dusk::speedrun::start; - speedrunGameMode.mOnActivatedFunction = onSpeedrunModeActive; - speedrunGameMode.mOnDeactivatedFunction = onSpeedrunModeDeactive; - speedrunGameMode.mOnTickFunction = dusk::speedrun::onGameFrame; + dusk::gamemode::GameMode speedrunGameMode{ + kSpeedrunGameModeId, "Speedrun", "gczelda2-speedrun"}; + speedrunGameMode.mOnSaveLoadedFunction = [] { + dusk::speedrun::start(); + return true; + }; + speedrunGameMode.mOnActivatedFunction = [] { + onSpeedrunModeActive(); + return true; + }; + speedrunGameMode.mOnDeactivatedFunction = [] { + onSpeedrunModeDeactive(); + return true; + }; + speedrunGameMode.mOnTickFunction = [] { + dusk::speedrun::onGameFrame(); + return true; + }; dusk::gamemode::getGameModeManager().registerGameMode(speedrunGameMode); } @@ -72,9 +85,7 @@ void resetForSpeedrunMode() { } static void clearSpeedrunOverrides() { - config::EnumerateRegistered([](config::ConfigVarBase& cvar) { - cvar.clearSpeedrunOverride(); - }); + config::EnumerateRegistered([](config::ConfigVarBase& cvar) { cvar.clearSpeedrunOverride(); }); } void restoreFromSpeedrunMode() { @@ -82,4 +93,4 @@ void restoreFromSpeedrunMode() { aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue()); } -} // namespace dusk +} // namespace dusk::speedrun diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index cfaffff0f2..c1973587b1 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -37,6 +37,8 @@ namespace dusk::ui { namespace { constexpr borealis::Log PrelaunchLog{"dusk::ui::prelaunch"}; +PrelaunchState sPrelaunchState; + const Rml::String kDocumentSource = R"RML( @@ -529,7 +531,144 @@ void file_dialog_callback(borealis::file_select::Result result) { begin_disc_verification(result.locations.front()); } -PrelaunchState sPrelaunchState; +std::vector carousel_game_modes() { + const auto& registered = gamemode::getGameModeManager().getRegisteredGameModes(); + std::vector modes; + modes.reserve(registered.size()); + + if (const auto vanilla = registered.find(gamemode::kVanillaGameModeId); + vanilla != registered.end()) + { + modes.push_back(&vanilla->second); + } + for (const auto& [id, mode] : registered) { + if (id != gamemode::kVanillaGameModeId) { + modes.push_back(&mode); + } + } + std::ranges::sort(modes.begin() + std::min(1, modes.size()), modes.end(), + [](const auto* lhs, const auto* rhs) { return lhs->getFullName() < rhs->getFullName(); }); + return modes; +} + +std::string game_mode_button_text() { + if (prelaunch_state().activeDiscPath.empty()) { + return "Select Disc Image"; + } + const auto* currentGameMode = gamemode::getGameModeManager().getCurrentGameMode(); + if (currentGameMode == nullptr || currentGameMode->getId() == gamemode::kVanillaGameModeId) { + return "Play"; + } + return currentGameMode->getFullName(); +} + +class GameModeButton final : public Button { +public: + GameModeButton(Rml::Element* parent, ButtonCallback onPressed) : Button{parent, ""} { + root()->SetClass("game-mode-button", true); + mPrevious = append(root(), "game-mode-previous"); + mLabelViewport = append(root(), "game-mode-label-viewport"); + for (auto& label : mLabels) { + label = append(mLabelViewport, "game-mode-label"); + } + mNext = append(root(), "game-mode-next"); + + Component::listen(mPrevious, Rml::EventId::Click, [this](Rml::Event& event) { + cycle(-1); + event.StopPropagation(); + }); + Component::listen(mNext, Rml::EventId::Click, [this](Rml::Event& event) { + cycle(1); + event.StopPropagation(); + }); + Component::listen(root(), Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto command = map_nav_event(event); + if (command == NavCommand::Left || command == NavCommand::Right) { + cycle(command == NavCommand::Left ? -1 : 1); + event.StopPropagation(); + } + }); + on_pressed(std::move(onPressed)); + refresh(0); + } + + void update() override { + refresh(0); + Button::update(); + } + +private: + bool can_cycle() const { + return !prelaunch_state().activeDiscPath.empty() && + gamemode::getGameModeManager().getRegisteredGameModes().size() > 1; + } + + void cycle(int direction) { + const auto modes = carousel_game_modes(); + if (!can_cycle() || modes.empty()) { + return; + } + + const auto* current = gamemode::getGameModeManager().getCurrentGameMode(); + const auto currentIt = std::ranges::find_if(modes, [current](const auto* mode) { + return current != nullptr && mode->getId() == current->getId(); + }); + const int currentIndex = + currentIt == modes.end() ? 0 : static_cast(currentIt - modes.begin()); + const int count = static_cast(modes.size()); + const int nextIndex = ((currentIndex + direction) % count + count) % count; + const auto nextId = modes[nextIndex]->getId(); + if (gamemode::getGameModeManager().setCurrentGameMode(nextId)) { + mDoAud_seStartMenu(kSoundItemChange); + } + refresh(direction); + } + + void refresh(int direction) { + root()->SetClass("can-cycle", can_cycle()); + + const auto text = game_mode_button_text(); + if (text == mText) { + return; + } + mText = text; + if (direction == 0) { + mLabels[mActiveLabel]->SetInnerRML(escape(text)); + mLabels[mActiveLabel]->SetProperty( + Rml::PropertyId::Left, Rml::Property{0.0f, Rml::Unit::PERCENT}); + mLabels[mActiveLabel]->SetClass("active", true); + mLabels[1 - mActiveLabel]->SetClass("active", false); + return; + } + + constexpr float kSlideDistance = 100.0f; + constexpr float kSlideDuration = 0.24f; + auto* outgoing = mLabels[mActiveLabel]; + mActiveLabel = 1 - mActiveLabel; + auto* incoming = mLabels[mActiveLabel]; + incoming->SetInnerRML(escape(text)); + + const Rml::Property incomingOffset{ + static_cast(direction) * kSlideDistance, Rml::Unit::PERCENT}; + const Rml::Property outgoingOffset{ + static_cast(-direction) * kSlideDistance, Rml::Unit::PERCENT}; + + outgoing->Animate(Rml::PropertyId::Left, outgoingOffset, kSlideDuration, + Rml::Tween{Rml::Tween::Cubic, Rml::Tween::InOut}); + incoming->Animate(Rml::PropertyId::Left, Rml::Property{0.0f, Rml::Unit::PERCENT}, + kSlideDuration, Rml::Tween{Rml::Tween::Cubic, Rml::Tween::InOut}, 1, false, 0.0f, + &incomingOffset); + outgoing->SetClass("active", false); + incoming->SetClass("active", true); + } + + Rml::Element* mPrevious = nullptr; + Rml::Element* mLabelViewport = nullptr; + std::array mLabels{}; + Rml::Element* mNext = nullptr; + Rml::String mText; + std::size_t mActiveLabel = 0; +}; } // namespace @@ -758,22 +897,6 @@ void Prelaunch::refresh_menu_buttons() { prelaunch->build_menu_buttons(); } -static std::string get_playbutton_text() { - auto& state = prelaunch_state(); - const bool activeDiscLoaded = !state.activeDiscPath.empty(); - if (activeDiscLoaded == false) { - return "Select Disc Image"; - } - std::string playText; - const gamemode::GameMode* currentGameMode = - gamemode::getGameModeManager().getCurrentGameMode(); - if (currentGameMode != nullptr) { - return currentGameMode->getId() == gamemode::kVanillaGameModeId ? "Play" : - "Play " + currentGameMode->getFullName(); - } - return "Play"; -} - Prelaunch::Prelaunch() : Document(kDocumentSource, false, DocumentScope::Prelaunch) { mRoot = mDocument->GetElementById("root"); ensure_initialized(); @@ -819,16 +942,22 @@ Prelaunch::Prelaunch() : Document(kDocumentSource, false, DocumentScope::Prelaun void Prelaunch::build_menu_buttons() { if (auto* menuList = mDocument->GetElementById("menu-list")) { - // Set the gamemode to the last used before showing the play button + // Restore the previously selected game mode before creating the play control. gamemode::getGameModeManager().setGameModeToPrevious(); - mMenuButtons.push_back(std::make_unique