mirror of
https://github.com/IvarK/AntimatterDimensionsSourceCode.git
synced 2024-11-10 06:02:13 +00:00
Address PR comments (android-backup-windows)
Refactor backup timer code
This commit is contained in:
parent
45473a25b4
commit
c56fd9c99d
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user