speedrun mode (#7073)

prevent Esc menu while speedrun file loaded, instead toggles gameplay stats / time splits

render RUN tag on speedrun files

don't persist speedrun preset into player's settings
This commit is contained in:
Philip Dubé
2026-10-05 05:19:52 +00:00
committed by GitHub
parent b69cb6607e
commit 4afcb78331
33 changed files with 1480 additions and 308 deletions
@@ -25,5 +25,7 @@
"quest_sel_vanilla": "Quest - Original",
"quest_sel_mq": "Quest - Master Quest",
"quest_sel_randomizer": "Quest - Randomizer",
"quest_sel_boss_rush": "Quest - Boss Rush"
"quest_sel_boss_rush": "Quest - Boss Rush",
"quest_sel_speedrun": "Quest - Speedrun",
"quest_sel_speedrun_mq": "Quest - Speedrun Master Quest"
}
@@ -25,5 +25,7 @@
"quest_sel_vanilla": "Quête - Originale",
"quest_sel_mq": "Quête - Master Quest",
"quest_sel_randomizer": "Quête - Randomizer",
"quest_sel_boss_rush": "Quête - Boss Rush"
"quest_sel_boss_rush": "Quête - Boss Rush",
"quest_sel_speedrun": "Quête - Speedrun",
"quest_sel_speedrun_mq": "Quête - Speedrun Master Quest"
}
@@ -25,5 +25,7 @@
"quest_sel_vanilla": "Quest - Original",
"quest_sel_mq": "Quest - Master Quest",
"quest_sel_randomizer": "Quest - Randomizer",
"quest_sel_boss_rush": "Quest - Boss Rush"
"quest_sel_boss_rush": "Quest - Boss Rush",
"quest_sel_speedrun": "Quest - Speedrun",
"quest_sel_speedrun_mq": "Quest - Speedrun Master Quest"
}
@@ -0,0 +1,119 @@
{
"blocks": {
"enhancements": {
"gCheats": {
"EasyFrameAdvance": 0,
"HookshotReachMultiplier": 1.0,
"MoonJumpOnL": 0,
"SpeedModifier": {
"Value": 1.0
}
},
"gDeveloperTools": null,
"gEnhancements": {
"3DSceneRender": 0,
"AlwaysShowDungeonMinimapIcon": 0,
"AskToEquip": 0,
"AssignableTunicsAndBoots": 0,
"Autosave": 0,
"BetterAmmoRendering": 0,
"BetterFarore": 0,
"BombchuBowlingFirstPrize": 4,
"BombchusOOB": 1,
"BowArrowCycle": 1,
"ChildHookshotSoftlock": 1,
"CustomizeBombchuBowling": 1,
"CustomizeFishing": 1,
"CustomizeOcarinaGame": 0,
"DampeWin": 1,
"DisableBlackBars": 0,
"DisableBombBillboarding": 0,
"DisableCritWiggle": 1,
"DisableFixedCamera": 0,
"DisableGrottoRotation": 0,
"DisableLOD": 1,
"DisableLinkSwordTrail": 0,
"DpadEquips": 1,
"DpadNoDropOcarinaInput": 1,
"DrawLineupTick": 0,
"DynamicWalletIcon": 1,
"EarlyEyeballFrog": 1,
"EquipmentAlwaysVisible": 0,
"EquipmentCanBeRemoved": 0,
"FastBoomerang": 0,
"FasterPauseMenu": 0,
"FileSelectMoreInfo": 1,
"FirstPersonGauntlets": 0,
"FishNeverEscape": 1,
"FixDampeGoingBackwards": 0,
"FixDaruniaDanceSpeed": 1,
"FixDungeonMinimapIcon": 1,
"FixSawSoftlock": 1,
"FixVineFall": 1,
"GravediggingTourFix": 1,
"HoverFishing": 1,
"HyperBosses": 0,
"HyperEnemies": 0,
"IncludeHeldInputsBufferWindow": 0,
"InjectItemCounts": {
"GoldSkulltula": 1,
"HeartContainer": 1,
"HeartPiece": 1
},
"ItemUnequip": 0,
"MinimalUI": 0,
"MweepSpeed": 1.0,
"N64WeirdFrames": 1,
"NewDrops": 0,
"NoHUDHeartAnimation": 0,
"NoInputForCredits": 1,
"OcarinaGame": {
"StartingNotes": 8
},
"PauseAnyCursor": 2,
"PauseBufferWindow": 5,
"PauseMenuAnimatedLink": 0,
"QuickBongoKill": 1,
"QuickPutaway": 1,
"RedGanonBlood": 1,
"RememberMapToggleState": 0,
"ReworkedTargeting": {
"Enabled": 0
},
"ScaleAdultEquipmentAsChild": 1,
"SceneSpecificDirtPathFix": 1,
"ShowDoorLocksOnBothSides": 0,
"SkipArrowAnimation": 1,
"SkipText": 0,
"SlowTextSpeed": 1,
"StaticExplosionRadius": 0,
"StrengthDisabled": 0,
"SwordToggle": 0,
"TextSpeed": 1,
"TimeFlowFileSelect": 0,
"TimeSavers": {
"SkipCutscene": {
"Intro": 1
}
},
"ToTMedallionsColors": 0,
"ToggleStrength": 0,
"VisualAgony": 0,
"WideShutterDoorRange": 1
},
"gRandoEnhancements": null
}
},
"exempt": [
"/gDeveloperTools/LogLevel",
"/gDeveloperTools/ResourceLogging",
"/gEnhancements/DrawLineupTick",
"/gEnhancements/GraveHoles",
"/gEnhancements/IncludeHeldInputsBufferWindow",
"/gEnhancements/PauseBufferWindow"
],
"fileType": 2,
"presetName": "Speedrun - 1 Base",
"isBuiltIn": true,
"ConfigVersion": 7
}
@@ -0,0 +1,130 @@
{
"blocks": {
"enhancements": {
"gCheats": {
"EasyFrameAdvance": 0,
"HookshotReachMultiplier": 1.0,
"MoonJumpOnL": 0,
"SpeedModifier": {
"Value": 1.0
}
},
"gDeveloperTools": null,
"gEnhancements": {
"3DSceneRender": 0,
"AlwaysShowDungeonMinimapIcon": 0,
"AskToEquip": 0,
"AssignableTunicsAndBoots": 0,
"Autosave": 0,
"BetterAmmoRendering": 0,
"BetterFarore": 0,
"BombchuBowlingFirstPrize": 4,
"BombchusOOB": 1,
"BowArrowCycle": 1,
"ChildHookshotSoftlock": 1,
"CustomizeBombchuBowling": 1,
"CustomizeFishing": 1,
"CustomizeOcarinaGame": 0,
"DampeWin": 1,
"DisableBlackBars": 0,
"DisableBombBillboarding": 0,
"DisableCritWiggle": 1,
"DisableFixedCamera": 0,
"DisableGrottoRotation": 0,
"DisableLOD": 1,
"DisableLinkSwordTrail": 0,
"DpadEquips": 1,
"DpadNoDropOcarinaInput": 1,
"DrawLineupTick": 0,
"DynamicWalletIcon": 1,
"EarlyEyeballFrog": 1,
"EquipmentAlwaysVisible": 0,
"EquipmentCanBeRemoved": 0,
"FastBoomerang": 0,
"FastChests": 1,
"FasterPauseMenu": 0,
"FileSelectMoreInfo": 1,
"FirstPersonGauntlets": 0,
"FishNeverEscape": 1,
"FixDampeGoingBackwards": 0,
"FixDaruniaDanceSpeed": 1,
"FixDungeonMinimapIcon": 1,
"FixSawSoftlock": 1,
"FixVineFall": 1,
"ForgeTime": 1,
"GravediggingTourFix": 1,
"HoverFishing": 1,
"HyperBosses": 0,
"HyperEnemies": 0,
"IncludeHeldInputsBufferWindow": 0,
"InjectItemCounts": {
"GoldSkulltula": 1,
"HeartContainer": 1,
"HeartPiece": 1
},
"ItemUnequip": 0,
"MinimalUI": 0,
"MweepSpeed": 5.0,
"N64WeirdFrames": 1,
"NewDrops": 0,
"NoHUDHeartAnimation": 0,
"NoInputForCredits": 1,
"OcarinaGame": {
"StartingNotes": 8
},
"PauseAnyCursor": 2,
"PauseBufferWindow": 5,
"PauseMenuAnimatedLink": 0,
"QuickBongoKill": 1,
"QuickPutaway": 1,
"QuitFishingAtDoor": 1,
"RedGanonBlood": 1,
"RememberMapToggleState": 0,
"ReworkedTargeting": {
"Enabled": 0
},
"ScaleAdultEquipmentAsChild": 1,
"SceneSpecificDirtPathFix": 1,
"ShowDoorLocksOnBothSides": 0,
"SkipArrowAnimation": 1,
"SkipText": 1,
"SlowTextSpeed": 5,
"StaticExplosionRadius": 0,
"StrengthDisabled": 0,
"SwordToggle": 0,
"TextSpeed": 5,
"TimeFlowFileSelect": 0,
"TimeSavers": {
"SkipCutscene": {
"BossIntro": 1,
"Entrances": 1,
"Intro": 1,
"LearnSong": 0,
"OnePoint": 0,
"QuickBossDeaths": 1,
"Story": 1
},
"SkipOwlInteractions": 1
},
"ToTMedallionsColors": 0,
"ToggleStrength": 0,
"VisualAgony": 0,
"WarpSongSkipAnimation": 1,
"WideShutterDoorRange": 1
},
"gRandoEnhancements": null
}
},
"exempt": [
"/gDeveloperTools/LogLevel",
"/gDeveloperTools/ResourceLogging",
"/gEnhancements/DrawLineupTick",
"/gEnhancements/GraveHoles",
"/gEnhancements/IncludeHeldInputsBufferWindow",
"/gEnhancements/PauseBufferWindow"
],
"fileType": 2,
"presetName": "Speedrun - 2 Faster Quest",
"isBuiltIn": true,
"ConfigVersion": 7
}
@@ -0,0 +1,123 @@
{
"blocks": {
"enhancements": {
"gCheats": {
"EasyFrameAdvance": 0,
"HookshotReachMultiplier": 1.0,
"MoonJumpOnL": 0,
"SpeedModifier": {
"Value": 1.0
}
},
"gDeveloperTools": null,
"gEnhancements": {
"3DSceneRender": 0,
"AlwaysShowDungeonMinimapIcon": 0,
"AskToEquip": 0,
"AssignableTunicsAndBoots": 0,
"Autosave": 0,
"BetterAmmoRendering": 0,
"BetterFarore": 0,
"BombchuBowlingFirstPrize": 4,
"BombchusOOB": 1,
"BowArrowCycle": 1,
"ChildHookshotSoftlock": 1,
"CustomizeBombchuBowling": 1,
"CustomizeFishing": 1,
"CustomizeOcarinaGame": 0,
"DampeWin": 1,
"DisableBlackBars": 0,
"DisableBombBillboarding": 0,
"DisableCritWiggle": 1,
"DisableFixedCamera": 0,
"DisableGrottoRotation": 0,
"DisableLOD": 1,
"DisableLinkSwordTrail": 0,
"DpadEquips": 1,
"DpadNoDropOcarinaInput": 1,
"DrawLineupTick": 0,
"DynamicWalletIcon": 1,
"EarlyEyeballFrog": 1,
"EquipmentAlwaysVisible": 0,
"EquipmentCanBeRemoved": 0,
"FastBoomerang": 0,
"FastChests": 1,
"FasterPauseMenu": 0,
"FileSelectMoreInfo": 1,
"FirstPersonGauntlets": 0,
"FishNeverEscape": 1,
"FixDampeGoingBackwards": 0,
"FixDaruniaDanceSpeed": 1,
"FixDungeonMinimapIcon": 1,
"FixSawSoftlock": 1,
"FixVineFall": 1,
"ForgeTime": 1,
"GravediggingTourFix": 1,
"HoverFishing": 1,
"HyperBosses": 0,
"HyperEnemies": 0,
"IncludeHeldInputsBufferWindow": 0,
"InjectItemCounts": {
"GoldSkulltula": 1,
"HeartContainer": 1,
"HeartPiece": 1
},
"ItemUnequip": 0,
"MinimalUI": 0,
"MweepSpeed": 5.0,
"N64WeirdFrames": 1,
"NewDrops": 0,
"NoHUDHeartAnimation": 0,
"NoInputForCredits": 1,
"OcarinaGame": {
"StartingNotes": 8
},
"PauseAnyCursor": 2,
"PauseBufferWindow": 5,
"PauseMenuAnimatedLink": 0,
"QuickBongoKill": 1,
"QuickPutaway": 1,
"QuitFishingAtDoor": 1,
"RedGanonBlood": 1,
"RememberMapToggleState": 0,
"ReworkedTargeting": {
"Enabled": 0
},
"ScaleAdultEquipmentAsChild": 1,
"SceneSpecificDirtPathFix": 1,
"ShowDoorLocksOnBothSides": 0,
"SkipArrowAnimation": 1,
"SkipText": 1,
"SlowTextSpeed": 5,
"StaticExplosionRadius": 0,
"StrengthDisabled": 0,
"SwordToggle": 0,
"TextSpeed": 5,
"TimeFlowFileSelect": 0,
"TimeSavers": {
"SkipCutscene": {
"Intro": 1,
"QuickBossDeaths": 1
}
},
"ToTMedallionsColors": 0,
"ToggleStrength": 0,
"VisualAgony": 0,
"WideShutterDoorRange": 1
},
"gRandoEnhancements": null
}
},
"exempt": [
"/gDeveloperTools/LogLevel",
"/gDeveloperTools/ResourceLogging",
"/gEnhancements/DrawLineupTick",
"/gEnhancements/GraveHoles",
"/gEnhancements/IncludeHeldInputsBufferWindow",
"/gEnhancements/PauseBufferWindow"
],
"fileType": 2,
"presetName": "Speedrun - 3 Faster Quest with Cutscenes",
"isBuiltIn": true,
"ConfigVersion": 7
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

-6
View File
@@ -407,9 +407,6 @@ static const ALIGN_ASSET(2) char gSunIconTex[] = dgSunIcon;
#define dgNaviIcon "__OTR__textures/parameter_static/gNavi"
static const ALIGN_ASSET(2) char gNaviIconTex[] = dgNaviIcon;
#define dgFileSelMQButtonTex "__OTR__textures/title_static/gFileSelMQButtonTex"
static const ALIGN_ASSET(2) char gFileSelMQButtonTex[] = dgFileSelMQButtonTex;
#define dgFileSelPleaseChooseAQuestENGTex "__OTR__textures/title_static/gFileSelPleaseChooseAQuestENGTex"
static const ALIGN_ASSET(2) char gFileSelPleaseChooseAQuestENGTex[] = dgFileSelPleaseChooseAQuestENGTex;
@@ -434,9 +431,6 @@ static const ALIGN_ASSET(2) char gFileSelBossRushSettingsGERText[] = dgFileSelBo
#define dgFileSelBossRushSettingsJPNTex "__OTR__textures/title_static/gFileSelBossRushSettingsJPNTex"
static const ALIGN_ASSET(2) char gFileSelBossRushSettingsJPNText[] = dgFileSelBossRushSettingsJPNTex;
#define dgFileSelRANDButtonTex "__OTR__textures/title_static/gFileSelRANDButtonTex"
static const ALIGN_ASSET(2) char gFileSelRANDButtonTex[] = dgFileSelRANDButtonTex;
#define dgFileSelLangEnglishENGTex "__OTR__textures/title_static/gFileSelLangEnglishENGTex"
static const ALIGN_ASSET(2) char gFileSelLangEnglishENGTex[] = dgFileSelLangEnglishENGTex;
+4
View File
@@ -1608,6 +1608,10 @@ typedef struct FileChooseContext {
uint8_t randomizerIndex;
int16_t randomizerUIAlpha;
uint16_t randomizerArrowOffset;
uint8_t speedrunIndex;
uint8_t speedrunOffset;
int16_t speedrunUIAlpha;
uint16_t speedrunArrowOffset;
} FileChooseContext; // size = 0x1CAE0
// Macros for `EntranceInfo.field`
+7 -2
View File
@@ -403,12 +403,17 @@ typedef enum {
/* 01 */ QUEST_MASTER,
/* 02 */ QUEST_RANDOMIZER,
/* 03 */ QUEST_BOSSRUSH,
/* 04 */ QUEST_SPEEDRUN,
/* 05 */ QUEST_SPEEDRUN_MASTER,
} Quest;
#define IS_VANILLA (gSaveContext.ship.quest.id == QUEST_NORMAL)
#define IS_MASTER_QUEST (gSaveContext.ship.quest.id == QUEST_MASTER)
#define IS_VANILLA (gSaveContext.ship.quest.id == QUEST_NORMAL || gSaveContext.ship.quest.id == QUEST_SPEEDRUN)
#define IS_MASTER_QUEST \
(gSaveContext.ship.quest.id == QUEST_MASTER || gSaveContext.ship.quest.id == QUEST_SPEEDRUN_MASTER)
#define IS_RANDO (gSaveContext.ship.quest.id == QUEST_RANDOMIZER)
#define IS_BOSS_RUSH (gSaveContext.ship.quest.id == QUEST_BOSSRUSH)
#define IS_SPEEDRUN \
(gSaveContext.ship.quest.id == QUEST_SPEEDRUN || gSaveContext.ship.quest.id == QUEST_SPEEDRUN_MASTER)
typedef enum {
/* 0x00 */ BTN_ENABLED,
@@ -80,6 +80,12 @@ bool SohFileSelect_IsQuestHidden(u8 quest) {
return CVarGetInteger(CVAR_ENHANCEMENT("FileSelect.HideRandomizerQuest"), 0);
case QUEST_BOSSRUSH:
return CVarGetInteger(CVAR_ENHANCEMENT("FileSelect.HideBossRushQuest"), 0);
case QUEST_SPEEDRUN:
return !ResourceMgr_GameHasOriginal() ||
CVarGetInteger(CVAR_ENHANCEMENT("FileSelect.HideSpeedrunQuest"), 0);
case QUEST_SPEEDRUN_MASTER:
return !ResourceMgr_GameHasMasterQuest() ||
CVarGetInteger(CVAR_ENHANCEMENT("FileSelect.HideSpeedrunMasterQuest"), 0);
default:
return false;
}
@@ -87,7 +93,7 @@ bool SohFileSelect_IsQuestHidden(u8 quest) {
u8 SohFileSelect_CountVisibleQuests() {
u8 count = 0;
for (u8 quest = QUEST_NORMAL; quest <= QUEST_BOSSRUSH; ++quest) {
for (u8 quest = QUEST_NORMAL; quest <= QUEST_SPEEDRUN_MASTER; ++quest) {
if (!SohFileSelect_IsQuestHidden(quest)) {
count++;
}
+80 -6
View File
@@ -1,4 +1,5 @@
#include "Presets.h"
#include <algorithm>
#include <string>
#include <fstream>
#include <spdlog/common.h>
@@ -106,6 +107,20 @@ std::string FormatPresetPath(std::string name) {
return spdlog::fmt_lib::format("{}/{}.json", presetFolder, SanitizeFilename(name));
}
enum class SectionStrategy {
Overwrite,
Merge,
};
static SectionStrategy GetSectionStrategy(const PresetInfo& info, int section) {
const std::string& name = blockInfo[section].names[1];
if (info.presetValues.contains("blockStrategy") && info.presetValues["blockStrategy"].contains(name) &&
info.presetValues["blockStrategy"][name] == "merge") {
return SectionStrategy::Merge;
}
return SectionStrategy::Overwrite;
}
void applyPreset(std::string presetName, std::vector<PresetSection> includeSections) {
auto& info = presets[presetName];
bool randoApplied = false;
@@ -126,18 +141,14 @@ void applyPreset(std::string presetName, std::vector<PresetSection> includeSecti
}
}
auto section = info.presetValues["blocks"][blockInfo[i].names[1]];
std::string sectionStrategy = "overwrite";
if (info.presetValues.contains("blockStrategy") &&
info.presetValues["blockStrategy"].contains(blockInfo[i].names[1])) {
sectionStrategy = info.presetValues["blockStrategy"][blockInfo[i].names[1]];
}
SectionStrategy sectionStrategy = GetSectionStrategy(info, i);
for (auto& item : section.items()) {
if (section[item.key()].is_null()) {
CVarClearBlock(item.key().c_str());
} else {
auto block = item.value();
if (sectionStrategy == "merge") {
if (sectionStrategy == SectionStrategy::Merge) {
auto currentJson = Ship::Context::GetRawInstance()->GetConfig()->GetNestedJson();
if (currentJson.contains("CVars") && currentJson["CVars"].contains(item.key())) {
block = currentJson["CVars"][item.key()];
@@ -172,6 +183,69 @@ void applyPreset(std::string presetName, std::vector<PresetSection> includeSecti
OTRGlobals::Instance->ScaleImGui();
}
nlohmann::json applyPresetToBlocks(std::string presetName, nlohmann::json blocks,
std::vector<PresetSection> includeSections) {
auto entry = presets.find(presetName);
if (entry == presets.end()) {
return blocks;
}
auto& info = entry->second;
for (int i = PRESET_SECTION_SETTINGS; i < PRESET_SECTION_MAX; i++) {
if (!info.apply[i] || !info.presetValues["blocks"].contains(blockInfo[i].names[1])) {
continue;
}
if (!includeSections.empty() && !SohUtils::Contains(i, includeSections)) {
continue;
}
auto section = info.presetValues["blocks"][blockInfo[i].names[1]];
SectionStrategy sectionStrategy = GetSectionStrategy(info, i);
for (auto& item : section.items()) {
if (!blocks.contains(item.key())) {
continue;
}
if (item.value().is_null()) {
blocks[item.key()] = nlohmann::json::object();
} else if (sectionStrategy == SectionStrategy::Merge) {
blocks[item.key()].update(item.value(), true);
} else {
blocks[item.key()] = item.value();
}
}
}
return blocks;
}
std::vector<std::pair<std::string, std::string>> GetSpeedrunPresets() {
static const std::string prefix = "Speedrun - ";
std::vector<std::pair<std::string, std::string>> found;
for (auto& [name, info] : presets) {
if (name.rfind(prefix, 0) == 0) {
std::string label = name.substr(prefix.size());
size_t digits = label.find_first_not_of("0123456789");
if (digits > 0 && digits != std::string::npos && label[digits] == ' ') {
label = label.substr(digits + 1);
}
found.emplace_back(label, name);
}
}
return found;
}
nlohmann::json GetPresetExempt(const std::string& presetName) {
auto entry = presets.find(presetName);
if (entry == presets.end() || !entry->second.presetValues.contains("exempt") ||
!entry->second.presetValues["exempt"].is_array()) {
return nlohmann::json::array();
}
return entry->second.presetValues["exempt"];
}
void DrawPresetSelector(std::vector<PresetSection> includeSections, std::string presetLoc, bool disabled) {
std::vector<std::string> includedPresets;
for (auto& [name, info] : presets) {
+13
View File
@@ -1,6 +1,8 @@
#pragma once
#include <nlohmann/json_fwd.hpp>
#include <string>
#include <utility>
#include <vector>
enum PresetSection {
@@ -16,3 +18,14 @@ enum PresetSection {
void DrawPresetSelector(std::vector<PresetSection> includeSections, std::string currentIndex, bool disabled);
void applyPreset(std::string presetName, std::vector<PresetSection> includeSections = {});
// Same as applyPreset, but works on a copy of the cvar blocks instead of the live config, and only touches blocks the
// copy already holds. For callers that want a preset's result without changing the player's settings.
nlohmann::json applyPresetToBlocks(std::string presetName, nlohmann::json blocks,
std::vector<PresetSection> includeSections = {});
// {display name, preset name} of every preset whose file is named "Speedrun - <display name>"
std::vector<std::pair<std::string, std::string>> GetSpeedrunPresets();
// The preset's "exempt" list of cvar json pointers, or an empty array if it has none.
nlohmann::json GetPresetExempt(const std::string& presetName);
+9 -88
View File
@@ -29,8 +29,6 @@ extern "C" {
extern PlayState* gPlayState;
Gfx* KaleidoScope_QuadTextureIA8(Gfx* gfx, void* texture, s16 width, s16 height, u16 point);
void FileChoose_UpdateStickDirectionPromptAnim(GameState* thisx);
void FileChoose_DrawTextRec(GraphicsContext* gfxCtx, s32 r, s32 g, s32 b, s32 a, f32 x, f32 y, f32 z, s32 s, s32 t,
f32 dx, f32 dy);
}
typedef enum {
@@ -201,53 +199,9 @@ void FileChoose_UpdateBossRushMenu(GameState* gameState) {
Input* input = &fileChooseContext->state.input[0];
bool dpad = CVarGetInteger(CVAR_SETTING("DpadInText"), 0);
// Fade in elements after opening Boss Rush options menu
fileChooseContext->bossRushUIAlpha += 25;
if (fileChooseContext->bossRushUIAlpha > 255) {
fileChooseContext->bossRushUIAlpha = 255;
}
// Animate up/down arrows.
fileChooseContext->bossRushArrowOffset += 1;
if (fileChooseContext->bossRushArrowOffset >= 30) {
fileChooseContext->bossRushArrowOffset = 0;
}
// Move menu selection up or down.
if (ABS(fileChooseContext->stickRelY) > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN | BTN_DUP))) {
// Move down
if (fileChooseContext->stickRelY < -30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN))) {
// When selecting past the last option, cycle back to the first option.
if ((fileChooseContext->bossRushIndex + 1) > BR_OPTIONS_MAX - 1) {
fileChooseContext->bossRushIndex = 0;
fileChooseContext->bossRushOffset = 0;
} else {
fileChooseContext->bossRushIndex++;
// When last visible option is selected when moving down, offset the list down by one.
if (fileChooseContext->bossRushIndex - fileChooseContext->bossRushOffset >
BOSSRUSH_MAX_OPTIONS_ON_SCREEN - 1) {
fileChooseContext->bossRushOffset++;
}
}
} else if (fileChooseContext->stickRelY > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DUP))) {
// When selecting past the first option, cycle back to the last option and offset the list to view it
// properly.
if ((fileChooseContext->bossRushIndex - 1) < 0) {
fileChooseContext->bossRushIndex = BR_OPTIONS_MAX - 1;
fileChooseContext->bossRushOffset =
fileChooseContext->bossRushIndex - BOSSRUSH_MAX_OPTIONS_ON_SCREEN + 1;
} else {
// When first visible option is selected when moving up, offset the list up by one.
if (fileChooseContext->bossRushIndex - fileChooseContext->bossRushOffset == 0) {
fileChooseContext->bossRushOffset--;
}
fileChooseContext->bossRushIndex--;
}
}
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_CURSOR, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
}
FileChoose_UpdateListMenuAnim(&fileChooseContext->bossRushUIAlpha, &fileChooseContext->bossRushArrowOffset);
FileChoose_MoveListCursor(fileChooseContext, &fileChooseContext->bossRushIndex, &fileChooseContext->bossRushOffset,
BR_OPTIONS_MAX, BOSSRUSH_MAX_OPTIONS_ON_SCREEN);
// Cycle through choices for currently selected option.
if (ABS(fileChooseContext->stickRelX) > 30 ||
@@ -301,33 +255,12 @@ void FileChoose_UpdateBossRushMenu(GameState* gameState) {
}
void FileChoose_DrawBossRushMenuWindowContents(FileChooseContext* fileChooseContext) {
OPEN_DISPS(fileChooseContext->state.gfxCtx);
uint8_t language = (gSaveContext.language == LANGUAGE_JPN) ? (uint8_t)LANGUAGE_ENG : gSaveContext.language;
uint8_t listOffset = fileChooseContext->bossRushOffset;
int16_t textAlpha = fileChooseContext->bossRushUIAlpha;
// Draw arrows to indicate that the list can scroll up or down.
// Arrow up
if (listOffset > 0) {
uint16_t arrowUpX = 140;
uint16_t arrowUpY = 76 - (fileChooseContext->bossRushArrowOffset / 10);
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowUpTex, G_IM_FMT_IA, G_IM_SIZ_16b, 16, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
gSPWideTextureRectangle(POLY_OPA_DISP++, arrowUpX << 2, arrowUpY << 2, (arrowUpX + 8) << 2, (arrowUpY + 8) << 2,
G_TX_RENDERTILE, 0, 0, (1 << 11), (1 << 11));
}
// Arrow down
if (BR_OPTIONS_MAX - listOffset > BOSSRUSH_MAX_OPTIONS_ON_SCREEN) {
uint16_t arrowDownX = 140;
uint16_t arrowDownY = 181 + (fileChooseContext->bossRushArrowOffset / 10);
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowDownTex, G_IM_FMT_IA, G_IM_SIZ_16b, 16, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
gSPWideTextureRectangle(POLY_OPA_DISP++, arrowDownX << 2, arrowDownY << 2, (arrowDownX + 8) << 2,
(arrowDownY + 8) << 2, G_TX_RENDERTILE, 0, 0, (1 << 11), (1 << 11));
}
FileChoose_DrawListScrollArrows(fileChooseContext, listOffset, BR_OPTIONS_MAX, BOSSRUSH_MAX_OPTIONS_ON_SCREEN,
fileChooseContext->bossRushArrowOffset);
// Draw options. There's more options than what fits on the screen, so the visible options
// depend on the current offset of the list. Currently selected option pulses in
@@ -347,24 +280,12 @@ void FileChoose_DrawBossRushMenuWindowContents(FileChooseContext* fileChooseCont
// Draw arrows around selected option.
if (fileChooseContext->bossRushIndex == i) {
Gfx_SetupDL_39Opa(fileChooseContext->state.gfxCtx);
gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowCursorTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 24, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
FileChoose_DrawTextRec(fileChooseContext->state.gfxCtx, fileChooseContext->stickLeftPrompt.arrowColorR,
fileChooseContext->stickLeftPrompt.arrowColorG,
fileChooseContext->stickLeftPrompt.arrowColorB, textAlpha, 160.0f,
static_cast<f32>(92 + textYOffset), 0.42f, 0, 0, -1.0f, 1.0f);
FileChoose_DrawTextRec(fileChooseContext->state.gfxCtx, fileChooseContext->stickRightPrompt.arrowColorR,
fileChooseContext->stickRightPrompt.arrowColorG,
fileChooseContext->stickRightPrompt.arrowColorB, textAlpha,
static_cast<f32>(171 + finalKerning), static_cast<f32>(92 + textYOffset), 0.42f, 0,
0, 1.0f, 1.0f);
FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, 160.0f, static_cast<f32>(92 + textYOffset),
true);
FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, static_cast<f32>(171 + finalKerning),
static_cast<f32>(92 + textYOffset), false);
}
}
CLOSE_DISPS(fileChooseContext->state.gfxCtx);
}
void BossRush_SpawnBlueWarps(PlayState* play) {
@@ -480,7 +480,7 @@ static bool ShouldRenderItem(s16 fileIndex, u8 item) {
// greg
if (item == ITEM_RUPEE_GREEN) {
return Save_GetSaveMetaInfo(fileIndex)->randoSave;
return Save_GetSaveMetaInfo(fileIndex)->quest == QUEST_RANDOMIZER;
}
if (item == ITEM_FISHING_POLE) {
+435
View File
@@ -0,0 +1,435 @@
#include "Speedrun.h"
#include "soh/Enhancements/Presets/Presets.h"
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Notification/Notification.h"
#include "soh/OTRGlobals.h"
#include "soh/SaveManager.h"
#include "soh/ShipInit.hpp"
#include "soh/SohGui/SohGui.hpp"
#include "soh/config/ConfigUpdaters.h"
#include <ship/Context.h>
#include <ship/config/Config.h>
#include <ship/window/Window.h>
#include <ship/window/gui/Gui.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
extern "C" {
#include "functions.h"
#include "macros.h"
#include "variables.h"
#include "src/overlays/gamestates/ovl_file_choose/file_choose.h"
void FileChoose_UpdateStickDirectionPromptAnim(GameState* thisx);
int16_t OTRGetRectDimensionFromRightEdge(float v);
}
namespace fs = std::filesystem;
// The cvar blocks a speedrun file owns. They are copied onto the file when it is created and put back every time it is
// loaded. Everything else (controls, resolution, audio) stays with the player, except cosmetics which a run turns off.
static const std::array<const char*, 4> sOwnedBlocks = {
CVAR_PREFIX_ENHANCEMENT,
CVAR_PREFIX_RANDOMIZER_ENHANCEMENT,
CVAR_PREFIX_CHEAT,
CVAR_PREFIX_DEVELOPER_TOOLS,
};
#define SPEEDRUN_PRESET_NONE "None"
#define SPEEDRUN_MAX_OPTIONS_ON_SCREEN 6
// {display name, preset name}, ending with "None" which has no preset.
static std::vector<std::pair<std::string, std::string>> sPresetChoices;
static nlohmann::json sSettings = nlohmann::json::object();
static uint32_t sConfigVersion = 0;
// Paths of settings the player keeps even under a preset, from the preset's "exempt" list. Copied over file's
// settings on create and load. Values left out of settings hash, but paths are not. Saved with file.
static nlohmann::json sExempt = nlohmann::json::array();
static std::string sPresetName = SPEEDRUN_PRESET_NONE;
static uint32_t sSettingsHash = 0;
static bool sMenuLocked = false;
// Everything else, menu included, is hidden so settings can't be changed mid-run.
static const std::vector<std::string> sAllowedWindows = {
"Time Splits", "Gameplay Stats", "Additional Timers", "Input Viewer", "Notifications Window", "Modal Window",
};
// What opening the menu toggles instead during a run.
static const std::vector<std::string> sRunToggleWindows = { "Time Splits", "Gameplay Stats" };
// Windows kept hidden while locked, and the ones that were open and get reopened on unlock.
static std::vector<std::shared_ptr<Ship::GuiWindow>> sHiddenWindows;
static std::vector<std::shared_ptr<Ship::GuiWindow>> sReopenWindows;
// The player's own settings are written here while a run replaces them, so they survive a crash mid-run.
static std::string GetBackupPath() {
return Ship::Context::GetPathRelativeToAppDirectory("speedrun_settings_backup.json");
}
static nlohmann::json GetOwnedBlocks() {
auto config = Ship::Context::GetRawInstance()->GetConfig()->GetNestedJson();
nlohmann::json blocks = nlohmann::json::object();
for (const char* block : sOwnedBlocks) {
if (config.contains("CVars") && config["CVars"].contains(block)) {
blocks[block] = config["CVars"][block];
} else {
blocks[block] = nlohmann::json::object();
}
}
return blocks;
}
static void SetOwnedBlocks(const nlohmann::json& blocks) {
auto config = Ship::Context::GetRawInstance()->GetConfig();
for (auto& item : blocks.items()) {
config->SetBlock(spdlog::fmt_lib::format("{}.{}", "CVars", item.key()), item.value());
}
// SetBlock writes the config file, so reload cvars from it.
Ship::Context::GetRawInstance()->GetConsoleVariables()->Load();
ShipInit::InitAll();
}
static std::vector<nlohmann::json::json_pointer> GetExemptPaths() {
std::vector<nlohmann::json::json_pointer> paths;
for (const auto& path : sExempt) {
try {
paths.emplace_back(path.get<std::string>());
} catch (const std::exception& e) { SPDLOG_ERROR("Speedrun: bad exempt path {}: {}", path.dump(), e.what()); }
}
return paths;
}
// Copies the player's exempt settings onto blocks. Ones the player never set keep the preset's value.
static void OverlayExemptSettings(nlohmann::json& blocks, const nlohmann::json& custom) {
for (const auto& path : GetExemptPaths()) {
if (custom.contains(path)) {
blocks[path] = custom[path];
}
}
}
// Saves the player's settings before a speedrun file replaces them. An existing backup is kept, since it means the
// last run never restored it and it still holds the player's real settings.
static void BackupSettings() {
if (fs::exists(GetBackupPath())) {
return;
}
std::ofstream file(GetBackupPath());
if (!file.is_open()) {
SPDLOG_ERROR("Speedrun: could not write settings backup");
return;
}
// Cosmetics are backed up with the owned blocks, since a run clears them too.
auto config = Ship::Context::GetRawInstance()->GetConfig()->GetNestedJson();
nlohmann::json blocks = GetOwnedBlocks();
blocks[CVAR_PREFIX_COSMETIC] = nlohmann::json::object();
if (config.contains("CVars") && config["CVars"].contains(CVAR_PREFIX_COSMETIC)) {
blocks[CVAR_PREFIX_COSMETIC] = config["CVars"][CVAR_PREFIX_COSMETIC];
}
nlohmann::json backup;
backup["blocks"] = blocks;
file << backup.dump(4);
}
static void LockMenu() {
if (sMenuLocked) {
return;
}
auto gui = Ship::Context::GetRawInstance()->GetWindow()->GetGui();
if (auto menu = gui->GetMenu()) {
menu->Hide();
}
for (auto& window : SohGui::GetAllGuiWindows()) {
if (window == nullptr ||
std::find(sAllowedWindows.begin(), sAllowedWindows.end(), window->GetName()) != sAllowedWindows.end()) {
continue;
}
if (window->IsVisible()) {
sReopenWindows.push_back(window);
window->Hide();
}
sHiddenWindows.push_back(window);
}
sMenuLocked = true;
}
static void UnlockMenu() {
if (!sMenuLocked) {
return;
}
sMenuLocked = false;
for (auto& window : sReopenWindows) {
window->Show();
}
sReopenWindows.clear();
sHiddenWindows.clear();
}
bool Speedrun_EnforceGuiLockdown(Ship::GuiWindow& menu) {
if (!sMenuLocked) {
return false;
}
// Opening the menu (Esc) toggles the run info windows instead.
if (menu.IsVisible()) {
menu.Hide();
auto gui = Ship::Context::GetRawInstance()->GetWindow()->GetGui();
bool anyVisible = false;
for (auto& name : sRunToggleWindows) {
auto window = gui->GetGuiWindow(name);
anyVisible |= window != nullptr && window->IsVisible();
}
for (auto& name : sRunToggleWindows) {
if (auto window = gui->GetGuiWindow(name)) {
anyVisible ? window->Hide() : window->Show();
}
}
}
for (auto& window : sHiddenWindows) {
if (window->IsVisible()) {
window->Hide();
}
}
return true;
}
static void RestoreSettings() {
UnlockMenu();
if (fs::exists(GetBackupPath())) {
try {
std::ifstream file(GetBackupPath());
nlohmann::json backup = nlohmann::json::parse(file);
file.close();
SetOwnedBlocks(backup.value("blocks", nlohmann::json::object()));
} catch (const std::exception& e) { SPDLOG_ERROR("Speedrun: could not read settings backup: {}", e.what()); }
fs::remove(GetBackupPath());
}
}
extern "C" void Ship_PinSpeedrunTimer(s32* left, s32* top) {
if (IS_SPEEDRUN) {
*left = OTRGetRectDimensionFromRightEdge(247);
*top = 0;
}
}
extern "C" bool Ship_QuestDebugEnabled(u8 questId) {
return questId != QUEST_SPEEDRUN && questId != QUEST_SPEEDRUN_MASTER &&
CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0);
}
// FNV-1a hash of the build version, the file's settings without exempt values, and the exempt paths. Same hash means
// same build and settings. nlohmann sorts object keys, so the dump is stable.
static uint32_t HashSettings() {
nlohmann::json stripped = sSettings;
for (const auto& path : GetExemptPaths()) {
if (stripped.contains(path)) {
stripped[path.parent_pointer()].erase(path.back());
}
}
std::string data = std::string((const char*)gBuildVersion) + stripped.dump() + sExempt.dump();
uint32_t hash = 0x811C9DC5;
for (char c : data) {
hash ^= (uint8_t)c;
hash *= 0x01000193;
}
return hash;
}
static void EmitHashNotification() {
Notification::Emit({
.prefix = "Speedrun",
.message = sPresetName,
.suffix = spdlog::fmt_lib::format("{:08X}", sSettingsHash),
.remainingTime = 15.0f,
});
}
extern "C" void Speedrun_LoadPresetChoices(FileChooseContext* fileChooseContext) {
// "None" keeps the player's current settings.
sPresetChoices = GetSpeedrunPresets();
sPresetChoices.emplace_back(SPEEDRUN_PRESET_NONE, "");
if (fileChooseContext->speedrunIndex >= sPresetChoices.size()) {
fileChooseContext->speedrunIndex = 0;
fileChooseContext->speedrunOffset = 0;
}
}
extern "C" void FileChoose_UpdateSpeedrunMenu(GameState* gameState) {
FileChoose_UpdateStickDirectionPromptAnim(gameState);
FileChooseContext* fileChooseContext = (FileChooseContext*)gameState;
Input* input = &fileChooseContext->state.input[0];
FileChoose_UpdateListMenuAnim(&fileChooseContext->speedrunUIAlpha, &fileChooseContext->speedrunArrowOffset);
FileChoose_MoveListCursor(fileChooseContext, &fileChooseContext->speedrunIndex, &fileChooseContext->speedrunOffset,
(uint8_t)sPresetChoices.size(), SPEEDRUN_MAX_OPTIONS_ON_SCREEN);
if (CHECK_BTN_ALL(input->press.button, BTN_B)) {
fileChooseContext->configMode = CM_SETTINGS_MENU_TO_QUEST;
return;
}
// Go to name entry. The chosen preset is applied when the file gets created.
if (CHECK_BTN_ALL(input->press.button, BTN_A) || CHECK_BTN_ALL(input->press.button, BTN_START)) {
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
FileChoose_StartNameEntryFromMenu(fileChooseContext);
}
}
extern "C" void FileChoose_DrawSpeedrunMenuWindowContents(FileChooseContext* fileChooseContext) {
uint8_t listOffset = fileChooseContext->speedrunOffset;
int16_t textAlpha = fileChooseContext->speedrunUIAlpha;
uint8_t optionCount = (uint8_t)sPresetChoices.size();
FileChoose_DrawListScrollArrows(fileChooseContext, listOffset, optionCount, SPEEDRUN_MAX_OPTIONS_ON_SCREEN,
fileChooseContext->speedrunArrowOffset);
for (uint8_t i = listOffset; i < optionCount && i - listOffset < SPEEDRUN_MAX_OPTIONS_ON_SCREEN; i++) {
uint16_t textYOffset = (i - listOffset) * 16;
bool selected = fileChooseContext->speedrunIndex == i;
uint16_t finalKerning =
Interface_DrawTextLine(fileChooseContext->state.gfxCtx, (char*)sPresetChoices[i].first.c_str(), 75,
(87 + textYOffset), 255, 255, selected ? 80 : 255, textAlpha, 0.8f, true);
// Draw arrows around selected option.
if (selected) {
FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, 70.0f, static_cast<f32>(92 + textYOffset),
true);
FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, static_cast<f32>(81 + finalKerning),
static_cast<f32>(92 + textYOffset), false);
}
}
}
extern "C" void Speedrun_InitSaveFile(u8 presetIndex) {
const std::string& presetKey = sPresetChoices[presetIndex].second;
sPresetName = sPresetChoices[presetIndex].first;
nlohmann::json custom = GetOwnedBlocks();
sSettings = custom;
sConfigVersion = SOH::GetLatestConfigVersion();
sExempt = GetPresetExempt(presetKey);
if (!presetKey.empty()) {
// Applied to the file's copy only, never the player's config.
sSettings = applyPresetToBlocks(presetKey, sSettings, { PRESET_SECTION_ENHANCEMENTS });
}
OverlayExemptSettings(sSettings, custom);
// Runs are always timed in real time, whatever the "RTA Timing on new files" option says.
gSaveContext.ship.stats.rtaTiming = 1;
sSettingsHash = HashSettings();
EmitHashNotification();
}
// only calls this for speedrun files
static void SaveSaveSection(const SaveContext& saveContext, int sectionID, bool fullSave) {
SaveManager::Instance->SaveData("presetName", sPresetName);
SaveManager::Instance->SaveData("settingsHash", sSettingsHash);
SaveManager::Instance->SaveData("settings", sSettings);
SaveManager::Instance->SaveData("configVersion", sConfigVersion);
SaveManager::Instance->SaveData("exempt", sExempt);
}
static void LoadSaveSection() {
if (!IS_SPEEDRUN) {
return;
}
// Lock even if the settings fail to load, it's still a speedrun file.
LockMenu();
SaveManager::Instance->LoadData("presetName", sPresetName);
SaveManager::Instance->LoadData("settings", sSettings);
SaveManager::Instance->LoadData("configVersion", sConfigVersion);
SaveManager::Instance->LoadData("exempt", sExempt);
if (!sExempt.is_array()) {
sExempt = nlohmann::json::array();
}
if (!sSettings.is_object()) {
SPDLOG_ERROR("Speedrun: file has no settings to load");
return;
}
// Blocks missing from the file are cleared rather than left as the player's.
nlohmann::json blocks = sSettings;
for (const char* block : sOwnedBlocks) {
if (!blocks.contains(block)) {
blocks[block] = nlohmann::json::object();
}
}
OverlayExemptSettings(blocks, GetOwnedBlocks());
// No cosmetics during a run. Some scale Link or his sword, which changes his hitbox and reach.
blocks[CVAR_PREFIX_COSMETIC] = nlohmann::json::object();
BackupSettings();
SetOwnedBlocks(blocks);
sSettingsHash = HashSettings();
EmitHashNotification();
}
void Speedrun_Register() {
SaveManager::Instance->AddLoadFunction("speedrun", 1, LoadSaveSection);
SaveManager::Instance->AddSaveFunction("speedrun", 1, SaveSaveSection, true, SECTION_PARENT_NONE);
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnExitGame>([](uint32_t fileNum) { RestoreSettings(); });
// No gyro aiming in a run. Cleared after the pad is read and before the frame uses it.
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameStateMainStart>([]() {
if (IS_SPEEDRUN && gGameState != nullptr) {
gGameState->input[0].cur.gyro_x = 0.0f;
gGameState->input[0].cur.gyro_y = 0.0f;
}
});
REGISTER_VB_SHOULD(VB_SHOW_GAMEPLAY_TIMER, {
if (IS_SPEEDRUN) {
*should = true;
}
});
// A backup left from last session means the game closed mid-run without restoring settings.
RestoreSettings();
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <libultraship/libultra/types.h>
#ifdef __cplusplus
extern "C" {
#endif
struct GameState;
struct FileChooseContext;
// Reads the preset list when the speedrun menu opens, so presets saved since last time show up.
void Speedrun_LoadPresetChoices(struct FileChooseContext* fileChooseContext);
void FileChoose_UpdateSpeedrunMenu(struct GameState* gameState);
void FileChoose_DrawSpeedrunMenuWindowContents(struct FileChooseContext* fileChooseContext);
// Saves the settings the new file plays with: the player's current ones with the preset at presetIndex in the menu
// applied.
void Speedrun_InitSaveFile(u8 presetIndex);
// Speedrun files show the in-game timer at a fixed spot, top right, whatever the cosmetics editor says.
void Ship_PinSpeedrunTimer(s32* left, s32* top);
// Whether debug mode applies to a file of this quest type.
bool Ship_QuestDebugEnabled(u8 questId);
#ifdef __cplusplus
};
// Registers the save section and the hook that restores settings when a run is exited. Call once at startup.
void Speedrun_Register();
namespace Ship {
class GuiWindow;
}
// Called by the menu every frame, before other windows draw. While a speedrun file is loaded it keeps disallowed
// windows hidden and makes opening the menu toggle the run info windows. Returns true when the menu must not draw.
bool Speedrun_EnforceGuiLockdown(Ship::GuiWindow& menu);
#endif
+10
View File
@@ -818,6 +818,16 @@ void RegisterOnUpdateMainMenuSelection() {
SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode());
break;
}
case QUEST_SPEEDRUN: {
auto translation = GetParameritizedText("quest_sel_speedrun", TEXT_BANK_FILECHOOSE, nullptr);
SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode());
break;
}
case QUEST_SPEEDRUN_MASTER: {
auto translation = GetParameritizedText("quest_sel_speedrun_mq", TEXT_BANK_FILECHOOSE, nullptr);
SpeechSynthesizer::Instance->Speak(translation.c_str(), GetLanguageCode());
break;
}
default:
break;
}
+3
View File
@@ -39,6 +39,7 @@
#include "Enhancements/randomizer/static_data.h"
#include "soh/Enhancements/randomizer/settings.h"
#include "soh/Enhancements/savestates.h"
#include "soh/Enhancements/speedrun/Speedrun.h"
#include "frame_interpolation.h"
#include "SohGui/SohMenu.h"
#include "SohGui/SohGui.hpp"
@@ -1589,6 +1590,8 @@ extern "C" void InitOTR(int argc, char* argv[]) {
if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) {
Anchor::Instance->Enable();
}
// Restores settings left over from a run that never exited cleanly, so it must come after other setup.
Speedrun_Register();
ShipInit::InitAll();
Rando::StaticData::InitHashMaps();
OTRGlobals::Instance->gRandoContext->AddExcludedOptions();
+28 -11
View File
@@ -216,7 +216,7 @@ SaveManager::SaveManager() {
info.seedHash[i] = 0;
}
info.randoSave = 0;
info.quest = QUEST_NORMAL;
info.requiresMasterQuest = 0;
info.requiresOriginal = 0;
@@ -691,7 +691,8 @@ void SaveManager::StartupCheckAndInitMeta(int fileNum) {
}
}
}
bool isRando = metaSaveBlock.value("fileType", (int)FILE_TYPE_SAVE_VANILLA) == FILE_TYPE_SAVE_RANDO;
int fileType = metaSaveBlock.value("fileType", (int)FILE_TYPE_SAVE_VANILLA);
bool isRando = fileType == FILE_TYPE_SAVE_RANDO;
// Keys that came and went between base section versions are read with defaults, the rest are covered by the
// catch: a file we can't make sense of is left on disk and hidden rather than taking the game down.
@@ -731,8 +732,13 @@ void SaveManager::StartupCheckAndInitMeta(int fileNum) {
fileMetaInfo[fileNum].requiresOriginal = !isMasterQuest;
fileMetaInfo[fileNum].requiresMasterQuest = isMasterQuest;
fileMetaInfo[fileNum].randoSave = isRando;
if (fileType == FILE_TYPE_SAVE_SPEEDRUN) {
fileMetaInfo[fileNum].quest = isMasterQuest ? QUEST_SPEEDRUN_MASTER : QUEST_SPEEDRUN;
} else {
fileMetaInfo[fileNum].quest = isMasterQuest ? QUEST_MASTER : QUEST_NORMAL;
}
if (isRando) {
fileMetaInfo[fileNum].quest = QUEST_RANDOMIZER;
nlohmann::json& randoBlock = sections["randomizer"]["data"];
for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].seedHash); i++) {
@@ -805,7 +811,7 @@ void SaveManager::InitMeta(int fileNum) {
fileMetaInfo[fileNum].seedHash[i] = randoContext->hashIconIndexes[i];
}
fileMetaInfo[fileNum].randoSave = IS_RANDO;
fileMetaInfo[fileNum].quest = gSaveContext.ship.quest.id;
// If the file is marked as a Master Quest file or if we're randomized and have at least one master quest dungeon,
// we need the mq otr.
fileMetaInfo[fileNum].requiresMasterQuest =
@@ -1254,6 +1260,8 @@ void SaveManager::SaveFileThreaded(int fileNum, const SaveContext& saveContext,
saveBlock["version"] = 1;
if (IS_RANDO) {
saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO;
} else if (IS_SPEEDRUN) {
saveBlock["fileType"] = FILE_TYPE_SAVE_SPEEDRUN;
} else {
saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA;
}
@@ -1261,7 +1269,8 @@ void SaveManager::SaveFileThreaded(int fileNum, const SaveContext& saveContext,
for (auto& sectionHandlerPair : sectionSaveHandlers) {
auto& saveFuncInfo = sectionHandlerPair.second;
// Don't call SaveFuncs for sections that aren't tied to game save
if (!saveFuncInfo.saveWithBase || (saveFuncInfo.name == "randomizer" && !IS_RANDO)) {
if (!saveFuncInfo.saveWithBase || (saveFuncInfo.name == "randomizer" && !IS_RANDO) ||
(saveFuncInfo.name == "speedrun" && !IS_SPEEDRUN)) {
continue;
}
nlohmann::json& sectionBlock = saveBlock["sections"][saveFuncInfo.name];
@@ -1355,8 +1364,12 @@ void SaveManager::LoadFile(int fileNum) {
SPDLOG_ERROR("Save at {} contains no version", fileName.string());
assert(false);
}
if (saveBlock.contains("fileType") && saveBlock["fileType"] == FILE_TYPE_SAVE_RANDO) {
int fileType = saveBlock.value("fileType", (int)FILE_TYPE_SAVE_VANILLA);
if (fileType == FILE_TYPE_SAVE_RANDO) {
gSaveContext.ship.quest.id = QUEST_RANDOMIZER;
} else if (fileType == FILE_TYPE_SAVE_SPEEDRUN) {
// The base section changes this to QUEST_SPEEDRUN_MASTER for master quest files.
gSaveContext.ship.quest.id = QUEST_SPEEDRUN;
}
switch (saveBlock["version"].get<int>()) {
case 1:
@@ -1814,7 +1827,8 @@ void SaveManager::LoadBaseVersion2() {
int isMQ = 0;
SaveManager::Instance->LoadData("isMasterQuest", isMQ);
if (isMQ) {
gSaveContext.ship.quest.id = QUEST_MASTER;
// Speedrun files have their own master quest id.
gSaveContext.ship.quest.id = IS_SPEEDRUN ? QUEST_SPEEDRUN_MASTER : QUEST_MASTER;
}
// Workaround for breaking save compatibility from 5.0.2 -> 5.1.0 in commit d7c35221421bf712b5ead56a360f81f624aca4bc
@@ -2040,7 +2054,8 @@ void SaveManager::LoadBaseVersion3() {
int isMQ = 0;
SaveManager::Instance->LoadData("isMasterQuest", isMQ);
if (isMQ) {
gSaveContext.ship.quest.id = QUEST_MASTER;
// Speedrun files have their own master quest id.
gSaveContext.ship.quest.id = IS_SPEEDRUN ? QUEST_SPEEDRUN_MASTER : QUEST_MASTER;
}
SaveManager::Instance->LoadStruct("backupFW", []() {
SaveManager::Instance->LoadStruct("pos", []() {
@@ -2213,7 +2228,8 @@ void SaveManager::LoadBaseVersion4() {
int isMQ = 0;
SaveManager::Instance->LoadData("isMasterQuest", isMQ);
if (isMQ) {
gSaveContext.ship.quest.id = QUEST_MASTER;
// Speedrun files have their own master quest id.
gSaveContext.ship.quest.id = IS_SPEEDRUN ? QUEST_SPEEDRUN_MASTER : QUEST_MASTER;
}
SaveManager::Instance->LoadStruct("backupFW", []() {
SaveManager::Instance->LoadStruct("pos", []() {
@@ -2377,7 +2393,8 @@ void SaveManager::SaveBase(const SaveContext& saveContext, int sectionID, bool f
SaveManager::Instance->SaveArray("randomizerInf", ARRAY_COUNT(saveContext.ship.randomizerInf), [&](size_t i) {
SaveManager::Instance->SaveData("", saveContext.ship.randomizerInf[i]);
});
SaveManager::Instance->SaveData("isMasterQuest", saveContext.ship.quest.id == QUEST_MASTER);
SaveManager::Instance->SaveData("isMasterQuest", saveContext.ship.quest.id == QUEST_MASTER ||
saveContext.ship.quest.id == QUEST_SPEEDRUN_MASTER);
SaveManager::Instance->SaveStruct("backupFW", [&]() {
SaveManager::Instance->SaveStruct("pos", [&]() {
SaveManager::Instance->SaveData("x", saveContext.ship.backupFW.pos.x);
@@ -2490,7 +2507,7 @@ void SaveManager::DeleteZeldaFile(int fileNum) {
std::filesystem::remove(GetFileName(fileNum));
}
fileMetaInfo[fileNum].valid = false;
fileMetaInfo[fileNum].randoSave = false;
fileMetaInfo[fileNum].quest = QUEST_NORMAL;
fileMetaInfo[fileNum].requiresMasterQuest = false;
fileMetaInfo[fileNum].requiresOriginal = false;
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnDeleteFile>(fileNum);
+1 -1
View File
@@ -14,7 +14,7 @@ typedef struct {
u32 requiresMasterQuest;
u32 requiresOriginal;
u8 seedHash[5];
u8 randoSave;
u8 quest;
char buildVersion[50];
s16 buildVersionMajor;
s16 buildVersionMinor;
+5
View File
@@ -8,6 +8,7 @@
#include "Menu.h"
#include "BackendTypes.h"
#include "UIWidgets.hpp"
#include "soh/Enhancements/speedrun/Speedrun.h"
#include "soh/OTRGlobals.h"
#include "SohModals.h"
@@ -569,6 +570,10 @@ void Menu::MenuDrawItem(WidgetInfo& widget, UIWidgets::Colors menuThemeIndex) {
}
void Menu::Draw() {
// Runs before the other windows draw, so this also keeps them hidden during a speedrun.
if (Speedrun_EnforceGuiLockdown(*this)) {
return;
}
if (!IsVisible()) {
return;
}
+42
View File
@@ -120,6 +120,48 @@ std::shared_ptr<SohMenu> GetSohMenu() {
return mSohMenu;
}
std::vector<std::shared_ptr<Ship::GuiWindow>> GetAllGuiWindows() {
auto gui = Ship::Context::GetRawInstance()->GetWindow()->GetGui();
return {
mConsoleWindow,
mStatsWindow,
mGfxDebuggerWindow,
mModMenuWindow,
mAudioEditorWindow,
mInputViewer,
mInputViewerSettings,
mCosmeticsEditorWindow,
mActorViewerWindow,
mColViewerWindow,
mSaveEditorWindow,
mHookDebuggerWindow,
mDLViewerWindow,
mValueViewerWindow,
mMessageViewerWindow,
mGameplayStatsWindow,
mCheckTrackerWindow,
mCheckTrackerSettingsWindow,
mEntranceTrackerWindow,
mEntranceTrackerSettingsWindow,
mHintTrackerWindow,
mHintTrackerSettingsWindow,
mItemTrackerWindow,
mItemTrackerSettingsWindow,
mTimeSplitsWindow,
mPlandomizerWindow,
mModalWindow,
mNotificationWindow,
mTimeDisplayWindow,
mAnchorRoomWindow,
// Registered outside SohGui.
gui->GetGuiWindow("Configure Controller"),
gui->GetGuiWindow("Console"),
gui->GetGuiWindow("Stats"),
gui->GetGuiWindow("FileBrowser"),
gui->GetGuiWindow("SDLAddRemoveDeviceEventHandler"),
};
}
void SetupMenu() {
auto gui = Ship::Context::GetRawInstance()->GetWindow()->GetGui();
mSohMenu = std::make_shared<SohMenu>(CVAR_WINDOW("Menu"), "Port Menu");
+2
View File
@@ -24,6 +24,8 @@ void ShowRandomizerSettingsMenu();
void ShowEscMenu();
UIWidgets::Colors GetMenuThemeColor();
std::shared_ptr<SohMenu> GetSohMenu();
// Every window registered with the Gui, the ones LUS registers itself included. May contain nulls before setup.
std::vector<std::shared_ptr<Ship::GuiWindow>> GetAllGuiWindows();
void DrawStartingItemsMenu(WidgetInfo& info);
} // namespace SohGui
+14 -2
View File
@@ -181,10 +181,10 @@ static const std::map<int32_t, const char*> mirroredWorldModes = {
// Disables a File Select "Hide" checkbox when its O2R is missing, or when it would hide the last visible quest
static WidgetFunc HideQuestPreFunc(Quest quest) {
return [quest](WidgetInfo& info) {
if (quest == QUEST_NORMAL && !ResourceMgr_GameHasOriginal()) {
if ((quest == QUEST_NORMAL || quest == QUEST_SPEEDRUN) && !ResourceMgr_GameHasOriginal()) {
info.options->disabled = true;
info.options->disabledTooltip = "This option requires a loaded original O2R.";
} else if (quest == QUEST_MASTER && !ResourceMgr_GameHasMasterQuest()) {
} else if ((quest == QUEST_MASTER || quest == QUEST_SPEEDRUN_MASTER) && !ResourceMgr_GameHasMasterQuest()) {
info.options->disabled = true;
info.options->disabledTooltip = "This option requires a loaded Master Quest O2R.";
} else if (!SohFileSelect_IsQuestHidden(quest) && SohFileSelect_CountVisibleQuests() <= 1) {
@@ -824,6 +824,18 @@ void SohMenu::AddMenuEnhancements() {
.PreFunc(HideQuestPreFunc(QUEST_BOSSRUSH))
.Options(CheckboxOptions().Tooltip(
"Hides the Boss Rush option when selecting a quest type on the File Select screen."));
AddWidget(path, "Hide Speedrun", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("FileSelect.HideSpeedrunQuest"))
.RaceDisable(false)
.PreFunc(HideQuestPreFunc(QUEST_SPEEDRUN))
.Options(CheckboxOptions().Tooltip(
"Hides the Speedrun option when selecting a quest type on the File Select screen."));
AddWidget(path, "Hide Speedrun Master Quest", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("FileSelect.HideSpeedrunMasterQuest"))
.RaceDisable(false)
.PreFunc(HideQuestPreFunc(QUEST_SPEEDRUN_MASTER))
.Options(CheckboxOptions().Tooltip(
"Hides the Speedrun Master Quest option when selecting a quest type on the File Select screen."));
path.column = SECTION_COLUMN_3;
AddWidget(path, "Misc.", WIDGET_SEPARATOR_TEXT);
+7 -1
View File
@@ -5,7 +5,13 @@
#include <algorithm>
#include <iterator>
typedef enum FileType { FILE_TYPE_SAVE_VANILLA, FILE_TYPE_SAVE_RANDO, FILE_TYPE_PRESET, FILE_TYPE_SPOILER } FileType;
typedef enum FileType {
FILE_TYPE_SAVE_VANILLA,
FILE_TYPE_SAVE_RANDO,
FILE_TYPE_PRESET,
FILE_TYPE_SPOILER,
FILE_TYPE_SAVE_SPEEDRUN
} FileType;
namespace SohUtils {
const std::string& GetSceneName(int32_t scene);
+2
View File
@@ -21,6 +21,7 @@
#include "soh/ResourceManagerHelpers.h"
#include "soh/Enhancements/gameplaystats.h"
#include "soh/ObjectExtension/ActorMaximumHealth.h"
#include "soh/Enhancements/speedrun/Speedrun.h"
#include "message_data_static.h"
#include <libultraship/bridge/consolevariablebridge.h>
@@ -6434,6 +6435,7 @@ void Interface_DrawTotalGameplayTimer(PlayState* play) {
rectLeftOri = -9999;
}
}
Ship_PinSpeedrunTimer(&rectLeftOri, &rectTopOri);
s32 rectLeft;
s32 rectTop;
+9 -2
View File
@@ -8,6 +8,7 @@
#include "soh/Enhancements/randomizer/savefile.h"
#include "soh/OTRGlobals.h"
#include "soh/SaveManager.h"
#include "soh/Enhancements/speedrun/Speedrun.h"
#include "soh/ResourceManagerHelpers.h"
#include <libultraship/bridge/consolevariablebridge.h>
@@ -229,7 +230,8 @@ void Sram_InitSave(FileChooseContext* fileChooseCtx) {
u16* ptr;
u16 checksum;
if (fileChooseCtx->buttonIndex != 0 || !CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0)) {
if (fileChooseCtx->buttonIndex != 0 ||
!Ship_QuestDebugEnabled(fileChooseCtx->questType[fileChooseCtx->buttonIndex])) {
Sram_InitNewSave();
} else {
Sram_InitDebugSave();
@@ -246,7 +248,8 @@ void Sram_InitSave(FileChooseContext* fileChooseCtx) {
(gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG;
}
if ((fileChooseCtx->buttonIndex == 0 && CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0))) {
if ((fileChooseCtx->buttonIndex == 0 &&
Ship_QuestDebugEnabled(fileChooseCtx->questType[fileChooseCtx->buttonIndex]))) {
gSaveContext.cutsceneIndex = 0;
}
@@ -266,6 +269,10 @@ void Sram_InitSave(FileChooseContext* fileChooseCtx) {
gSaveContext.ship.quest.id = currentQuest;
}
if (IS_SPEEDRUN) {
Speedrun_InitSaveFile(fileChooseCtx->speedrunIndex);
}
Save_SaveFile();
SaveManager_ThreadPoolWait();
}
@@ -65,11 +65,11 @@ typedef enum {
CM_BOSS_RUSH_MENU,
CM_START_BOSS_RUSH_MENU,
CM_BOSS_RUSH_TO_QUEST,
CM_ROTATE_TO_RANDOMIZER_SETTINGS_MENU,
CM_RANDOMIZER_SETTINGS_MENU,
CM_START_RANDOMIZER_SETTINGS_MENU,
CM_RANDOMIZER_SETTINGS_MENU_TO_QUEST,
CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU,
CM_ROTATE_TO_SETTINGS_MENU,
CM_SETTINGS_MENU,
CM_START_SETTINGS_MENU,
CM_SETTINGS_MENU_TO_QUEST,
CM_NAME_ENTRY_TO_SETTINGS_MENU,
} ConfigMode;
typedef enum {
@@ -215,6 +215,20 @@ void FileChoose_ExitEraseToMain(GameState* thisx);
void FileChoose_UpdateKeyboardCursor(GameState* thisx);
void FileChoose_StartNameEntry(GameState* thisx);
void FileChoose_StartNameEntryFromMenu(FileChooseContext* fileChooseContext);
// Helpers for the scrolling option lists in the boss rush, randomizer and speedrun menus.
// Fades in the menu text and bobs the scroll arrows. Call once per frame.
void FileChoose_UpdateListMenuAnim(int16_t* uiAlpha, uint16_t* arrowOffset);
// Moves the cursor through count options with the stick or d-pad, wrapping at both ends. When offset is given it is the
// first option on screen, and is scrolled to keep the cursor among the visible ones. Returns true if the cursor moved.
bool FileChoose_MoveListCursor(FileChooseContext* fileChooseContext, uint8_t* index, uint8_t* offset, uint8_t count,
uint8_t visible);
// Draws arrows above and below the list when there are options off screen that way.
void FileChoose_DrawListScrollArrows(FileChooseContext* fileChooseContext, uint8_t offset, uint8_t count,
uint8_t visible, uint16_t arrowOffset);
// Draws one cursor arrow next to the selected option.
void FileChoose_DrawListCursorArrow(FileChooseContext* fileChooseContext, int16_t alpha, f32 x, f32 y, bool pointLeft);
void FileChoose_UpdateOptionsMenu(GameState* thisx);
void FileChoose_StartOptions(GameState* thisx);
@@ -16,6 +16,7 @@
#include "objects/gameplay_keep/gameplay_keep.h"
#include "soh_assets.h"
#include "soh/Enhancements/boss-rush/BossRush.h"
#include "soh/Enhancements/speedrun/Speedrun.h"
#include "soh/Enhancements/FileSelectEnhancements.h"
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#include <assert.h>
@@ -28,7 +29,7 @@
#include <libultraship/bridge/consolevariablebridge.h>
#define MIN_QUEST (ResourceMgr_GameHasOriginal() ? QUEST_NORMAL : QUEST_MASTER)
#define MAX_QUEST QUEST_BOSSRUSH
#define MAX_QUEST QUEST_SPEEDRUN_MASTER
// #region SOH [Enhancement] - Hide Quest Modes
// Step from quest in the given direction (1 or -1) to the next visible quest, wrapping around at the ends
@@ -336,10 +337,11 @@ void DrawSeedHashSprites(FileChooseContext* this) {
// Draw icons on the main menu, when a rando file is selected, and on name entry when quest selection is set to
// rando
if (this->configMode == CM_MAIN_MENU &&
(this->selectMode != SM_CONFIRM_FILE || Save_GetSaveMetaInfo(this->selectedFileIndex)->randoSave == 1)) {
(this->selectMode != SM_CONFIRM_FILE ||
Save_GetSaveMetaInfo(this->selectedFileIndex)->quest == QUEST_RANDOMIZER)) {
if (this->fileInfoAlpha[this->selectedFileIndex] > 0 &&
Save_GetSaveMetaInfo(this->selectedFileIndex)->randoSave) {
Save_GetSaveMetaInfo(this->selectedFileIndex)->quest == QUEST_RANDOMIZER) {
// Use file info alpha to match fading
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0xFF, 0xFF, 0xFF, this->fileInfoAlpha[this->selectedFileIndex]);
@@ -358,9 +360,9 @@ void DrawSeedHashSprites(FileChooseContext* this) {
// 2. On Quest Menu if a spoiler has been dropped and the Randomizer quest option is currently hovered.
if ((Randomizer_IsSeedGenerated() || Randomizer_IsSpoilerLoaded()) &&
(((this->configMode == CM_NAME_ENTRY || this->configMode == CM_ROTATE_TO_NAME_ENTRY ||
this->configMode == CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU || this->configMode == CM_START_NAME_ENTRY ||
this->configMode == CM_START_RANDOMIZER_SETTINGS_MENU) ||
this->configMode == CM_RANDOMIZER_SETTINGS_MENU) &&
this->configMode == CM_NAME_ENTRY_TO_SETTINGS_MENU || this->configMode == CM_START_NAME_ENTRY ||
this->configMode == CM_START_SETTINGS_MENU) ||
this->configMode == CM_SETTINGS_MENU) &&
gSaveContext.ship.quest.id == QUEST_RANDOMIZER)) {
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF);
@@ -658,30 +660,121 @@ void FileChoose_StartQuestMenu(GameState* thisx) {
}
}
void FileChoose_StartBossRushMenu(GameState* thisx) {
FileChooseContext* this = (FileChooseContext*)thisx;
// #region SOH - list menus shared by the boss rush, randomizer and speedrun menus
// Fade out the quest logo, then switch to the menu. The menu's text starts hidden and fades in once it's open.
// Returns true on the frame the menu opens.
static bool FileChoose_StartListMenu(FileChooseContext* this, int16_t* uiAlpha, uint16_t* arrowOffset,
ConfigMode menuMode) {
this->logoAlpha -= 25;
this->bossRushUIAlpha = 0;
this->bossRushArrowOffset = 0;
*uiAlpha = 0;
*arrowOffset = 0;
if (this->logoAlpha <= 0) {
this->logoAlpha = 0;
this->configMode = CM_BOSS_RUSH_MENU;
this->configMode = menuMode;
return true;
}
return false;
}
void FileChoose_StartRandomizerMenu(GameState* thisx) {
FileChooseContext* this = (FileChooseContext*)thisx;
void FileChoose_UpdateListMenuAnim(int16_t* uiAlpha, uint16_t* arrowOffset) {
*uiAlpha = MIN(*uiAlpha + 25, 255);
*arrowOffset = (*arrowOffset + 1) % 30;
}
this->logoAlpha -= 25;
this->randomizerUIAlpha = 0;
this->randomizerArrowOffset = 0;
bool FileChoose_MoveListCursor(FileChooseContext* this, uint8_t* index, uint8_t* offset, uint8_t count,
uint8_t visible) {
Input* input = &this->state.input[0];
bool dpad = CVarGetInteger(CVAR_SETTING("DpadInText"), 0);
if (this->logoAlpha <= 0) {
this->logoAlpha = 0;
this->configMode = CM_RANDOMIZER_SETTINGS_MENU;
if (this->stickRelY < -30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN))) {
*index = (*index + 1) % count;
} else if (this->stickRelY > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DUP))) {
*index = (*index + count - 1) % count;
} else {
return false;
}
// scroll just enough to keep the cursor on screen
if (offset != NULL) {
if (*index < *offset) {
*offset = *index;
} else if (*index >= *offset + visible) {
*offset = *index - visible + 1;
}
}
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_CURSOR, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
return true;
}
void FileChoose_DrawListScrollArrows(FileChooseContext* this, uint8_t offset, uint8_t count, uint8_t visible,
uint16_t arrowOffset) {
const int x = 140;
int bob = arrowOffset / 10;
OPEN_DISPS(this->state.gfxCtx);
if (offset > 0) {
int y = 76 - bob;
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowUpTex, G_IM_FMT_IA, G_IM_SIZ_16b, 16, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
gSPWideTextureRectangle(POLY_OPA_DISP++, x << 2, y << 2, (x + 8) << 2, (y + 8) << 2, G_TX_RENDERTILE, 0, 0,
(1 << 11), (1 << 11));
}
if (count - offset > visible) {
int y = 181 + bob;
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowDownTex, G_IM_FMT_IA, G_IM_SIZ_16b, 16, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
gSPWideTextureRectangle(POLY_OPA_DISP++, x << 2, y << 2, (x + 8) << 2, (y + 8) << 2, G_TX_RENDERTILE, 0, 0,
(1 << 11), (1 << 11));
}
CLOSE_DISPS(this->state.gfxCtx);
}
void FileChoose_DrawListCursorArrow(FileChooseContext* this, int16_t alpha, f32 x, f32 y, bool pointLeft) {
StickDirectionPrompt* prompt = pointLeft ? &this->stickLeftPrompt : &this->stickRightPrompt;
OPEN_DISPS(this->state.gfxCtx);
Gfx_SetupDL_39Opa(this->state.gfxCtx);
gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowCursorTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 24, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD);
FileChoose_DrawTextRec(this->state.gfxCtx, prompt->arrowColorR, prompt->arrowColorG, prompt->arrowColorB, alpha, x,
y, 0.42f, 0, 0, pointLeft ? -1.0f : 1.0f, 1.0f);
CLOSE_DISPS(this->state.gfxCtx);
}
void FileChoose_StartBossRushMenu(GameState* thisx) {
FileChooseContext* this = (FileChooseContext*)thisx;
FileChoose_StartListMenu(this, &this->bossRushUIAlpha, &this->bossRushArrowOffset, CM_BOSS_RUSH_MENU);
}
void FileChoose_StartSettings(GameState* thisx) {
FileChooseContext* this = (FileChooseContext*)thisx;
if (!IS_SPEEDRUN) {
FileChoose_StartListMenu(this, &this->randomizerUIAlpha, &this->randomizerArrowOffset, CM_SETTINGS_MENU);
} else if (FileChoose_StartListMenu(this, &this->speedrunUIAlpha, &this->speedrunArrowOffset, CM_SETTINGS_MENU)) {
Speedrun_LoadPresetChoices(this);
}
}
// #endregion
/**
* Leave a quest's settings menu for the name entry keyboard, which is the last step before the file is created.
* The name itself was already filled in when the empty file was picked on the main menu.
*/
void FileChoose_StartNameEntryFromMenu(FileChooseContext* this) {
this->prevConfigMode = this->configMode;
this->configMode = CM_ROTATE_TO_NAME_ENTRY;
CVarSetInteger(CVAR_GENERAL("OnFileSelectNameEntry"), 1);
}
void FileChoose_UpdateQuestMenu(GameState* thisx) {
@@ -690,7 +783,6 @@ void FileChoose_UpdateQuestMenu(GameState* thisx) {
Input* input = &this->state.input[0];
s8 i = 0;
bool dpad = CVarGetInteger(CVAR_SETTING("DpadInText"), 0);
void* defaultName;
FileChoose_UpdateRandomizer();
@@ -729,11 +821,12 @@ void FileChoose_UpdateQuestMenu(GameState* thisx) {
this->prevConfigMode = this->configMode;
this->configMode = CM_ROTATE_TO_BOSS_RUSH_MENU;
return;
} else if (this->questType[this->buttonIndex] == QUEST_RANDOMIZER) {
} else if (IS_RANDO || IS_SPEEDRUN) {
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
this->prevConfigMode = this->configMode;
this->configMode = CM_ROTATE_TO_RANDOMIZER_SETTINGS_MENU;
this->configMode = CM_ROTATE_TO_SETTINGS_MENU;
return;
} else {
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
@@ -756,7 +849,6 @@ void FileChoose_UpdateRandomizerMenu(GameState* thisx) {
FileChoose_UpdateStickDirectionPromptAnim(thisx);
FileChooseContext* this = (FileChooseContext*)thisx;
Input* input = &this->state.input[0];
bool dpad = CVarGetInteger(CVAR_SETTING("DpadInText"), 0);
FileChoose_UpdateRandomizer();
@@ -764,40 +856,14 @@ void FileChoose_UpdateRandomizerMenu(GameState* thisx) {
return;
}
// Fade in elements after opening Randomizer options menu
this->randomizerUIAlpha += 25;
if (this->randomizerUIAlpha > 255) {
this->randomizerUIAlpha = 255;
}
// Move menu selection up or down.
if (ABS(this->stickRelY) > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN | BTN_DUP))) {
// Move down
if (this->stickRelY < -30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN))) {
// When selecting past the last option, cycle back to the first option.
if ((this->randomizerIndex + 1) > RSM_OPEN_RANDOMIZER_SETTINGS) {
this->randomizerIndex = RSM_START_RANDOMIZER;
} else {
this->randomizerIndex++;
}
} else if (this->stickRelY > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DUP))) {
// When selecting past the first option, cycle back to the last option and offset the list to view it
// properly.
if ((this->randomizerIndex - 1) < RSM_START_RANDOMIZER) {
this->randomizerIndex = RSM_OPEN_RANDOMIZER_SETTINGS;
} else {
this->randomizerIndex--;
}
}
FileChoose_UpdateListMenuAnim(&this->randomizerUIAlpha, &this->randomizerArrowOffset);
if (FileChoose_MoveListCursor(this, &this->randomizerIndex, NULL, RSM_OPEN_RANDOMIZER_SETTINGS + 1, 0)) {
GameInteractor_ExecuteOnUpdateFileRandomizerOptionSelection(this->randomizerIndex);
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_CURSOR, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
}
if (CHECK_BTN_ALL(input->press.button, BTN_B)) {
this->configMode = CM_RANDOMIZER_SETTINGS_MENU_TO_QUEST;
this->configMode = CM_SETTINGS_MENU_TO_QUEST;
return;
}
@@ -808,9 +874,7 @@ void FileChoose_UpdateRandomizerMenu(GameState* thisx) {
Audio_PlaySfxGeneral(NA_SE_SY_FSEL_DECIDE_L, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
this->prevConfigMode = this->configMode;
this->configMode = CM_ROTATE_TO_NAME_ENTRY;
CVarSetInteger(CVAR_GENERAL("OnFileSelectNameEntry"), 1);
FileChoose_StartNameEntryFromMenu(this);
} else {
Sfx_PlaySfxCentered(NA_SE_SY_OCARINA_ERROR);
}
@@ -824,6 +888,14 @@ void FileChoose_UpdateRandomizerMenu(GameState* thisx) {
}
}
void FileChoose_UpdateSettings(GameState* thisx) {
if (IS_SPEEDRUN) {
FileChoose_UpdateSpeedrunMenu(thisx);
} else {
FileChoose_UpdateRandomizerMenu(thisx);
}
}
/**
* Update function for `CM_UNUSED_31`
*/
@@ -856,7 +928,7 @@ void FileChoose_RotateToNameEntry(GameState* thisx) {
this->windowRot += VREG(16);
if (this->prevConfigMode == CM_RANDOMIZER_SETTINGS_MENU) {
if (this->prevConfigMode == CM_SETTINGS_MENU) {
if (this->windowRot >= 942.0f) {
this->windowRot = 628.0f;
this->configMode = CM_START_NAME_ENTRY;
@@ -906,7 +978,7 @@ void FileChoose_RotateToQuest(GameState* thisx) {
FileChooseContext* this = (FileChooseContext*)thisx;
if (this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU || this->configMode == CM_BOSS_RUSH_TO_QUEST ||
this->configMode == CM_RANDOMIZER_SETTINGS_MENU_TO_QUEST) {
this->configMode == CM_SETTINGS_MENU_TO_QUEST) {
this->windowRot -= VREG(16);
if (this->windowRot <= 314.0f) {
@@ -934,7 +1006,9 @@ void FileChoose_RotateToQuest(GameState* thisx) {
switch (this->questType[this->buttonIndex]) {
case QUEST_RANDOMIZER:
this->configMode = CM_START_RANDOMIZER_SETTINGS_MENU;
case QUEST_SPEEDRUN:
case QUEST_SPEEDRUN_MASTER:
this->configMode = CM_START_SETTINGS_MENU;
break;
case QUEST_BOSSRUSH:
this->configMode = CM_START_BOSS_RUSH_MENU;
@@ -962,86 +1036,87 @@ void FileChoose_RotateToBossRush(GameState* thisx) {
}
}
void FileChoose_RotateToRandomizer(GameState* thisx) {
// Rotate to a quest's settings menu, backwards when coming back from name entry
void FileChoose_RotateToSettings(GameState* thisx) {
FileChooseContext* this = (FileChooseContext*)thisx;
if (this->configMode == CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU) {
if (this->configMode == CM_NAME_ENTRY_TO_SETTINGS_MENU) {
this->windowRot -= VREG(16);
if (this->windowRot <= 314.0f) {
this->windowRot = 628.0f;
this->configMode = CM_START_RANDOMIZER_SETTINGS_MENU;
this->configMode = CM_START_SETTINGS_MENU;
}
} else {
this->windowRot += VREG(16);
if (this->windowRot >= 628.0f) {
this->windowRot = 628.0f;
this->configMode = CM_START_RANDOMIZER_SETTINGS_MENU;
this->configMode = CM_START_SETTINGS_MENU;
}
}
}
static void (*gConfigModeUpdateFuncs[])(GameState*) = {
FileChoose_StartFadeIn, FileChoose_FinishFadeIn,
FileChoose_UpdateMainMenu, FileChoose_SetupCopySource,
FileChoose_SelectCopySource, FileChoose_SetupCopyDest1,
FileChoose_SetupCopyDest2, FileChoose_SelectCopyDest,
FileChoose_ExitToCopySource1, FileChoose_ExitToCopySource2,
FileChoose_SetupCopyConfirm1, FileChoose_SetupCopyConfirm2,
FileChoose_CopyConfirm, FileChoose_ReturnToCopyDest,
FileChoose_CopyAnim1, FileChoose_CopyAnim2,
FileChoose_CopyAnim3, FileChoose_CopyAnim4,
FileChoose_CopyAnim5, FileChoose_ExitCopyToMain,
FileChoose_SetupEraseSelect, FileChoose_EraseSelect,
FileChoose_SetupEraseConfirm1, FileChoose_SetupEraseConfirm2,
FileChoose_EraseConfirm, FileChoose_ExitToEraseSelect1,
FileChoose_ExitToEraseSelect2, FileChoose_EraseAnim1,
FileChoose_EraseAnim2, FileChoose_EraseAnim3,
FileChoose_ExitEraseToMain, FileChoose_UnusedCM31,
FileChoose_RotateToNameEntry, FileChoose_UpdateKeyboardCursor,
FileChoose_StartNameEntry, FileChoose_RotateToMain,
FileChoose_RotateToOptions, FileChoose_UpdateOptionsMenu,
FileChoose_StartOptions, FileChoose_RotateToMain,
FileChoose_UnusedCMDelay, FileChoose_RotateToQuest,
FileChoose_UpdateQuestMenu, FileChoose_StartQuestMenu,
FileChoose_RotateToMain, FileChoose_RotateToQuest,
FileChoose_RotateToBossRush, FileChoose_UpdateBossRushMenu,
FileChoose_StartBossRushMenu, FileChoose_RotateToQuest,
FileChoose_RotateToRandomizer, FileChoose_UpdateRandomizerMenu,
FileChoose_StartRandomizerMenu, FileChoose_RotateToQuest,
FileChoose_RotateToRandomizer,
FileChoose_StartFadeIn, FileChoose_FinishFadeIn,
FileChoose_UpdateMainMenu, FileChoose_SetupCopySource,
FileChoose_SelectCopySource, FileChoose_SetupCopyDest1,
FileChoose_SetupCopyDest2, FileChoose_SelectCopyDest,
FileChoose_ExitToCopySource1, FileChoose_ExitToCopySource2,
FileChoose_SetupCopyConfirm1, FileChoose_SetupCopyConfirm2,
FileChoose_CopyConfirm, FileChoose_ReturnToCopyDest,
FileChoose_CopyAnim1, FileChoose_CopyAnim2,
FileChoose_CopyAnim3, FileChoose_CopyAnim4,
FileChoose_CopyAnim5, FileChoose_ExitCopyToMain,
FileChoose_SetupEraseSelect, FileChoose_EraseSelect,
FileChoose_SetupEraseConfirm1, FileChoose_SetupEraseConfirm2,
FileChoose_EraseConfirm, FileChoose_ExitToEraseSelect1,
FileChoose_ExitToEraseSelect2, FileChoose_EraseAnim1,
FileChoose_EraseAnim2, FileChoose_EraseAnim3,
FileChoose_ExitEraseToMain, FileChoose_UnusedCM31,
FileChoose_RotateToNameEntry, FileChoose_UpdateKeyboardCursor,
FileChoose_StartNameEntry, FileChoose_RotateToMain,
FileChoose_RotateToOptions, FileChoose_UpdateOptionsMenu,
FileChoose_StartOptions, FileChoose_RotateToMain,
FileChoose_UnusedCMDelay, FileChoose_RotateToQuest,
FileChoose_UpdateQuestMenu, FileChoose_StartQuestMenu,
FileChoose_RotateToMain, FileChoose_RotateToQuest,
FileChoose_RotateToBossRush, FileChoose_UpdateBossRushMenu,
FileChoose_StartBossRushMenu, FileChoose_RotateToQuest,
FileChoose_RotateToSettings, FileChoose_UpdateSettings,
FileChoose_StartSettings, FileChoose_RotateToQuest,
FileChoose_RotateToSettings,
};
static void (*gConfigModeUpdateFuncsNES[])(GameState*) = {
FileChoose_StartFadeIn, FileChoose_FinishFadeIn,
FileChoose_UpdateMainMenu, FileChoose_SetupCopySource,
FileChoose_SelectCopySource, FileChoose_SetupCopyDest1,
FileChoose_SetupCopyDest2, FileChoose_SelectCopyDest,
FileChoose_ExitToCopySource1, FileChoose_ExitToCopySource2,
FileChoose_SetupCopyConfirm1, FileChoose_SetupCopyConfirm2,
FileChoose_CopyConfirm, FileChoose_ReturnToCopyDest,
FileChoose_CopyAnim1, FileChoose_CopyAnim2,
FileChoose_CopyAnim3, FileChoose_CopyAnim4,
FileChoose_CopyAnim5, FileChoose_ExitCopyToMain,
FileChoose_SetupEraseSelect, FileChoose_EraseSelect,
FileChoose_SetupEraseConfirm1, FileChoose_SetupEraseConfirm2,
FileChoose_EraseConfirm, FileChoose_ExitToEraseSelect1,
FileChoose_ExitToEraseSelect2, FileChoose_EraseAnim1,
FileChoose_EraseAnim2, FileChoose_EraseAnim3,
FileChoose_ExitEraseToMain, FileChoose_UnusedCM31,
FileChoose_RotateToNameEntry, FileChoose_UpdateKeyboardCursorNES,
FileChoose_StartNameEntryNES, FileChoose_RotateToMain,
FileChoose_RotateToOptions, FileChoose_UpdateOptionsMenuNES,
FileChoose_StartOptionsNES, FileChoose_RotateToMain,
FileChoose_UnusedCMDelay, FileChoose_RotateToQuest,
FileChoose_UpdateQuestMenu, FileChoose_StartQuestMenu,
FileChoose_RotateToMain, FileChoose_RotateToQuest,
FileChoose_RotateToBossRush, FileChoose_UpdateBossRushMenu,
FileChoose_StartBossRushMenu, FileChoose_RotateToQuest,
FileChoose_RotateToRandomizer, FileChoose_UpdateRandomizerMenu,
FileChoose_StartRandomizerMenu, FileChoose_RotateToQuest,
FileChoose_RotateToRandomizer,
FileChoose_StartFadeIn, FileChoose_FinishFadeIn,
FileChoose_UpdateMainMenu, FileChoose_SetupCopySource,
FileChoose_SelectCopySource, FileChoose_SetupCopyDest1,
FileChoose_SetupCopyDest2, FileChoose_SelectCopyDest,
FileChoose_ExitToCopySource1, FileChoose_ExitToCopySource2,
FileChoose_SetupCopyConfirm1, FileChoose_SetupCopyConfirm2,
FileChoose_CopyConfirm, FileChoose_ReturnToCopyDest,
FileChoose_CopyAnim1, FileChoose_CopyAnim2,
FileChoose_CopyAnim3, FileChoose_CopyAnim4,
FileChoose_CopyAnim5, FileChoose_ExitCopyToMain,
FileChoose_SetupEraseSelect, FileChoose_EraseSelect,
FileChoose_SetupEraseConfirm1, FileChoose_SetupEraseConfirm2,
FileChoose_EraseConfirm, FileChoose_ExitToEraseSelect1,
FileChoose_ExitToEraseSelect2, FileChoose_EraseAnim1,
FileChoose_EraseAnim2, FileChoose_EraseAnim3,
FileChoose_ExitEraseToMain, FileChoose_UnusedCM31,
FileChoose_RotateToNameEntry, FileChoose_UpdateKeyboardCursorNES,
FileChoose_StartNameEntryNES, FileChoose_RotateToMain,
FileChoose_RotateToOptions, FileChoose_UpdateOptionsMenuNES,
FileChoose_StartOptionsNES, FileChoose_RotateToMain,
FileChoose_UnusedCMDelay, FileChoose_RotateToQuest,
FileChoose_UpdateQuestMenu, FileChoose_StartQuestMenu,
FileChoose_RotateToMain, FileChoose_RotateToQuest,
FileChoose_RotateToBossRush, FileChoose_UpdateBossRushMenu,
FileChoose_StartBossRushMenu, FileChoose_RotateToQuest,
FileChoose_RotateToSettings, FileChoose_UpdateSettings,
FileChoose_StartSettings, FileChoose_RotateToQuest,
FileChoose_RotateToSettings,
};
/**
@@ -1680,6 +1755,99 @@ const char* FileChoose_GetSohOptionsTitleTexName(Language lang) {
}
}
// #region SOH - quest label, drawn as the 64DD disk label with its lettering covered and our own text on top
static void FileChoose_DrawQuestLabelBox(FileChooseContext* this, Vtx* labelVtx) {
// pieces of the 44x16 disk label as left, right, top, bottom in texels.
// "DISK" covers columns 11 to 37 and rows 3 to 12, so that area is filled by stretching row 2 down over it.
// filtering reads half a texel lower, so each texel shows half a row higher than its place on the quad.
// the stretched piece ends on row 13 rather than 14 so the bottom bevel lines up with the sides again
static const s16 pieces[][4] = {
{ 0, 10, 0, 16 }, { 40, 44, 0, 16 }, { 10, 40, 0, 2 }, { 10, 40, 2, 13 }, { 10, 40, 13, 16 },
};
const int stretchedPiece = 3;
Vtx* vtx = Graph_Alloc(this->state.gfxCtx, ARRAY_COUNT(pieces) * 4 * sizeof(Vtx));
OPEN_DISPS(this->state.gfxCtx);
for (int i = 0; i < ARRAY_COUNT(pieces); i++) {
Vtx* quad = &vtx[i * 4];
for (int j = 0; j < 4; j++) {
int x = pieces[i][(j & 1) ? 1 : 0];
int y = pieces[i][(j & 2) ? 3 : 2];
quad[j] = labelVtx[0];
quad[j].v.ob[0] = labelVtx[0].v.ob[0] + x;
quad[j].v.ob[1] = labelVtx[0].v.ob[1] - y;
quad[j].v.tc[0] = x << 5;
// filtering adds half a texel, so 2 lands on the middle of row 2
quad[j].v.tc[1] = (i == stretchedPiece) ? 2 << 5 : y << 5;
}
}
gDPLoadTextureBlock(POLY_OPA_DISP++, gFileSelDISKButtonTex, G_IM_FMT_IA, G_IM_SIZ_16b, 44, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
gSPVertex(POLY_OPA_DISP++, vtx, ARRAY_COUNT(pieces) * 4, 0);
for (int i = 0; i < ARRAY_COUNT(pieces); i++) {
gSP1Quadrangle(POLY_OPA_DISP++, i * 4, i * 4 + 2, i * 4 + 3, i * 4 + 1, 0);
}
CLOSE_DISPS(this->state.gfxCtx);
}
static void FileChoose_DrawQuestLabelText(FileChooseContext* this, Vtx* labelVtx, const char* text, u8 alpha) {
// the blank part of the label spans columns 8 to 40 and rows 3 to 12
// glyphs are 16x16 with capitals on rows 1 to 12, so 12 makes capitals 9 tall
const int glyphSize = 12;
const float scale = glyphSize / 16.0f;
// the font puts A-Z at 0x0A when loaded in PAL order, 0xAB when loaded in NTSC order
int letterA =
(ResourceMgr_GetGameRegion(0) == GAME_REGION_PAL && gSaveContext.language != LANGUAGE_JPN) ? 0x0A : 0xAB;
int len = strlen(text);
// space letters by their font width, pulled half a unit closer so they stay inside the label
float offsets[8];
float inkWidth = 0.0f;
for (int i = 0; i < len; i++) {
offsets[i] = inkWidth;
inkWidth += (i + 1 < len) ? Ship_GetCharFontWidth(text[i]) * scale - 0.5f
: (Ship_GetCharFontWidth(text[i]) - 1) * scale;
}
// glyph ink starts one texel in from the left of the cell
float left = labelVtx[0].v.ob[0] + 24 - inkWidth / 2 - scale;
int top = labelVtx[0].v.ob[1] - 3;
Vtx* vtx = Graph_Alloc(this->state.gfxCtx, len * 4 * sizeof(Vtx));
OPEN_DISPS(this->state.gfxCtx);
for (int i = 0; i < len; i++) {
Vtx* quad = &vtx[i * 4];
for (int j = 0; j < 4; j++) {
quad[j] = labelVtx[0];
quad[j].v.ob[0] = (int)(left + offsets[i] + 0.5f) + ((j & 1) ? glyphSize : 0);
quad[j].v.ob[1] = top - ((j & 2) ? glyphSize : 0);
quad[j].v.tc[0] = (j & 1) ? 16 << 5 : 0;
quad[j].v.tc[1] = (j & 2) ? 16 << 5 : 0;
}
}
gDPPipeSync(POLY_OPA_DISP++);
gDPSetCombineLERP(POLY_OPA_DISP++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0,
PRIMITIVE, 0);
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, alpha);
gSPVertex(POLY_OPA_DISP++, vtx, len * 4, 0);
for (int i = 0; i < len; i++) {
FileChoose_DrawCharacter(this->state.gfxCtx,
this->font.fontBuf + (letterA + text[i] - 'A') * FONT_CHAR_TEX_SIZE, i * 4);
}
// back to the combiner the file buttons use
gDPPipeSync(POLY_OPA_DISP++);
gDPSetCombineLERP(POLY_OPA_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0, PRIMITIVE,
ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0);
CLOSE_DISPS(this->state.gfxCtx);
}
// #endregion
/**
* Draw most window contents including buttons, labels, and icons.
* Does not include anything from the keyboard and settings windows.
@@ -1701,16 +1869,16 @@ void FileChoose_DrawWindowContents(GameState* thisx) {
case CM_QUEST_TO_MAIN:
case CM_NAME_ENTRY_TO_QUEST_MENU:
case CM_ROTATE_TO_BOSS_RUSH_MENU:
case CM_ROTATE_TO_RANDOMIZER_SETTINGS_MENU:
case CM_ROTATE_TO_SETTINGS_MENU:
tex = FileChoose_GetQuestChooseTitleTexName(gSaveContext.language);
break;
case CM_BOSS_RUSH_MENU:
case CM_START_BOSS_RUSH_MENU:
case CM_BOSS_RUSH_TO_QUEST:
case CM_RANDOMIZER_SETTINGS_MENU:
case CM_START_RANDOMIZER_SETTINGS_MENU:
case CM_RANDOMIZER_SETTINGS_MENU_TO_QUEST:
case CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU:
case CM_SETTINGS_MENU:
case CM_START_SETTINGS_MENU:
case CM_SETTINGS_MENU_TO_QUEST:
case CM_NAME_ENTRY_TO_SETTINGS_MENU:
tex = FileChoose_GetSohOptionsTitleTexName(gSaveContext.language);
break;
default:
@@ -1734,8 +1902,7 @@ void FileChoose_DrawWindowContents(GameState* thisx) {
// draw next title label
if ((this->configMode == CM_QUEST_MENU) || (this->configMode == CM_START_QUEST_MENU) ||
this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU ||
this->configMode == CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU) {
this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU || this->configMode == CM_NAME_ENTRY_TO_SETTINGS_MENU) {
// #region SOH [Enhancement] - Hide Quest Modes
// Only draw the control stick prompts and arrows when there's more than one quest to cycle through.
if (SohFileSelect_CountVisibleQuests() > 1) {
@@ -1833,10 +2000,31 @@ void FileChoose_DrawWindowContents(GameState* thisx) {
ResourceMgr_GameHasOriginal() ? gTitleZeldaShieldLogoTex : gTitleZeldaShieldLogoMQTex, 160, 160);
FileChoose_DrawImageRGBA32(this->state.gfxCtx, 182, 180, gTitleBossRushSubtitleTex, 128, 32);
break;
case QUEST_SPEEDRUN:
case QUEST_SPEEDRUN_MASTER:
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->logoAlpha);
FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleTheLegendOfTextTex, 72, 8, 156, 108, 72, 8, 1024,
1024);
FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleOcarinaOfTimeTMTextTex, 96, 8, 154, 163, 96, 8, 1024,
1024);
FileChoose_DrawImageRGBA32(this->state.gfxCtx, 160, 135,
this->questType[this->buttonIndex] == QUEST_SPEEDRUN_MASTER
? gTitleZeldaShieldLogoMQTex
: gTitleZeldaShieldLogoTex,
160, 160);
// No subtitle texture exists for speedrun, so the name is drawn as text instead.
Interface_DrawTextLine(this->state.gfxCtx,
this->questType[this->buttonIndex] == QUEST_SPEEDRUN_MASTER ? "Speedrun MQ"
: "Speedrun",
130, 176, 255, 255, 255, this->logoAlpha, 1.0f, true);
break;
}
} else if (this->configMode == CM_BOSS_RUSH_MENU) {
FileChoose_DrawBossRushMenuWindowContents(this);
} else if (this->configMode == CM_RANDOMIZER_SETTINGS_MENU) {
} else if (this->configMode == CM_SETTINGS_MENU && IS_SPEEDRUN) {
FileChoose_DrawSpeedrunMenuWindowContents(this);
} else if (this->configMode == CM_SETTINGS_MENU) {
uint8_t language = (gSaveContext.language == LANGUAGE_JPN) ? LANGUAGE_ENG : gSaveContext.language;
uint8_t textAlpha = this->randomizerUIAlpha;
@@ -1876,22 +2064,12 @@ void FileChoose_DrawWindowContents(GameState* thisx) {
240, 80, 80, textAlpha, 0.8f, true);
}
uint16_t textOffset = 16 * this->randomizerIndex;
Gfx_SetupDL_39Opa(this->state.gfxCtx);
gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowCursorTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 24, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD,
G_TX_NOLOD);
FileChoose_DrawTextRec(this->state.gfxCtx, this->stickRightPrompt.arrowColorR,
this->stickRightPrompt.arrowColorG, this->stickRightPrompt.arrowColorB, textAlpha, 62,
(85 + textOffset), 0.42f, 0, 0, 1.0f, 1.0f);
FileChoose_DrawListCursorArrow(this, textAlpha, 62, 85 + 16 * this->randomizerIndex, false);
} else if (this->configMode != CM_ROTATE_TO_NAME_ENTRY && this->configMode != CM_START_BOSS_RUSH_MENU &&
this->configMode != CM_ROTATE_TO_BOSS_RUSH_MENU && this->configMode != CM_BOSS_RUSH_TO_QUEST &&
this->configMode != CM_START_RANDOMIZER_SETTINGS_MENU &&
this->configMode != CM_ROTATE_TO_RANDOMIZER_SETTINGS_MENU &&
this->configMode != CM_RANDOMIZER_SETTINGS_MENU_TO_QUEST &&
this->configMode != CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU) {
this->configMode != CM_START_SETTINGS_MENU && this->configMode != CM_ROTATE_TO_SETTINGS_MENU &&
this->configMode != CM_SETTINGS_MENU_TO_QUEST && this->configMode != CM_NAME_ENTRY_TO_SETTINGS_MENU) {
gDPPipeSync(POLY_OPA_DISP++);
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->titleAlpha[1]);
gDPLoadTextureBlock(POLY_OPA_DISP++, sTitleLabels[gSaveContext.language][this->nextTitleLabel], G_IM_FMT_IA,
@@ -1968,36 +2146,38 @@ void FileChoose_DrawWindowContents(GameState* thisx) {
gSP1Quadrangle(POLY_OPA_DISP++, 8, 10, 11, 9, 0);
}
// draw rando label
if (Save_GetSaveMetaInfo(i)->randoSave) {
if (!FileChoose_IsSaveCompatible(Save_GetSaveMetaInfo(i))) {
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, sWindowContentColors[1][0], sWindowContentColors[1][1],
sWindowContentColors[1][2], this->nameBoxAlpha[i]);
} else {
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, sWindowContentColors[isActive][0],
sWindowContentColors[isActive][1], sWindowContentColors[isActive][2],
this->nameAlpha[i]);
}
gDPLoadTextureBlock(POLY_OPA_DISP++, gFileSelRANDButtonTex, G_IM_FMT_IA, G_IM_SIZ_16b, 44, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK,
G_TX_NOLOD, G_TX_NOLOD);
gSP1Quadrangle(POLY_OPA_DISP++, 8, 10, 11, 9, 0);
// draw quest label (rando, speedrun or MQ)
const char* questLabelText = NULL;
u8 questLabelAlpha = 0;
switch (Save_GetSaveMetaInfo(i)->quest) {
case QUEST_RANDOMIZER:
questLabelText = "RAND";
break;
case QUEST_SPEEDRUN:
case QUEST_SPEEDRUN_MASTER:
questLabelText = "RUN";
break;
case QUEST_MASTER:
questLabelText = "MQ";
break;
}
// Draw MQ label
if (Save_GetSaveMetaInfo(i)->requiresMasterQuest && !Save_GetSaveMetaInfo(i)->randoSave &&
Save_GetSaveMetaInfo(i)->valid) {
if (!Save_GetSaveMetaInfo(i)->valid) {
questLabelText = NULL;
}
if (questLabelText != NULL) {
if (!FileChoose_IsSaveCompatible(Save_GetSaveMetaInfo(i))) {
questLabelAlpha = this->nameBoxAlpha[i];
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, sWindowContentColors[1][0], sWindowContentColors[1][1],
sWindowContentColors[1][2], this->nameBoxAlpha[i]);
sWindowContentColors[1][2], questLabelAlpha);
} else {
questLabelAlpha = this->nameAlpha[i];
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, sWindowContentColors[isActive][0],
sWindowContentColors[isActive][1], sWindowContentColors[isActive][2],
this->nameAlpha[i]);
questLabelAlpha);
}
gDPLoadTextureBlock(POLY_OPA_DISP++, gFileSelMQButtonTex, G_IM_FMT_IA, G_IM_SIZ_16b, 44, 16, 0,
G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK,
G_TX_NOLOD, G_TX_NOLOD);
gSP1Quadrangle(POLY_OPA_DISP++, 8, 10, 11, 9, 0);
FileChoose_DrawQuestLabelBox(this, &this->windowContentVtx[temp + 8]);
// put back the file button vertices the label replaced
gSPVertex(POLY_OPA_DISP++, &this->windowContentVtx[temp], 20, 0);
}
// draw connectors
@@ -2014,10 +2194,14 @@ void FileChoose_DrawWindowContents(GameState* thisx) {
G_TX_NOLOD, G_TX_NOLOD);
gSP1Quadrangle(POLY_OPA_DISP++, 12, 14, 15, 13, 0);
if (this->n64ddFlags[i] || Save_GetSaveMetaInfo(i)->randoSave ||
Save_GetSaveMetaInfo(i)->requiresMasterQuest) {
if (this->n64ddFlags[i] || Save_GetSaveMetaInfo(i)->quest != QUEST_NORMAL) {
gSP1Quadrangle(POLY_OPA_DISP++, 16, 18, 19, 17, 0);
}
// drawn last since it loads its own vertices over the ones above
if (questLabelText != NULL) {
FileChoose_DrawQuestLabelText(this, &this->windowContentVtx[temp + 8], questLabelText, questLabelAlpha);
}
}
// draw file info
@@ -2132,8 +2316,7 @@ void FileChoose_ConfigModeDraw(GameState* thisx) {
if (this->configMode != CM_NAME_ENTRY && this->configMode != CM_START_NAME_ENTRY &&
this->configMode != CM_QUEST_MENU && this->configMode != CM_NAME_ENTRY_TO_QUEST_MENU &&
this->configMode != CM_RANDOMIZER_SETTINGS_MENU &&
this->configMode != CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU) {
this->configMode != CM_SETTINGS_MENU && this->configMode != CM_NAME_ENTRY_TO_SETTINGS_MENU) {
gDPPipeSync(POLY_OPA_DISP++);
gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, this->windowColor[0], this->windowColor[1], this->windowColor[2],
@@ -2180,7 +2363,7 @@ void FileChoose_ConfigModeDraw(GameState* thisx) {
Matrix_Scale(0.78f, 0.78f, 0.78f, MTXMODE_APPLY);
// Invert name select when switching from randomizer settings menu to name entry, otherwise
// it'll show on the backside while rotating to the menu.
if (this->configMode == CM_ROTATE_TO_NAME_ENTRY && this->prevConfigMode == CM_RANDOMIZER_SETTINGS_MENU) {
if (this->configMode == CM_ROTATE_TO_NAME_ENTRY && this->prevConfigMode == CM_SETTINGS_MENU) {
Matrix_RotateX((this->windowRot - 314.0f) / 100.0f, MTXMODE_APPLY);
} else {
Matrix_RotateX((this->windowRot - 628.0f) / 100.0f, MTXMODE_APPLY);
@@ -2241,8 +2424,7 @@ void FileChoose_ConfigModeDraw(GameState* thisx) {
if (this->configMode == CM_QUEST_MENU || (this->configMode == CM_ROTATE_TO_QUEST_MENU) ||
this->configMode == CM_ROTATE_TO_NAME_ENTRY || this->configMode == CM_QUEST_TO_MAIN ||
this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU || this->configMode == CM_ROTATE_TO_BOSS_RUSH_MENU ||
this->configMode == CM_ROTATE_TO_RANDOMIZER_SETTINGS_MENU ||
this->configMode == CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU) {
this->configMode == CM_ROTATE_TO_SETTINGS_MENU || this->configMode == CM_NAME_ENTRY_TO_SETTINGS_MENU) {
// window
gDPPipeSync(POLY_OPA_DISP++);
gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
@@ -2270,12 +2452,11 @@ void FileChoose_ConfigModeDraw(GameState* thisx) {
FileChoose_DrawWindowContents(&this->state);
}
// Draw Boss Rush / Randomizer Options Menu
// Draw Boss Rush / Randomizer / Speedrun Options Menu
if (this->configMode == CM_BOSS_RUSH_MENU || this->configMode == CM_ROTATE_TO_BOSS_RUSH_MENU ||
this->configMode == CM_START_BOSS_RUSH_MENU || this->configMode == CM_BOSS_RUSH_TO_QUEST ||
this->configMode == CM_RANDOMIZER_SETTINGS_MENU || this->configMode == CM_ROTATE_TO_RANDOMIZER_SETTINGS_MENU ||
this->configMode == CM_START_RANDOMIZER_SETTINGS_MENU ||
this->configMode == CM_RANDOMIZER_SETTINGS_MENU_TO_QUEST) {
this->configMode == CM_SETTINGS_MENU || this->configMode == CM_ROTATE_TO_SETTINGS_MENU ||
this->configMode == CM_START_SETTINGS_MENU || this->configMode == CM_SETTINGS_MENU_TO_QUEST) {
// window
gDPPipeSync(POLY_OPA_DISP++);
gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
@@ -2534,7 +2715,8 @@ void FileChoose_LoadGame(GameState* thisx) {
gSaveContext.gameMode = GAMEMODE_NORMAL;
Randomizer_WaitForGeneration();
if ((this->buttonIndex == FS_BTN_SELECT_FILE_1 && CVarGetInteger(CVAR_DEVELOPER_TOOLS("DebugEnabled"), 0)) ||
if ((this->buttonIndex == FS_BTN_SELECT_FILE_1 &&
Ship_QuestDebugEnabled(Save_GetSaveMetaInfo(this->buttonIndex)->quest)) ||
this->buttonIndex == 0xFF) {
if (this->buttonIndex == 0xFF) {
Sram_InitDebugSave();
@@ -2694,7 +2876,7 @@ void FileChoose_DrawRandoSaveVersionWarning(GameState* thisx) {
// Draw rando seed warning when build version doesn't match for Major or Minor number
for (int fileIndex = 0; fileIndex < 3; fileIndex++) {
if (Save_GetSaveMetaInfo(fileIndex)->randoSave == 1 && this->menuMode == FS_MENU_MODE_SELECT &&
if (Save_GetSaveMetaInfo(fileIndex)->quest == QUEST_RANDOMIZER && this->menuMode == FS_MENU_MODE_SELECT &&
(gBuildVersionMajor != Save_GetSaveMetaInfo(fileIndex)->buildVersionMajor ||
gBuildVersionMinor != Save_GetSaveMetaInfo(fileIndex)->buildVersionMinor)) {
@@ -3086,6 +3268,8 @@ void FileChoose_InitContext(GameState* thisx) {
this->bossRushIndex = 0;
this->bossRushOffset = 0;
this->randomizerIndex = 0;
this->speedrunIndex = 0;
this->speedrunOffset = 0;
Letterbox_SetSizeTarget(0);
@@ -548,8 +548,8 @@ void FileChoose_DrawNameEntryNES(GameState* thisx) {
this->newFileNameCharCount = 0;
if (this->prevConfigMode == CM_QUEST_MENU) {
this->configMode = CM_NAME_ENTRY_TO_QUEST_MENU;
} else if (this->prevConfigMode == CM_RANDOMIZER_SETTINGS_MENU) {
this->configMode = CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU;
} else if (this->prevConfigMode == CM_SETTINGS_MENU) {
this->configMode = CM_NAME_ENTRY_TO_SETTINGS_MENU;
} else {
this->configMode = CM_NAME_ENTRY_TO_MAIN;
}
@@ -378,8 +378,8 @@ void FileChoose_DrawNameEntry(GameState* thisx) {
this->newFileNameCharCount = 0;
if (this->prevConfigMode == CM_QUEST_MENU) {
this->configMode = CM_NAME_ENTRY_TO_QUEST_MENU;
} else if (this->prevConfigMode == CM_RANDOMIZER_SETTINGS_MENU) {
this->configMode = CM_NAME_ENTRY_TO_RANDOMIZER_SETTINGS_MENU;
} else if (this->prevConfigMode == CM_SETTINGS_MENU) {
this->configMode = CM_NAME_ENTRY_TO_SETTINGS_MENU;
} else {
this->configMode = CM_NAME_ENTRY_TO_MAIN;
}