Autosave Overhaul (#5022)

* Autosave interval based

* Move to save on soft reset, remove adjustable interval

* Use new BeforeExitGame hook to prevent non-existent data problems

* Fix check tracker crash, remove BeforeExitGame hook

* update comment
This commit is contained in:
aMannus 2025-02-14 21:29:22 +01:00 committed by GitHub
parent dbf7fcf775
commit 668040562f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 96 additions and 114 deletions

View File

@ -1073,7 +1073,6 @@ uint16_t Interface_DrawTextLine(GraphicsContext* gfx, char text[], int16_t x, in
u8 Item_Give(PlayState* play, u8 item); u8 Item_Give(PlayState* play, u8 item);
u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry); u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry);
u8 Item_CheckObtainability(u8 item); u8 Item_CheckObtainability(u8 item);
void PerformAutosave(PlayState* play, u8 item);
void Inventory_DeleteItem(u16 item, u16 invSlot); void Inventory_DeleteItem(u16 item, u16 invSlot);
s32 Inventory_ReplaceItem(PlayState* play, u16 oldItem, u16 newItem); s32 Inventory_ReplaceItem(PlayState* play, u16 oldItem, u16 newItem);
s32 Inventory_HasEmptyBottle(void); s32 Inventory_HasEmptyBottle(void);

View File

@ -0,0 +1,78 @@
#include <libultraship/bridge.h>
#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 });

View File

@ -0,0 +1,4 @@
typedef enum {
AUTOSAVE_OFF,
AUTOSAVE_ON,
} AutosaveOptions;

View File

@ -208,7 +208,6 @@ static bool ResetHandler(std::shared_ptr<Ship::Console> Console, std::vector<std
ERROR_MESSAGE("gGameState == nullptr"); ERROR_MESSAGE("gGameState == nullptr");
return 1; return 1;
} }
SET_NEXT_GAMESTATE(gGameState, TitleSetup_Init, GameState); SET_NEXT_GAMESTATE(gGameState, TitleSetup_Init, GameState);
gGameState->running = false; gGameState->running = false;
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnExitGame>(gSaveContext.fileNum); GameInteractor::Instance->ExecuteHooks<GameInteractor::OnExitGame>(gSaveContext.fileNum);

View File

@ -52,15 +52,6 @@ typedef enum {
ENEMY_RANDOMIZER_RANDOM_SEEDED, ENEMY_RANDOMIZER_RANDOM_SEEDED,
} EnemyRandomizerMode; } 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 { typedef enum {
BOOTSEQUENCE_DEFAULT, BOOTSEQUENCE_DEFAULT,
BOOTSEQUENCE_AUTHENTIC, BOOTSEQUENCE_AUTHENTIC,

View File

@ -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<GameInteractor::OnItemReceive>([](GetItemEntry itemEntry) { AutoSave(itemEntry); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSaleEnd>([](GetItemEntry itemEntry) { AutoSave(itemEntry); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnTransitionEnd>([](int32_t sceneNum) { AutoSave(GET_ITEM_NONE); });
}
void RegisterRupeeDash() { void RegisterRupeeDash() {
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPlayerUpdate>([]() { GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPlayerUpdate>([]() {
if (!CVarGetInteger(CVAR_ENHANCEMENT("RupeeDash"), 0)) { if (!CVarGetInteger(CVAR_ENHANCEMENT("RupeeDash"), 0)) {
@ -1168,7 +1087,6 @@ void InitMods() {
TimeSavers_Register(); TimeSavers_Register();
RegisterTTS(); RegisterTTS();
RegisterOcarinaTimeTravel(); RegisterOcarinaTimeTravel();
RegisterAutoSave();
RegisterDaytimeGoldSkultullas(); RegisterDaytimeGoldSkultullas();
RegisterRupeeDash(); RegisterRupeeDash();
RegisterShadowTag(); RegisterShadowTag();

View File

@ -157,7 +157,6 @@ const std::vector<const char*> enhancementsCvars = {
CVAR_ENHANCEMENT("GSCutscene"), CVAR_ENHANCEMENT("GSCutscene"),
CVAR_ENHANCEMENT("RestoreRBAValues"), CVAR_ENHANCEMENT("RestoreRBAValues"),
CVAR_ENHANCEMENT("SkipSaveConfirmation"), CVAR_ENHANCEMENT("SkipSaveConfirmation"),
CVAR_ENHANCEMENT("Autosave"),
CVAR_ENHANCEMENT("DisableCritWiggle"), CVAR_ENHANCEMENT("DisableCritWiggle"),
CVAR_ENHANCEMENT("ChestSizeDependsStoneOfAgony"), CVAR_ENHANCEMENT("ChestSizeDependsStoneOfAgony"),
CVAR_ENHANCEMENT("SkipArrowAnimation"), CVAR_ENHANCEMENT("SkipArrowAnimation"),
@ -754,7 +753,7 @@ const std::vector<PresetEntry> enhancedPresetEntries = {
PRESET_ENTRY_S32(CVAR_ENHANCEMENT("AnubisFix"), 1), PRESET_ENTRY_S32(CVAR_ENHANCEMENT("AnubisFix"), 1),
// Autosave // 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 // Bombchu shop doesn't sell out, and 10 bombchus cost 99 instead of 100
PRESET_ENTRY_S32(CVAR_ENHANCEMENT("BetterBombchuShopping"), 1), PRESET_ENTRY_S32(CVAR_ENHANCEMENT("BetterBombchuShopping"), 1),
@ -887,7 +886,7 @@ const std::vector<PresetEntry> randomizerPresetEntries = {
PRESET_ENTRY_S32(CVAR_ENHANCEMENT("AnubisFix"), 1), PRESET_ENTRY_S32(CVAR_ENHANCEMENT("AnubisFix"), 1),
// Autosave // Autosave
PRESET_ENTRY_S32(CVAR_ENHANCEMENT("Autosave"), AUTOSAVE_LOCATION_AND_MAJOR_ITEMS), PRESET_ENTRY_S32(CVAR_ENHANCEMENT("Autosave"), 1),
// Customize Fishing Behaviour // Customize Fishing Behaviour
PRESET_ENTRY_S32(CVAR_ENHANCEMENT("CustomizeFishing"), 1), PRESET_ENTRY_S32(CVAR_ENHANCEMENT("CustomizeFishing"), 1),

View File

@ -39,6 +39,7 @@
#include "soh/util.h" #include "soh/util.h"
#include "fishsanity.h" #include "fishsanity.h"
#include "randomizerTypes.h" #include "randomizerTypes.h"
#include "soh/Notification/Notification.h"
extern std::map<RandomizerCheckArea, std::string> rcAreaNames; extern std::map<RandomizerCheckArea, std::string> rcAreaNames;
@ -4105,6 +4106,9 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) {
gSaveContext.ship.stats.gameComplete = 1; gSaveContext.ship.stats.gameComplete = 1;
Flags_SetRandomizerInf(RAND_INF_GRANT_GANONS_BOSSKEY); Flags_SetRandomizerInf(RAND_INF_GRANT_GANONS_BOSSKEY);
Play_PerformSave(play); Play_PerformSave(play);
Notification::Emit({
.message = "Game autosaved",
});
GameInteractor_SetTriforceHuntCreditsWarpActive(true); GameInteractor_SetTriforceHuntCreditsWarpActive(true);
} }

View File

@ -1382,8 +1382,10 @@ void UpdateAllAreas() {
} }
void UpdateAreas(RandomizerCheckArea area) { void UpdateAreas(RandomizerCheckArea area) {
if (checksByArea.contains(area)) {
areasFullyChecked[area] = areaChecksGotten[area] == checksByArea.find(area)->second.size(); areasFullyChecked[area] = areaChecksGotten[area] == checksByArea.find(area)->second.size();
} }
}
void UpdateAllOrdering() { void UpdateAllOrdering() {
// Sort the entire thing // Sort the entire thing

View File

@ -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* 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* 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* 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] = { static const char* bonkDamageValues[8] = {
"No Damage", "No Damage",
"0.25 Heart", "0.25 Heart",

View File

@ -45,6 +45,7 @@
#include "soh/Enhancements/randomizer/Plandomizer.h" #include "soh/Enhancements/randomizer/Plandomizer.h"
#include "soh/Enhancements/TimeDisplay/TimeDisplay.h" #include "soh/Enhancements/TimeDisplay/TimeDisplay.h"
#include "soh/AboutWindow.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 // 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 // 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* 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* 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* 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* bootSequenceLabels[3] = { "Default", "Authentic", "File Select" };
static const char* DebugSaveFileModes[3] = { "Off", "Vanilla", "Maxed" }; static const char* DebugSaveFileModes[3] = { "Off", "Vanilla", "Maxed" };
static const char* DekuStickCheat[3] = { "Normal", "Unbreakable", "Unbreakable + Always on Fire" }; static const char* DekuStickCheat[3] = { "Normal", "Unbreakable", "Unbreakable + Always on Fire" };
@ -1569,13 +1569,11 @@ void DrawEnhancementsMenu() {
ImGui::EndMenu(); 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::EnhancementCheckbox("Autosave", CVAR_ENHANCEMENT("Autosave"));
UIWidgets::PaddedText("Autosave", false, true); UIWidgets::Tooltip("Save the game automatically on a 3 minute interval and when soft-resetting the game.\n\n"
UIWidgets::EnhancementCombobox(CVAR_ENHANCEMENT("Autosave"), autosaveLabels, AUTOSAVE_OFF); "The interval autosave will wait if the game is paused in any way (dialogue, pause screen up, cutscenes).");
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::PaddedSeparator(true, true, 2.0f, 2.0f); UIWidgets::PaddedSeparator(true, true, 2.0f, 2.0f);

View File

@ -145,7 +145,6 @@ namespace SOH {
{ MigrationAction::Rename, "gAskToEquip", "gEnhancements.AskToEquip" }, { MigrationAction::Rename, "gAskToEquip", "gEnhancements.AskToEquip" },
{ MigrationAction::Rename, "gAssignableTunicsAndBoots", "gEnhancements.AssignableTunicsAndBoots" }, { MigrationAction::Rename, "gAssignableTunicsAndBoots", "gEnhancements.AssignableTunicsAndBoots" },
{ MigrationAction::Rename, "gAuthenticLogo", "gEnhancements.AuthenticLogo" }, { MigrationAction::Rename, "gAuthenticLogo", "gEnhancements.AuthenticLogo" },
{ MigrationAction::Rename, "gAutosave", "gEnhancements.Autosave" },
{ MigrationAction::Rename, "gBetterFW", "gEnhancements.BetterFarore" }, { MigrationAction::Rename, "gBetterFW", "gEnhancements.BetterFarore" },
{ MigrationAction::Rename, "gBetterOwl", "gEnhancements.BetterOwl" }, { MigrationAction::Rename, "gBetterOwl", "gEnhancements.BetterOwl" },
{ MigrationAction::Rename, "gBlueFireArrows", "gEnhancements.BlueFireArrows" }, { MigrationAction::Rename, "gBlueFireArrows", "gEnhancements.BlueFireArrows" },

View File

@ -2206,13 +2206,5 @@ void Play_PerformSave(PlayState* play) {
// Restore temp B values back // Restore temp B values back
gSaveContext.equips.buttonItems[0] = prevB; gSaveContext.equips.buttonItems[0] = prevB;
gSaveContext.buttonStatus[0] = prevStatus; 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");
}
} }
} }