diff --git a/soh/include/functions.h b/soh/include/functions.h index 8f902c5a8..147500c66 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -1073,7 +1073,6 @@ uint16_t Interface_DrawTextLine(GraphicsContext* gfx, char text[], int16_t x, in u8 Item_Give(PlayState* play, u8 item); u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry); u8 Item_CheckObtainability(u8 item); -void PerformAutosave(PlayState* play, u8 item); void Inventory_DeleteItem(u16 item, u16 invSlot); s32 Inventory_ReplaceItem(PlayState* play, u16 oldItem, u16 newItem); s32 Inventory_HasEmptyBottle(void); diff --git a/soh/soh/Enhancements/Autosave.cpp b/soh/soh/Enhancements/Autosave.cpp new file mode 100644 index 000000000..74b548dab --- /dev/null +++ b/soh/soh/Enhancements/Autosave.cpp @@ -0,0 +1,78 @@ +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" +#include "soh/ShipInit.hpp" +#include "soh/SaveManager.h" + +extern "C" { +extern PlayState* gPlayState; +#include "functions.h" +#include "variables.h" +} + +static uint64_t lastSaveTimestamp = GetUnixTimestamp(); + +#define CVAR_AUTOSAVE_NAME CVAR_ENHANCEMENT("Autosave") +#define CVAR_AUTOSAVE_DEFAULT AUTOSAVE_OFF +#define CVAR_AUTOSAVE_VALUE CVarGetInteger(CVAR_AUTOSAVE_NAME, CVAR_AUTOSAVE_DEFAULT) +#define THREE_MINUTES_IN_UNIX 3 * 60000 + +typedef enum { + AUTOSAVE_OFF, + AUTOSAVE_ON, +} AutosaveOptions; + +bool Autosave_CanSave() { + + // Don't save when in title screen + // Don't save the first 60 frames to not save the magic meter when it's still in the animation of filling it. + // Don't save in Ganon's fight and chamber of sages because of master sword and remember save location issues. + if (!GameInteractor::IsSaveLoaded(true) || gPlayState->gameplayFrames < 60 || + gPlayState->sceneNum == SCENE_GANON_BOSS || gPlayState->sceneNum == SCENE_CHAMBER_OF_THE_SAGES) { + return false; + } + + return true; +} + +void Autosave_PerformSave() { + Play_PerformSave(gPlayState); + + // Send notification + Notification::Emit({ + .message = "Game autosaved", + }); +} + +void Autosave_IntervalSave() { + // Check if the interval has passed in minutes. + uint64_t currentTimestamp = GetUnixTimestamp(); + if ((currentTimestamp - lastSaveTimestamp) < THREE_MINUTES_IN_UNIX) { + return; + } + + // If save available to create, do it and reset the interval. + // Interval gets extra check for being paused to avoid rare issues like bypassing shop + // rupees draining after buying an item. Since the interval can just retry until it + // passes, it can use more conditions without hampering the player experience. + if (Autosave_CanSave() && !GameInteractor::IsGameplayPaused()) { + + // Reset timestamp, set icon timer to show autosave icon for 5 seconds (100 frames) + lastSaveTimestamp = currentTimestamp; + + Autosave_PerformSave(); + } +} + +void Autosave_SoftResetSave() { + if (Autosave_CanSave()) { + Autosave_PerformSave(); + } +} + +void RegisterAutosave() { + COND_HOOK(GameInteractor::OnGameFrameUpdate, CVAR_AUTOSAVE_VALUE, Autosave_IntervalSave); + COND_HOOK(GameInteractor::OnExitGame, CVAR_AUTOSAVE_VALUE, [](int32_t fileNum) { Autosave_SoftResetSave(); }); +} + +static RegisterShipInitFunc initFunc(RegisterAutosave, { CVAR_AUTOSAVE_NAME }); diff --git a/soh/soh/Enhancements/Autosave.h b/soh/soh/Enhancements/Autosave.h new file mode 100644 index 000000000..202fcde4f --- /dev/null +++ b/soh/soh/Enhancements/Autosave.h @@ -0,0 +1,4 @@ +typedef enum { + AUTOSAVE_OFF, + AUTOSAVE_ON, +} AutosaveOptions; diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index 7037f9b0a..5df00dfab 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -208,7 +208,6 @@ static bool ResetHandler(std::shared_ptr Console, std::vectorrunning = false; GameInteractor::Instance->ExecuteHooks(gSaveContext.fileNum); diff --git a/soh/soh/Enhancements/enhancementTypes.h b/soh/soh/Enhancements/enhancementTypes.h index 4b2f825f5..db96a7700 100644 --- a/soh/soh/Enhancements/enhancementTypes.h +++ b/soh/soh/Enhancements/enhancementTypes.h @@ -52,15 +52,6 @@ typedef enum { ENEMY_RANDOMIZER_RANDOM_SEEDED, } EnemyRandomizerMode; -typedef enum { - AUTOSAVE_OFF, - AUTOSAVE_LOCATION_AND_MAJOR_ITEMS, - AUTOSAVE_LOCATION_AND_ALL_ITEMS, - AUTOSAVE_LOCATION, - AUTOSAVE_MAJOR_ITEMS, - AUTOSAVE_ALL_ITEMS -} AutosaveType; - typedef enum { BOOTSEQUENCE_DEFAULT, BOOTSEQUENCE_AUTHENTIC, diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index bbfcf1052..e6777644e 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -135,87 +135,6 @@ void RegisterOcarinaTimeTravel() { }); } -void AutoSave(GetItemEntry itemEntry) { - u8 item = itemEntry.itemId; - bool performSave = false; - // Don't autosave immediately after buying items from shops to prevent getting them for free! - // Don't autosave in the Chamber of Sages since resuming from that map breaks the game - // Don't autosave during the Ganon fight when picking up the Master Sword - if ((CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) != AUTOSAVE_OFF) && (gPlayState != NULL) && (gSaveContext.ship.pendingSale == ITEM_NONE) && - (gPlayState->gameplayFrames > 60 && gSaveContext.cutsceneIndex < 0xFFF0) && (gPlayState->sceneNum != SCENE_GANON_BOSS) && (gPlayState->sceneNum != SCENE_CHAMBER_OF_THE_SAGES)) { - if (((CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_ALL_ITEMS) || (CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_ALL_ITEMS)) && (item != ITEM_NONE)) { - // Autosave for all items - performSave = true; - - } else if (((CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_MAJOR_ITEMS) || (CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_MAJOR_ITEMS)) && (item != ITEM_NONE)) { - // Autosave for major items - if (itemEntry.modIndex == 0) { - switch (item) { - case ITEM_STICK: - case ITEM_NUT: - case ITEM_BOMB: - case ITEM_BOW: - case ITEM_SEEDS: - case ITEM_FISHING_POLE: - case ITEM_MAGIC_SMALL: - case ITEM_MAGIC_LARGE: - case ITEM_INVALID_4: - case ITEM_INVALID_5: - case ITEM_INVALID_6: - case ITEM_INVALID_7: - case ITEM_HEART: - case ITEM_RUPEE_GREEN: - case ITEM_RUPEE_BLUE: - case ITEM_RUPEE_RED: - case ITEM_RUPEE_PURPLE: - case ITEM_RUPEE_GOLD: - case ITEM_INVALID_8: - case ITEM_STICKS_5: - case ITEM_STICKS_10: - case ITEM_NUTS_5: - case ITEM_NUTS_10: - case ITEM_BOMBS_5: - case ITEM_BOMBS_10: - case ITEM_BOMBS_20: - case ITEM_BOMBS_30: - case ITEM_ARROWS_SMALL: - case ITEM_ARROWS_MEDIUM: - case ITEM_ARROWS_LARGE: - case ITEM_SEEDS_30: - case ITEM_NONE: - break; - case ITEM_BOMBCHU: - case ITEM_BOMBCHUS_5: - case ITEM_BOMBCHUS_20: - if (!CVarGetInteger(CVAR_ENHANCEMENT("EnableBombchuDrops"), 0)) { - performSave = true; - } - break; - default: - performSave = true; - break; - } - } else if (itemEntry.modIndex == 1 && item != RG_ICE_TRAP) { - performSave = true; - } - } else if (CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_MAJOR_ITEMS || - CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_ALL_ITEMS || - CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) == AUTOSAVE_LOCATION) { - performSave = true; - } - if (performSave) { - Play_PerformSave(gPlayState); - performSave = false; - } - } -} - -void RegisterAutoSave() { - GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { AutoSave(itemEntry); }); - GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { AutoSave(itemEntry); }); - GameInteractor::Instance->RegisterGameHook([](int32_t sceneNum) { AutoSave(GET_ITEM_NONE); }); -} - void RegisterRupeeDash() { GameInteractor::Instance->RegisterGameHook([]() { if (!CVarGetInteger(CVAR_ENHANCEMENT("RupeeDash"), 0)) { @@ -1168,7 +1087,6 @@ void InitMods() { TimeSavers_Register(); RegisterTTS(); RegisterOcarinaTimeTravel(); - RegisterAutoSave(); RegisterDaytimeGoldSkultullas(); RegisterRupeeDash(); RegisterShadowTag(); diff --git a/soh/soh/Enhancements/presets.h b/soh/soh/Enhancements/presets.h index d61ff6ade..83b0e94d5 100644 --- a/soh/soh/Enhancements/presets.h +++ b/soh/soh/Enhancements/presets.h @@ -157,7 +157,6 @@ const std::vector enhancementsCvars = { CVAR_ENHANCEMENT("GSCutscene"), CVAR_ENHANCEMENT("RestoreRBAValues"), CVAR_ENHANCEMENT("SkipSaveConfirmation"), - CVAR_ENHANCEMENT("Autosave"), CVAR_ENHANCEMENT("DisableCritWiggle"), CVAR_ENHANCEMENT("ChestSizeDependsStoneOfAgony"), CVAR_ENHANCEMENT("SkipArrowAnimation"), @@ -754,7 +753,7 @@ const std::vector enhancedPresetEntries = { PRESET_ENTRY_S32(CVAR_ENHANCEMENT("AnubisFix"), 1), // Autosave - PRESET_ENTRY_S32(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_LOCATION_AND_MAJOR_ITEMS), + PRESET_ENTRY_S32(CVAR_ENHANCEMENT("Autosave"), 1), // Bombchu shop doesn't sell out, and 10 bombchus cost 99 instead of 100 PRESET_ENTRY_S32(CVAR_ENHANCEMENT("BetterBombchuShopping"), 1), @@ -887,7 +886,7 @@ const std::vector randomizerPresetEntries = { PRESET_ENTRY_S32(CVAR_ENHANCEMENT("AnubisFix"), 1), // Autosave - PRESET_ENTRY_S32(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_LOCATION_AND_MAJOR_ITEMS), + PRESET_ENTRY_S32(CVAR_ENHANCEMENT("Autosave"), 1), // Customize Fishing Behaviour PRESET_ENTRY_S32(CVAR_ENHANCEMENT("CustomizeFishing"), 1), diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index d1ca49162..44831c7c0 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -39,6 +39,7 @@ #include "soh/util.h" #include "fishsanity.h" #include "randomizerTypes.h" +#include "soh/Notification/Notification.h" extern std::map rcAreaNames; @@ -4105,6 +4106,9 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { gSaveContext.ship.stats.gameComplete = 1; Flags_SetRandomizerInf(RAND_INF_GRANT_GANONS_BOSSKEY); Play_PerformSave(play); + Notification::Emit({ + .message = "Game autosaved", + }); GameInteractor_SetTriforceHuntCreditsWarpActive(true); } diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index a84922ba7..e3b07b672 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -1382,7 +1382,9 @@ void UpdateAllAreas() { } void UpdateAreas(RandomizerCheckArea area) { - areasFullyChecked[area] = areaChecksGotten[area] == checksByArea.find(area)->second.size(); + if (checksByArea.contains(area)) { + areasFullyChecked[area] = areaChecksGotten[area] == checksByArea.find(area)->second.size(); + } } void UpdateAllOrdering() { diff --git a/soh/soh/SohGui/SohGui.cpp b/soh/soh/SohGui/SohGui.cpp index 84a44b052..8d7a4ba45 100644 --- a/soh/soh/SohGui/SohGui.cpp +++ b/soh/soh/SohGui/SohGui.cpp @@ -64,7 +64,6 @@ namespace SohGui { static const char* subPowers[8] = { allPowers[0], allPowers[1], allPowers[2], allPowers[3], allPowers[4], allPowers[5], allPowers[6], allPowers[7] }; static const char* subSubPowers[7] = { allPowers[0], allPowers[1], allPowers[2], allPowers[3], allPowers[4], allPowers[5], allPowers[6] }; static const char* zFightingOptions[3] = { "Disabled", "Consistent Vanish", "No Vanish" }; - static const char* autosaveLabels[6] = { "Off", "New Location + Major Item", "New Location + Any Item", "New Location", "Major Item", "Any Item" }; static const char* bonkDamageValues[8] = { "No Damage", "0.25 Heart", diff --git a/soh/soh/SohGui/SohMenuBar.cpp b/soh/soh/SohGui/SohMenuBar.cpp index e6337dae6..bad852f7a 100644 --- a/soh/soh/SohGui/SohMenuBar.cpp +++ b/soh/soh/SohGui/SohMenuBar.cpp @@ -45,6 +45,7 @@ #include "soh/Enhancements/randomizer/Plandomizer.h" #include "soh/Enhancements/TimeDisplay/TimeDisplay.h" #include "soh/AboutWindow.h" +#include "soh/Enhancements/Autosave.h" // FA icons are kind of wonky, if they worked how I expected them to the "+ 2.0f" wouldn't be needed, but // they don't work how I expect them to so I added that because it looked good when I eyeballed it @@ -104,7 +105,6 @@ static const char* imguiScaleOptions[4] = { "Small", "Normal", "Large", "X-Large static const char* subPowers[8] = { allPowers[0], allPowers[1], allPowers[2], allPowers[3], allPowers[4], allPowers[5], allPowers[6], allPowers[7] }; static const char* subSubPowers[7] = { allPowers[0], allPowers[1], allPowers[2], allPowers[3], allPowers[4], allPowers[5], allPowers[6] }; static const char* zFightingOptions[3] = { "Disabled", "Consistent Vanish", "No Vanish" }; - static const char* autosaveLabels[6] = { "Off", "New Location + Major Item", "New Location + Any Item", "New Location", "Major Item", "Any Item" }; static const char* bootSequenceLabels[3] = { "Default", "Authentic", "File Select" }; static const char* DebugSaveFileModes[3] = { "Off", "Vanilla", "Maxed" }; static const char* DekuStickCheat[3] = { "Normal", "Unbreakable", "Unbreakable + Always on Fire" }; @@ -1569,13 +1569,11 @@ void DrawEnhancementsMenu() { ImGui::EndMenu(); } - UIWidgets::PaddedSeparator(false, true); + UIWidgets::PaddedSeparator(); - // Autosave enum value of 1 is the default in presets and the old checkbox "on" state for backwards compatibility - UIWidgets::PaddedText("Autosave", false, true); - UIWidgets::EnhancementCombobox(CVAR_ENHANCEMENT("Autosave"), autosaveLabels, AUTOSAVE_OFF); - UIWidgets::Tooltip("Automatically save the game when changing locations and/or obtaining items\n" - "Major items exclude rupees and health/magic/ammo refills (but include bombchus unless bombchu drops are enabled)"); + UIWidgets::EnhancementCheckbox("Autosave", CVAR_ENHANCEMENT("Autosave")); + UIWidgets::Tooltip("Save the game automatically on a 3 minute interval and when soft-resetting the game.\n\n" + "The interval autosave will wait if the game is paused in any way (dialogue, pause screen up, cutscenes)."); UIWidgets::PaddedSeparator(true, true, 2.0f, 2.0f); diff --git a/soh/soh/config/ConfigMigrators.h b/soh/soh/config/ConfigMigrators.h index 3a566f493..29cbb0774 100644 --- a/soh/soh/config/ConfigMigrators.h +++ b/soh/soh/config/ConfigMigrators.h @@ -145,7 +145,6 @@ namespace SOH { { MigrationAction::Rename, "gAskToEquip", "gEnhancements.AskToEquip" }, { MigrationAction::Rename, "gAssignableTunicsAndBoots", "gEnhancements.AssignableTunicsAndBoots" }, { MigrationAction::Rename, "gAuthenticLogo", "gEnhancements.AuthenticLogo" }, - { MigrationAction::Rename, "gAutosave", "gEnhancements.Autosave" }, { MigrationAction::Rename, "gBetterFW", "gEnhancements.BetterFarore" }, { MigrationAction::Rename, "gBetterOwl", "gEnhancements.BetterOwl" }, { MigrationAction::Rename, "gBlueFireArrows", "gEnhancements.BlueFireArrows" }, diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 0b6db75b1..115369171 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -2206,13 +2206,5 @@ void Play_PerformSave(PlayState* play) { // Restore temp B values back gSaveContext.equips.buttonItems[0] = prevB; gSaveContext.buttonStatus[0] = prevStatus; - - uint8_t triforceHuntCompleted = - IS_RANDO && - gSaveContext.ship.quest.data.randomizer.triforcePiecesCollected == (Randomizer_GetSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED) + 1) && - Randomizer_GetSettingValue(RSK_TRIFORCE_HUNT); - if (CVarGetInteger(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_OFF) != AUTOSAVE_OFF || triforceHuntCompleted) { - Overlay_DisplayText(3.0f, "Game Saved"); - } } }