summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Dubé <159546+serprex@users.noreply.github.com>2026-10-05 05:19:52 +0000
committerGitHub <noreply@github.com>2026-10-05 05:19:52 +0000
commit4afcb78331b8b847a92c42fafa5fbea00f4da748 (patch)
tree31b54e848855933a008993784e40331d4b9546ff
parentb69cb6607e493f3a8f347dbf4bf32ae852fc9142 (diff)
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
-rw-r--r--soh/assets/custom/accessibility/texts/filechoose_eng.json4
-rw-r--r--soh/assets/custom/accessibility/texts/filechoose_fra.json4
-rw-r--r--soh/assets/custom/accessibility/texts/filechoose_ger.json4
-rw-r--r--soh/assets/custom/presets/Speedrun - 1 Base.json119
-rw-r--r--soh/assets/custom/presets/Speedrun - 2 Faster Quest.json130
-rw-r--r--soh/assets/custom/presets/Speedrun - 3 Faster Quest with Cutscenes.json123
-rw-r--r--soh/assets/custom/textures/title_static/gFileSelMQButtonTex.ia16.pngbin883 -> 0 bytes
-rw-r--r--soh/assets/custom/textures/title_static/gFileSelRANDButtonTex.ia16.pngbin1045 -> 0 bytes
-rw-r--r--soh/assets/soh_assets.h6
-rw-r--r--soh/include/z64.h4
-rw-r--r--soh/include/z64save.h9
-rw-r--r--soh/soh/Enhancements/FileSelectEnhancements.cpp8
-rw-r--r--soh/soh/Enhancements/Presets/Presets.cpp86
-rw-r--r--soh/soh/Enhancements/Presets/Presets.h13
-rw-r--r--soh/soh/Enhancements/boss-rush/BossRush.cpp97
-rw-r--r--soh/soh/Enhancements/cosmetics/FileSelectMoreInfo.cpp2
-rw-r--r--soh/soh/Enhancements/speedrun/Speedrun.cpp435
-rw-r--r--soh/soh/Enhancements/speedrun/Speedrun.h38
-rw-r--r--soh/soh/Enhancements/tts/tts.cpp10
-rw-r--r--soh/soh/OTRGlobals.cpp3
-rw-r--r--soh/soh/SaveManager.cpp39
-rw-r--r--soh/soh/SaveManager.h2
-rw-r--r--soh/soh/SohGui/Menu.cpp5
-rw-r--r--soh/soh/SohGui/SohGui.cpp42
-rw-r--r--soh/soh/SohGui/SohGui.hpp2
-rw-r--r--soh/soh/SohGui/SohMenuEnhancements.cpp16
-rw-r--r--soh/soh/util.h8
-rw-r--r--soh/src/code/z_parameter.c2
-rw-r--r--soh/src/code/z_sram.c11
-rw-r--r--soh/src/overlays/gamestates/ovl_file_choose/file_choose.h24
-rw-r--r--soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c534
-rw-r--r--soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_NES.c4
-rw-r--r--soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c4
33 files changed, 1480 insertions, 308 deletions
diff --git a/soh/assets/custom/accessibility/texts/filechoose_eng.json b/soh/assets/custom/accessibility/texts/filechoose_eng.json
index c72e68020..6d4decfaa 100644
--- a/soh/assets/custom/accessibility/texts/filechoose_eng.json
+++ b/soh/assets/custom/accessibility/texts/filechoose_eng.json
@@ -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"
}
diff --git a/soh/assets/custom/accessibility/texts/filechoose_fra.json b/soh/assets/custom/accessibility/texts/filechoose_fra.json
index 7fc37072f..ef619090d 100644
--- a/soh/assets/custom/accessibility/texts/filechoose_fra.json
+++ b/soh/assets/custom/accessibility/texts/filechoose_fra.json
@@ -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"
}
diff --git a/soh/assets/custom/accessibility/texts/filechoose_ger.json b/soh/assets/custom/accessibility/texts/filechoose_ger.json
index b0fd8b899..82c4c15f0 100644
--- a/soh/assets/custom/accessibility/texts/filechoose_ger.json
+++ b/soh/assets/custom/accessibility/texts/filechoose_ger.json
@@ -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"
}
diff --git a/soh/assets/custom/presets/Speedrun - 1 Base.json b/soh/assets/custom/presets/Speedrun - 1 Base.json
new file mode 100644
index 000000000..a362300e5
--- /dev/null
+++ b/soh/assets/custom/presets/Speedrun - 1 Base.json
@@ -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
+}
diff --git a/soh/assets/custom/presets/Speedrun - 2 Faster Quest.json b/soh/assets/custom/presets/Speedrun - 2 Faster Quest.json
new file mode 100644
index 000000000..70fbdfaa5
--- /dev/null
+++ b/soh/assets/custom/presets/Speedrun - 2 Faster Quest.json
@@ -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
+}
diff --git a/soh/assets/custom/presets/Speedrun - 3 Faster Quest with Cutscenes.json b/soh/assets/custom/presets/Speedrun - 3 Faster Quest with Cutscenes.json
new file mode 100644
index 000000000..d1a2a94f7
--- /dev/null
+++ b/soh/assets/custom/presets/Speedrun - 3 Faster Quest with Cutscenes.json
@@ -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
+}
diff --git a/soh/assets/custom/textures/title_static/gFileSelMQButtonTex.ia16.png b/soh/assets/custom/textures/title_static/gFileSelMQButtonTex.ia16.png
deleted file mode 100644
index 076b9365f..000000000
--- a/soh/assets/custom/textures/title_static/gFileSelMQButtonTex.ia16.png
+++ /dev/null
Binary files differ
diff --git a/soh/assets/custom/textures/title_static/gFileSelRANDButtonTex.ia16.png b/soh/assets/custom/textures/title_static/gFileSelRANDButtonTex.ia16.png
deleted file mode 100644
index f43114e81..000000000
--- a/soh/assets/custom/textures/title_static/gFileSelRANDButtonTex.ia16.png
+++ /dev/null
Binary files differ
diff --git a/soh/assets/soh_assets.h b/soh/assets/soh_assets.h
index 3f8d44db0..3361943c9 100644
--- a/soh/assets/soh_assets.h
+++ b/soh/assets/soh_assets.h
@@ -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;
diff --git a/soh/include/z64.h b/soh/include/z64.h
index e47f8386e..5bb0bd16c 100644
--- a/soh/include/z64.h
+++ b/soh/include/z64.h
@@ -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`
diff --git a/soh/include/z64save.h b/soh/include/z64save.h
index 11d91f7dc..511dc8955 100644
--- a/soh/include/z64save.h
+++ b/soh/include/z64save.h
@@ -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,
diff --git a/soh/soh/Enhancements/FileSelectEnhancements.cpp b/soh/soh/Enhancements/FileSelectEnhancements.cpp
index 0900d202f..8dd94e803 100644
--- a/soh/soh/Enhancements/FileSelectEnhancements.cpp
+++ b/soh/soh/Enhancements/FileSelectEnhancements.cpp
@@ -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++;
}
diff --git a/soh/soh/Enhancements/Presets/Presets.cpp b/soh/soh/Enhancements/Presets/Presets.cpp
index e40f6ca36..b9daf1c3b 100644
--- a/soh/soh/Enhancements/Presets/Presets.cpp
+++ b/soh/soh/Enhancements/Presets/Presets.cpp
@@ -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) {
diff --git a/soh/soh/Enhancements/Presets/Presets.h b/soh/soh/Enhancements/Presets/Presets.h
index 5edb265a3..5856a0b93 100644
--- a/soh/soh/Enhancements/Presets/Presets.h
+++ b/soh/soh/Enhancements/Presets/Presets.h
@@ -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);
diff --git a/soh/soh/Enhancements/boss-rush/BossRush.cpp b/soh/soh/Enhancements/boss-rush/BossRush.cpp
index b35c30b2b..edaa5d103 100644
--- a/soh/soh/Enhancements/boss-rush/BossRush.cpp
+++ b/soh/soh/Enhancements/boss-rush/BossRush.cpp
@@ -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) {
diff --git a/soh/soh/Enhancements/cosmetics/FileSelectMoreInfo.cpp b/soh/soh/Enhancements/cosmetics/FileSelectMoreInfo.cpp
index 59a76de13..6b80b567d 100644
--- a/soh/soh/Enhancements/cosmetics/FileSelectMoreInfo.cpp
+++ b/soh/soh/Enhancements/cosmetics/FileSelectMoreInfo.cpp
@@ -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) {
diff --git a/soh/soh/Enhancements/speedrun/Speedrun.cpp b/soh/soh/Enhancements/speedrun/Speedrun.cpp
new file mode 100644
index 000000000..68ae98be5
--- /dev/null
+++ b/soh/soh/Enhancements/speedrun/Speedrun.cpp
@@ -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();
+}
diff --git a/soh/soh/Enhancements/speedrun/Speedrun.h b/soh/soh/Enhancements/speedrun/Speedrun.h
new file mode 100644
index 000000000..7780c7048
--- /dev/null
+++ b/soh/soh/Enhancements/speedrun/Speedrun.h
@@ -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
diff --git a/soh/soh/Enhancements/tts/tts.cpp b/soh/soh/Enhancements/tts/tts.cpp
index 226cebe87..34b90f8c5 100644
--- a/soh/soh/Enhancements/tts/tts.cpp
+++ b/soh/soh/Enhancements/tts/tts.cpp
@@ -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;
}
diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp
index 34642e3c5..1b4f62c47 100644
--- a/soh/soh/OTRGlobals.cpp
+++ b/soh/soh/OTRGlobals.cpp
@@ -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();
diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp
index 4f552f971..f9464b5b7 100644
--- a/soh/soh/SaveManager.cpp
+++ b/soh/soh/SaveManager.cpp
@@ -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);
diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h
index 095cf7e6d..03e9f6831 100644
--- a/soh/soh/SaveManager.h
+++ b/soh/soh/SaveManager.h
@@ -14,7 +14,7 @@ typedef struct {
u32 requiresMasterQuest;
u32 requiresOriginal;
u8 seedHash[5];
- u8 randoSave;
+ u8 quest;
char buildVersion[50];
s16 buildVersionMajor;
s16 buildVersionMinor;
diff --git a/soh/soh/SohGui/Menu.cpp b/soh/soh/SohGui/Menu.cpp
index 2c4407f33..642bc54a3 100644
--- a/soh/soh/SohGui/Menu.cpp
+++ b/soh/soh/SohGui/Menu.cpp
@@ -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;
}
diff --git a/soh/soh/SohGui/SohGui.cpp b/soh/soh/SohGui/SohGui.cpp
index 15db09539..986f93376 100644
--- a/soh/soh/SohGui/SohGui.cpp
+++ b/soh/soh/SohGui/SohGui.cpp
@@ -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");
diff --git a/soh/soh/SohGui/SohGui.hpp b/soh/soh/SohGui/SohGui.hpp
index c0cef1fa5..17aff4079 100644
--- a/soh/soh/SohGui/SohGui.hpp
+++ b/soh/soh/SohGui/SohGui.hpp
@@ -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
diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp
index 3bf31116a..759e0605a 100644
--- a/soh/soh/SohGui/SohMenuEnhancements.cpp
+++ b/soh/soh/SohGui/SohMenuEnhancements.cpp
@@ -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);
diff --git a/soh/soh/util.h b/soh/soh/util.h
index 0407d918a..90fee9961 100644
--- a/soh/soh/util.h
+++ b/soh/soh/util.h
@@ -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);
diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c
index 8a4e00c20..524ecd89f 100644
--- a/soh/src/code/z_parameter.c
+++ b/soh/src/code/z_parameter.c
@@ -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;
diff --git a/soh/src/code/z_sram.c b/soh/src/code/z_sram.c
index 1e0dffca6..6dd90496e 100644
--- a/soh/src/code/z_sram.c
+++ b/soh/src/code/z_sram.c
@@ -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();
}
diff --git a/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h b/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h
index 5f6e3f202..7f3734779 100644
--- a/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h
+++ b/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h
@@ -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);
diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c
index 266c0114e..7149b09b6 100644
--- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c
+++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c
@@ -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);
diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_NES.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_NES.c
index d502446d9..fb65a3b07 100644
--- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_NES.c
+++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_NES.c
@@ -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;
}
diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c
index ea489f57f..3a8ab6f83 100644
--- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c
+++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c
@@ -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;
}