Address PR comments (android-backup-windows)

Refactor backup timer code
This commit is contained in:
SpectralFlame 2023-07-06 14:12:32 -05:00 committed by cyip92
parent 45473a25b4
commit c56fd9c99d
4 changed files with 94 additions and 91 deletions

View File

@ -17,7 +17,6 @@ export default {
data() {
return {
currTime: 0,
untilNextSave: 0,
};
},
computed: {
@ -64,19 +63,15 @@ export default {
}
},
lastSaved() {
const lastSave = GameStorage.backupTimeData[this.slotData.id]?.last;
const lastSave = GameStorage.lastBackupTimes[this.slotData.id]?.date ?? 0;
return lastSave
? `Last saved: ${TimeSpan.fromMilliseconds(this.currTime - lastSave)} ago`
: "Slot not currently in use";
},
nextSave() {
return `Next save in ${TimeSpan.fromMilliseconds(this.untilNextSave)}`;
}
},
methods: {
update() {
this.currTime = Date.now();
this.untilNextSave = GameStorage.timeUntilNextSave(this.slotData.id);
},
load() {
if (!this.save) return;
@ -94,10 +89,10 @@ export default {
GameStorage.oldBackupTimer = player.backupTimer;
GameStorage.loadPlayerObject(toLoad);
GameUI.notify.info(`Game loaded from backup slot #${this.slotData.id}`);
GameStorage.processLocalBackups();
GameStorage.loadBackupTimes();
GameStorage.ignoreBackupTimer = false;
GameStorage.offlineEnabled = undefined;
player.backupTimer = Math.max(GameStorage.oldBackupTimer, player.backupTimer);
GameStorage.resetBackupTimer();
GameStorage.save(true);
},
},
@ -110,12 +105,6 @@ export default {
<span>{{ progressStr }}</span>
<span>
{{ slotType }}
<span
v-if="untilNextSave > 0"
:ach-tooltip="nextSave"
>
<i class="fas fa-question-circle" />
</span>
</span>
<span class="c-fixed-height">{{ lastSaved }}</span>
<PrimaryButton

View File

@ -31,7 +31,7 @@ export default {
},
methods: {
update() {
this.nextSave = GameStorage.nextBackup;
this.nextSave = GameStorage.lastBackupTimes.map(t => t && t.backupTimer).sum();
this.ignoreOffline = player.options.loadBackupWithoutOffline;
},
offlineOptionClass() {
@ -62,8 +62,10 @@ export default {
<template #header>
Automatic Backup Saves
</template>
<div class="c-info">
<div class="c-info c-modal--short">
The game makes automatic backups based on time you have spent online or offline.
Timers for online backups only run when the game is open, and offline backups only save to the slot
with the longest applicable timer.
Additionally, your current save is saved into the last slot any time a backup from here is loaded.
<div
class="c-modal__confirmation-toggle"
@ -114,6 +116,20 @@ export default {
<style scoped>
.c-info {
width: 60rem;
overflow-x: hidden;
padding-right: 1rem;
}
.c-info::-webkit-scrollbar {
width: 1rem;
}
.c-info::-webkit-scrollbar-thumb {
border: none;
}
.s-base--metro .c-info::-webkit-scrollbar-thumb {
border-radius: 0;
}
.c-backup-file-ops {

View File

@ -57,14 +57,14 @@ export const GameIntervals = (function() {
save: interval(() => GameStorage.save(), () =>
player.options.autosaveInterval - Math.clampMin(0, Date.now() - GameStorage.lastSaveTime)
),
backup: interval(() => GameStorage.backupOnlineSlots(), () =>
Math.clampMin(0, GameStorage.nextBackup - player.backupTimer)
),
checkCloudSave: interval(() => {
if (player.options.cloudEnabled && Cloud.loggedIn) Cloud.saveCheck();
}, 600 * 1000),
randomSecretAchievement: interval(() => {
// This simplifies auto-backup code to check every second instead of dynamically stopping and
// restarting the interval every save operation, and is how it's structured on Android as well
checkEverySecond: interval(() => {
if (Math.random() < 0.00001) SecretAchievement(18).unlock();
GameStorage.tryOnlineBackups();
}, 1000),
checkForUpdates: interval(() => {
if (isLocalEnvironment()) return;

View File

@ -76,14 +76,9 @@ export const GameStorage = {
offlineEnabled: undefined,
offlineTicks: undefined,
lastUpdateOnLoad: 0,
shortestOnlineInterval: 1000 * AutoBackupSlots
.filter(slot => slot.type === BACKUP_SLOT_TYPE.ONLINE)
.map(slot => slot.interval)
.min(),
nextBackup: 0,
backupTimeData: {},
ignoreBackupTimer: true,
lastBackupTimes: [],
oldBackupTimer: 0,
ignoreBackupTimer: true,
maxOfflineTicks(simulatedMs, defaultTicks = this.offlineTicks) {
return Math.clampMax(defaultTicks, Math.floor(simulatedMs / 33));
@ -125,7 +120,8 @@ export const GameStorage = {
};
this.currentSlot = 0;
this.loadPlayerObject(root);
this.processLocalBackups();
this.loadBackupTimes();
this.backupOfflineSlots();
this.save(true);
return;
}
@ -133,7 +129,8 @@ export const GameStorage = {
this.saves = root.saves;
this.currentSlot = root.current;
this.loadPlayerObject(this.saves[this.currentSlot]);
this.processLocalBackups();
this.loadBackupTimes();
this.backupOfflineSlots();
},
loadSlot(slot) {
@ -141,7 +138,8 @@ export const GameStorage = {
// Save current slot to make sure no changes are lost
this.save(true);
this.loadPlayerObject(this.saves[slot] ?? Player.defaultStart);
this.processLocalBackups();
this.loadBackupTimes();
this.backupOfflineSlots();
Tabs.all.find(t => t.id === player.options.lastOpenTab).show(false);
Modal.hideAll();
Cloud.resetTempState();
@ -167,12 +165,7 @@ export const GameStorage = {
if (player.speedrun?.isActive) Speedrun.setSegmented(true);
this.save(true);
Cloud.resetTempState();
// If we don't advance the backup timer when loading saves with a much lower one, this causes backups to be
// effectively disabled until the old timer is reached again
const largestBackupTimer = Object.values(GameStorage.backupTimeData).map(x => x.timer ?? 0).max();
player.backupTimer = Math.max(this.oldBackupTimer, player.backupTimer, largestBackupTimer);
this.resetBackupInterval();
this.resetBackupTimer();
// This is to fix a very specific exploit: When the game is ending, some tabs get hidden
// The options tab is the first one of those, which makes the player redirect to the Pelle tab
@ -244,10 +237,17 @@ export const GameStorage = {
${invalidProps.join(", ")}`;
},
// A few things in the current game state can prevent saving, which we want to do for all forms of saving
canSave() {
const isSelectingGlyph = GlyphSelection.active;
const isSimulating = ui.$viewModel.modal.progressBar !== undefined;
const isEnd = (GameEnd.endState >= END_STATE_MARKERS.SAVE_DISABLED && !GameEnd.removeAdditionalEnd) ||
GameEnd.endState >= END_STATE_MARKERS.INTERACTIVITY_DISABLED;
return !isEnd && !(isSelectingGlyph || isSimulating);
},
save(silent = true, manual = false) {
if (GameEnd.endState >= END_STATE_MARKERS.SAVE_DISABLED && !GameEnd.removeAdditionalEnd) return;
if (GameEnd.endState >= END_STATE_MARKERS.INTERACTIVITY_DISABLED) return;
if (GlyphSelection.active || ui.$viewModel.modal.progressBar !== undefined) return;
if (!this.canSave()) return;
this.lastSaveTime = Date.now();
GameIntervals.save.restart();
if (manual && ++this.saved > 99) SecretAchievement(12).unlock();
@ -260,13 +260,14 @@ export const GameStorage = {
},
// Saves a backup, updates save timers (this is called before nextBackup is updated), and then saves the timers too
saveToBackup(backupSlot, saveTime) {
saveToBackup(backupSlot, backupTimer) {
if (!this.canSave()) return;
localStorage.setItem(this.backupDataKey(this.currentSlot, backupSlot), GameSaveSerializer.serialize(player));
this.backupTimeData[backupSlot] = {
timer: this.nextBackup,
last: saveTime,
this.lastBackupTimes[backupSlot] = {
backupTimer,
date: Date.now(),
};
localStorage.setItem(this.backupTimeKey(this.currentSlot), GameSaveSerializer.serialize(this.backupTimeData));
localStorage.setItem(this.backupTimeKey(this.currentSlot), GameSaveSerializer.serialize(this.lastBackupTimes));
},
// Does not actually load, but returns an object which is meant to be passed on to loadPlayerObject()
@ -275,69 +276,66 @@ export const GameStorage = {
return GameSaveSerializer.deserialize(data);
},
// This is only ever called directly after the player object is loaded
processLocalBackups() {
// Set the next backup timer to whatever the next multiple of the shortest online interval is
this.nextBackup = Math.ceil(player.backupTimer / this.shortestOnlineInterval) * this.shortestOnlineInterval;
GameIntervals.backup.restart();
// Check for the amount of time spent offline and perform immediate backups for any slots
// which have had more than their timers elapse since the last time the game was open and saved
// Check for the amount of time spent offline and perform an immediate backup for the longest applicable slot
// which has had more than its timer elapse since the last time the game was open and saved
backupOfflineSlots() {
const currentTime = Date.now();
const offlineTimeMs = currentTime - this.lastUpdateOnLoad;
for (const backupInfo of AutoBackupSlots.filter(slot => slot.type === BACKUP_SLOT_TYPE.OFFLINE)) {
if (offlineTimeMs < 1000 * backupInfo.interval) continue;
const id = backupInfo.id;
this.saveToBackup(id, currentTime);
const offlineSlots = AutoBackupSlots
.filter(slot => slot.type === BACKUP_SLOT_TYPE.OFFLINE)
.sort((a, b) => b.interval - a.interval);
for (const backupInfo of offlineSlots) {
if (offlineTimeMs > 1000 * backupInfo.interval) {
this.saveToBackup(backupInfo.id, player.backupTimer);
break;
}
}
},
// Load in all the data from previous backup times
this.backupTimeData = GameSaveSerializer.deserialize(localStorage.getItem(this.backupTimeKey(this.currentSlot)));
if (!this.backupTimeData) this.backupTimeData = {};
backupOnlineSlots(slotsToBackup) {
const currentTime = player.backupTimer;
for (const slot of slotsToBackup) this.saveToBackup(slot, currentTime);
},
// Loads in all the data from previous backup times in localStorage
loadBackupTimes() {
this.lastBackupTimes = GameSaveSerializer.deserialize(localStorage.getItem(this.backupTimeKey(this.currentSlot)));
if (!this.lastBackupTimes) this.lastBackupTimes = {};
for (const backupInfo of AutoBackupSlots) {
const key = backupInfo.id;
if (!this.backupTimeData[key]) this.backupTimeData[key] = {};
if (!this.lastBackupTimes[key]) {
this.lastBackupTimes[key] = {
backupTimer: 0,
date: 0,
};
}
}
},
// Used for both checking if a backup should be done, and in the UI to tell the player how long until the next backup
timeUntilNextSave(slotID) {
const entry = AutoBackupSlots.find(slot => slot.id === slotID);
if (entry.type !== BACKUP_SLOT_TYPE.ONLINE) return 0;
const timeSinceLast = player.backupTimer - (this.backupTimeData[slotID]?.timer ?? 0);
const totalInterval = 1000 * entry.interval;
// When loading from the reserve slot, all the timers get screwed up relative to backupTimer and may otherwise give
// times which are longer than the actual saving interval. Using mod doesn't necessarily properly "fix" them, but it
// at least ensures it's less than the interval
return (totalInterval - timeSinceLast) % totalInterval;
},
// Combining all the backup slots into a single call like this only works because all the longer intervals
// are divisible by the shortest one. We want to make sure we pass the same timestamp into saving calls on
// all slots, or else the displayed times will gradually desync due to the saving process itself taking time.
backupOnlineSlots() {
const currentTime = Date.now();
// This is checked in the checkEverySecond game interval. Determining which slots to save has a 800ms grace time to
// account for delays occurring from the saving operation itself; without this, the timer slips backwards by a second
// every time it saves
tryOnlineBackups() {
const toBackup = [];
for (const backupInfo of AutoBackupSlots.filter(slot => slot.type === BACKUP_SLOT_TYPE.ONLINE)) {
// This may get called during player object loading, before the times are properly loaded in
if (!this.backupTimeData) break;
const id = backupInfo.id;
if (this.timeUntilNextSave(id) <= 0) this.saveToBackup(id, currentTime);
const timeSinceLast = player.backupTimer - (this.lastBackupTimes[id]?.backupTimer ?? 0);
if (1000 * backupInfo.interval - timeSinceLast <= 800) toBackup.push(id);
}
this.resetBackupInterval();
this.backupOnlineSlots(toBackup);
},
// Set the next backup time, but make sure to skip forward an appropriate amount if a load or import happened,
// since these may cause the backup timer to be significantly behind
resetBackupInterval() {
const largestBackupTimer = Object.values(GameStorage.backupTimeData).map(x => x.timer ?? 0).max();
this.nextBackup = Math.max(this.nextBackup, player.backupTimer, largestBackupTimer) + this.shortestOnlineInterval;
GameIntervals.backup.restart();
resetBackupTimer() {
const latestBackupTime = this.lastBackupTimes.map(t => t && t.backupTimer).max();
player.backupTimer = Math.max(this.oldBackupTimer, player.backupTimer, latestBackupTime);
},
// Saves the current game state to the first reserve slot it finds
saveToReserveSlot() {
const targetSlot = AutoBackupSlots.find(slot => slot.type === 2).id;
this.saveToBackup(targetSlot, Date.now());
const targetSlot = AutoBackupSlots.find(slot => slot.type === BACKUP_SLOT_TYPE.RESERVE).id;
this.saveToBackup(targetSlot, player.backupTimer);
},
export() {
@ -354,6 +352,7 @@ export const GameStorage = {
},
exportAsFile() {
if (!this.canSave()) return;
player.options.exportedFileCount++;
this.save(true);
const saveFileName = player.options.saveFileName ? ` - ${player.options.saveFileName},` : "";
@ -387,12 +386,11 @@ export const GameStorage = {
const storageKey = this.backupDataKey(this.currentSlot, id);
localStorage.setItem(storageKey, GameSaveSerializer.serialize(backupData[backupKey]));
this.backupTimeData[id] = {
timer: backupData.time[id].timer,
last: backupData.time[id].last,
backupTimer: backupData.time[id].backupTimer,
date: backupData.time[id].date,
};
}
this.nextBackup = Math.ceil(player.backupTimer / this.shortestOnlineInterval) * this.shortestOnlineInterval;
this.resetBackupInterval();
this.resetBackupTimer();
GameUI.notify.info("Successfully imported save file backups from file");
},