Use carousel button on prelaunch; more cleanup

This commit is contained in:
Luke Street
2026-08-19 22:58:43 -06:00
parent 3ecd785ac2
commit fd4a5f0cfb
10 changed files with 476 additions and 257 deletions
+45 -45
View File
@@ -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<CreateItem>(svc_hook, myFunctionHook);
ModResult result = mods::hook_add_pre<CreateItem>(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<CreateItem>();
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<UiWindowHandle*>(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);
```
+5 -2
View File
@@ -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
+68 -30
View File
@@ -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("&#xe5cb;" center center);
}
#menu-list game-mode-next {
right: -48dp;
decorator: text("&#xe5cc;" 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;
}
+35 -27
View File
@@ -3,40 +3,48 @@
#include <mods/api.h>
#include <mods/svc/config.h>
#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<GameModeService> {
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);
+6 -9
View File
@@ -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;
}
+15 -6
View File
@@ -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
+43 -29
View File
@@ -1,6 +1,7 @@
#pragma once
#include "d/d_file_select.h"
#include "mods/svc/game_mode.h"
#include <functional>
#include <map>
@@ -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<bool()>;
using NewSaveSelectCallback = std::function<bool(GameModeNewSaveState* state)>;
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<void()> mOnActivatedFunction;
std::function<void()> mOnDeactivatedFunction;
std::function<void()> mOnPlayFunction;
std::function<void()> mOnSaveLoadedFunction;
std::function<void()> mOnNewSaveFunction;
std::function<void(bool* out_proceedToNameSelect, bool* out_returnToFileSelect)>
mOnNewSaveSelectFunction;
std::function<void()> mOnGameResetFunction;
std::function<void()> 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<GameModeId, GameMode>& getRegisteredGameModes() const {
+86 -27
View File
@@ -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 <algorithm>
#include <cctype>
#include <exception>
#include <string>
#include <unordered_map>
#include <vector>
@@ -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<std::string, std::vector<std::string>> s_gameModesByMod;
template <typename Fn>
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<int>(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,
};
+20 -9
View File
@@ -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
+153 -73
View File
@@ -37,6 +37,8 @@ namespace dusk::ui {
namespace {
constexpr borealis::Log PrelaunchLog{"dusk::ui::prelaunch"};
PrelaunchState sPrelaunchState;
const Rml::String kDocumentSource = R"RML(
<rml>
<head>
@@ -529,7 +531,144 @@ void file_dialog_callback(borealis::file_select::Result result) {
begin_disc_verification(result.locations.front());
}
PrelaunchState sPrelaunchState;
std::vector<const gamemode::GameMode*> carousel_game_modes() {
const auto& registered = gamemode::getGameModeManager().getRegisteredGameModes();
std::vector<const gamemode::GameMode*> 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<size_t>(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<int>(currentIt - modes.begin());
const int count = static_cast<int>(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<float>(direction) * kSlideDistance, Rml::Unit::PERCENT};
const Rml::Property outgoingOffset{
static_cast<float>(-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<Rml::Element*, 2> 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<Button>(menuList, get_playbutton_text()));
mMenuButtons.back()->on_pressed([this] {
auto playButton = std::make_unique<GameModeButton>(menuList, [this] {
if (prelaunch_state().activeDiscPath.empty()) {
open_iso_picker();
return;
}
if (const auto* gameMode = gamemode::getGameModeManager().getCurrentGameMode();
gameMode != nullptr && !gameMode->invokeOnPlayFunction())
{
gamemode::getGameModeManager().setCurrentGameMode(gamemode::kVanillaGameModeId);
return;
}
mDoAud_seStartMenu(kSoundPlay);
show_menu_notification();
@@ -845,61 +974,12 @@ void Prelaunch::build_menu_buttons() {
}
prelaunch_state().firstLaunch = false;
const gamemode::GameMode* gameMode =
gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnPlayFunction();
}
IsGameLaunched = true;
hide(true);
MenuBar::refresh_tabs();
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
// Only show selection when game modes are registered (besides vanilla)
if (gamemode::getGameModeManager().getRegisteredGameModes().size() > 1) {
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Select Game Mode"));
mMenuButtons.back()->on_pressed([this] {
std::vector<ModalAction> gameModeActions;
gameModeActions.push_back(ModalAction{
.label = "Vanilla", .onPressed = [this](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
gamemode::getGameModeManager().setCurrentGameMode(gamemode::kVanillaGameModeId);
modal.pop();
update();
}});
for (const auto& [id, gameMode] :
gamemode::getGameModeManager().getRegisteredGameModes())
{
if (id == gamemode::kVanillaGameModeId) {
// Force vanilla to the top
continue;
}
gameModeActions.push_back(ModalAction{.label = gameMode.getFullName(),
.onPressed = [this, id](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
gamemode::getGameModeManager().setCurrentGameMode(id);
modal.pop();
update();
}});
}
mRestartSuppressed = false;
push(std::make_unique<Modal>(Modal::Props{
.title = "Play Type",
.bodyRml = "What mode would you like to play?",
.actions = gameModeActions,
.onDismiss =
[this](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
modal.pop();
},
.icon = "question-mark",
.isVertical = true,
}));
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
}
apply_intro_animation(playButton->root(), "delay-1");
mMenuButtons.push_back(std::move(playButton));
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Settings"));
mMenuButtons.back()->on_pressed([this] {
@@ -1001,8 +1081,8 @@ void Prelaunch::update() {
mEntranceAnimationStarted = true;
}
if (!mMenuButtons.empty()) {
mMenuButtons[0]->set_text(get_playbutton_text());
for (const auto& button : mMenuButtons) {
button->update();
}
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");