From 4afcb78331b8b847a92c42fafa5fbea00f4da748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:19:52 +0000 Subject: [PATCH] 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 --- .../accessibility/texts/filechoose_eng.json | 4 +- .../accessibility/texts/filechoose_fra.json | 4 +- .../accessibility/texts/filechoose_ger.json | 4 +- .../custom/presets/Speedrun - 1 Base.json | 119 ++++ .../presets/Speedrun - 2 Faster Quest.json | 130 +++++ ...edrun - 3 Faster Quest with Cutscenes.json | 123 ++++ .../title_static/gFileSelMQButtonTex.ia16.png | Bin 883 -> 0 bytes .../gFileSelRANDButtonTex.ia16.png | Bin 1045 -> 0 bytes soh/assets/soh_assets.h | 6 - soh/include/z64.h | 4 + soh/include/z64save.h | 9 +- .../Enhancements/FileSelectEnhancements.cpp | 8 +- soh/soh/Enhancements/Presets/Presets.cpp | 86 ++- soh/soh/Enhancements/Presets/Presets.h | 13 + soh/soh/Enhancements/boss-rush/BossRush.cpp | 97 +--- .../cosmetics/FileSelectMoreInfo.cpp | 2 +- soh/soh/Enhancements/speedrun/Speedrun.cpp | 435 ++++++++++++++ soh/soh/Enhancements/speedrun/Speedrun.h | 38 ++ soh/soh/Enhancements/tts/tts.cpp | 10 + soh/soh/OTRGlobals.cpp | 3 + soh/soh/SaveManager.cpp | 39 +- soh/soh/SaveManager.h | 2 +- soh/soh/SohGui/Menu.cpp | 5 + soh/soh/SohGui/SohGui.cpp | 42 ++ soh/soh/SohGui/SohGui.hpp | 2 + soh/soh/SohGui/SohMenuEnhancements.cpp | 16 +- soh/soh/util.h | 8 +- soh/src/code/z_parameter.c | 2 + soh/src/code/z_sram.c | 11 +- .../gamestates/ovl_file_choose/file_choose.h | 24 +- .../ovl_file_choose/z_file_choose.c | 534 ++++++++++++------ .../ovl_file_choose/z_file_nameset_NES.c | 4 +- .../ovl_file_choose/z_file_nameset_PAL.c | 4 +- 33 files changed, 1480 insertions(+), 308 deletions(-) create mode 100644 soh/assets/custom/presets/Speedrun - 1 Base.json create mode 100644 soh/assets/custom/presets/Speedrun - 2 Faster Quest.json create mode 100644 soh/assets/custom/presets/Speedrun - 3 Faster Quest with Cutscenes.json delete mode 100644 soh/assets/custom/textures/title_static/gFileSelMQButtonTex.ia16.png delete mode 100644 soh/assets/custom/textures/title_static/gFileSelRANDButtonTex.ia16.png create mode 100644 soh/soh/Enhancements/speedrun/Speedrun.cpp create mode 100644 soh/soh/Enhancements/speedrun/Speedrun.h diff --git a/soh/assets/custom/accessibility/texts/filechoose_eng.json b/soh/assets/custom/accessibility/texts/filechoose_eng.json index c72e680205..6d4decfaaa 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 7fc37072f9..ef619090d2 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 b0fd8b8996..82c4c15f07 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 0000000000..a362300e5e --- /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 0000000000..70fbdfaa5f --- /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 0000000000..d1a2a94f76 --- /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 076b9365f2c790d9e9bac24124591ca71894fe29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 883 zcmV-(1C0EMP)vuTZw1`k|ronKMsd{uR-N&uLcm>6?9oqy)@ zdA_3E+}toaI{L%yc26mXhK9b+&(Hs*R4U2ka$geI>-A!_T7MG+0U;a?7tLlfvMjT( zuz*^vrn0iK^u6@eYIVt1DwW8xOfHvu^Z!WurS`I~P$;0$XwYi4Xti2&Ivs)_@bK`! z@$oSg$z&2qlJIyus8p&C&w6KPhpw)!H~P`h5%u-;)Ya9MwA0g5VzC&7LIIP>L~Cnn zSu1QX7+79jCY4H20g%mR%L-t#*~sVfSglr;mX=Do)9J+T_XDuIyNk=^!sGG$YmDLH zVVawpKb$j_N&!3pmt*O4n&-nA3WZ+PeIO7hwIxZy=ko#3)6;{);UJ&ScLS`iua{td-YHQOF&d4yTrR4st1+2O^!N9p zQmKeUBCpEk)4+*Df@m~KCX@M4fR&XMG#U+VxBHbY%Q65B4Glyh5sJkk0EI#Uy+==e>tya&i)Yjg1Wey1Tn+YH9-D+qZ83+~41$R4SR7nPF;bs&ozjv|8=219x_I z($Ud@PN&0QF#PA>v9U1#78e&w{_|=+LtR~6v9+~DdwV<4Xq4&cX(UNPp-|A+*!b(f zH!&KG1cN~w4hI&C1-spjEXxcI4zj(yO*kB;qN0LOC`2NWKomtTFE7gm7EVr1 ziU$V=h@wbMP0j!IH0$+xip3(COop|!H7phj0|NsD0)cm*=GE0z9v>f{fLmKzzxVa^ z{S9z+9VP)lW`cszSvotJ>Tb0--V}EYdwee>Hq%U|MM;ZpslU#kFKt+KCjoy zSG3d9Q(9VDzE4k2|0&kg)O;Hl82C#RMIIg=z9it;*;!IjQhpNz0paNA=t-l|;PH4E z85u#TR1y#nAU`jCrBW&TqA23=czAet@Ogja`(^B9?(y-Fpr9avgM$eU4n~qB1VO;* zbh5U#hX3W|C2qHynVA_B3dK)%X|Y(a+wCY63jF>35k(P=Mng(U$}eZsX0wr;oGkll zwHkxLfXn5=AK?D}{>=sl2M07ZHvTYIP*A|==qQJWhra~u)YKGKtCjirdCX?B9Cx`~ z0RIAh&n_-508pt^6c!c&;PrYj8jYAtCRSHhiI0zGWo3n(ogF+L4^dH3=xBL;Z*MOum5Rp3Mpjo>IX*td>2#t}sTdp_#B4Ux+1dFfV88zdlu9KL5fP-O zrVS`)0D}6Mbl$1nMQp49=rL?q^p`jrpNqU7vSYKa%T3lS@`udvi@bEABYaSmTmp3b< zr>B1_DJl6Iz+^J #include #include #include @@ -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 includeSections) { auto& info = presets[presetName]; bool randoApplied = false; @@ -126,18 +141,14 @@ void applyPreset(std::string presetName, std::vector 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 includeSecti OTRGlobals::Instance->ScaleImGui(); } +nlohmann::json applyPresetToBlocks(std::string presetName, nlohmann::json blocks, + std::vector 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> GetSpeedrunPresets() { + static const std::string prefix = "Speedrun - "; + std::vector> 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 includeSections, std::string presetLoc, bool disabled) { std::vector includedPresets; for (auto& [name, info] : presets) { diff --git a/soh/soh/Enhancements/Presets/Presets.h b/soh/soh/Enhancements/Presets/Presets.h index 5edb265a3e..5856a0b93d 100644 --- a/soh/soh/Enhancements/Presets/Presets.h +++ b/soh/soh/Enhancements/Presets/Presets.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include enum PresetSection { @@ -16,3 +18,14 @@ enum PresetSection { void DrawPresetSelector(std::vector includeSections, std::string currentIndex, bool disabled); void applyPreset(std::string presetName, std::vector 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 includeSections = {}); + +// {display name, preset name} of every preset whose file is named "Speedrun - " +std::vector> 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 b35c30b2bd..edaa5d1037 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(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(171 + finalKerning), static_cast(92 + textYOffset), 0.42f, 0, - 0, 1.0f, 1.0f); + FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, 160.0f, static_cast(92 + textYOffset), + true); + FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, static_cast(171 + finalKerning), + static_cast(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 59a76de139..6b80b567da 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 0000000000..68ae98be5e --- /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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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 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> 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 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 sRunToggleWindows = { "Time Splits", "Gameplay Stats" }; + +// Windows kept hidden while locked, and the ones that were open and get reopened on unlock. +static std::vector> sHiddenWindows; +static std::vector> 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 GetExemptPaths() { + std::vector paths; + + for (const auto& path : sExempt) { + try { + paths.emplace_back(path.get()); + } 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(92 + textYOffset), + true); + FileChoose_DrawListCursorArrow(fileChooseContext, textAlpha, static_cast(81 + finalKerning), + static_cast(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([](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([]() { + 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 0000000000..7780c7048b --- /dev/null +++ b/soh/soh/Enhancements/speedrun/Speedrun.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#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 226cebe876..34b90f8c51 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 34642e3c59..1b4f62c475 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 4f552f9711..f9464b5b72 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()) { 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(fileNum); diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 095cf7e6d8..03e9f68310 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 2c4407f335..642bc54a30 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 15db095397..986f93376e 100644 --- a/soh/soh/SohGui/SohGui.cpp +++ b/soh/soh/SohGui/SohGui.cpp @@ -120,6 +120,48 @@ std::shared_ptr GetSohMenu() { return mSohMenu; } +std::vector> 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(CVAR_WINDOW("Menu"), "Port Menu"); diff --git a/soh/soh/SohGui/SohGui.hpp b/soh/soh/SohGui/SohGui.hpp index c0cef1fa53..17aff40796 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 GetSohMenu(); +// Every window registered with the Gui, the ones LUS registers itself included. May contain nulls before setup. +std::vector> GetAllGuiWindows(); void DrawStartingItemsMenu(WidgetInfo& info); } // namespace SohGui diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index 3bf31116af..759e0605ab 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -181,10 +181,10 @@ static const std::map 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 0407d918ae..90fee99619 100644 --- a/soh/soh/util.h +++ b/soh/soh/util.h @@ -5,7 +5,13 @@ #include #include -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 8a4e00c207..524ecd89f0 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 @@ -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 1e0dffca60..6dd90496ee 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 @@ -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 5f6e3f202e..7f3734779d 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 266c0114ea..7149b09b6b 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 @@ -28,7 +29,7 @@ #include #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 d502446d9d..fb65a3b07f 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 ea489f57ff..3a8ab6f833 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; }