Merge pull request #2230 from jdflyer/main

Mod SDK: Game Mode Service
This commit is contained in:
TakaRikka
2026-08-19 22:51:24 -07:00
committed by GitHub
53 changed files with 1640 additions and 415 deletions
+119
View File
@@ -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<CreateItem>(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<CreateItem>();
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<UiWindowHandle*>(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
+2
View File
@@ -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
+13
View File
@@ -9,6 +9,10 @@
#include "JSystem/J3DGraphLoader/J3DModelLoader.h"
#include "JSystem/J3DGraphLoader/J3DAnmLoader.h"
#if TARGET_PC
#include "mods/svc/game_mode.h"
#endif
class dFile_info_c;
class J2DPicture;
@@ -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
+20
View File
@@ -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 */
+68
View File
@@ -143,6 +143,74 @@ eyebrow span {
transition: decorator color opacity 0.1s linear-in-out;
}
#menu-list button.game-mode-button {
position: relative;
padding: 0;
overflow: visible;
}
#menu-list game-mode-label-viewport {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 8dp;
pointer-events: none;
z-index: 0;
}
#menu-list game-mode-label {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 8dp 16dp;
opacity: 0;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity 0.24s cubic-in-out;
}
#menu-list game-mode-label.active {
opacity: 1;
}
#menu-list game-mode-previous,
#menu-list game-mode-next {
display: none;
position: absolute;
top: 0;
width: 48dp;
height: 54dp;
align-items: center;
justify-content: center;
color: #FFFFFF;
font-family: "Material Symbols Rounded";
font-weight: normal;
font-size: 30dp;
z-index: 1;
}
#menu-list game-mode-previous {
left: -48dp;
decorator: text("&#xe5cb;" center center);
}
#menu-list game-mode-next {
right: -48dp;
decorator: text("&#xe5cc;" center center);
}
#menu-list button.game-mode-button.can-cycle game-mode-previous,
#menu-list button.game-mode-button.can-cycle game-mode-next {
display: flex;
}
#menu-list button:hover,
#menu-list button:focus-visible {
color: black;
+10
View File
@@ -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%;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <mods/api.h>
#include <mods/svc/config.h>
#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);
+6 -3
View File
@@ -23,10 +23,13 @@
#include "d/actor/d_a_npc_tkc.h"
#include <cstring>
#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
+7 -12
View File
@@ -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 <dusk/autosave.h>
#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;
+45 -2
View File
@@ -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;
}
+6
View File
@@ -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();
+10 -15
View File
@@ -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 <dusk/autosave.h>
#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
}
+2 -1
View File
@@ -28,8 +28,9 @@
#endif
#if TARGET_PC
#include "dusk/settings.h"
#include <f_ap/f_ap_game.h>
#include "dusk/game_mode.hpp"
#include "dusk/settings.h"
#include "helpers/string.hpp"
#define strcpy SafeStringCopy
+6
View File
@@ -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;
+19 -12
View File
@@ -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 <filesystem>
#include <algorithm>
@@ -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();
}
+97
View File
@@ -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
+142
View File
@@ -0,0 +1,142 @@
#pragma once
#include "d/d_file_select.h"
#include "mods/svc/game_mode.h"
#include <functional>
#include <map>
#include <string>
#include <utility>
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<bool()>;
using NewSaveSelectCallback = std::function<bool(GameModeNewSaveState* state)>;
GameMode(GameModeId id, std::string fullName, std::string saveName = {})
: mId{std::move(id)}, mFullName{std::move(fullName)},
mSaveName{saveName.empty() ? kDefaultGameModeSaveName : std::move(saveName)} {}
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<GameModeId, GameMode>& getRegisteredGameModes() const {
return mRegisteredGameModes;
}
private:
GameModeId mCurrentGameModeId;
std::map<GameModeId, GameMode> mRegisteredGameModes;
};
extern GameModeManager g_GameModeManager;
inline GameModeManager& getGameModeManager() {
return g_GameModeManager;
}
} // namespace dusk::gamemode
+4 -3
View File
@@ -9,7 +9,6 @@
#include "imgui.h"
#include <imgui_internal.h>
#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();
+3 -2
View File
@@ -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)) {
+1
View File
@@ -1,6 +1,7 @@
#ifndef DUSK_ISO_VALIDATE_HPP
#define DUSK_ISO_VALIDATE_HPP
#include "dusk/settings.h"
#include <borealis/disc.hpp>
#include <cstdint>
+73 -63
View File
@@ -1,46 +1,48 @@
#if _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using socket_t = SOCKET;
static void closeSocket(socket_t s) {
LINGER li{1, 0};
setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char*>(&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<char*>(&err), &len);
return err;
}
static constexpr int kSendFlags = 0;
#include <winsock2.h>
#include <ws2tcpip.h>
using socket_t = SOCKET;
static void closeSocket(socket_t s) {
LINGER li{1, 0};
setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char*>(&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<char*>(&err), &len);
return err;
}
static constexpr int kSendFlags = 0;
#else
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
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 <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
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 <cstdio>
@@ -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<uint32_t>(totalSec / 3600),
static_cast<uint32_t>((totalSec / 60) % 60),
static_cast<uint32_t>(totalSec % 60),
static_cast<uint32_t>(totalMs % 1000)
);
static_cast<uint32_t>(totalSec / 3600), static_cast<uint32_t>((totalSec / 60) % 60),
static_cast<uint32_t>(totalSec % 60), static_cast<uint32_t>(totalMs % 1000));
sendCmd(cmd);
}
@@ -299,4 +309,4 @@ void shutdown() {
#endif
}
}
} // namespace dusk::speedrun
+2
View File
@@ -1,6 +1,8 @@
#pragma once
#include <cstdint>
#include "dusk/game_mode.hpp"
#include "dusk/speedrun.h"
namespace dusk::speedrun {
void onGameFrame();
-3
View File
@@ -1,7 +1,6 @@
#pragma once
#include <filesystem>
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;
+199
View File
@@ -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 <algorithm>
#include <cctype>
#include <exception>
#include <string>
#include <unordered_map>
#include <vector>
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<std::string, std::vector<std::string>> s_gameModesByMod;
template <typename Fn>
bool invoke_mod_callback(LoadedMod& mod, const char* what, Fn&& fn) {
if (!mod.active) {
return false;
}
ModError error = MOD_ERROR_INIT;
ModResult result = MOD_OK;
try {
result = fn(&error);
} catch (const std::exception& exception) {
fail_mod(mod, MOD_ERROR, fmt::format("exception in {}: {}", what, exception.what()));
return false;
} catch (...) {
fail_mod(mod, MOD_ERROR, fmt::format("unknown exception in {}", what));
return false;
}
if (result != MOD_OK && mod.active) {
fail_mod(mod, result,
error.message[0] != '\0' ?
error.message :
fmt::format("{} failed with result {}", what, static_cast<int>(result)));
}
return result == MOD_OK && mod.active;
}
gamemode::GameMode::Callback wrap_callback(
LoadedMod& mod, GameModeCallback callback, void* userData, const char* what) {
return [&mod, callback, userData, what] {
return invoke_mod_callback(
mod, what, [callback, userData](ModError* error) { return callback(userData, error); });
};
}
gamemode::GameMode::NewSaveSelectCallback wrap_new_save_select_callback(
LoadedMod& mod, GameModeNewSaveSelectCallback callback, void* userData, const char* what) {
return [&mod, callback, userData, what](GameModeNewSaveState* state) {
return invoke_mod_callback(
mod, what, [=](ModError* error) { return callback(userData, state, error); });
};
}
std::string get_mod_game_mode_id(ModContext* ctx, const std::string& id) {
// Include the mod ID to prevent clashes and normalize to lowercase
std::string fullId = id + "_" + ctx->mod->metadata.id;
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
+1
View File
@@ -224,6 +224,7 @@ void ModLoader::init_services() {
&svc::g_itemModule,
&svc::g_flowModule,
&svc::g_messageModule,
&svc::g_gamemodeModule,
})
{
svc::register_module(*module);
+1
View File
@@ -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
+3 -4
View File
@@ -426,7 +426,6 @@ public:
}
void close() { pop(); }
void force_close() { Document::hide(true); }
private:
std::function<void()> 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<ui::ModWindow*>(slot.document);
if (window != nullptr) {
window->force_close();
window->force_hide(true);
}
break;
}
case UiSlotKind::Dialog: {
auto* dialog = static_cast<ModDialog*>(slot.document);
if (dialog != nullptr) {
dialog->force_close();
dialog->force_hide(true);
}
break;
}
+5 -2
View File
@@ -1,6 +1,7 @@
#include "dusk/settings.h"
#include "dusk/config.hpp"
#include <aurora/aurora.h>
#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);
+2
View File
@@ -285,6 +285,8 @@ struct UserSettings {
ConfigVar<bool> removeQuestMapMarkers;
ConfigVar<bool> showInputViewer;
ConfigVar<bool> showInputViewerGyro;
ConfigVar<std::string> lastSelectedGameModeId;
} game;
struct {
+47 -9
View File
@@ -1,12 +1,52 @@
#include "dusk/speedrun.h"
#include "dusk/settings.h"
#include "dusk/config.hpp"
#include "m_Do/m_Do_main.h"
#include <aurora/aurora.h>
#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
+12 -2
View File
@@ -1,9 +1,13 @@
#pragma once
#include <aurora/aurora.h>
#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
+9
View File
@@ -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;
+5
View File
@@ -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; }
+43 -28
View File
@@ -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<SettingsWindow>()); });
if (getSettings().backend.enableAdvancedSettings) {
@@ -61,7 +77,11 @@ MenuBar::MenuBar()
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
}
mTabBar->add_tab("Achievements", [this] { push(std::make_unique<AchievementsWindow>()); });
// 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<AchievementsWindow>()); });
}
mTabBar->add_tab("Mods", [this] { push(std::make_unique<ModsWindow>()); });
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<MenuBar*>(doc.get())) {
const bool wasVisible = menuBar->visible();
auto next = std::make_unique<MenuBar>();
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<MenuBar*>(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();
}
}
+3 -2
View File
@@ -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<Button> mCloseButton;
Insets mTabBarPadding;
float mTopMargin = 0.f;
int mFocusedTabIndex = -1;
Rml::String mFocusedTabTitle;
};
} // namespace dusk::ui
-1
View File
@@ -59,7 +59,6 @@ public:
~ModWindow() override;
void update() override;
void force_close() { Document::hide(true); }
private:
Desc mDesc;
+7 -2
View File
@@ -25,6 +25,9 @@ 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);
}
for (auto& action : mProps.actions) {
add_action(std::move(action));
@@ -87,9 +90,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;
+1
View File
@@ -20,6 +20,7 @@ public:
std::function<void(Modal&)> onDismiss;
Rml::String variant;
Rml::String icon = "";
bool isVertical;
};
explicit Modal(Props props);
+13 -13
View File
@@ -301,7 +301,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)});
@@ -313,33 +313,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.stopRun();
if (speedrun::g_speedrunInfo.m_isRunStarted) {
speedrun::g_speedrunInfo.stopRun();
}
}
OSTime rtaElapsedTime = 0;
if (m_speedrunInfo.m_isRunStarted) {
rtaElapsedTime = OSGetNativeTime() - m_speedrunInfo.m_rtaStartTimestamp;
} else if (m_speedrunInfo.m_rtaTimer != 0) {
rtaElapsedTime = m_speedrunInfo.m_rtaTimer;
if (speedrun::g_speedrunInfo.m_isRunStarted) {
rtaElapsedTime = OSGetNativeTime() - speedrun::g_speedrunInfo.m_rtaStartTimestamp;
} else if (speedrun::g_speedrunInfo.m_rtaTimer != 0) {
rtaElapsedTime = speedrun::g_speedrunInfo.m_rtaTimer;
}
if (m_speedrunInfo.m_isRunStarted && !m_speedrunInfo.m_isPauseIGT) {
m_speedrunInfo.m_igtTimer = OSGetTime() - m_speedrunInfo.m_igtStartTimestamp -
m_speedrunInfo.m_totalLoadTime;
if (speedrun::g_speedrunInfo.m_isRunStarted && !speedrun::g_speedrunInfo.m_isPauseIGT) {
speedrun::g_speedrunInfo.m_igtTimer = OSGetTime() - speedrun::g_speedrunInfo.m_igtStartTimestamp -
speedrun::g_speedrunInfo.m_totalLoadTime;
}
mSpeedrunTimer->SetAttribute("open", "");
@@ -352,7 +352,7 @@ void Overlay::update() {
}
mSpeedrunIgt->SetInnerRML(
escape(fmt::format("IGT {}", FormatElapsedTime(m_speedrunInfo.m_igtTimer))));
escape(fmt::format("IGT {}", FormatElapsedTime(speedrun::g_speedrunInfo.m_igtTimer))));
} else {
mSpeedrunTimer->RemoveAttribute("open");
}
+233 -58
View File
@@ -3,10 +3,12 @@
#include "dusk/app_info.hpp"
#include "dusk/config.hpp"
#include "dusk/data.hpp"
#include "dusk/game_mode.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/language.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "dusk/ui/menu_bar.hpp"
#include "modal.hpp"
#include "mods_window.hpp"
#include "preset.hpp"
@@ -35,6 +37,8 @@ namespace dusk::ui {
namespace {
constexpr borealis::Log PrelaunchLog{"dusk::ui::prelaunch"};
PrelaunchState sPrelaunchState;
const Rml::String kDocumentSource = R"RML(
<rml>
<head>
@@ -527,7 +531,144 @@ void file_dialog_callback(borealis::file_select::Result result) {
begin_disc_verification(result.locations.front());
}
PrelaunchState sPrelaunchState;
std::vector<const gamemode::GameMode*> carousel_game_modes() {
const auto& registered = gamemode::getGameModeManager().getRegisteredGameModes();
std::vector<const gamemode::GameMode*> modes;
modes.reserve(registered.size());
if (const auto vanilla = registered.find(gamemode::kVanillaGameModeId);
vanilla != registered.end())
{
modes.push_back(&vanilla->second);
}
for (const auto& [id, mode] : registered) {
if (id != gamemode::kVanillaGameModeId) {
modes.push_back(&mode);
}
}
std::ranges::sort(modes.begin() + std::min<size_t>(1, modes.size()), modes.end(),
[](const auto* lhs, const auto* rhs) { return lhs->getFullName() < rhs->getFullName(); });
return modes;
}
std::string game_mode_button_text() {
if (prelaunch_state().activeDiscPath.empty()) {
return "Select Disc Image";
}
const auto* currentGameMode = gamemode::getGameModeManager().getCurrentGameMode();
if (currentGameMode == nullptr || currentGameMode->getId() == gamemode::kVanillaGameModeId) {
return "Play";
}
return currentGameMode->getFullName();
}
class GameModeButton final : public Button {
public:
GameModeButton(Rml::Element* parent, ButtonCallback onPressed) : Button{parent, ""} {
root()->SetClass("game-mode-button", true);
mPrevious = append(root(), "game-mode-previous");
mLabelViewport = append(root(), "game-mode-label-viewport");
for (auto& label : mLabels) {
label = append(mLabelViewport, "game-mode-label");
}
mNext = append(root(), "game-mode-next");
Component::listen(mPrevious, Rml::EventId::Click, [this](Rml::Event& event) {
cycle(-1);
event.StopPropagation();
});
Component::listen(mNext, Rml::EventId::Click, [this](Rml::Event& event) {
cycle(1);
event.StopPropagation();
});
Component::listen(root(), Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto command = map_nav_event(event);
if (command == NavCommand::Left || command == NavCommand::Right) {
cycle(command == NavCommand::Left ? -1 : 1);
event.StopPropagation();
}
});
on_pressed(std::move(onPressed));
refresh(0);
}
void update() override {
refresh(0);
Button::update();
}
private:
bool can_cycle() const {
return !prelaunch_state().activeDiscPath.empty() &&
gamemode::getGameModeManager().getRegisteredGameModes().size() > 1;
}
void cycle(int direction) {
const auto modes = carousel_game_modes();
if (!can_cycle() || modes.empty()) {
return;
}
const auto* current = gamemode::getGameModeManager().getCurrentGameMode();
const auto currentIt = std::ranges::find_if(modes, [current](const auto* mode) {
return current != nullptr && mode->getId() == current->getId();
});
const int currentIndex =
currentIt == modes.end() ? 0 : static_cast<int>(currentIt - modes.begin());
const int count = static_cast<int>(modes.size());
const int nextIndex = ((currentIndex + direction) % count + count) % count;
const auto nextId = modes[nextIndex]->getId();
if (gamemode::getGameModeManager().setCurrentGameMode(nextId)) {
mDoAud_seStartMenu(kSoundItemChange);
}
refresh(direction);
}
void refresh(int direction) {
root()->SetClass("can-cycle", can_cycle());
const auto text = game_mode_button_text();
if (text == mText) {
return;
}
mText = text;
if (direction == 0) {
mLabels[mActiveLabel]->SetInnerRML(escape(text));
mLabels[mActiveLabel]->SetProperty(
Rml::PropertyId::Left, Rml::Property{0.0f, Rml::Unit::PERCENT});
mLabels[mActiveLabel]->SetClass("active", true);
mLabels[1 - mActiveLabel]->SetClass("active", false);
return;
}
constexpr float kSlideDistance = 100.0f;
constexpr float kSlideDuration = 0.24f;
auto* outgoing = mLabels[mActiveLabel];
mActiveLabel = 1 - mActiveLabel;
auto* incoming = mLabels[mActiveLabel];
incoming->SetInnerRML(escape(text));
const Rml::Property incomingOffset{
static_cast<float>(direction) * kSlideDistance, Rml::Unit::PERCENT};
const Rml::Property outgoingOffset{
static_cast<float>(-direction) * kSlideDistance, Rml::Unit::PERCENT};
outgoing->Animate(Rml::PropertyId::Left, outgoingOffset, kSlideDuration,
Rml::Tween{Rml::Tween::Cubic, Rml::Tween::InOut});
incoming->Animate(Rml::PropertyId::Left, Rml::Property{0.0f, Rml::Unit::PERCENT},
kSlideDuration, Rml::Tween{Rml::Tween::Cubic, Rml::Tween::InOut}, 1, false, 0.0f,
&incomingOffset);
outgoing->SetClass("active", false);
incoming->SetClass("active", true);
}
Rml::Element* mPrevious = nullptr;
Rml::Element* mLabelViewport = nullptr;
std::array<Rml::Element*, 2> mLabels{};
Rml::Element* mNext = nullptr;
Rml::String mText;
std::size_t mActiveLabel = 0;
};
} // namespace
@@ -743,61 +884,25 @@ 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::refresh_menu_buttons() {
auto* prelaunch = static_cast<Prelaunch*>(find_document(DocumentScope::Prelaunch));
if (prelaunch == nullptr) {
return;
}
auto* menuList = prelaunch->mDocument->GetElementById("menu-list");
while (menuList->GetNumChildren() > 0) {
menuList->RemoveChild(menuList->GetChild(0));
}
prelaunch->mMenuButtons.clear();
prelaunch->build_menu_buttons();
}
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<Button>(menuList, activeDiscLoaded ? "Play" : "Select Disc Image"));
mMenuButtons.back()->on_pressed([this] {
if (prelaunch_state().activeDiscPath.empty()) {
open_iso_picker();
return;
}
mDoAud_seStartMenu(kSoundPlay);
show_menu_notification();
if (getSettings().audio.menuSounds) {
JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle();
if (*handle) {
(*handle)->stop(60);
(*handle)->releaseHandle();
}
}
if (g_mDoMemCd_control.mCardCommand == mDoMemCd_Ctrl_c::Command_e::COMM_NONE_e) {
mDoMemCd_ThdInit();
}
IsGameLaunched = true;
pop();
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Settings"));
mMenuButtons.back()->on_pressed([this] {
mRestartSuppressed = false;
push(std::make_unique<SettingsWindow>(true));
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Mods"));
mMenuButtons.back()->on_pressed([this] {
mRestartSuppressed = false;
push(std::make_unique<ModsWindow>());
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit"));
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
apply_intro_animation(mMenuButtons.back()->root(), "delay-4");
}
build_menu_buttons();
mDiscStatus = mDocument->GetElementById("disc-status");
mDiscDetail = mDocument->GetElementById("disc-version");
@@ -835,6 +940,68 @@ Prelaunch::Prelaunch()
});
}
void Prelaunch::build_menu_buttons() {
if (auto* menuList = mDocument->GetElementById("menu-list")) {
// Restore the previously selected game mode before creating the play control.
gamemode::getGameModeManager().setGameModeToPrevious();
auto playButton = std::make_unique<GameModeButton>(menuList, [this] {
if (prelaunch_state().activeDiscPath.empty()) {
open_iso_picker();
return;
}
if (const auto* gameMode = gamemode::getGameModeManager().getCurrentGameMode();
gameMode != nullptr && !gameMode->invokeOnPlayFunction())
{
gamemode::getGameModeManager().setCurrentGameMode(gamemode::kVanillaGameModeId);
return;
}
mDoAud_seStartMenu(kSoundPlay);
show_menu_notification();
if (getSettings().audio.menuSounds) {
JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle();
if (*handle) {
(*handle)->stop(60);
(*handle)->releaseHandle();
}
}
if (g_mDoMemCd_control.mCardCommand == mDoMemCd_Ctrl_c::Command_e::COMM_NONE_e) {
mDoMemCd_ThdInit();
}
prelaunch_state().firstLaunch = false;
IsGameLaunched = true;
hide(true);
MenuBar::refresh_tabs();
});
apply_intro_animation(playButton->root(), "delay-1");
mMenuButtons.push_back(std::move(playButton));
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Settings"));
mMenuButtons.back()->on_pressed([this] {
mRestartSuppressed = false;
bool showPrelaunchSettings = prelaunch_state().firstLaunch;
push(std::make_unique<SettingsWindow>(showPrelaunchSettings));
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Mods"));
mMenuButtons.back()->on_pressed([this] {
mRestartSuppressed = false;
push(std::make_unique<ModsWindow>());
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit"));
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
apply_intro_animation(mMenuButtons.back()->root(), "delay-4");
}
}
void Prelaunch::show() {
Document::show();
mDocument->SetAttribute("open", "");
@@ -846,14 +1013,14 @@ void Prelaunch::show() {
modal.pop();
};
std::vector<ModalAction> actions;
if constexpr (dusk::SupportsProcessRestart) {
if constexpr (SupportsProcessRestart) {
actions.push_back(ModalAction{
.label = "Restart later",
.onPressed = dismiss,
});
actions.push_back(ModalAction{
.label = "Restart now",
.onPressed = [](Modal&) { dusk::RequestRestart(); },
.onPressed = [](Modal&) { RequestRestart(); },
});
} else {
actions.push_back(ModalAction{
@@ -864,7 +1031,7 @@ void Prelaunch::show() {
push(std::make_unique<Modal>(Modal::Props{
.title = "Apply Options",
.bodyRml =
dusk::SupportsProcessRestart ?
SupportsProcessRestart ?
"A restart is required to apply selected options.<br/><br/>Restart now to "
"apply them immediately?" :
"A restart is required to apply selected options.<br/><br/>Close and reopen "
@@ -914,8 +1081,8 @@ void Prelaunch::update() {
mEntranceAnimationStarted = true;
}
if (!mMenuButtons.empty()) {
mMenuButtons[0]->set_text(activeDiscLoaded ? "Play" : "Select Disc Image");
for (const auto& button : mMenuButtons) {
button->update();
}
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");
@@ -1024,6 +1191,14 @@ void Prelaunch::update() {
Document::update();
}
void return_to_prelaunch() noexcept {
close_documents_except(DocumentScope::MenuBar);
if (auto* menuBar = find_document(DocumentScope::MenuBar)) {
menuBar->force_hide(false);
}
push_document(std::make_unique<Prelaunch>(), true);
}
bool Prelaunch::focus() {
if (mMenuButtons.empty()) {
return false;
+8 -1
View File
@@ -3,6 +3,7 @@
#include "button.hpp"
#include "document.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/settings.h"
#include <memory>
#include <string>
@@ -13,6 +14,7 @@ namespace dusk::ui {
class Prelaunch : public Document {
public:
Prelaunch();
void build_menu_buttons();
void show() override;
void hide(bool close) override;
@@ -21,13 +23,15 @@ public:
bool visible() const override;
bool obscures_game() const override { return true; }
static void refresh_menu_buttons();
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
bool mEntranceAnimationStarted = false;
bool mRestartSuppressed = false;
std::vector<std::unique_ptr<Button> > mMenuButtons;
std::vector<std::unique_ptr<Button>> mMenuButtons;
Rml::Element* mRoot = nullptr;
Rml::Element* mDiscStatus = nullptr;
Rml::Element* mDiscDetail = nullptr;
@@ -42,6 +46,7 @@ class PrelaunchOptions;
struct PrelaunchState {
bool initialized = false;
bool firstLaunch = true;
std::string configuredDiscPath;
bool configuredDiscCanLaunch = false;
iso::DiscInfo configuredDiscInfo{};
@@ -57,9 +62,11 @@ struct PrelaunchState {
std::string pendingDiscPath;
iso::DiscInfo pendingDiscInfo{};
iso::ValidationError pendingDiscValidation = iso::ValidationError::Unknown;
bool returnToPrelaunchOnReset = false;
};
PrelaunchState& prelaunch_state() noexcept;
void return_to_prelaunch() noexcept;
void ensure_initialized() noexcept;
void refresh_configured_disc_state() noexcept;
void open_iso_picker() noexcept;
+1
View File
@@ -108,6 +108,7 @@ PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") {
getSettings().backend.wasPresetChosen.setValue(true);
config::save();
hide(true);
mDoAud_seStartMenu(kSoundClick);
return true;
}
return false;
+1
View File
@@ -61,6 +61,7 @@ CrashReportWindow::CrashReportWindow() : WindowSmall("modal", "modal-dialog") {
if (cmd == NavCommand::Confirm) {
apply();
hide(true);
mDoAud_seStartMenu(kSoundClick);
return true;
}
return false;
+29 -17
View File
@@ -381,7 +381,7 @@ void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar<boo
config_bool_select(leftPane, rightPane, var, {
.key = key,
.helpText = helpText,
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return dusk::speedrun::isActive(); },
});
}
@@ -702,7 +702,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
{
.key = "Pause on Focus Lost",
.helpText = "Pause the game when window focus is lost.",
.isDisabled = [] { return IsMobile || getSettings().game.speedrunMode; },
.isDisabled = [] { return IsMobile || dusk::speedrun::isActive(); },
});
leftPane.register_control(
leftPane.add_select_button({
@@ -1063,7 +1063,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
leftPane.add_section("Tools");
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
"Hold Tab to increase game speed by up to 4x.",
[] { return getSettings().game.speedrunMode.getValue(); });
[] { return dusk::speedrun::isActive(); });
addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")",
getSettings().game.enableResetKeybind,
"Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game.");
@@ -1176,7 +1176,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
getSettings().game.damageMultiplier.setValue(value);
config::save();
},
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return dusk::speedrun::isActive(); },
.isModified =
[] {
return getSettings().game.damageMultiplier.getValue() !=
@@ -1237,16 +1237,16 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.helpText =
"Enables speedrunning options while restricting certain gameplay modifiers.",
.onChange =
[](bool enabled) {
[this](bool enabled) {
if (enabled) {
resetForSpeedrunMode();
dusk::speedrun::registerSpeedrunGameMode();
} else {
restoreFromSpeedrunMode();
if (getSettings().game.liveSplitEnabled) {
speedrun::disconnectLiveSplit();
if (dusk::speedrun::isActive()) {
pop();
}
dusk::speedrun::unregisterSpeedrunGameMode();
}
MenuBar::rebuild();
MenuBar::refresh_tabs();
},
});
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
@@ -1262,13 +1262,13 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
speedrun::disconnectLiveSplit();
}
},
.isDisabled = [] { return IsMobile || !getSettings().game.speedrunMode; },
.isDisabled = [] { return IsMobile || !dusk::speedrun::isActive(); },
});
config_bool_select(leftPane, rightPane, getSettings().game.showSpeedrunRTATimer,
{
.key = "Show RTA",
.helpText = "Display the RTA timer. IGT is always visible.",
.isDisabled = [] { return !getSettings().game.speedrunMode; },
.isDisabled = [] { return !dusk::speedrun::isActive(); },
});
});
@@ -1317,7 +1317,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] {
return kMagicArmorModes[static_cast<u8>(getSettings().game.armorRupeeDrain.getValue())];
},
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return dusk::speedrun::isActive(); },
.isModified =
[] {
return getSettings().game.armorRupeeDrain.getValue() !=
@@ -1363,6 +1363,16 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
"replacements, and other app data.");
});
#endif
leftPane.register_control(leftPane.add_button("Restart to Main Menu").on_pressed([this] {
mDoAud_seStartMenu(kSoundClick);
pop();
ui::prelaunch_state().returnToPrelaunchOnReset = true;
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
}),
rightPane, [](Pane& pane) {
pane.add_text("Restart Dusklight to the pre-launch menu to change settings, game "
"modes, or mods.");
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Notifications",
@@ -1451,8 +1461,10 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
{
.key = "Skip Dusklight Main Menu",
.helpText = "When starting Dusklight, skip the main menu and boot straight into the "
"game if a disc image is available.",
.helpText =
"When starting Dusklight, skip the main menu and boot straight into the "
"game if a disc image is available.<br/><br/>Note: If any mods register game "
"modes, this option will be ignored.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.checkForUpdates,
{
@@ -1481,8 +1493,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.helpText = "Show advanced settings and debugging tools with "
"Shift+F1.<br/><br/><icon class=\"warning\"/> WARNING: Debugging tools "
"can easily break your game. Do not use on a regular save!",
.onChange = [](bool) { MenuBar::rebuild(); },
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.onChange = [](bool) { MenuBar::refresh_tabs(); },
.isDisabled = [] { return dusk::speedrun::isActive(); },
});
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer,
{
+35 -13
View File
@@ -94,14 +94,14 @@ TabBar::TabBar(Rml::Element* parent, Props props)
bool TabBar::focus() {
if (mProps.selectedTabIndex >= 0 && mProps.selectedTabIndex < mTabs.size()) {
// Try to focus the currently selected tab
if (mTabs[mProps.selectedTabIndex].button.focus()) {
if (mTabs[mProps.selectedTabIndex].button->focus()) {
mLastFocusedTabIndex = mProps.selectedTabIndex;
return true;
}
}
// Otherwise, focus the first enabled tab
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
if (mTabs[i].button.focus()) {
if (mTabs[i].button->focus()) {
mLastFocusedTabIndex = i;
return true;
}
@@ -115,8 +115,8 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
if (selected && callback) {
callback();
}
auto& button = add_child<Button>(Button::Props{title}, "tab");
button.on_nav_command([this, index](Rml::Event&, NavCommand cmd) {
auto button = std::make_unique<Button>(mRoot, Button::Props{title}, "tab");
button->on_nav_command([this, index](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (mProps.autoSelect) {
mDoAud_seStartMenu(kSoundTabChanged);
@@ -127,7 +127,7 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
return false;
});
if (selected) {
button.set_selected(true);
button->set_selected(true);
}
if (mEndSpacer != nullptr) {
auto spacer = mRoot->RemoveChild(mEndSpacer);
@@ -135,16 +135,26 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
}
mTabs.emplace_back(Tab{
.title = title,
.button = button,
.button = std::move(button),
.callback = std::move(callback),
});
}
void TabBar::clear_tabs() {
mProps.selectedTabIndex = -1;
mLastFocusedTabIndex = -1;
while (!mTabs.empty()) {
auto* element = mTabs.back().button->root();
mTabs.pop_back();
mRoot->RemoveChild(element);
}
}
bool TabBar::set_active_tab(int index) {
if (index == -1) {
// Clear currently selected tab
for (auto& tab : mTabs) {
tab.button.set_selected(false);
tab.button->set_selected(false);
}
mProps.selectedTabIndex = -1;
return true;
@@ -154,10 +164,10 @@ bool TabBar::set_active_tab(int index) {
return false;
}
const auto& tab = mTabs[index];
if (tab.button.focus()) {
if (tab.button->focus()) {
mLastFocusedTabIndex = index;
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
mTabs[i].button.set_selected(i == index);
mTabs[i].button->set_selected(i == index);
}
mProps.selectedTabIndex = index;
if (tab.callback) {
@@ -177,24 +187,36 @@ void TabBar::refresh_active_tab() {
}
}
int TabBar::focused_tab_index() const {
return mLastFocusedTabIndex;
Rml::String TabBar::focused_tab_title() const {
if (mLastFocusedTabIndex < 0 || mLastFocusedTabIndex >= static_cast<int>(mTabs.size())) {
return {};
}
return mTabs[mLastFocusedTabIndex].title;
}
bool TabBar::focus_tab(int index) {
if (index < 0 || index >= mTabs.size() || index == mProps.selectedTabIndex) {
return false;
}
if (mTabs[index].button.focus()) {
if (mTabs[index].button->focus()) {
mLastFocusedTabIndex = index;
return true;
}
return false;
}
bool TabBar::focus_tab(const Rml::String& title) {
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
if (mTabs[i].title == title) {
return focus_tab(i);
}
}
return false;
}
int TabBar::tab_containing(Rml::Element* element) const {
for (int i = 0; i < mTabs.size(); ++i) {
if (mTabs[i].button.contains(element)) {
if (mTabs[i].button->contains(element)) {
return i;
}
}
+4 -2
View File
@@ -10,7 +10,7 @@ using TabCallback = std::function<void()>;
struct Tab {
Rml::String title;
Button& button;
std::unique_ptr<Button> button;
TabCallback callback;
};
@@ -27,10 +27,12 @@ public:
bool focus() override;
void add_tab(const Rml::String& title, TabCallback callback);
void clear_tabs();
bool set_active_tab(int index);
void refresh_active_tab();
bool focus_tab(int index);
int focused_tab_index() const;
bool focus_tab(const Rml::String& title);
Rml::String focused_tab_title() const;
bool handle_nav_command(Rml::Event& event, NavCommand cmd);
private:
+21 -10
View File
@@ -267,6 +267,24 @@ void uncover_top_document() noexcept {
input::sync_input_block();
}
Document* find_document(DocumentScope scope) noexcept {
for (auto& doc : std::views::reverse(sDocumentStack)) {
if (!doc->closed() && doc->scope() == scope) {
return doc.get();
}
}
return nullptr;
}
void close_documents_except(DocumentScope scope) noexcept {
for (auto& doc : sDocumentStack) {
if (!doc->closed() && doc->scope() != scope) {
doc->force_hide(true);
}
}
input::sync_input_block();
}
bool any_document_visible() noexcept {
return std::any_of(sDocumentStack.begin(), sDocumentStack.end(),
[](const auto& doc) { return doc && doc->visible(); });
@@ -330,13 +348,10 @@ void update() noexcept {
sPassiveDocuments.erase(first, last);
}
// If no documents have focus, explicitly focus the top one
if (auto* context = aurora::rmlui::get_context();
context != nullptr && (context->GetFocusElement() == nullptr ||
context->GetFocusElement() == context->GetRootElement()))
{
// Keep focus on the highest active document.
if (aurora::rmlui::get_context() != nullptr) {
for (auto& doc : std::views::reverse(sDocumentStack)) {
if (doc->active() && doc->focus()) {
if (doc->active() && (doc->has_focus() || doc->focus())) {
break;
}
}
@@ -462,10 +477,6 @@ void push_toast(Toast toast) noexcept {
sToasts.push_back(std::move(toast));
}
std::vector<std::unique_ptr<Document>>& get_document_stack() noexcept {
return sDocumentStack;
}
std::deque<Toast>& get_toasts() noexcept {
return sToasts;
}
+4 -2
View File
@@ -10,6 +10,8 @@
#include "nav_types.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
namespace dusk::ui {
class Document;
@@ -89,6 +91,8 @@ bool register_scoped_styles(DocumentScope scope, std::string id, const std::stri
void unregister_scoped_styles(DocumentScope scope, std::string_view id) noexcept;
void apply_scoped_styles(Document& doc) noexcept;
void uncover_top_document() noexcept;
Document* find_document(DocumentScope scope) noexcept;
void close_documents_except(DocumentScope scope) noexcept;
bool any_document_visible() noexcept;
bool is_prelaunch_open() noexcept;
bool game_obscured_below(const Document& doc) noexcept;
@@ -102,8 +106,6 @@ Rml::Element* append_text(Rml::Element* parent, const Rml::String& text) noexcep
NavCommand map_nav_event(const Rml::Event& event) noexcept;
Insets safe_area_insets(Rml::Context* context) noexcept;
std::vector<std::unique_ptr<Document>>& get_document_stack() noexcept;
void push_toast(Toast toast) noexcept;
std::deque<Toast>& get_toasts() noexcept;
void show_menu_notification() noexcept;
+8 -3
View File
@@ -27,10 +27,11 @@
#include "m_Do/m_Do_main.h"
#if TARGET_PC
#include "tracy/Tracy.hpp"
#include <dusk/gamepad_color.h>
#include <dusk/autosave.h>
#include <dusk/gamepad_color.h>
#include "dusk/game_mode.hpp"
#include "dusk/menu_pointer.h"
#include "tracy/Tracy.hpp"
#endif
fapGm_HIO_c::fapGm_HIO_c() {
@@ -845,7 +846,11 @@ void fapGm_Execute() {
cCt_Counter(0);
#ifdef TARGET_PC
dusk::speedrun::onGameFrame();
const dusk::gamemode::GameMode* gameMode =
dusk::gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnTickFunction();
}
dusk::AchievementSystem::get().tick();
dusk::menu_pointer::end_game_frame();
#endif
+12 -9
View File
@@ -7,7 +7,10 @@
#include "f_op/f_op_overlap_req.h"
#include "f_pc/f_pc_manager.h"
#ifdef TARGET_PC
#include "dusk/game_mode.hpp"
#include "dusk/speedrun.h"
#endif
void fopOvlpReq_SetPeektime(overlap_request_class*, u16);
@@ -20,12 +23,12 @@ static int fopOvlpReq_phase_Done(overlap_request_class* i_overlapReq) {
i_overlapReq->field_0xc = 0;
#if TARGET_PC
if (dusk::getSettings().game.speedrunMode) {
if (dusk::m_speedrunInfo.m_isRunStarted) {
dusk::m_speedrunInfo.m_isPauseIGT = false;
dusk::m_speedrunInfo.m_totalLoadTime +=
OSGetTime() - dusk::m_speedrunInfo.m_loadStartTimestamp;
dusk::m_speedrunInfo.m_loadStartTimestamp = OSGetTime();
if (dusk::speedrun::isActive()) {
if (dusk::speedrun::g_speedrunInfo.m_isRunStarted) {
dusk::speedrun::g_speedrunInfo.m_isPauseIGT = false;
dusk::speedrun::g_speedrunInfo.m_totalLoadTime +=
OSGetTime() - dusk::speedrun::g_speedrunInfo.m_loadStartTimestamp;
dusk::speedrun::g_speedrunInfo.m_loadStartTimestamp = OSGetTime();
}
}
#endif
@@ -96,9 +99,9 @@ static int fopOvlpReq_phase_Create(overlap_request_class* i_overlapReq) {
fpcM_Create(i_overlapReq->procname, NULL, NULL);
#if TARGET_PC
if (dusk::m_speedrunInfo.m_isRunStarted) {
dusk::m_speedrunInfo.m_isPauseIGT = true;
dusk::m_speedrunInfo.m_loadStartTimestamp = OSGetTime();
if (dusk::speedrun::isActive() && dusk::speedrun::g_speedrunInfo.m_isRunStarted) {
dusk::speedrun::g_speedrunInfo.m_isPauseIGT = true;
dusk::speedrun::g_speedrunInfo.m_loadStartTimestamp = OSGetTime();
}
#endif
+104 -48
View File
@@ -3,35 +3,35 @@
* Memory Card Control
*/
#include <card.h>
#include "m_Do/m_Do_MemCard.h"
#include <card.h>
#include "JSystem/JKernel/JKRAssertHeap.h"
#include "m_Do/m_Do_ext.h"
#include "dusk/main.h"
#include "dusk/os.h"
#include "dusk/version.hpp"
#include "m_Do/m_Do_MemCardRWmng.h"
#include "m_Do/m_Do_Reset.h"
#include "m_Do/m_Do_ext.h"
#include "os_report.h"
#include "dusk/os.h"
#include "dusk/main.h"
#include "dusk/version.hpp"
#if PLATFORM_WII || PLATFORM_SHIELD
#include <cstring>
#include <revolution/nand.h>
#include <revolution/sc.h>
#include <cstring>
#endif
#define SLOT_A 0
#define CHECKSPACE_RESULT_READY 0
#define CHECKSPACE_RESULT_READY 0
#define CHECKSPACE_RESULT_INSSPACE 1
#define CHECKSPACE_RESULT_NOENT 2
#define CHECKSPACE_RESULT_ERROR 3
#define CHECKSPACE_RESULT_NOENT 2
#define CHECKSPACE_RESULT_ERROR 3
#if PLATFORM_WII
s32 my_CARDOpen(s32 chan, const char* fileName, CARDFileInfo* fileInfo) {
CARDStat stat;
DVDDiskID* diskID = DVDGetCurrentDiskID();
for (int i = 0; i < CARD_MAX_FILE; i++) {
s32 ret = CARDGetStatus(chan, i, &stat);
if (ret == CARD_RESULT_READY) {
@@ -57,10 +57,10 @@ s32 my_CARDOpen(s32 chan, const char* fileName, CARDFileInfo* fileInfo) {
#endif
#if PLATFORM_WII
#define NAND_OPEN NANDSafeOpen
#define NAND_OPEN NANDSafeOpen
#define NAND_CLOSE NANDSafeClose
#elif PLATFORM_SHIELD
#define NAND_OPEN NANDSimpleSafeOpen
#define NAND_OPEN NANDSimpleSafeOpen
#define NAND_CLOSE NANDSimpleSafeClose
#endif
@@ -77,15 +77,20 @@ static u8 MemCardStack[STACK_SIZE];
static OSThread MemCardThread;
void mDoMemCd_Ctrl_c::ThdInit() {
#if !PLATFORM_SHIELD
#ifdef TARGET_PC
if (mInitialized) {
return;
}
CARDSetLoadType((CARDFileType)dusk::getSettings().backend.cardFileType.getValue());
#endif
#if !PLATFORM_SHIELD
char version[5] = {};
char maker[3] = {};
std::memcpy(version, dusk::version::getDiskID().gameName, 4);
std::memcpy(maker, dusk::version::getDiskID().company, 2);
CARDInit(version, maker);
#endif
#endif
mCopyToPos = 0;
mProbeStat = 2;
@@ -93,6 +98,7 @@ void mDoMemCd_Ctrl_c::ThdInit() {
#if TARGET_PC
mCardCommand = COMM_ATTACH_e;
mInitialized = true;
#else
mCardCommand = COMM_NONE_e;
#endif
@@ -101,8 +107,9 @@ void mDoMemCd_Ctrl_c::ThdInit() {
OSInitMutex(&mMutex);
OSInitCond(&mCond);
OSCreateThread(&MemCardThread, (void*(*)(void*))mDoMemCd_main, NULL, MemCardStack + sizeof(MemCardStack),
sizeof(MemCardStack), OSGetThreadPriority(OSGetCurrentThread()) + 1, 1);
OSCreateThread(&MemCardThread, (void* (*)(void*))mDoMemCd_main, NULL,
MemCardStack + sizeof(MemCardStack), sizeof(MemCardStack),
OSGetThreadPriority(OSGetCurrentThread()) + 1, 1);
OSResumeThread(&MemCardThread);
// "Memory Card Thread Init\n"
@@ -112,23 +119,26 @@ void mDoMemCd_Ctrl_c::ThdInit() {
void mDoMemCd_Ctrl_c::main() {
do {
OSLockMutex(&mMutex);
while (mCardCommand == COMM_NONE_e
#ifdef TARGET_PC
&& !dusk::IsShuttingDown
#endif
) {
bool shutdownThread = dusk::IsShuttingDown;
while (mCardCommand == COMM_NONE_e && !shutdownThread) {
OSWaitCond(&mCond, &mMutex);
shutdownThread = dusk::IsShuttingDown;
}
OSUnlockMutex(&mMutex);
#ifdef TARGET_PC
if (dusk::IsShuttingDown) {
if (shutdownThread) {
break;
}
#else
while (mCardCommand == COMM_NONE_e) {
OSWaitCond(&mCond, &mMutex);
}
OSUnlockMutex(&mMutex);
#endif
switch (mCardCommand) {
#if PLATFORM_GCN || PLATFORM_WII
#if PLATFORM_GCN || PLATFORM_WII
case COMM_RESTORE_e:
restore();
break;
@@ -144,16 +154,16 @@ void mDoMemCd_Ctrl_c::main() {
case COMM_DETACH_e:
detach();
break;
#elif PLATFORM_SHIELD
#elif PLATFORM_SHIELD
case COMM_RESTORE_e:
case COMM_STORE_e:
case COMM_FORMAT_e:
case COMM_ATTACH_e:
case COMM_DETACH_e:
break;
#endif
#endif
#if PLATFORM_WII || PLATFORM_SHIELD
#if PLATFORM_WII || PLATFORM_SHIELD
case COMM_RESTORE_NAND_e:
restoreNAND();
break;
@@ -163,7 +173,7 @@ void mDoMemCd_Ctrl_c::main() {
case COMM_STORE_SETUP_NAND_e:
storeSetUpNAND();
break;
#endif
#endif
}
OSLockMutex(&mMutex);
@@ -180,7 +190,7 @@ void mDoMemCd_Ctrl_c::update() {
OSUnlockMutex(&mMutex);
OSSignalCond(&mCond);
} else if (getStatus(0) != 14) {
#if PLATFORM_GCN || PLATFORM_WII
#if PLATFORM_GCN || PLATFORM_WII
if (CARDProbe(SLOT_A) && getStatus(0) == 0) {
OSLockMutex(&mMutex);
mProbeStat = 0;
@@ -196,7 +206,7 @@ void mDoMemCd_Ctrl_c::update() {
OSUnlockMutex(&mMutex);
OSSignalCond(&mCond);
}
#endif
#endif
}
}
@@ -214,7 +224,12 @@ void mDoMemCd_Ctrl_c::restore() {
CARDFileInfo file;
field_0x1fc8 = 0;
s32 ret = CARD_OPEN(mChannel, "gczelda2", &file);
#ifdef TARGET_PC
const char* fileName = getFileName();
#else
const char* fileName = "gczelda2";
#endif
s32 ret = CARD_OPEN(mChannel, fileName, &file);
OS_REPORT("\x1b[43;30mCret=%d\n\x1b[m", ret);
if (ret == CARD_RESULT_READY) {
s32 ret2 = mDoMemCdRWm_Restore(&file, this, sizeof(mData));
@@ -271,12 +286,18 @@ void mDoMemCd_Ctrl_c::store() {
s32 ret;
field_0x1fc8 = 0;
#ifdef TARGET_PC
const char* fileName = getFileName();
#else
const char* fileName = "gczelda2";
#endif
if (mCardState == CARD_STATE_NO_FILE_e) {
#if PLATFORM_GCN
ret = CARDCreate(mChannel, "gczelda2", CARD_FILE_SIZE, &file);
#else
#if PLATFORM_GCN
ret = CARDCreate(mChannel, fileName, CARD_FILE_SIZE, &file);
#else
ret = CARDCreate(mChannel, "zeldaTp.dat", CARD_FILE_SIZE, &file);
#endif
#endif
if (ret == CARD_RESULT_READY || ret == CARD_RESULT_EXIST) {
mCardState = CARD_STATE_READY_e;
} else {
@@ -285,7 +306,7 @@ void mDoMemCd_Ctrl_c::store() {
}
if (mCardState == CARD_STATE_READY_e) {
ret = CARD_OPEN(mChannel, "gczelda2", &file);
ret = CARD_OPEN(mChannel, fileName, &file);
if (ret == CARD_RESULT_READY) {
ret = mDoMemCdRWm_Store(&file, this, sizeof(mData));
if (ret != CARD_RESULT_READY) {
@@ -527,7 +548,13 @@ s32 mDoMemCd_Ctrl_c::mount() {
s32 mDoMemCd_Ctrl_c::loadfile() {
CARDFileInfo file;
s32 ret = CARD_OPEN(mChannel, "gczelda2", &file);
#ifdef TARGET_PC
const char* fileName = getFileName();
#else
const char* fileName = "gczelda2";
#endif
s32 ret = CARD_OPEN(mChannel, fileName, &file);
if (ret == CARD_RESULT_READY) {
CARDClose(&file);
return TRUE;
@@ -543,7 +570,7 @@ s32 mDoMemCd_Ctrl_c::checkspace() {
if (result != CARD_RESULT_READY) {
setCardState(result);
return CHECKSPACE_RESULT_ERROR;
return CHECKSPACE_RESULT_ERROR;
}
if (bytesNotUsed < CARD_FILE_SIZE) {
@@ -553,7 +580,7 @@ s32 mDoMemCd_Ctrl_c::checkspace() {
if (filesNotUsed < 1) {
return CHECKSPACE_RESULT_NOENT;
}
return CHECKSPACE_RESULT_READY;
}
@@ -596,7 +623,7 @@ void mDoMemCd_Ctrl_c::restoreNAND() {
NANDFileInfo file;
s32 ret, ret2;
field_0x1fc8 = 0;
field_0x1fc8 = 0;
ret = NANDOpen("zeldaTp.dat", &file, NAND_ACCESS_RW);
OS_REPORT("\x1b[43;30mCret=%d\n\x1b[m", ret);
@@ -668,7 +695,8 @@ void mDoMemCd_Ctrl_c::storeNAND() {
ret = NANDCreate("banner.bin", NAND_PERM_RUSR | NAND_PERM_WUSR | NAND_PERM_RGRP, 0);
printf("NAND bannerFile Create ret:%d\n", ret);
if (ret == NAND_RESULT_OK || ret == NAND_RESULT_EXISTS) {
ret = NAND_OPEN("banner.bin", &file, NAND_ACCESS_RW, l_safeCopyBuf, sizeof(l_safeCopyBuf));
ret = NAND_OPEN(
"banner.bin", &file, NAND_ACCESS_RW, l_safeCopyBuf, sizeof(l_safeCopyBuf));
if (ret == NAND_RESULT_OK) {
ret = mDoMemCdRWm_StoreBannerNAND(&file);
if (ret == NAND_RESULT_OK) {
@@ -686,7 +714,8 @@ void mDoMemCd_Ctrl_c::storeNAND() {
}
if (ret == NAND_RESULT_OK) {
ret = NAND_OPEN("zeldaTp.dat", &file, NAND_ACCESS_RW, l_safeCopyBuf, sizeof(l_safeCopyBuf));
ret = NAND_OPEN(
"zeldaTp.dat", &file, NAND_ACCESS_RW, l_safeCopyBuf, sizeof(l_safeCopyBuf));
if (ret == NAND_RESULT_OK) {
ret = mDoMemCdRWm_StoreNAND(&file, this, sizeof(mData));
if (ret == NAND_RESULT_OK) {
@@ -732,10 +761,11 @@ s32 mDoMemCd_Ctrl_c::SaveSyncNAND() {
void mDoMemCd_Ctrl_c::storeSetUpNAND() {
field_0x1fc8 = 0;
while ((int)SCCheckStatus() != 0) {}
#if PLATFORM_WII
while ((int)SCCheckStatus() != 0) {
}
#if PLATFORM_WII
if (!SCFlush()) {
mNandState = NAND_STATE_WRITE_e;
printf("== 本体設定Write OK ==\n");
@@ -743,9 +773,9 @@ void mDoMemCd_Ctrl_c::storeSetUpNAND() {
mNandState = NAND_STATE_FATAL_ERROR_e;
printf("== 本体設定Write ERR ==\n");
}
#else
#else
mNandState = NAND_STATE_WRITE_e;
#endif
#endif
field_0x1fc8 = 1;
}
@@ -866,7 +896,7 @@ s32 mDoMemCd_Ctrl_c::checkspaceNAND() {
s32 result = NANDCheck(3, 2, &answer);
if (result != NAND_RESULT_OK) {
setNandState(result);
return CHECKSPACE_RESULT_ERROR;
return CHECKSPACE_RESULT_ERROR;
}
if (answer == 0) {
@@ -876,11 +906,37 @@ s32 mDoMemCd_Ctrl_c::checkspaceNAND() {
} else if (answer & 10) {
ret = 2;
}
return ret;
}
#endif
#ifdef TARGET_PC
void mDoMemCd_Ctrl_c::setFileName(const std::string& fileName) {
if (mInitialized == false) {
mFileName = fileName;
} else {
OSLockMutex(&mMutex);
mFileName = fileName;
OSUnlockMutex(&mMutex);
}
}
const char* mDoMemCd_Ctrl_c::getFileName() {
const char* fileName = "gczelda2";
if (mInitialized) {
OSLockMutex(&mMutex);
}
if (!mFileName.empty()) {
fileName = mFileName.c_str();
}
if (mInitialized) {
OSUnlockMutex(&mMutex);
}
return fileName;
}
#endif
DUSK_GAME_DATA mDoMemCd_Ctrl_c g_mDoMemCd_control;
static int mDoMemCd_main(void*) {
+24 -4
View File
@@ -4,21 +4,26 @@
*/
#include "m_Do/m_Do_Reset.h"
#include <gx.h>
#include "JSystem/JAudio2/JASDvdThread.h"
#include "JSystem/JUtility/JUTGamePad.h"
#include "JSystem/JUtility/JUTXfb.h"
#include "SSystem/SComponent/c_API_controller_pad.h"
#include <gx.h>
#include "m_Do/m_Do_audio.h"
#include "m_Do/m_Do_DVDError.h"
#include "m_Do/m_Do_ext.h"
#include "m_Do/m_Do_MemCard.h"
#include "m_Do/m_Do_audio.h"
#include "m_Do/m_Do_ext.h"
#if !PLATFORM_GCN
#include <revolution/os.h>
#endif
#include "os_report.h"
#ifdef TARGET_PC
#include "dusk/game_mode.hpp"
#include "dusk/ui/prelaunch.hpp"
#endif
static void my_OSCancelAlarmAll() {}
static void destroyVideo() {
@@ -104,6 +109,14 @@ void checkDiskCallback(s32 result, DVDCommandBlock* block) {
}
void mDoRst_resetCallBack(int port, void*) {
#ifdef TARGET_PC
const dusk::gamemode::GameMode* gameMode =
dusk::gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnGameResetFunction();
}
#endif
if (mDoRst::isReset()) {
return;
}
@@ -129,7 +142,8 @@ void mDoRst_resetCallBack(int port, void*) {
#else
DVDCommandBlock block;
block.userData = (void*)-1;
while (DVDCheckDiskAsync(&block, checkDiskCallback));
while (DVDCheckDiskAsync(&block, checkDiskCallback))
;
do {
check = (int)block.userData;
} while (check == -1);
@@ -141,6 +155,12 @@ void mDoRst_resetCallBack(int port, void*) {
}
}
mDoRst::onReset();
#ifdef TARGET_PC
if (dusk::ui::prelaunch_state().returnToPrelaunchOnReset) {
dusk::ui::return_to_prelaunch();
dusk::ui::prelaunch_state().returnToPrelaunchOnReset = false;
}
#endif
}
void mDoRst_shutdownCallBack() {
+100 -69
View File
@@ -502,6 +502,56 @@ static void log_build_info() {
DuskLog.info("Platform: {}", BOREALIS_PLATFORM_NAME);
}
static void mods_init(const std::filesystem::path& mods_dir) {
// Mod search directories, highest priority first: user dir (--mods replaces it), then
// mods/ next to the app, then install-bundled mods inside the app bundle.
{
std::vector<dusk::mods::ModSearchDir> modDirs;
modDirs.push_back({.path = mods_dir});
#if TARGET_ANDROID
// APK-bundled mods are extracted to internal storage
// by DuskActivity before SDL_main runs.
modDirs.push_back({
.path = dusk::CachePath / "bundled_mods",
});
#elif defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)
modDirs.push_back({
.path = dusk::data::base_path_relative("mods"),
.inPlaceNative = true,
.nativeLibDir = dusk::data::base_path_relative("Frameworks"),
});
#else
#if defined(__APPLE__)
// Base path is Contents/Resources; search up for dev mods
// TODO: scope to non-CI builds
modDirs.push_back({
.path = dusk::data::base_path_relative("../../../mods").lexically_normal(),
.inPlaceNative = true,
});
// Contents/Resources/mods
modDirs.push_back({
.path = dusk::data::base_path_relative("mods"),
.inPlaceNative = true,
});
#else
modDirs.push_back({
.path = dusk::data::base_path_relative("mods"),
.inPlaceNative = true,
});
#endif
#endif
dusk::mods::ModLoader::instance().set_search_dirs(std::move(modDirs));
}
#if TARGET_ANDROID
// A user-relocated data dir can live on external storage, which is mounted noexec.
// Native mod libraries must be extracted to internal storage.
dusk::mods::ModLoader::instance().set_cache_dir(dusk::CachePath / "mod_cache");
#endif
DuskLog.info("Initializing mods...");
dusk::mods::ModLoader::instance().init();
}
// =========================================================================
// PC ENTRY POINT
// =========================================================================
@@ -675,12 +725,6 @@ int game_main(int argc, char* argv[]) {
dusk::presentation::update_frame_rate_preference();
// Apply after aurora_initialize: speedrun mode mutates cvars whose change callbacks push
// values into aurora.
if (dusk::getSettings().game.speedrunMode) {
dusk::resetForSpeedrunMode();
}
#if BOREALIS_HAS_DISCORD
if (dusk::getSettings().game.enableDiscordPresence) {
dusk::discord::initialize();
@@ -753,6 +797,8 @@ int game_main(int argc, char* argv[]) {
saveConfigBeforePrelaunch = true;
}
bool skipPreLaunchUI = dusk::getSettings().backend.skipPreLaunchUI.getValue();
std::string dvd_path = dusk::getSettings().backend.isoPath;
bool dvd_opened = false;
if (parsed_arg_options.count("dvd")) {
@@ -769,6 +815,7 @@ int game_main(int argc, char* argv[]) {
dusk::DiscVerificationState::Unknown);
dusk::config::save();
dusk::IsGameLaunched = true;
skipPreLaunchUI = true;
}
} else {
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
@@ -776,8 +823,6 @@ int game_main(int argc, char* argv[]) {
}
}
bool skipPreLaunchUI = dusk::getSettings().backend.skipPreLaunchUI.getValue();
// If we can't load right into the game, stop requesting to load a stage or save
if (forcePreLaunchUI || dvd_path.empty()) {
if (dusk::StageRequested.set) {
@@ -796,6 +841,16 @@ int game_main(int argc, char* argv[]) {
dusk::getSettings().backend.isoPath.getValue(),
dusk::getSettings().backend.isoVerification.getValue());
bool showPrelaunchAfterInit = true;
if (!dvd_opened && (dusk::getSettings().backend.isoPath.getValue().empty() || (forcePreLaunchUI && skipPreLaunchUI))) {
showPrelaunchAfterInit = false;
}
if (showPrelaunchAfterInit) {
// Force launchUILoop to not run, we know that the ISO will be loaded.
dusk::IsGameLaunched = true;
}
if (!dvd_opened) {
if (dusk::getSettings().backend.isoPath.getValue().empty()) {
forcePreLaunchUI = true;
@@ -810,7 +865,9 @@ int game_main(int argc, char* argv[]) {
}
if (!skipPreLaunchUI) {
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
if (!showPrelaunchAfterInit) {
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
}
// pre game launch ui main loop
if (!launchUILoop()) {
@@ -844,16 +901,6 @@ int game_main(int argc, char* argv[]) {
dusk::IsGameLaunched = true;
}
#if BOREALIS_HAS_SENTRY
if (borealis::sentry::get_consent() == borealis::sentry::Consent::Unknown) {
dusk::ui::push_document(std::make_unique<dusk::ui::CrashReportWindow>());
}
#endif
if (!dusk::getSettings().backend.wasPresetChosen) {
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
}
dusk::version::init();
LanguageInit();
@@ -872,59 +919,43 @@ int game_main(int argc, char* argv[]) {
mDoDvdThd::SyncWidthSound = false;
// Mod search directories, highest priority first: user dir (--mods replaces it), then
// mods/ next to the app, then install-bundled mods inside the app bundle.
{
std::vector<dusk::mods::ModSearchDir> modDirs;
if (parsed_arg_options.contains("mods") &&
!parsed_arg_options["mods"].as<std::string>().empty())
{
modDirs.push_back({.path = parsed_arg_options["mods"].as<std::string>()});
} else {
modDirs.push_back({.path = dusk::ConfigPath / "mods"});
}
#if TARGET_ANDROID
// APK-bundled mods are extracted to internal storage
// by DuskActivity before SDL_main runs.
modDirs.push_back({
.path = dusk::CachePath / "bundled_mods",
});
#elif defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)
modDirs.push_back({
.path = dusk::data::base_path_relative("mods"),
.inPlaceNative = true,
.nativeLibDir = dusk::data::base_path_relative("Frameworks"),
});
#else
#if defined(__APPLE__)
// Base path is Contents/Resources; search up for dev mods
// TODO: scope to non-CI builds
modDirs.push_back({
.path = dusk::data::base_path_relative("../../../mods").lexically_normal(),
.inPlaceNative = true,
});
// Contents/Resources/mods
modDirs.push_back({
.path = dusk::data::base_path_relative("mods"),
.inPlaceNative = true,
});
#else
modDirs.push_back({
.path = dusk::data::base_path_relative("mods"),
.inPlaceNative = true,
});
#endif
#endif
dusk::mods::ModLoader::instance().set_search_dirs(std::move(modDirs));
// Apply after aurora_initialize: speedrun mode mutates cvars whose change callbacks push
// values into aurora.
if (dusk::getSettings().game.speedrunMode) {
dusk::speedrun::registerSpeedrunGameMode();
}
if (parsed_arg_options.contains("mods") &&
!parsed_arg_options["mods"].as<std::string>().empty())
{
mods_init(parsed_arg_options["mods"].as<std::string>());
} else {
mods_init(dusk::ConfigPath / "mods");
}
if (!skipPreLaunchUI && showPrelaunchAfterInit) {
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
}
if (skipPreLaunchUI == true) {
if (dusk::gamemode::getGameModeManager().getRegisteredGameModes().size() > 1 && dusk::getSettings().backend.skipPreLaunchUI.getValue()) {
// Force pre-launch if we have registered gamemodes that we need to choose from
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
} else {
// If we get back to prelaunch later, tell it that we've already started the game
dusk::ui::prelaunch_state().firstLaunch = false;
}
}
#if BOREALIS_HAS_SENTRY
if (borealis::sentry::get_consent() == borealis::sentry::Consent::Unknown) {
dusk::ui::push_document(std::make_unique<dusk::ui::CrashReportWindow>());
}
#if TARGET_ANDROID
// A user-relocated data dir can live on external storage, which is mounted noexec.
// Native mod libraries must be extracted to internal storage.
dusk::mods::ModLoader::instance().set_cache_dir(dusk::CachePath / "mod_cache");
#endif
DuskLog.info("Initializing mods...");
dusk::mods::ModLoader::instance().init();
if (!dusk::getSettings().backend.wasPresetChosen) {
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
}
OSReport("Starting main01 (Game Loop)...\n");