From 61a4ab3117e2c19810fe370bbbd3774abe55467a Mon Sep 17 00:00:00 2001 From: jdflyer Date: Sun, 19 Jul 2026 00:50:49 -0700 Subject: [PATCH 01/38] Mod SDK: Gamemode Service, Change Speedrun mode to a gamemode, Allow prelaunch to be restarted --- docs/modding.md | 47 +++++++ files.cmake | 2 + include/m_Do/m_Do_MemCard.h | 20 +++ res/rml/window.rcss | 10 ++ sdk/include/mods/svc/gamemode.h | 39 ++++++ src/d/actor/d_a_alink_demo.inc | 9 +- src/d/d_bright_check.cpp | 18 +-- src/d/d_s_name.cpp | 26 ++-- src/d/d_save.cpp | 8 ++ src/dusk/OSThread.cpp | 6 + src/dusk/gamemode.cpp | 93 +++++++++++++ src/dusk/gamemode.hpp | 69 ++++++++++ src/dusk/imgui/ImGuiConsole.cpp | 5 +- src/dusk/imgui/ImGuiMenuTools.cpp | 5 +- src/dusk/iso_validate.hpp | 1 + src/dusk/livesplit.cpp | 136 ++++++++++--------- src/dusk/livesplit.h | 2 + src/dusk/main.h | 3 - src/dusk/mods/svc/gamemode.cpp | 144 +++++++++++++++++++++ src/dusk/mods/svc/registry.cpp | 1 + src/dusk/mods/svc/registry.hpp | 1 + src/dusk/settings.cpp | 4 +- src/dusk/settings.h | 2 + src/dusk/speedrun.cpp | 31 ++++- src/dusk/speedrun.h | 13 +- src/dusk/ui/menu_bar.cpp | 9 +- src/dusk/ui/modal.cpp | 10 +- src/dusk/ui/modal.hpp | 1 + src/dusk/ui/overlay.cpp | 26 ++-- src/dusk/ui/prelaunch.cpp | 208 ++++++++++++++++++++++-------- src/dusk/ui/prelaunch.hpp | 8 +- src/dusk/ui/settings.cpp | 26 ++-- src/dusk/ui/ui.hpp | 2 + src/f_ap/f_ap_game.cpp | 6 +- src/f_op/f_op_overlap_req.cpp | 19 +-- src/m_Do/m_Do_MemCard.cpp | 152 +++++++++++++++------- src/m_Do/m_Do_Reset.cpp | 45 ++++++- src/m_Do/m_Do_main.cpp | 17 ++- 38 files changed, 974 insertions(+), 250 deletions(-) create mode 100644 sdk/include/mods/svc/gamemode.h create mode 100644 src/dusk/gamemode.cpp create mode 100644 src/dusk/gamemode.hpp create mode 100644 src/dusk/mods/svc/gamemode.cpp diff --git a/docs/modding.md b/docs/modding.md index 2600b4721d..f19609757d 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -470,6 +470,53 @@ if (svc_camera->get_camera(mod_ctx, game_view, &camera) == MOD_OK) { first in-game frame. Projection matrices match the renderer's WebGPU clip convention and renderer depth convention (reversed-Z by default). +### GamemodeService (`mods/svc/gamemode.h`) + +Allows a mod to register a gamemode that allows the game to designate one form of gameplay (named a gamemode). This +is intended to allow large mods that change large amounts of game logic (such as a randomizer) to have explicit control +over how the game will function at certain points. When a gamemode is registered via the service, it will add an entry +to the pre-launch menu. When selected, the game will use a unique set of savefiles (designated by the `saveName` field) +to store save data while the gamemode is active. Any function pointers registered with the gamemode will be called by +dusklight when their condition is met. + +Note: for any gamemode wishing to use the vanilla set of savefiles, use `gczelda2` as the save file name. + +```cpp +IMPORT_SERVICE(GamemodeService, svc_gamemode); + +void onSaveLoaded() { + // This function will be invoked by the game as a save is loaded +} + +#define ONLY_GAMEMODE(ctx, id) \ + { \ + bool isGamemodeActive; \ + svc_gamemode->is_active(ctx, id, &isGamemodeActive); \ + if (!isGamemodeActive) { \ + return; \ + } \ + } + +static HookAction myFunctionHook(ModContext *ctx, void *args, void *, void *) { + // Note: normal function hooks will need to check if the gamemode is active + ONLY_GAMEMODE(ctx,"id"); // A macro like this can make it easy +} + +const GamemodeDesc gamemodeDesc = { + .gamemodeId = "my-unique-gamemode-id", + .fullName = "Gamemode Name", + .saveName = "my-unique-save-name", + .onActivatedFunction = nullptr, + .onDeactivatedFunction = nullptr, + .onPlayFunction = nullptr, + .onSaveLoadedFunction = onSaveLoaded, + .onNewSaveFunction = nullptr, + .onGameResetFunction = nullptr, + .onTickFunction = nullptr, +}; +svc_gamemode->register_gamemode(mod_ctx, &gamemodeDesc); +``` + --- ## Hooking Game Functions diff --git a/files.cmake b/files.cmake index c8c92c9b75..0a0b46140a 100644 --- a/files.cmake +++ b/files.cmake @@ -1441,6 +1441,7 @@ set(DUSK_FILES src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp src/dusk/game_clock.cpp + src/dusk/gamemode.cpp src/dusk/gamepad_color.cpp src/dusk/globals.cpp src/dusk/gyro.cpp @@ -1498,6 +1499,7 @@ set(DUSK_FILES src/dusk/mods/svc/texture.cpp src/dusk/mods/svc/ui.cpp src/dusk/mods/svc/ui.hpp + src/dusk/mods/svc/gamemode.cpp src/dusk/mouse.cpp src/dusk/scope_guard.hpp src/dusk/settings.cpp diff --git a/include/m_Do/m_Do_MemCard.h b/include/m_Do/m_Do_MemCard.h index 555d7a6a9f..84fc246e09 100644 --- a/include/m_Do/m_Do_MemCard.h +++ b/include/m_Do/m_Do_MemCard.h @@ -112,6 +112,11 @@ public: mSerialNo = serial_no; } +#ifdef TARGET_PC + void setFileName(const std::string& fileName); + const char* getFileName(); +#endif + /* 0x0000 */ u8 mData[SAVEFILE_SIZE]; /* 0x1FBC */ u8 mChannel; /* 0x1FBD */ u8 mCopyToPos; @@ -124,6 +129,11 @@ public: /* 0x1FEC */ s32 mNandState; /* 0x1FF0 */ u64 mSerialNo; /* 0x1FF8 */ u32 mDataVersion; +#ifdef TARGET_PC + bool mInitialized; + std::string mFileName; +#endif + }; // Size: 0x2000 STATIC_ASSERT(sizeof(mDoMemCd_Ctrl_c) == 8192); @@ -230,4 +240,14 @@ inline s32 mDoMemCd_checkNANDFile() { } #endif +#ifdef TARGET_PC +inline void mDoMemCd_SetFileName(const std::string& fileName) { + g_mDoMemCd_control.setFileName(fileName); +} + +inline const char* mDoMemCd_GetFileName() { + return g_mDoMemCd_control.getFileName(); +} +#endif + #endif /* M_DO_M_DO_MEMCARD_H */ diff --git a/res/rml/window.rcss b/res/rml/window.rcss index ab338e97c7..8ca2f80760 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -519,3 +519,13 @@ progress.verification-progress-bar { flex: 0 0 auto; padding-top: 4dp; } + +.modal-actions-vertical { + flex-direction: column; + align-items: stretch; +} + +.modal-actions-vertical button.modal-btn { + flex: 0 0 auto; + width: 100%; +} diff --git a/sdk/include/mods/svc/gamemode.h b/sdk/include/mods/svc/gamemode.h new file mode 100644 index 0000000000..2132002578 --- /dev/null +++ b/sdk/include/mods/svc/gamemode.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#define GAMEMODE_SERVICE_ID "dev.twilitrealm.dusklight.gamemode" +#define GAMEMODE_SERVICE_MAJOR 1u +#define GAMEMODE_SERVICE_MINOR 0u + +typedef struct { + const char* gamemodeId; + const char* fullName; + const char* saveName; + void (*onActivatedFunction)(); + void (*onDeactivatedFunction)(); + void (*onPlayFunction)(); + void (*onSaveLoadedFunction)(); + void (*onNewSaveFunction)(); + void (*onGameResetFunction)(); + void (*onTickFunction)(); +} GamemodeDesc; + +typedef struct GamemodeService { + ServiceHeader header; + ModResult (*register_gamemode)(ModContext* ctx, const GamemodeDesc* desc); + ModResult (*unregister_gamemode)(ModContext* ctx, const char* id); + ModResult (*is_active)(ModContext* ctx, const char* gamemodeId, 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 diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index b1eb71a85d..2c6fcac2b3 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,9 +23,12 @@ #include "d/actor/d_a_npc_tkc.h" #include +#ifdef TARGET_PC #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" #include "dusk/speedrun.h" +#include "dusk/gamemode.hpp" +#endif BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); @@ -4009,9 +4012,9 @@ int daAlink_c::procGanonFinishInit() { onEndResetFlg1(ERFLG1_SHIELD_BACKBONE); #if TARGET_PC - if (dusk::getSettings().game.speedrunMode) { - if (dusk::m_speedrunInfo.m_isRunStarted) { - dusk::m_speedrunInfo.stopRun(); + if (dusk::speedrun::isActive()) { + if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) { + dusk::speedrun::g_speedrunInfo.stopRun(); } } #endif diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index ccb04c69e5..cef3a5adf4 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -9,11 +9,15 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" +#include "m_Do/m_Do_controller_pad.h" + +#ifdef TARGET_PC +#include #include "dusk/livesplit.h" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/speedrun.h" -#include "m_Do/m_Do_controller_pad.h" -#include +#include "dusk/gamemode.hpp" +#endif dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { mArchive = i_archive; @@ -143,12 +147,10 @@ void dBrightCheck_c::modeMove() { if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); #ifdef TARGET_PC - if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { - // start a new run if a run isn't already in progress - if (!dusk::m_speedrunInfo.m_isRunStarted) { - dusk::resetForSpeedrunMode(); - dusk::m_speedrunInfo.startRun(); - dusk::speedrun::start(); + if (!dusk::getSettings().game.hideTvSettingsScreen) { + const dusk::gamemode::Gamemode* gamemode = dusk::gamemode::getGamemodeManager().getCurrentGamemode(); + if (gamemode) { + gamemode->mOnSaveLoadedFunction(); } } diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index d0e33adce4..6136353e16 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -9,11 +9,6 @@ #include "d/d_com_inf_game.h" #include "d/d_meter2_info.h" #include "d/d_s_name.h" -#include "dusk/imgui/ImGuiConsole.hpp" -#include "dusk/livesplit.h" -#include "dusk/memory.h" -#include "dusk/speedrun.h" -#include "dusk/settings.h" #include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" @@ -21,7 +16,16 @@ #include "m_Do/m_Do_machine.h" #include "m_Do/m_Do_main.h" #include "m_Do/m_Do_mtx.h" -#include + +#ifdef TARGET_PC +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/memory.h" +#include "dusk/speedrun.h" +#include "dusk/settings.h" +#include "dusk/autosave.h" +#include "dusk/gamemode.hpp" +#endif #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -418,12 +422,10 @@ void dScnName_c::changeGameScene() { dComIfGs_setRestartRoomParam(0); #if TARGET_PC - if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) { - // start a new run on file load if a run isn't already in progress - if (!dusk::m_speedrunInfo.m_isRunStarted) { - dusk::resetForSpeedrunMode(); - dusk::m_speedrunInfo.startRun(); - dusk::speedrun::start(); + if (dusk::getSettings().game.hideTvSettingsScreen) { + const dusk::gamemode::Gamemode* gamemode = dusk::gamemode::getGamemodeManager().getCurrentGamemode(); + if (gamemode) { + gamemode->mOnSaveLoadedFunction(); } } diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index 22729ad43e..80923fd011 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -29,6 +29,7 @@ #if TARGET_PC #include "dusk/settings.h" +#include "dusk/gamemode.hpp" #include #include "helpers/string.hpp" @@ -1531,6 +1532,13 @@ void dSv_save_c::init() { mEvent.init(); mMiniGame.init(); + +#ifdef TARGET_PC + const dusk::gamemode::Gamemode* gamemode = dusk::gamemode::getGamemodeManager().getCurrentGamemode(); + if (gamemode) { + gamemode->mOnNewSaveFunction(); + } +#endif } dSv_memory2_c* dSv_save_c::getSave2(int i_stage2No) { diff --git a/src/dusk/OSThread.cpp b/src/dusk/OSThread.cpp index 7b97026e13..ac86a0ecce 100644 --- a/src/dusk/OSThread.cpp +++ b/src/dusk/OSThread.cpp @@ -509,6 +509,12 @@ BOOL OSJoinThread(OSThread* thread, void** val) { *(s32*)val = (s32)(intptr_t)thread->val; } thread->state = 0; + + { + std::lock_guard mapLock(GetThreadDataMutex()); + GetThreadDataMap().erase(thread); + } + sActiveThreadCount--; return 1; } return 0; diff --git a/src/dusk/gamemode.cpp b/src/dusk/gamemode.cpp new file mode 100644 index 0000000000..c3b62d7c75 --- /dev/null +++ b/src/dusk/gamemode.cpp @@ -0,0 +1,93 @@ +#include "dusk/gamemode.hpp" +#include "dusk/config.hpp" +#include "JSystem/JUtility/JUTGamePad.h" +#include "aurora/lib/logging.hpp" +#include "m_Do/m_Do_MemCard.h" +#include "dusk/ui/prelaunch.hpp" + +namespace dusk::gamemode { + +GamemodeManager g_GamemodeManager; + +aurora::Module DuskGamemodeLog("dusk::gamemode"); + +GamemodeManager::GamemodeManager() { + registerGamemode(Gamemode("vanilla","Vanilla","gczelda2")); + + mCurrentGamemodeId = "vanilla"; +} + +void GamemodeManager::setGamemodeToPrevious() { + // Gets the value from the settings of the last played gamemode id and sets that to the current gamemode (if registered) + GamemodeId id = dusk::getSettings().game.lastSelectedGamemodeId; + if (mRegisteredGamemodes.find(id) == mRegisteredGamemodes.end()) { + setCurrentGamemode("vanilla"); + return; + } + setCurrentGamemode(id); +} + +void GamemodeManager::registerGamemode(const Gamemode& gamemode) { + if (gamemode.getId().empty()) { + DuskGamemodeLog.fatal("No gamemode id specified in GamemodeManager::registerGamemode!"); + } + if (gamemode.getSaveName().empty()) { + DuskGamemodeLog.fatal("No save name provided for gamemode {}", gamemode.getId()); + } + if (gamemode.getFullName().empty()) { + DuskGamemodeLog.fatal("No Name Specified for gamemode {}", gamemode.getId()); + } + + if (mRegisteredGamemodes.find(gamemode.getId()) != mRegisteredGamemodes.end()) { + DuskGamemodeLog.warn("Attempting to register gamemode {} when it is already registered!", gamemode.getId()); + return; + } + + mRegisteredGamemodes.emplace(gamemode.getId(),gamemode); + dusk::ui::Prelaunch::rebuild_menu_buttons(); +} + +void GamemodeManager::unregisterGamemode(const GamemodeId& gamemodeId) { + const auto& it = mRegisteredGamemodes.find(gamemodeId); + if (it == mRegisteredGamemodes.end()) { + DuskGamemodeLog.warn( + "Attempting to unregister gamemode of id {} that isn't registered!", gamemodeId); + return; + } + + if (mCurrentGamemodeId == gamemodeId) { + // We need to be careful if we are unregistering a running gamemode, the easiest way is just + // to reset the game back to title as vanilla; + ui::prelaunch_state().showPrelaunchOnReset = true; + JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; + setCurrentGamemode("vanilla"); + } + mRegisteredGamemodes.erase(it); + dusk::ui::Prelaunch::rebuild_menu_buttons(); +} + +void GamemodeManager::setCurrentGamemode(const GamemodeId& id) { + if (mCurrentGamemodeId == id) { + return; + } + const Gamemode* currentGamemode = getCurrentGamemode(); + if (currentGamemode) { + currentGamemode->mOnDeactivatedFunction(); + } + if (mRegisteredGamemodes.find(id) == mRegisteredGamemodes.end()) { + DuskGamemodeLog.warn("Attempting to set current game mode to {} when it hasn't been registered!", id); + } + + mCurrentGamemodeId = id; + dusk::getSettings().game.lastSelectedGamemodeId.setValue(id); + dusk::config::save(); + + currentGamemode = getCurrentGamemode(); + if (currentGamemode) { + // Set the loaded save file to our gamemode's save name + mDoMemCd_SetFileName(currentGamemode->mSaveName); + currentGamemode->mOnActivatedFunction(); + } +} + +}; // namespace dusk::gamemode diff --git a/src/dusk/gamemode.hpp b/src/dusk/gamemode.hpp new file mode 100644 index 0000000000..6a48807d56 --- /dev/null +++ b/src/dusk/gamemode.hpp @@ -0,0 +1,69 @@ +#pragma once +#include +#include + +namespace dusk::gamemode { +using GamemodeId = std::string; + +// This class holds the definition for the gamemode and various function pointers to call +class Gamemode { +public: + Gamemode(const GamemodeId& id, const std::string& fullName, const std::string& saveName) { + mId = id; + mFullName = fullName; + mSaveName = saveName; + } + const GamemodeId& getId() const { return mId; } + const std::string& getFullName() const { return mFullName; } + const std::string& getSaveName() const { return mSaveName; } + + GamemodeId mId; + std::string mFullName; + std::string mSaveName; + + std::function mOnActivatedFunction = gamemodeStub; + std::function mOnDeactivatedFunction = gamemodeStub; + std::function mOnPlayFunction = gamemodeStub; + std::function mOnSaveLoadedFunction = gamemodeStub; + std::function mOnNewSaveFunction = gamemodeStub; + std::function mOnGameResetFunction = gamemodeStub; + std::function mOnTickFunction = gamemodeStub; + +private: + static void gamemodeStub() {} +}; + +class GamemodeManager { +public: + GamemodeManager(); + void registerGamemode(const Gamemode& gamemode); + void unregisterGamemode(const GamemodeId& gamemodeId); + + const Gamemode* getCurrentGamemode() const { + const auto& it = mRegisteredGamemodes.find(mCurrentGamemodeId); + return it != mRegisteredGamemodes.end() ? &it->second : &mRegisteredGamemodes.at("vanilla"); + } + bool isCurrentGamemode(const GamemodeId& id) const { + const Gamemode* gamemode = getCurrentGamemode(); + if (gamemode && gamemode->getId() == id) { + return true; + } + return false; + } + void setCurrentGamemode(const GamemodeId& id); + void setGamemodeToPrevious(); + + std::map& getRegisteredGamemodes() { return mRegisteredGamemodes; } + +private: + GamemodeId mCurrentGamemodeId; + std::map mRegisteredGamemodes; +}; + +extern GamemodeManager g_GamemodeManager; + +inline GamemodeManager& getGamemodeManager() { + return g_GamemodeManager; +} + +}; // namespace dusk::gamemode diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 3c0131c729..9e0e3fc6f8 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -22,6 +22,7 @@ #include "dusk/main.h" #include "dusk/settings.h" #include "dusk/ui/ui.hpp" +#include "dusk/gamemode.hpp" #include "f_pc/f_pc_manager.h" #include "f_pc/f_pc_name.h" #include "m_Do/m_Do_controller_pad.h" @@ -285,7 +286,7 @@ namespace dusk { if (dusk::IsGameLaunched && !m_isLaunchInitialized) { m_isLaunchInitialized = true; - if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) { + if (dusk::speedrun::isActive() && getSettings().game.liveSplitEnabled) { dusk::speedrun::connectLiveSplit(); } } @@ -357,7 +358,7 @@ namespace dusk { m_menuTools.ShowInputViewer(); - if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) { + if (dusk::IsGameLaunched && !dusk::speedrun::isActive()) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); m_menuTools.ShowProcessManager(); diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index b9ad5eb021..d0e1ad6999 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -14,6 +14,7 @@ #include "d/d_com_inf_game.h" #include "dusk/data.hpp" #include "dusk/dusk.h" +#include "dusk/speedrun.h" #include "dusk/main.h" #include "dusk/os.h" #include "m_Do/m_Do_main.h" @@ -38,7 +39,7 @@ namespace dusk { ImGui::BeginDisabled(); } - ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::BeginDisabled(dusk::speedrun::isActive()); ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); @@ -60,7 +61,7 @@ namespace dusk { } if (ImGui::BeginMenu("Debug")) { - ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::BeginDisabled(dusk::speedrun::isActive()); bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index 176284fd02..b814b604ae 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -2,6 +2,7 @@ #define DUSK_ISO_VALIDATE_HPP #include +#include "dusk/settings.h" namespace dusk::iso { struct KnownDisc; diff --git a/src/dusk/livesplit.cpp b/src/dusk/livesplit.cpp index 80c29379e5..d583ef75fa 100644 --- a/src/dusk/livesplit.cpp +++ b/src/dusk/livesplit.cpp @@ -1,46 +1,48 @@ #if _WIN32 - #include - #include - using socket_t = SOCKET; - static void closeSocket(socket_t s) { - LINGER li{1, 0}; - setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast(&li), sizeof(li)); - closesocket(s); - } - static int socketError(socket_t s) { - int err = 0; int len = sizeof(err); - getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); - return err; - } - static constexpr int kSendFlags = 0; +#include +#include +using socket_t = SOCKET; +static void closeSocket(socket_t s) { + LINGER li{1, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast(&li), sizeof(li)); + closesocket(s); +} +static int socketError(socket_t s) { + int err = 0; + int len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); + return err; +} +static constexpr int kSendFlags = 0; #else - #include - #include - #include - #include - #include - #include - #include - using socket_t = int; - static void closeSocket(socket_t s) { - struct linger li{1, 0}; - setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li)); - close(s); - } - static int socketError(socket_t s) { - int err = 0; socklen_t len = sizeof(err); - getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len); - return err; - } - #ifndef INVALID_SOCKET - #define INVALID_SOCKET -1 - #endif +#include +#include +#include +#include +#include +#include +#include +using socket_t = int; +static void closeSocket(socket_t s) { + struct linger li{1, 0}; + setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li)); + close(s); +} +static int socketError(socket_t s) { + int err = 0; + socklen_t len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len); + return err; +} +#ifndef INVALID_SOCKET +#define INVALID_SOCKET -1 +#endif - #if defined(__APPLE__) - static constexpr int kSendFlags = 0; - #else - static constexpr int kSendFlags = MSG_NOSIGNAL; - #endif +#if defined(__APPLE__) +static constexpr int kSendFlags = 0; +#else +static constexpr int kSendFlags = MSG_NOSIGNAL; +#endif #endif #include @@ -49,18 +51,18 @@ namespace dusk::speedrun { -static bool running = false; -static bool startPending = false; -static uint64_t frameCount = 0; -static socket_t sock = INVALID_SOCKET; -static bool wasLoading = false; -static bool connected = false; -static bool connectPending = false; -static bool disconnectPending = false; -static uint32_t idleProbeCounter = 0; -static uint32_t reconnectCounter = 0; -static char storedHost[64] = "127.0.0.1"; -static int storedPort = 16834; +static bool running = false; +static bool startPending = false; +static uint64_t frameCount = 0; +static socket_t sock = INVALID_SOCKET; +static bool wasLoading = false; +static bool connected = false; +static bool connectPending = false; +static bool disconnectPending = false; +static uint32_t idleProbeCounter = 0; +static uint32_t reconnectCounter = 0; +static char storedHost[64] = "127.0.0.1"; +static int storedPort = 16834; static void sendCmd(const char* cmd) { if (sock == INVALID_SOCKET) { @@ -122,9 +124,11 @@ void onGameFrame() { } void start() { - if (running) { + if (g_speedrunInfo.m_isRunStarted || running) { return; } + resetForSpeedrunMode(); + g_speedrunInfo.startRun(); running = true; startPending = true; @@ -214,8 +218,16 @@ void disconnectLiveSplit() { connected = connectPending = disconnectPending = false; } -bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; } -bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending = false; return v; } +bool consumeConnectedEvent() { + bool v = connectPending; + connectPending = false; + return v; +} +bool consumeDisconnectedEvent() { + bool v = disconnectPending; + disconnectPending = false; + return v; +} void updateLiveSplit() { if (sock == INVALID_SOCKET) { @@ -267,7 +279,8 @@ void updateLiveSplit() { #else || (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK) #endif - ) { + ) + { if (connected) { disconnectPending = true; } @@ -280,15 +293,12 @@ void updateLiveSplit() { return; } - const uint64_t totalMs = frameCount * 1000 / 30; + const uint64_t totalMs = frameCount * 1000 / 30; const uint64_t totalSec = totalMs / 1000; char cmd[32]; snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u", - static_cast(totalSec / 3600), - static_cast((totalSec / 60) % 60), - static_cast(totalSec % 60), - static_cast(totalMs % 1000) - ); + static_cast(totalSec / 3600), static_cast((totalSec / 60) % 60), + static_cast(totalSec % 60), static_cast(totalMs % 1000)); sendCmd(cmd); } @@ -299,4 +309,4 @@ void shutdown() { #endif } -} +} // namespace dusk::speedrun diff --git a/src/dusk/livesplit.h b/src/dusk/livesplit.h index b283a29af4..b803e9b50b 100644 --- a/src/dusk/livesplit.h +++ b/src/dusk/livesplit.h @@ -1,6 +1,8 @@ #pragma once #include +#include "dusk/gamemode.hpp" +#include "dusk/speedrun.h" namespace dusk::speedrun { void onGameFrame(); diff --git a/src/dusk/main.h b/src/dusk/main.h index 1378e48a1b..e3db4420d1 100644 --- a/src/dusk/main.h +++ b/src/dusk/main.h @@ -1,7 +1,6 @@ #pragma once #include - namespace dusk { extern bool IsRunning; @@ -21,8 +20,6 @@ struct StageRequest { }; extern StageRequest StageRequested; - - #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ (defined(TARGET_OS_TV) && TARGET_OS_TV) inline constexpr bool SupportsProcessRestart = false; diff --git a/src/dusk/mods/svc/gamemode.cpp b/src/dusk/mods/svc/gamemode.cpp new file mode 100644 index 0000000000..d46e9f7cda --- /dev/null +++ b/src/dusk/mods/svc/gamemode.cpp @@ -0,0 +1,144 @@ +#include "mods/svc/gamemode.h" +#include "dusk/gamemode.hpp" + +#include "config.hpp" +#include "registry.hpp" +#include "slot_map.hpp" + +#include "aurora/lib/logging.hpp" +#include "dusk/mod_loader.hpp" + +#include + + +namespace dusk::mods::svc::gamemode_impl { +namespace { + +aurora::Module Log("dusk::mods::gamemode"); + +// These track which gamemodes are registered by which mods, allowing us to automatically unregister them +std::unordered_map> s_gamemodesRegisteredToMods; + +std::string get_mod_gamemode_id(ModContext* ctx, const std::string& id) { + return id + "_" + ctx->mod->metadata.id; +} + +void gamemode_remove_mod(LoadedMod& mod) { + const auto it = s_gamemodesRegisteredToMods.find(mod.metadata.id); + if (it != s_gamemodesRegisteredToMods.end()) { + for (const auto& id : it->second) { + dusk::gamemode::getGamemodeManager().unregisterGamemode(id); + } + s_gamemodesRegisteredToMods.erase(it); + } +} +} // namespace + + +ModResult register_gamemode(ModContext* ctx, const GamemodeDesc* desc) { + std::string id; + if (!desc->gamemodeId) { + Log.error("Attempted to register a gamemode with a null id!"); + return MOD_ERROR; + } + id = desc->gamemodeId; + if (id.empty()) { + Log.error("Attempted to register a gamemode with an empty id!"); + return MOD_ERROR; + } + id = get_mod_gamemode_id(ctx, id); // Append the mod id to the end of the gamemode id to ensure they are unique + + std::string fullName; + if (!desc->fullName) { + Log.warn("Attempted to register gamemode {} with a null full name! Defaulting to: ({})",id,id); + fullName = id; + }else{ + fullName = desc->fullName; + if (fullName.empty()) { + Log.warn("Attempted to register gamemode {} with an empty full name! Defaulting to: ({})",id,id); + fullName = id; + } + } + + std::string saveName; + if (!desc->saveName) { + Log.warn("Attempted to register gamemode {} with a null save name! Defaulting to: (gczelda2)",id); + saveName = "gczelda2"; + }else{ + saveName = desc->saveName; + if (saveName.empty()) { + Log.warn("Attempted to register gamemode {} with an empty save name! Defaulting to: (gczelda2)",id); + saveName = "gczelda2"; + } + } + + dusk::gamemode::Gamemode gamemode(id, fullName, saveName); + + if (desc->onActivatedFunction) { + gamemode.mOnActivatedFunction = desc->onActivatedFunction; + } + if (desc->onDeactivatedFunction) { + gamemode.mOnDeactivatedFunction = desc->onDeactivatedFunction; + } + if (desc->onPlayFunction) { + gamemode.mOnPlayFunction = desc->onPlayFunction; + } + if (desc->onSaveLoadedFunction) { + gamemode.mOnSaveLoadedFunction = desc->onSaveLoadedFunction; + } + if (desc->onNewSaveFunction) { + gamemode.mOnNewSaveFunction = desc->onNewSaveFunction; + } + if (desc->onGameResetFunction) { + gamemode.mOnGameResetFunction = desc->onGameResetFunction; + } + if (desc->onTickFunction) { + gamemode.mOnTickFunction = desc->onTickFunction; + } + + dusk::gamemode::getGamemodeManager().registerGamemode(gamemode); + + const auto it = s_gamemodesRegisteredToMods.find(ctx->mod->metadata.id); + if (it == s_gamemodesRegisteredToMods.end()) { + std::vector registeredGamemodes = {id}; + s_gamemodesRegisteredToMods.emplace(ctx->mod->metadata.id, registeredGamemodes); + }else { + it->second.push_back(id); + } + + return MOD_OK; +} + +ModResult unregister_gamemode(ModContext* ctx, const char* id) { + dusk::gamemode::getGamemodeManager().unregisterGamemode(get_mod_gamemode_id(ctx,id)); + return MOD_OK; +} + +ModResult is_active(ModContext* ctx, const char* gamemodeId, bool* out_active) { + *out_active = dusk::gamemode::getGamemodeManager().isCurrentGamemode(get_mod_gamemode_id(ctx,gamemodeId)); + return MOD_OK; +} + +} + +namespace dusk::mods::svc { +namespace { + +constexpr GamemodeService s_gamemodeService{ + .header = SERVICE_HEADER(GamemodeService, GAMEMODE_SERVICE_MAJOR, GAMEMODE_SERVICE_MINOR), + .register_gamemode = gamemode_impl::register_gamemode, + .unregister_gamemode = gamemode_impl::unregister_gamemode, + .is_active = gamemode_impl::is_active +}; + +} + +constinit const ServiceModule g_gamemodeModule{ + .id = GAMEMODE_SERVICE_ID, + .majorVersion = GAMEMODE_SERVICE_MAJOR, + .minorVersion = GAMEMODE_SERVICE_MINOR, + .service = &s_gamemodeService, + .modDetached = gamemode_impl::gamemode_remove_mod, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 2033d47475..a003eacc93 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -210,6 +210,7 @@ void ModLoader::init_services() { &svc::g_gameModule, &svc::g_cameraModule, &svc::g_gfxModule, + &svc::g_gamemodeModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 191626e0ef..25cdfdaab7 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -72,5 +72,6 @@ extern const ServiceModule g_uiModule; extern const ServiceModule g_gameModule; extern const ServiceModule g_cameraModule; extern const ServiceModule g_gfxModule; +extern const ServiceModule g_gamemodeModule; } // namespace dusk::mods::svc diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index e2a2169485..f44f788fdd 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -154,7 +154,8 @@ UserSettings g_userSettings = { .recordingMode {"game.recordingMode", false}, .removeQuestMapMarkers {"game.removeQuestMapMarkers", false}, .showInputViewer {"game.showInputViewer", false}, - .showInputViewerGyro {"game.showInputViewerGyro", false} + .showInputViewerGyro {"game.showInputViewerGyro", false}, + .lastSelectedGamemodeId {"game.lastSelectedGamemodeId", "vanilla"} }, .backend = { @@ -302,6 +303,7 @@ void registerSettings() { Register(g_userSettings.game.removeQuestMapMarkers); Register(g_userSettings.game.showInputViewer); Register(g_userSettings.game.showInputViewerGyro); + Register(g_userSettings.game.lastSelectedGamemodeId); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); Register(g_userSettings.game.infiniteArrows); diff --git a/src/dusk/settings.h b/src/dusk/settings.h index 326f7f321c..43247fa23f 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -282,6 +282,8 @@ struct UserSettings { ConfigVar removeQuestMapMarkers; ConfigVar showInputViewer; ConfigVar showInputViewerGyro; + + ConfigVar lastSelectedGamemodeId; } game; struct { diff --git a/src/dusk/speedrun.cpp b/src/dusk/speedrun.cpp index 275a8de900..3cf0c6184b 100644 --- a/src/dusk/speedrun.cpp +++ b/src/dusk/speedrun.cpp @@ -3,10 +3,37 @@ #include "dusk/config.hpp" #include "m_Do/m_Do_main.h" #include +#include "dusk/gamemode.hpp" +#include "dusk/livesplit.h" -namespace dusk { +namespace dusk::speedrun { -SpeedrunInfo m_speedrunInfo; +SpeedrunInfo g_speedrunInfo; + +static void onSpeedrunModeActive() { + resetForSpeedrunMode(); +} + +static void onSpeedrunModeDeactive() { + restoreFromSpeedrunMode(); + if (getSettings().game.liveSplitEnabled) { + speedrun::disconnectLiveSplit(); + } +} + +void registerSpeedrunGamemode() { + dusk::gamemode::Gamemode speedrunGamemode(DUSK_SPEEDRUN_GAMEMODE_ID,"Speedrun","gczelda2-speedrun"); + speedrunGamemode.mOnSaveLoadedFunction = dusk::speedrun::start; + speedrunGamemode.mOnActivatedFunction = onSpeedrunModeActive; + speedrunGamemode.mOnDeactivatedFunction = onSpeedrunModeDeactive; + speedrunGamemode.mOnTickFunction = dusk::speedrun::onGameFrame; + + dusk::gamemode::getGamemodeManager().registerGamemode(speedrunGamemode); +} + +void unregisterSpeedrunGamemode() { + dusk::gamemode::getGamemodeManager().unregisterGamemode(DUSK_SPEEDRUN_GAMEMODE_ID); +} void resetForSpeedrunMode() { mDoMain::developmentMode = -1; diff --git a/src/dusk/speedrun.h b/src/dusk/speedrun.h index d9e43c3ed7..f915ff6981 100644 --- a/src/dusk/speedrun.h +++ b/src/dusk/speedrun.h @@ -1,7 +1,10 @@ #pragma once #include +#include "dusk/gamemode.hpp" -namespace dusk { +#define DUSK_SPEEDRUN_GAMEMODE_ID "vanilla_speedrun" + +namespace dusk::speedrun { struct SpeedrunInfo { void startRun() { @@ -34,9 +37,15 @@ struct SpeedrunInfo { OSTime m_igtTimer = 0; }; -extern SpeedrunInfo m_speedrunInfo; +extern SpeedrunInfo g_speedrunInfo; +void registerSpeedrunGamemode(); +void unregisterSpeedrunGamemode(); void resetForSpeedrunMode(); void restoreFromSpeedrunMode(); +inline bool isActive() { + return dusk::gamemode::getGamemodeManager().isCurrentGamemode(DUSK_SPEEDRUN_GAMEMODE_ID); +} + } // namespace dusk diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 0c6528e424..018f02e47e 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -8,6 +8,7 @@ #include "achievements.hpp" #include "aurora/rmlui.hpp" #include "dusk/livesplit.h" +#include "dusk/gamemode.hpp" #include "dusk/main.h" #include "dusk/mods/svc/ui.hpp" #include "dusk/settings.h" @@ -20,6 +21,7 @@ #include "mods_window.hpp" #include "settings.hpp" #include "ui.hpp" +#include "dusk/ui/prelaunch.hpp" #include "warp.hpp" #include "window.hpp" @@ -93,9 +95,10 @@ MenuBar::MenuBar() dismiss(modal); return; } + prelaunch_state().showPrelaunchOnReset = true; JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; dismiss(modal); - hide(false); + Document::hide(true); }, }, }, @@ -134,11 +137,11 @@ MenuBar::MenuBar() })); }); - if (getSettings().game.speedrunMode) { + if (dusk::speedrun::isActive()) { mTabBar->add_tab("Reset Timer", [this] { mTabBar->set_active_tab(-1); mDoAud_seStartMenu(kSoundClick); - m_speedrunInfo.reset(); + dusk::speedrun::g_speedrunInfo.reset(); if (getSettings().game.liveSplitEnabled) { dusk::speedrun::reset(); } diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp index 8d9956ca13..db4e04ba88 100644 --- a/src/dusk/ui/modal.cpp +++ b/src/dusk/ui/modal.cpp @@ -25,6 +25,10 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo auto* actions = append(mDialog, "div"); actions->SetClass("modal-actions", true); + if (props.isVertical) { + actions->SetClass("modal-actions-vertical", true); + }else{ + } for (auto& action : mProps.actions) { add_action(std::move(action)); @@ -87,9 +91,11 @@ bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) { } int direction = 0; - if (cmd == NavCommand::Left) { + NavCommand prevCommand = mProps.isVertical ? NavCommand::Up : NavCommand::Left; + NavCommand nextCommand = mProps.isVertical ? NavCommand::Down : NavCommand::Right; + if (cmd == prevCommand) { direction = -1; - } else if (cmd == NavCommand::Right) { + } else if (cmd == nextCommand) { direction = 1; } else { return false; diff --git a/src/dusk/ui/modal.hpp b/src/dusk/ui/modal.hpp index 400f55bd64..dd2e79ff91 100644 --- a/src/dusk/ui/modal.hpp +++ b/src/dusk/ui/modal.hpp @@ -20,6 +20,7 @@ public: std::function onDismiss; Rml::String variant; Rml::String icon = ""; + bool isVertical; }; explicit Modal(Props props); diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index d2bd3fe6c1..c987f660ac 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -289,7 +289,7 @@ void Overlay::update() { update_pipeline_progress(); #if !(defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST)) - if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) { + if (dusk::speedrun::isActive() && getSettings().game.liveSplitEnabled) { dusk::speedrun::updateLiveSplit(); if (dusk::speedrun::consumeConnectedEvent()) { push_toast({.title = "LiveSplit connected", .duration = std::chrono::seconds(3)}); @@ -301,33 +301,33 @@ void Overlay::update() { #endif if (mSpeedrunTimer != nullptr && mSpeedrunRta != nullptr && mSpeedrunIgt != nullptr) { - if (getSettings().game.speedrunMode) { + if (dusk::speedrun::isActive()) { // L+R+A+Start to reset timer if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { - m_speedrunInfo.reset(); + dusk::speedrun::g_speedrunInfo.reset(); } // L+R+A+Y to manually stop timer if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { - if (m_speedrunInfo.m_isRunStarted) { - m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; - m_speedrunInfo.m_isRunStarted = false; + if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) { + dusk::speedrun::g_speedrunInfo.m_endTimestamp = OSGetTime() - dusk::speedrun::g_speedrunInfo.m_startTimestamp; + dusk::speedrun::g_speedrunInfo.m_isRunStarted = false; } } OSTime elapsedTime = 0; - if (m_speedrunInfo.m_isRunStarted) { - elapsedTime = OSGetTime() - m_speedrunInfo.m_startTimestamp; - } else if (m_speedrunInfo.m_endTimestamp != 0) { - elapsedTime = m_speedrunInfo.m_endTimestamp; + if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) { + elapsedTime = OSGetTime() - dusk::speedrun::g_speedrunInfo.m_startTimestamp; + } else if (dusk::speedrun::g_speedrunInfo.m_endTimestamp != 0) { + elapsedTime = dusk::speedrun::g_speedrunInfo.m_endTimestamp; } - if (!m_speedrunInfo.m_isPauseIGT) { - m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime; + if (!dusk::speedrun::g_speedrunInfo.m_isPauseIGT) { + dusk::speedrun::g_speedrunInfo.m_igtTimer = elapsedTime - dusk::speedrun::g_speedrunInfo.m_totalLoadTime; } mSpeedrunTimer->SetAttribute("open", ""); @@ -340,7 +340,7 @@ void Overlay::update() { } mSpeedrunIgt->SetInnerRML( - escape(fmt::format("IGT {}", FormatTime(m_speedrunInfo.m_igtTimer)))); + escape(fmt::format("IGT {}", FormatTime(dusk::speedrun::g_speedrunInfo.m_igtTimer)))); } else { mSpeedrunTimer->RemoveAttribute("open"); } diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 8379d4bef0..2d885b8373 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -3,9 +3,11 @@ #include "dusk/config.hpp" #include "dusk/data.hpp" #include "dusk/file_select.hpp" +#include "dusk/gamemode.hpp" #include "dusk/iso_validate.hpp" #include "dusk/main.h" #include "dusk/settings.h" +#include "dusk/ui/menu_bar.hpp" #include "dusk/update_check.hpp" #include "modal.hpp" #include "mods_window.hpp" @@ -685,61 +687,42 @@ void try_apply_mirrored_layout(Rml::Element* body) { body->SetClass("mirrored", getSettings().game.enableMirrorMode.getValue()); } -Prelaunch::Prelaunch() - : Document(kDocumentSource, false, DocumentScope::Prelaunch), - mRoot(mDocument->GetElementById("root")) { +void Prelaunch::rebuild_menu_buttons() { + for (auto& doc : get_document_stack()) { + if (auto* prelaunch = dynamic_cast(doc.get())) { + auto* menuList = prelaunch->mDocument->GetElementById("menu-list"); + while (menuList->GetNumChildren() > 0) { + menuList->RemoveChild(menuList->GetChild(0)); + } + prelaunch->mMenuButtons.clear(); + prelaunch->build_menu_buttons(); + break; + } + } +} + +static std::string getPlayButtonText() { + auto& state = prelaunch_state(); + const bool activeDiscLoaded = !state.activeDiscPath.empty(); + if (activeDiscLoaded == false) { + return "Select Disc Image"; + } + std::string playText; + const dusk::gamemode::Gamemode* currentGameMode = + dusk::gamemode::getGamemodeManager().getCurrentGamemode(); + if (currentGameMode != nullptr) { + return currentGameMode->getId() == "vanilla" ? "Play" : + "Play " + currentGameMode->getFullName(); + } + return "Play"; +} + +Prelaunch::Prelaunch() : Document(kDocumentSource, false, DocumentScope::Prelaunch) { + mRoot = mDocument->GetElementById("root"); ensure_initialized(); begin_update_check(); - if (auto* menuList = mDocument->GetElementById("menu-list")) { - auto& state = prelaunch_state(); - const bool activeDiscLoaded = !state.activeDiscPath.empty(); - mMenuButtons.push_back( - std::make_unique