diff --git a/docs/modding.md b/docs/modding.md index ab100b5add..b4508d46d4 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -636,6 +636,125 @@ 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/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 `save_name`; leave it empty to use +the vanilla `gczelda2` save. + +```cpp +IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(HookService, svc_hook); +IMPORT_SERVICE(GameModeService, svc_game_mode); + +DEFINE_HOOK(fopAcM_createItem, CreateItem); + +#define MY_GAME_MODE_ID "game-mode-id" + +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_game_mode->is_active(mod_ctx, MY_GAME_MODE_ID, &active) == MOD_OK && active`. + return HOOK_CONTINUE; +} + +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, my_function_hook); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to install fopAcM_createItem hook"); + } + return MOD_OK; +} + +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) { + return mods::set_error(outError, result, "failed to uninstall fopAcM_createItem hook"); + } + return MOD_OK; +} + +ModResult on_save_loaded(void*, ModError*) { + // This function will be invoked by the game as a save is loaded + return MOD_OK; +} + +const GameModeDesc gameModeDesc = { + .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_game_mode->register_game_mode(mod_ctx, &gameModeDesc); +``` + +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_game_mode); +IMPORT_SERVICE(UiService, svc_ui); + +ModResult on_new_save_select(void*, GameModeNewSaveState* state, ModError* outError) { + static GameModeNewSaveState* newSaveState; + static UiWindowHandle windowHandle; + + newSaveState = state; + + UiTabDesc tabs[1]{}; + + tabs[0].struct_size = sizeof(UiTabDesc); + tabs[0].title = "Play"; + tabs[0].build = [](ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_BUTTON; + desc.label = "Play"; + desc.help_rml = "Play Button"; + desc.on_pressed = [](ModContext* ctx, void* userdata) { + *newSaveState = GAME_MODE_STATE_PROCEED; + svc_ui->window_close(ctx, *static_cast(userdata)); + }; + desc.user_data = &windowHandle; + svc_ui->pane_add_control(mod_ctx, leftPane, &desc, nullptr); + return MOD_OK; + }; + + UiWindowDesc desc = UI_WINDOW_DESC_INIT; + desc.tabs = tabs; + desc.tab_count = 1; + desc.on_closed = [](ModContext *, UiWindowHandle, void *userdata) { + // If closing the window through backing out, return to file select + if (*newSaveState == GAME_MODE_STATE_PENDING) { + *newSaveState = GAME_MODE_STATE_RETURN; + } + }; + + 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 = { + .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_game_mode->register_game_mode(mod_ctx, &gameModeDesc); + +``` + --- ## Hooking Game Functions diff --git a/files.cmake b/files.cmake index f63f0c81f5..da97c3779f 100644 --- a/files.cmake +++ b/files.cmake @@ -1434,6 +1434,7 @@ set(DUSK_FILES src/dusk/extras.c src/dusk/frame_interpolation.cpp src/dusk/game_clock.cpp + src/dusk/game_mode.cpp src/dusk/gamepad_color.cpp src/dusk/globals.cpp src/dusk/gyro.cpp @@ -1501,6 +1502,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/game_mode.cpp src/dusk/mods/svc/window.cpp src/dusk/mods/svc/window.hpp src/dusk/mods/svc/save.cpp diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index 634c0db352..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; @@ -420,6 +424,13 @@ public: bool pointerMenuSelect(); bool pointerCopyDataToSelect(); bool pointerYesNoSelect(bool errorSelect); + void backToDataSelectMove() { + headerTxtSet(0x43, 1, 0); + fileRecScaleAnmInitSet2(0.0f, 1.0f); + nameMoveAnmInitSet(0xd29, 0xd1f); + modoruTxtDispAnmInit(0); + mDataSelProc = DATASELPROC_NAME_TO_DATA_SELECT_MOVE; + } #endif void _draw(); void errorMoveAnmInitSet(int, int); @@ -733,6 +744,8 @@ public: #endif #ifdef TARGET_PC dDlst_FileSelFade_c mFadeDlst; + bool mGameModeSaveStartBuildUi = true; + GameModeNewSaveState mGameModeNewSaveState = GAME_MODE_STATE_PENDING; #endif #if PLATFORM_WII || PLATFORM_SHIELD 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/prelaunch.rcss b/res/rml/prelaunch.rcss index 054b6e0183..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; 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/game_mode.h b/sdk/include/mods/svc/game_mode.h new file mode 100644 index 0000000000..8ccc5d64f1 --- /dev/null +++ b/sdk/include/mods/svc/game_mode.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#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 { + 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* game_mode_id, bool* out_active); +} GameModeService; + +MOD_DECLARE_SERVICE(GameModeService, svc_game_mode, GAME_MODE_SERVICE_ID, GAME_MODE_SERVICE_MAJOR, + GAME_MODE_SERVICE_MINOR); diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index c38b430908..e8812c6e41 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,10 +23,13 @@ #include "d/actor/d_a_npc_tkc.h" #include +#ifdef TARGET_PC +#include "dusk/game_mode.hpp" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" #include "dusk/speedrun.h" #include "dusk/version.hpp" +#endif BOOL daAlink_c::checkEventRun() const { return dComIfGp_event_runCheck() || checkPlayerDemoMode(); @@ -4016,9 +4019,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 3d36e72232..7ec4f09b10 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 "dusk/livesplit.h" -#include "dusk/imgui/ImGuiConsole.hpp" -#include "dusk/speedrun.h" #include "m_Do/m_Do_controller_pad.h" + +#ifdef TARGET_PC #include +#include "dusk/game_mode.hpp" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/speedrun.h" +#endif #include "dusk/version.hpp" @@ -186,15 +190,6 @@ 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(); - } - } - toggleAutoSave(true); #endif mCompleteCheck = true; diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 936a368f81..e075e2b8a6 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -26,9 +26,10 @@ #include "dusk/version.hpp" #if TARGET_PC +#include "dusk/game_mode.hpp" #include "dusk/menu_pointer.h" -#include "helpers/string.hpp" #include "dusk/mods/svc/save.hpp" +#include "helpers/string.hpp" namespace { constexpr u8 pointer_target(u8 group, u8 index) noexcept { @@ -1310,12 +1311,42 @@ void dFile_select_c::selectDataOpenMove() { void dFile_select_c::selectDataNameMove() { bool isHeaderTxtChange = headerTxtChangeAnm(); bool isFileRecScale = fileRecScaleAnm2(); - bool isNameMove = nameMoveAnm(); + IF_NOT_DUSK(bool isNameMove = nameMoveAnm();) bool isModoruTxtDisp = modoruTxtDispAnm(); +#ifdef TARGET_PC + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + if (isHeaderTxtChange == true && isFileRecScale == true && isModoruTxtDisp == true) { + if (mGameModeSaveStartBuildUi) { + gameMode->invokeOnNewSaveSelectFunction(&mGameModeNewSaveState); + mGameModeSaveStartBuildUi = false; + } + if (mGameModeNewSaveState == GAME_MODE_STATE_RETURN) { + backToDataSelectMove(); + mGameModeSaveStartBuildUi = true; + mGameModeNewSaveState = GAME_MODE_STATE_PENDING; + return; + } + if (mGameModeNewSaveState != GAME_MODE_STATE_PROCEED) { + return; + } + } else { + return; + } + } +#endif + + IF_DUSK(bool isNameMove = nameMoveAnm();) + if (isHeaderTxtChange == true && isFileRecScale == true && isNameMove == true && isModoruTxtDisp == true) { +#ifdef TARGET_PC + mGameModeSaveStartBuildUi = true; + mGameModeNewSaveState = GAME_MODE_STATE_PENDING; +#endif mDataSelProc = DATASELPROC_NAME_INPUT_WAIT; } } @@ -1397,6 +1428,12 @@ void dFile_select_c::menuSelectStart() { dComIfGs_setDataNum(mSelectNum); #if TARGET_PC dusk::mods::svc::save_slot_loaded(mSelectNum, &mSaveData[mSelectNum]); + + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + gameMode->invokeOnSaveLoadedFunction(); + } #endif } else if (mSelectMenuNum == 0) { mSelIcon->setAlphaRate(0.0f); @@ -1750,6 +1787,12 @@ void dFile_select_c::nameInput2() { mIsSelectEnd = true; #if TARGET_PC dusk::mods::svc::save_slot_new(mSelectNum); + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + gameMode->invokeOnNewSaveFunction(); + gameMode->invokeOnSaveLoadedFunction(); + } #endif mDataSelProc = DATASELPROC_NEXT_MODE_WAIT; } diff --git a/src/d/d_s_logo.cpp b/src/d/d_s_logo.cpp index bd5b1f7783..48c7519ded 100644 --- a/src/d/d_s_logo.cpp +++ b/src/d/d_s_logo.cpp @@ -24,6 +24,7 @@ #include "JSystem/JUtility/JUTConsole.h" #ifdef TARGET_PC +#include "dusk/game_mode.hpp" #include "dusk/language.hpp" #include "dusk/logging.h" #include "dusk/main.h" @@ -808,6 +809,11 @@ void dScnLogo_c::nextSceneChange() { if (status == 1) { dusk::mods::svc::save_slot_loaded( saveSlot, buf + saveSlot * SAVEDATA_SIZE); + const dusk::gamemode::GameMode* gameMode = + dusk::gamemode::getGameModeManager().getCurrentGameMode(); + if (gameMode) { + gameMode->invokeOnSaveLoadedFunction(); + } } dComIfGs_gameStart(); diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index d0e33adce4..de66b0317d 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/autosave.h" +#include "dusk/game_mode.hpp" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/memory.h" +#include "dusk/settings.h" +#include "dusk/speedrun.h" +#endif #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -418,15 +422,6 @@ 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(); - } - } - toggleAutoSave(true); #endif } diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index e638d23ab1..7caba5c9c8 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -28,8 +28,9 @@ #endif #if TARGET_PC -#include "dusk/settings.h" #include +#include "dusk/game_mode.hpp" +#include "dusk/settings.h" #include "helpers/string.hpp" #define strcpy SafeStringCopy 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/achievements.cpp b/src/dusk/achievements.cpp index 6c4de43487..1c8f74f004 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -1,25 +1,26 @@ #include "dusk/achievements.h" -#include "dusk/io.hpp" -#include "dusk/main.h" -#include "d/d_com_inf_game.h" -#include "d/d_item_data.h" -#include "d/d_map_path_fmap.h" -#include "d/d_stage.h" -#include "d/d_menu_fmap.h" #include "JSystem/JKernel/JKRArchive.h" -#include "d/d_meter2_info.h" #include "d/actor/d_a_alink.h" -#include "d/actor/d_a_ni.h" -#include "d/actor/d_a_npc4.h" #include "d/actor/d_a_b_gnd.h" #include "d/actor/d_a_b_ob.h" +#include "d/actor/d_a_ni.h" +#include "d/actor/d_a_npc4.h" #include "d/actor/d_a_player.h" +#include "d/d_com_inf_game.h" #include "d/d_demo.h" +#include "d/d_item_data.h" +#include "d/d_map_path_fmap.h" +#include "d/d_menu_fmap.h" +#include "d/d_meter2_info.h" +#include "d/d_stage.h" +#include "dusk/game_mode.hpp" +#include "dusk/io.hpp" +#include "dusk/logging.h" +#include "dusk/main.h" +#include "dusk/speedrun.h" #include "dusk/ui/ui.hpp" -#include "f_pc/f_pc_name.h" #include "f_op/f_op_actor_mng.h" #include "f_pc/f_pc_name.h" -#include "dusk/logging.h" #include #include @@ -1302,6 +1303,12 @@ void AchievementSystem::processEntry(Entry& e) { } void AchievementSystem::tick() { + // Until we implement an AchievementService, achievements will be unavailible in custom gamemodes + if (dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::gamemode::kVanillaGameModeId) == false + && dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::speedrun::kSpeedrunGameModeId) == false) { + m_signals.clear(); + return; + } if (!m_loaded) { load(); } diff --git a/src/dusk/game_mode.cpp b/src/dusk/game_mode.cpp new file mode 100644 index 0000000000..8fdb3106f3 --- /dev/null +++ b/src/dusk/game_mode.cpp @@ -0,0 +1,97 @@ +#include "dusk/game_mode.hpp" +#include "JSystem/JUtility/JUTGamePad.h" +#include "aurora/lib/logging.hpp" +#include "dusk/config.hpp" +#include "dusk/ui/prelaunch.hpp" +#include "m_Do/m_Do_MemCard.h" + +namespace dusk::gamemode { +namespace { +aurora::Module Log("dusk::gamemode"); +} + +GameModeManager g_GameModeManager; + +GameModeManager::GameModeManager() { + GameMode vanilla{kVanillaGameModeId, "Vanilla"}; + mRegisteredGameModes.emplace(vanilla.getId(), std::move(vanilla)); + mCurrentGameModeId = kVanillaGameModeId; +} + +void GameModeManager::setGameModeToPrevious() { + // Restore the previously selected game mode if still registered. + GameModeId id = getSettings().game.lastSelectedGameModeId; + if (!mRegisteredGameModes.contains(id)) { + setCurrentGameMode(kVanillaGameModeId); + return; + } + setCurrentGameMode(id); +} + +void GameModeManager::registerGameMode(const GameMode& gameMode) { + if (gameMode.getId().empty()) { + Log.fatal("No game mode ID specified in GameModeManager::registerGameMode"); + } + if (gameMode.getFullName().empty()) { + Log.fatal("No display name specified for game mode {}", gameMode.getId()); + } + + if (mRegisteredGameModes.contains(gameMode.getId())) { + Log.warn("Attempting to re-register existing game mode {}", gameMode.getId()); + return; + } + + mRegisteredGameModes.emplace(gameMode.getId(), gameMode); + ui::Prelaunch::refresh_menu_buttons(); +} + +void GameModeManager::unregisterGameMode(const GameModeId& gameModeId) { + const auto& it = mRegisteredGameModes.find(gameModeId); + if (it == mRegisteredGameModes.end()) { + Log.warn("Attempting to unregister unknown game mode {}", gameModeId); + return; + } + + if (mCurrentGameModeId == gameModeId) { + // Reset to prelaunch before unloading callbacks belonging to the active mod. + ui::prelaunch_state().returnToPrelaunchOnReset = true; + JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; + setCurrentGameMode(kVanillaGameModeId); + } + mRegisteredGameModes.erase(it); + ui::Prelaunch::refresh_menu_buttons(); +} + +bool GameModeManager::setCurrentGameMode(const GameModeId& id) { + if (mCurrentGameModeId == id) { + return true; + } + if (!mRegisteredGameModes.contains(id)) { + Log.warn("Attempting to configure unknown game mode {}", id); + return false; + } + const GameMode* currentGameMode = getCurrentGameMode(); + if (currentGameMode) { + currentGameMode->invokeOnDeactivatedFunction(); + } + mCurrentGameModeId = id; + + currentGameMode = getCurrentGameMode(); + if (currentGameMode) { + mDoMemCd_SetFileName(currentGameMode->getSaveName()); + 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 new file mode 100644 index 0000000000..3c2b63a484 --- /dev/null +++ b/src/dusk/game_mode.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include "d/d_file_select.h" +#include "mods/svc/game_mode.h" + +#include +#include +#include +#include + +namespace dusk::gamemode { +using GameModeId = std::string; + +constexpr const char* kVanillaGameModeId = "vanilla"; +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)} {} + 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; + + bool invokeOnActivatedFunction() const { + if (mOnActivatedFunction) { + return mOnActivatedFunction(); + } + return true; + } + + bool invokeOnDeactivatedFunction() const { + if (mOnDeactivatedFunction) { + return mOnDeactivatedFunction(); + } + return true; + } + + bool invokeOnPlayFunction() const { + if (mOnPlayFunction) { + return mOnPlayFunction(); + } + return true; + } + + bool invokeOnSaveLoadedFunction() const { + if (mOnSaveLoadedFunction) { + return mOnSaveLoadedFunction(); + } + return true; + } + + bool invokeOnNewSaveFunction() const { + if (mOnNewSaveFunction) { + return mOnNewSaveFunction(); + } + return true; + } + + bool invokeOnNewSaveSelectFunction(GameModeNewSaveState* state) const { + *state = GAME_MODE_STATE_PENDING; + if (mOnNewSaveSelectFunction) { + if (mOnNewSaveSelectFunction(state)) { + return true; + } + *state = GAME_MODE_STATE_RETURN; + return false; + } + *state = GAME_MODE_STATE_PROCEED; + return true; + } + + bool invokeOnGameResetFunction() const { + if (mOnGameResetFunction) { + return mOnGameResetFunction(); + } + return true; + } + + bool invokeOnTickFunction() const { + if (mOnTickFunction) { + return mOnTickFunction(); + } + return true; + } + + Callback mOnActivatedFunction; + Callback mOnDeactivatedFunction; + Callback mOnPlayFunction; + Callback mOnSaveLoadedFunction; + Callback mOnNewSaveFunction; + NewSaveSelectCallback mOnNewSaveSelectFunction; + Callback mOnGameResetFunction; + Callback mOnTickFunction; +}; + +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(kVanillaGameModeId); + } + bool isCurrentGameMode(const GameModeId& id) const { + const GameMode* gameMode = getCurrentGameMode(); + if (gameMode && gameMode->getId() == id) { + return true; + } + return false; + } + bool setCurrentGameMode(const GameModeId& id); + void setGameModeToPrevious(); + + const std::map& getRegisteredGameModes() const { + 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 a102b0ca11..30cfa78021 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -9,7 +9,6 @@ #include "imgui.h" #include -#include "fmt/format.h" #include "ImGuiConsole.hpp" #include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" @@ -19,6 +18,7 @@ #include "dusk/data.hpp" #include "dusk/dusk.h" #include "dusk/frame_interpolation.h" +#include "dusk/game_mode.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/presentation.hpp" @@ -26,6 +26,7 @@ #include "dusk/ui/ui.hpp" #include "f_pc/f_pc_manager.h" #include "f_pc/f_pc_name.h" +#include "fmt/format.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_main.h" #include "tracy/Tracy.hpp" @@ -311,7 +312,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(); } } @@ -383,7 +384,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 c2f5e45754..3cb9a182a1 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" @@ -34,7 +35,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); @@ -56,7 +57,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 7b376815bd..e5a8eac1ef 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -1,6 +1,7 @@ #ifndef DUSK_ISO_VALIDATE_HPP #define DUSK_ISO_VALIDATE_HPP +#include "dusk/settings.h" #include #include 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..3196956be6 100644 --- a/src/dusk/livesplit.h +++ b/src/dusk/livesplit.h @@ -1,6 +1,8 @@ #pragma once #include +#include "dusk/game_mode.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/game_mode.cpp b/src/dusk/mods/svc/game_mode.cpp new file mode 100644 index 0000000000..cfa655baf8 --- /dev/null +++ b/src/dusk/mods/svc/game_mode.cpp @@ -0,0 +1,199 @@ +#include "mods/svc/game_mode.h" +#include "dusk/game_mode.hpp" + +#include "config.hpp" +#include "registry.hpp" +#include "slot_map.hpp" + +#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 + +namespace dusk::mods::svc::game_mode_impl { +namespace { + +aurora::Module Log("dusk::mods::game_mode"); + +// 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; + std::transform(fullId.begin(), fullId.end(), fullId.begin(), + [](unsigned char c) { return std::tolower(c); }); + return fullId; +} + +void game_mode_remove_mod(LoadedMod& mod) { + const auto it = s_gameModesByMod.find(mod.metadata.id); + if (it != s_gameModesByMod.end()) { + for (const auto& id : it->second) { + gamemode::getGameModeManager().unregisterGameMode(id); + } + s_gameModesByMod.erase(it); + } +} +} // 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->game_mode_id) { + Log.error("Attempted to register a game mode with a null ID"); + return MOD_ERROR; + } + id = desc->game_mode_id; + if (id.empty()) { + Log.error("Attempted to register a game mode with an empty ID"); + return MOD_ERROR; + } + id = get_mod_game_mode_id(ctx, id); + + std::string fullName; + if (!desc->full_name) { + Log.warn("Game mode {} has no display name; using its ID", id); + fullName = id; + } else { + 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->save_name}; + if (desc->on_activated) { + mode.mOnActivatedFunction = wrap_callback( + *owner, desc->on_activated, desc->user_data, "game mode activation callback"); + } + if (desc->on_deactivated) { + mode.mOnDeactivatedFunction = wrap_callback( + *owner, desc->on_deactivated, desc->user_data, "game mode deactivation callback"); + } + if (desc->on_play) { + mode.mOnPlayFunction = + wrap_callback(*owner, desc->on_play, desc->user_data, "game mode play callback"); + } + if (desc->on_save_loaded) { + mode.mOnSaveLoadedFunction = wrap_callback( + *owner, desc->on_save_loaded, desc->user_data, "game mode save-loaded callback"); + } + if (desc->on_new_save) { + mode.mOnNewSaveFunction = wrap_callback( + *owner, desc->on_new_save, desc->user_data, "game mode new-save callback"); + } + 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->on_game_reset) { + mode.mOnGameResetFunction = + wrap_callback(*owner, desc->on_game_reset, desc->user_data, "game mode reset callback"); + } + if (desc->on_tick) { + mode.mOnTickFunction = + wrap_callback(*owner, desc->on_tick, desc->user_data, "game mode tick callback"); + } + + gamemode::getGameModeManager().registerGameMode(mode); + s_gameModesByMod[ctx->mod->metadata.id].push_back(id); + return MOD_OK; +} + +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 ownership map + auto it = s_gameModesByMod.find(ctx->mod->metadata.id); + if (it != s_gameModesByMod.end()) { + std::erase(it->second, fullId); + } + return MOD_OK; +} + +ModResult is_active(ModContext* ctx, const char* gameModeId, bool* out_active) { + *out_active = + gamemode::getGameModeManager().isCurrentGameMode(get_mod_game_mode_id(ctx, gameModeId)); + return MOD_OK; +} + +} // namespace dusk::mods::svc::game_mode_impl + +namespace dusk::mods::svc { +namespace { + +constexpr GameModeService s_gamemodeService{ + .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, +}; + +} // namespace + +constinit const ServiceModule g_gamemodeModule{ + .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, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 7ff05d685a..68eeb58771 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -224,6 +224,7 @@ void ModLoader::init_services() { &svc::g_itemModule, &svc::g_flowModule, &svc::g_messageModule, + &svc::g_gamemodeModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index af6bb3829f..ba693dd958 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -82,5 +82,6 @@ extern const ServiceModule g_stageModule; extern const ServiceModule g_itemModule; extern const ServiceModule g_flowModule; extern const ServiceModule g_messageModule; +extern const ServiceModule g_gamemodeModule; } // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 304c4eb57b..320d2549ae 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -426,7 +426,6 @@ public: } void close() { pop(); } - void force_close() { Document::hide(true); } private: std::function m_onDestroyed; @@ -931,7 +930,7 @@ void ui_sync_menu_tabs() { } s_menuTabsDirty = false; if (aurora::rmlui::is_initialized()) { - ui::MenuBar::rebuild(); + ui::MenuBar::refresh_tabs(); } } @@ -1019,14 +1018,14 @@ void ui_remove_mod(LoadedMod& mod) { case UiSlotKind::Window: { auto* window = static_cast(slot.document); if (window != nullptr) { - window->force_close(); + window->force_hide(true); } break; } case UiSlotKind::Dialog: { auto* dialog = static_cast(slot.document); if (dialog != nullptr) { - dialog->force_close(); + dialog->force_hide(true); } break; } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 1af01faa81..16bc26e532 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -1,6 +1,7 @@ #include "dusk/settings.h" -#include "dusk/config.hpp" #include +#include "dusk/config.hpp" +#include "dusk/game_mode.hpp" namespace dusk { @@ -156,7 +157,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", gamemode::kVanillaGameModeId} }, .backend = { @@ -306,6 +308,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 3d29c0ec02..2713812ac3 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -285,6 +285,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..8527f9510a 100644 --- a/src/dusk/speedrun.cpp +++ b/src/dusk/speedrun.cpp @@ -1,12 +1,52 @@ #include "dusk/speedrun.h" -#include "dusk/settings.h" -#include "dusk/config.hpp" -#include "m_Do/m_Do_main.h" #include +#include "dusk/config.hpp" +#include "dusk/game_mode.hpp" +#include "dusk/livesplit.h" +#include "dusk/settings.h" +#include "m_Do/m_Do_main.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{ + 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); +} + +void unregisterSpeedrunGameMode() { + dusk::gamemode::getGameModeManager().unregisterGameMode(kSpeedrunGameModeId); +} void resetForSpeedrunMode() { mDoMain::developmentMode = -1; @@ -45,9 +85,7 @@ void resetForSpeedrunMode() { } static void clearSpeedrunOverrides() { - config::EnumerateRegistered([](config::ConfigVarBase& cvar) { - cvar.clearSpeedrunOverride(); - }); + config::EnumerateRegistered([](config::ConfigVarBase& cvar) { cvar.clearSpeedrunOverride(); }); } void restoreFromSpeedrunMode() { @@ -55,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/speedrun.h b/src/dusk/speedrun.h index 887d9e6842..add396726f 100644 --- a/src/dusk/speedrun.h +++ b/src/dusk/speedrun.h @@ -1,9 +1,13 @@ #pragma once #include +#include "dusk/game_mode.hpp" -namespace dusk { +namespace dusk::speedrun { + +constexpr const char* kSpeedrunGameModeId = "vanilla_speedrun"; struct SpeedrunInfo { + void startRun() { m_isRunStarted = true; m_rtaStartTimestamp = OSGetNativeTime(); @@ -40,9 +44,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(kSpeedrunGameModeId); +} + } // namespace dusk diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp index 2b387f3fe9..adb46e6334 100644 --- a/src/dusk/ui/document.cpp +++ b/src/dusk/ui/document.cpp @@ -101,6 +101,15 @@ bool Document::focus() { return false; } +bool Document::has_focus() const { + if (mDocument == nullptr) { + return false; + } + auto* context = mDocument->GetContext(); + const auto* focused = context != nullptr ? context->GetFocusElement() : nullptr; + return focused != nullptr && focused->GetOwnerDocument() == mDocument; +} + bool Document::set_document_styles(const Rml::String& rcss) { if (rcss.empty()) { mDocumentStyleSheets = nullptr; diff --git a/src/dusk/ui/document.hpp b/src/dusk/ui/document.hpp index e7bd35947d..0e2b3360d9 100644 --- a/src/dusk/ui/document.hpp +++ b/src/dusk/ui/document.hpp @@ -20,6 +20,7 @@ public: virtual void hide(bool close); virtual void update(); virtual bool focus(); + bool has_focus() const; virtual bool visible() const; virtual bool active() const; virtual bool obscures_game() const { return false; } @@ -64,6 +65,10 @@ public: hide(true); uncover_top_document(); } + void force_hide(bool close) { + hide(close); + Document::hide(close); + } bool closed() const { return mClosed; } diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 0c6528e424..44f3afa533 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -7,11 +7,13 @@ #include "achievements.hpp" #include "aurora/rmlui.hpp" +#include "dusk/game_mode.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/mods/svc/ui.hpp" #include "dusk/settings.h" #include "dusk/speedrun.h" +#include "dusk/ui/prelaunch.hpp" #include "editor.hpp" #include "f_pc/f_pc_manager.h" #include "f_pc/f_pc_name.h" @@ -54,6 +56,20 @@ MenuBar::MenuBar() }, .autoSelect = false, }); + + // Hide document after transition completion + listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { + if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && + Document::visible()) + { + Document::hide(mPendingClose); + } + }); + + build_tabs(); +} + +void MenuBar::build_tabs() { mTabBar->add_tab("Settings", [this] { push(std::make_unique()); }); if (getSettings().backend.enableAdvancedSettings) { @@ -61,7 +77,11 @@ MenuBar::MenuBar() mTabBar->add_tab("Editor", [this] { push(std::make_unique()); }); } - mTabBar->add_tab("Achievements", [this] { push(std::make_unique()); }); + // Only allow us to access achievements if we are playing on a game mode that uses them + if (dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::gamemode::kVanillaGameModeId) + || dusk::gamemode::getGameModeManager().isCurrentGameMode(dusk::speedrun::kSpeedrunGameModeId)) { + mTabBar->add_tab("Achievements", [this] { push(std::make_unique()); }); + } mTabBar->add_tab("Mods", [this] { push(std::make_unique()); }); for (auto& tab : mods::svc::ui_mod_menu_tabs()) { mTabBar->add_tab(tab.label, std::move(tab.onSelected)); @@ -93,9 +113,13 @@ MenuBar::MenuBar() dismiss(modal); return; } - JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; dismiss(modal); + if (gamemode::getGameModeManager().getRegisteredGameModes().size() > 1) { + // If game modes are registered, return to prelaunch on reset. + prelaunch_state().returnToPrelaunchOnReset = true; + } hide(false); + JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; }, }, }, @@ -134,39 +158,30 @@ 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(); } hide(false); }); } - - // Hide document after transition completion - listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { - if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && - Document::visible()) - { - Document::hide(mPendingClose); - } - }); } void MenuBar::show() { Document::show(); mRoot->SetAttribute("open", ""); mTabBar->set_active_tab(-1); - if (!mTabBar->focus_tab(mFocusedTabIndex)) { + if (!mTabBar->focus_tab(mFocusedTabTitle)) { mTabBar->focus(); } } void MenuBar::hide(bool close) { - mFocusedTabIndex = mTabBar->focused_tab_index(); + mFocusedTabTitle = mTabBar->focused_tab_title(); mRoot->RemoveAttribute("open"); if (close) { mPendingClose = true; @@ -236,19 +251,19 @@ bool MenuBar::focus() { return mTabBar->focus(); } -void MenuBar::rebuild() { - for (auto& doc : get_document_stack()) { - if (auto* menuBar = dynamic_cast(doc.get())) { - const bool wasVisible = menuBar->visible(); - auto next = std::make_unique(); - next->mFocusedTabIndex = menuBar->mFocusedTabIndex; - next->mWasVisible = menuBar->mWasVisible; - doc = std::move(next); - if (wasVisible) { - doc->show(); - } - break; - } +void MenuBar::refresh_tabs() { + auto* menuBar = static_cast(find_document(DocumentScope::MenuBar)); + if (menuBar == nullptr) { + return; + } + const auto focusedTitle = menuBar->mTabBar->focused_tab_title(); + if (!focusedTitle.empty()) { + menuBar->mFocusedTabTitle = focusedTitle; + } + menuBar->mTabBar->clear_tabs(); + menuBar->build_tabs(); + if (menuBar->visible() && !menuBar->mTabBar->focus_tab(menuBar->mFocusedTabTitle)) { + menuBar->mTabBar->focus(); } } diff --git a/src/dusk/ui/menu_bar.hpp b/src/dusk/ui/menu_bar.hpp index 29ce2199c5..44b4ee77cd 100644 --- a/src/dusk/ui/menu_bar.hpp +++ b/src/dusk/ui/menu_bar.hpp @@ -21,12 +21,13 @@ public: bool focus() override; bool visible() const override; - static void rebuild(); + static void refresh_tabs(); protected: bool handle_nav_command(Rml::Event& event, NavCommand cmd) override; private: + void build_tabs(); void update_safe_area() noexcept; Rml::Element* mRoot; @@ -34,7 +35,7 @@ private: std::unique_ptr