@@ -73,16 +78,18 @@
import Tooltip from "features/tooltips/Tooltip.vue";
import player from "game/player";
import { Direction } from "util/common";
-import { computed, ref, toRefs, watch } from "vue";
-import DangerButton from "./fields/DangerButton.vue";
-import FeedbackButton from "./fields/FeedbackButton.vue";
-import Text from "./fields/Text.vue";
+import { computed, ref, toRefs, unref, watch } from "vue";
+import DangerButton from "../fields/DangerButton.vue";
+import FeedbackButton from "../fields/FeedbackButton.vue";
+import Text from "../fields/Text.vue";
import type { LoadablePlayerData } from "./SavesManager.vue";
+import { galaxy, syncedSaves } from "util/galaxy";
const _props = defineProps<{
save: LoadablePlayerData;
+ readonly?: boolean;
}>();
-const { save } = toRefs(_props);
+const { save, readonly } = toRefs(_props);
const emit = defineEmits<{
(e: "export"): void;
(e: "open"): void;
@@ -106,10 +113,18 @@ const newName = ref("");
watch(isEditing, () => (newName.value = save.value.name ?? ""));
-const isActive = computed(() => save.value != null && save.value.id === player.id);
+const isActive = computed(
+ () => save.value != null && save.value.id === player.id && !unref(readonly)
+);
const currentTime = computed(() =>
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
);
+const synced = computed(
+ () =>
+ !unref(readonly) &&
+ galaxy.value?.loggedIn === true &&
+ syncedSaves.value.includes(save.value.id)
+);
function changeName() {
emit("editName", newName.value);
@@ -139,6 +154,13 @@ function changeName() {
padding-left: 0;
}
+.open:disabled {
+ cursor: inherit;
+ color: var(--foreground);
+ opacity: 1;
+ pointer-events: none;
+}
+
.handle {
flex-grow: 0;
margin-right: 8px;
@@ -152,6 +174,10 @@ function changeName() {
margin-right: 80px;
}
+.save.readonly .details {
+ margin-right: 0;
+}
+
.error {
font-size: 0.8em;
color: var(--danger);
@@ -176,6 +202,17 @@ function changeName() {
.editname {
margin: 0;
}
+
+.time {
+ font-size: small;
+}
+
+.synced {
+ font-size: 100%;
+ margin-right: 0.5em;
+ vertical-align: middle;
+ cursor: default;
+}
diff --git a/src/components/SavesManager.vue b/src/components/saves/SavesManager.vue
similarity index 70%
rename from src/components/SavesManager.vue
rename to src/components/saves/SavesManager.vue
index b1bf7e0..4edb2e0 100644
--- a/src/components/SavesManager.vue
+++ b/src/components/saves/SavesManager.vue
@@ -4,6 +4,9 @@
Saves Manager
+
+ Not all saves are synced! You may need to delete stale saves.
+
, "id"> & { id: string; error?: unknown };
@@ -90,16 +103,8 @@ watch(saveToImport, importedSave => {
if (importedSave) {
nextTick(() => {
try {
- if (importedSave[0] === "{") {
- // plaintext. No processing needed
- } else if (importedSave[0] === "e") {
- // Assumed to be base64, which starts with e
- importedSave = decodeURIComponent(escape(atob(importedSave)));
- } else if (importedSave[0] === "ᯡ") {
- // Assumed to be lz, which starts with ᯡ
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- importedSave = LZString.decompressFromUTF16(importedSave)!;
- } else {
+ importedSave = decodeSave(importedSave) ?? "";
+ if (importedSave === "") {
console.warn("Unable to determine preset encoding", importedSave);
importingFailed.value = true;
return;
@@ -139,48 +144,10 @@ let bank = ref(
}, [])
);
-const cachedSaves = shallowReactive>({});
-function getCachedSave(id: string) {
- if (cachedSaves[id] == null) {
- let save = localStorage.getItem(id);
- if (save == null) {
- cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
- } else if (save === "dW5kZWZpbmVk") {
- cachedSaves[id] = { error: `Save is undefined`, id };
- } else {
- try {
- if (save[0] === "{") {
- // plaintext. No processing needed
- } else if (save[0] === "e") {
- // Assumed to be base64, which starts with e
- save = decodeURIComponent(escape(atob(save)));
- } else if (save[0] === "ᯡ") {
- // Assumed to be lz, which starts with ᯡ
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- save = LZString.decompressFromUTF16(save)!;
- } else {
- console.warn("Unable to determine preset encoding", save);
- importingFailed.value = true;
- cachedSaves[id] = { error: "Unable to determine preset encoding", id };
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return cachedSaves[id]!;
- }
- cachedSaves[id] = { ...JSON.parse(save), id };
- } catch (error) {
- cachedSaves[id] = { error, id };
- console.warn(
- `SavesManager: Failed to load info about save with id ${id}:\n${error}\n${save}`
- );
- }
- }
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return cachedSaves[id]!;
-}
// Wipe cache whenever the modal is opened
watch(isOpen, isOpen => {
if (isOpen) {
- Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
+ clearCachedSaves();
}
});
@@ -191,6 +158,10 @@ const saves = computed(() =>
}, {})
);
+const showNotSyncedWarning = computed(
+ () => galaxy.value?.loggedIn === true && settings.saves.length < syncedSaves.value.length
+);
+
function exportSave(id: string) {
let saveToExport;
if (player.id === id) {
@@ -233,20 +204,37 @@ function duplicateSave(id: string) {
}
function deleteSave(id: string) {
+ if (galaxy.value?.loggedIn === true) {
+ galaxy.value.getSaveList().then(list => {
+ const slot = Object.keys(list).find(slot => {
+ const content = list[slot as unknown as number].content;
+ try {
+ if (JSON.parse(content).id === id) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ });
+ if (slot != null) {
+ galaxy.value?.save(parseInt(slot), "", "").catch(console.error);
+ }
+ });
+ }
settings.saves = settings.saves.filter((save: string) => save !== id);
localStorage.removeItem(id);
- cachedSaves[id] = undefined;
+ clearCachedSave(id);
}
function openSave(id: string) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
saves.value[player.id]!.time = player.time;
save();
- cachedSaves[player.id] = undefined;
+ clearCachedSave(player.id);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loadSave(saves.value[id]!);
// Delete cached version in case of opening it again
- cachedSaves[id] = undefined;
+ clearCachedSave(id);
}
function newFromPreset(preset: string) {
@@ -256,16 +244,8 @@ function newFromPreset(preset: string) {
selectedPreset.value = null;
});
- if (preset[0] === "{") {
- // plaintext. No processing needed
- } else if (preset[0] === "e") {
- // Assumed to be base64, which starts with e
- preset = decodeURIComponent(escape(atob(preset)));
- } else if (preset[0] === "ᯡ") {
- // Assumed to be lz, which starts with ᯡ
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- preset = LZString.decompressFromUTF16(preset)!;
- } else {
+ preset = decodeSave(preset) ?? "";
+ if (preset === "") {
console.warn("Unable to determine preset encoding", preset);
return;
}
@@ -287,7 +267,7 @@ function editSave(id: string, newName: string) {
save();
} else {
save(currSave as Player);
- cachedSaves[id] = undefined;
+ clearCachedSave(id);
}
}
}
diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx
index f69ac8b..e4640b6 100644
--- a/src/data/projEntry.tsx
+++ b/src/data/projEntry.tsx
@@ -1,4 +1,3 @@
-import Node from "components/Node.vue";
import Spacer from "components/layout/Spacer.vue";
import { jsx } from "features/feature";
import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource";
@@ -49,35 +48,19 @@ export const main = createLayer("main", function (this: BaseLayer) {
links: tree.links,
display: jsx(() => (
<>
- {player.devSpeed === 0 ? (
-
- Game Paused
-
-
- ) : null}
+ {player.devSpeed === 0 ? Game Paused
: null}
{player.devSpeed != null && player.devSpeed !== 0 && player.devSpeed !== 1 ? (
-
- Dev Speed: {format(player.devSpeed)}x
-
-
+ Dev Speed: {format(player.devSpeed)}x
) : null}
{player.offlineTime != null && player.offlineTime !== 0 ? (
-
- Offline Time: {formatTime(player.offlineTime)}
-
-
+ Offline Time: {formatTime(player.offlineTime)}
) : null}
{Decimal.lt(points.value, "1e1000") ? You have : null}
{format(points.value)}
{Decimal.lt(points.value, "1e1e6") ? points : null}
- {Decimal.gt(pointGain.value, 0) ? (
-
- ({oomps.value})
-
-
- ) : null}
+ {Decimal.gt(pointGain.value, 0) ? ({oomps.value})
: null}
{render(tree)}
>
diff --git a/src/features/action.tsx b/src/features/action.tsx
index 2919a9e..1fbb8d3 100644
--- a/src/features/action.tsx
+++ b/src/features/action.tsx
@@ -31,7 +31,7 @@ import { coerceComponent, isCoercableComponent, render } from "util/vue";
import { computed, Ref, ref, unref } from "vue";
import { BarOptions, createBar, GenericBar } from "./bars/bar";
import { ClickableOptions } from "./clickables/clickable";
-import { GenericDecorator } from "./decorators/common";
+import { Decorator, GenericDecorator } from "./decorators/common";
/** A symbol used to identify {@link Action} features. */
export const ActionType = Symbol("Action");
diff --git a/src/features/hotkey.tsx b/src/features/hotkey.tsx
index 80eebbd..51fafbb 100644
--- a/src/features/hotkey.tsx
+++ b/src/features/hotkey.tsx
@@ -108,7 +108,7 @@ document.onkeydown = function (e) {
if (e.ctrlKey) {
key = "ctrl+" + key;
}
- const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()];
+ const hotkey = hotkeys[key];
if (hotkey && unref(hotkey.enabled)) {
e.preventDefault();
hotkey.onPress();
diff --git a/src/features/tooltips/tooltip.ts b/src/features/tooltips/tooltip.ts
index 8d65efd..54d782c 100644
--- a/src/features/tooltips/tooltip.ts
+++ b/src/features/tooltips/tooltip.ts
@@ -1,6 +1,6 @@
import type { CoercableComponent, GenericComponent, Replace, StyleValue } from "features/feature";
import { Component, GatherProps, setDefault } from "features/feature";
-import { persistent } from "game/persistence";
+import { deletePersistent, Persistent, persistent } from "game/persistence";
import { Direction } from "util/common";
import type {
Computable,
diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts
index 5a03189..8ec317f 100644
--- a/src/features/trees/tree.ts
+++ b/src/features/trees/tree.ts
@@ -1,4 +1,4 @@
-import { GenericDecorator } from "features/decorators/common";
+import { Decorator, GenericDecorator } from "features/decorators/common";
import type {
CoercableComponent,
GenericComponent,
@@ -224,7 +224,7 @@ export interface BaseTree {
id: string;
/** The link objects for each of the branches of the tree. */
links: Ref ;
- /** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */
+ /** Cause a reset on this node and propagate it through the tree according to {@link resetPropagation}. */
reset: (node: GenericTreeNode) => void;
/** A flag that is true while the reset is still propagating through the tree. */
isResetting: Ref;
@@ -346,11 +346,11 @@ export const branchedResetPropagation = function (
const next: GenericTreeNode[] = [];
for (const node of current) {
for (const link of links.filter(link => link.startNode === node)) {
- if ([...reset, ...current].includes(link.endNode)) continue;
+ if ([...reset, ...current].includes(link.endNode)) continue
next.push(link.endNode);
link.endNode.reset?.reset();
}
- }
+ };
reset.push(...current);
current = next;
}
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index 7210cfb..586319e 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -552,9 +552,7 @@ export function tetrate(
export function invertTetrate(
value: DecimalSource,
base: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
height: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
payload: FormulaSource
) {
if (hasVariable(base)) {
@@ -578,7 +576,6 @@ export function invertIteratedExp(
value: DecimalSource,
lhs: FormulaSource,
height: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
payload: FormulaSource
) {
if (hasVariable(lhs)) {
@@ -629,7 +626,6 @@ export function invertLayeradd(
value: DecimalSource,
lhs: FormulaSource,
diff: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
base: FormulaSource
) {
if (hasVariable(lhs)) {
diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx
index 363fccd..ea82a64 100644
--- a/src/game/requirements.tsx
+++ b/src/game/requirements.tsx
@@ -222,9 +222,7 @@ export function createCostRequirement(
Decimal.gte(
req.resource.value,
unref(req.cost as ProcessedComputable)
- )
- ? 1
- : 0
+ ) ? 1 : 0
);
}
diff --git a/src/game/settings.ts b/src/game/settings.ts
index 0748d68..6f3a435 100644
--- a/src/game/settings.ts
+++ b/src/game/settings.ts
@@ -3,7 +3,7 @@ import { Themes } from "data/themes";
import type { CoercableComponent } from "features/feature";
import { globalBus } from "game/events";
import LZString from "lz-string";
-import { hardReset } from "util/save";
+import { decodeSave, hardReset } from "util/save";
import { reactive, watch } from "vue";
/** The player's settings object. */
@@ -78,16 +78,8 @@ export function loadSettings(): void {
try {
let item: string | null = localStorage.getItem(projInfo.id);
if (item != null && item !== "") {
- if (item[0] === "{") {
- // plaintext. No processing needed
- } else if (item[0] === "e") {
- // Assumed to be base64, which starts with e
- item = decodeURIComponent(escape(atob(item)));
- } else if (item[0] === "ᯡ") {
- // Assumed to be lz, which starts with ᯡ
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- item = LZString.decompressFromUTF16(item)!;
- } else {
+ item = decodeSave(item);
+ if (item == null) {
console.warn("Unable to determine settings encoding", item);
return;
}
diff --git a/src/main.ts b/src/main.ts
index e416fa5..3b5de9f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -8,6 +8,7 @@ import { useRegisterSW } from "virtual:pwa-register/vue";
import type { App as VueApp } from "vue";
import { createApp, nextTick } from "vue";
import { useToast } from "vue-toastification";
+import "util/galaxy";
declare global {
/**
diff --git a/src/util/galaxy.ts b/src/util/galaxy.ts
new file mode 100644
index 0000000..384ed3d
--- /dev/null
+++ b/src/util/galaxy.ts
@@ -0,0 +1,185 @@
+import { LoadablePlayerData } from "components/saves/SavesManager.vue";
+import player, { Player, stringifySave } from "game/player";
+import settings from "game/settings";
+import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
+import LZString from "lz-string";
+import { ref } from "vue";
+import { decodeSave, loadSave, save, setupInitialStore } from "./save";
+
+export const galaxy = ref();
+export const conflictingSaves = ref<
+ { id: string; local: LoadablePlayerData; cloud: LoadablePlayerData; slot: number }[]
+>([]);
+export const syncedSaves = ref([]);
+
+export function sync() {
+ if (galaxy.value?.loggedIn !== true) {
+ return;
+ }
+ if (conflictingSaves.value.length > 0) {
+ // Pause syncing while resolving conflicted saves
+ return;
+ }
+ galaxy.value
+ .getSaveList()
+ .then(syncSaves)
+ .then(list => {
+ syncedSaves.value = list.map(s => s.content.id);
+ })
+ .catch(console.error);
+}
+
+// Setup Galaxy API
+initGalaxy({
+ supportsSaving: true,
+ supportsSaveManager: true,
+ onLoggedInChanged
+})
+ .then(g => {
+ galaxy.value = g;
+ onLoggedInChanged(g);
+ })
+ .catch(console.error);
+
+function onLoggedInChanged(g: GalaxyApi) {
+ if (g.loggedIn !== true) {
+ return;
+ }
+ if (conflictingSaves.value.length > 0) {
+ // Pause syncing while resolving conflicted saves
+ return;
+ }
+
+ g.getSaveList()
+ .then(list => {
+ const saves = syncSaves(list);
+ syncedSaves.value = saves.map(s => s.content.id);
+
+ // If our current save has under 2 minutes of playtime, load the cloud save with the most recent time.
+ if (player.timePlayed < 120 * 1000 && saves.length > 0) {
+ const longestSave = saves.reduce((acc, curr) =>
+ acc.content.time < curr.content.time ? curr : acc
+ );
+ loadSave(longestSave.content);
+ }
+ })
+ .catch(console.error);
+
+ setInterval(sync, 60 * 1000);
+}
+
+function syncSaves(
+ list: Record<
+ number,
+ {
+ label: string;
+ content: string;
+ }
+ >
+) {
+ const savesToUpload = new Set(settings.saves.slice());
+ const availableSlots = new Set(new Array(11).fill(0).map((_, i) => i));
+ const saves = (
+ Object.keys(list)
+ .map(slot => {
+ const { label, content } = list[slot as unknown as number];
+ try {
+ return {
+ slot: parseInt(slot),
+ label,
+ content: JSON.parse(decodeSave(content) ?? "")
+ };
+ } catch (e) {
+ return null;
+ }
+ })
+ .filter(
+ n =>
+ n != null &&
+ typeof n.content.id === "string" &&
+ typeof n.content.time === "number" &&
+ typeof n.content.timePlayed === "number"
+ ) as {
+ slot: number;
+ label?: string;
+ content: Partial & { id: string; time: number; timePlayed: number };
+ }[]
+ ).filter(cloudSave => {
+ if (cloudSave.label != null) {
+ cloudSave.content.name = cloudSave.label;
+ }
+ availableSlots.delete(cloudSave.slot);
+ const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
+ if (localSaveId == undefined) {
+ settings.saves.push(cloudSave.content.id);
+ save(setupInitialStore(cloudSave.content));
+ } else {
+ savesToUpload.delete(localSaveId);
+ try {
+ const localSave = JSON.parse(
+ decodeSave(localStorage.getItem(localSaveId) ?? "") ?? ""
+ ) as Partial | null;
+ if (localSave == null) {
+ return false;
+ }
+ localSave.id = localSaveId;
+ localSave.time = localSave.time ?? 0;
+ localSave.timePlayed = localSave.timePlayed ?? 0;
+
+ const timePlayedDiff = Math.abs(
+ localSave.timePlayed - cloudSave.content.timePlayed
+ );
+ const timeDiff = Math.abs(localSave.time - cloudSave.content.time);
+ // If their last played time and total time played are both within 2 minutes, just use the newer save (very unlikely to be coincidence)
+ // Otherwise, ask the player
+ if (timePlayedDiff < 120 * 1000 && timeDiff < 120 * 1000) {
+ if (localSave.time < cloudSave.content.time) {
+ save(setupInitialStore(cloudSave.content));
+ if (settings.active === localSaveId) {
+ loadSave(cloudSave.content);
+ }
+ } else {
+ galaxy.value
+ ?.save(
+ cloudSave.slot,
+ LZString.compressToUTF16(
+ stringifySave(setupInitialStore(localSave))
+ ),
+ localSave.name ?? cloudSave.label
+ )
+ .catch(console.error);
+ // Update cloud save content for the return value
+ cloudSave.content = localSave as Player;
+ }
+ } else {
+ conflictingSaves.value.push({
+ id: localSaveId,
+ cloud: cloudSave.content,
+ local: localSave as LoadablePlayerData,
+ slot: cloudSave.slot
+ });
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+ return true;
+ });
+
+ savesToUpload.forEach(id => {
+ try {
+ if (availableSlots.size > 0) {
+ const localSave = localStorage.getItem(id) ?? "";
+ const parsedLocalSave = JSON.parse(decodeSave(localSave) ?? "");
+ const slot = availableSlots.values().next().value;
+ galaxy.value
+ ?.save(slot, localSave, parsedLocalSave.name)
+ .then(() => syncedSaves.value.push(parsedLocalSave.id))
+ .catch(console.error);
+ availableSlots.delete(slot);
+ }
+ } catch (e) {}
+ });
+
+ return saves;
+}
diff --git a/src/util/save.ts b/src/util/save.ts
index 4137e5b..a7830cd 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -1,10 +1,11 @@
+import { LoadablePlayerData } from "components/saves/SavesManager.vue";
import projInfo from "data/projInfo.json";
import { globalBus } from "game/events";
import type { Player } from "game/player";
import player, { stringifySave } from "game/player";
import settings, { loadSettings } from "game/settings";
import LZString from "lz-string";
-import { ref } from "vue";
+import { ref, shallowReactive } from "vue";
export function setupInitialStore(player: Partial = {}): Player {
return Object.assign(
@@ -42,17 +43,9 @@ export async function load(): Promise {
await loadSave(newSave());
return;
}
- if (save[0] === "{") {
- // plaintext. No processing needed
- } else if (save[0] === "e") {
- // Assumed to be base64, which starts with e
- save = decodeURIComponent(escape(atob(save)));
- } else if (save[0] === "ᯡ") {
- // Assumed to be lz, which starts with ᯡ
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- save = LZString.decompressFromUTF16(save)!;
- } else {
- throw `Unable to determine save encoding`;
+ save = decodeSave(save);
+ if (save == null) {
+ throw "Unable to determine save encoding";
}
const player = JSON.parse(save);
if (player.modID !== projInfo.id) {
@@ -67,6 +60,23 @@ export async function load(): Promise {
}
}
+export function decodeSave(save: string) {
+ if (save[0] === "{") {
+ // plaintext. No processing needed
+ } else if (save[0] === "e") {
+ // Assumed to be base64, which starts with e
+ save = decodeURIComponent(escape(atob(save)));
+ } else if (save[0] === "ᯡ") {
+ // Assumed to be lz, which starts with ᯡ
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ save = LZString.decompressFromUTF16(save)!;
+ } else {
+ console.warn("Unable to determine preset encoding", save);
+ return null;
+ }
+ return save;
+}
+
export function newSave(): Player {
const id = getUniqueID();
const player = setupInitialStore({ id });
@@ -127,6 +137,40 @@ export async function loadSave(playerObj: Partial): Promise {
globalBus.emit("onLoad");
}
+const cachedSaves = shallowReactive>({});
+export function getCachedSave(id: string) {
+ if (cachedSaves[id] == null) {
+ let save = localStorage.getItem(id);
+ if (save == null) {
+ cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
+ } else if (save === "dW5kZWZpbmVk") {
+ cachedSaves[id] = { error: `Save is undefined`, id };
+ } else {
+ try {
+ save = decodeSave(save);
+ if (save == null) {
+ console.warn("Unable to determine preset encoding", save);
+ cachedSaves[id] = { error: "Unable to determine preset encoding", id };
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return cachedSaves[id]!;
+ }
+ cachedSaves[id] = { ...JSON.parse(save), id };
+ } catch (error) {
+ cachedSaves[id] = { error, id };
+ console.warn(`Failed to load info about save with id ${id}:\n${error}\n${save}`);
+ }
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return cachedSaves[id]!;
+}
+export function clearCachedSaves() {
+ Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
+}
+export function clearCachedSave(id: string) {
+ cachedSaves[id] = undefined;
+}
+
setInterval(() => {
if (player.autosave) {
save();
diff --git a/tests/features/tree.test.ts b/tests/features/tree.test.ts
deleted file mode 100644
index 206988f..0000000
--- a/tests/features/tree.test.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
-import { Ref, ref } from "vue";
-import "../utils";
-import {
- createTree,
- createTreeNode,
- defaultResetPropagation,
- invertedResetPropagation,
- branchedResetPropagation
-} from "features/trees/tree";
-import { createReset, GenericReset } from "features/reset";
-
-describe("Reset propagation", () => {
- let shouldReset: Ref, shouldNotReset: Ref;
- let goodReset: GenericReset, badReset: GenericReset;
- beforeAll(() => {
- shouldReset = ref(false);
- shouldNotReset = ref(false);
- goodReset = createReset(() => ({
- thingsToReset: [],
- onReset() {
- shouldReset.value = true;
- }
- }));
- badReset = createReset(() => ({
- thingsToReset: [],
- onReset() {
- shouldNotReset.value = true;
- }
- }));
- });
- beforeEach(() => {
- shouldReset.value = false;
- shouldNotReset.value = false;
- });
- test("No resets", () => {
- expect(() => {
- const a = createTreeNode(() => ({}));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({}));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]]
- }));
- tree.reset(a);
- }).not.toThrowError();
- });
-
- test("Do not propagate resets", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({ reset: badReset }));
- const c = createTreeNode(() => ({ reset: badReset }));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]]
- }));
- tree.reset(b);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Default propagation", () => {
- const a = createTreeNode(() => ({ reset: goodReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: badReset }));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]],
- resetPropagation: defaultResetPropagation
- }));
- tree.reset(b);
- expect(shouldReset.value).toBe(true);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Inverted propagation", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: goodReset }));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]],
- resetPropagation: invertedResetPropagation
- }));
- tree.reset(b);
- expect(shouldReset.value).toBe(true);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Branched propagation", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: goodReset }));
- const tree = createTree(() => ({
- nodes: [[a, b, c]],
- resetPropagation: branchedResetPropagation,
- branches: [{ startNode: b, endNode: c }]
- }));
- tree.reset(b);
- expect(shouldReset.value).toBe(true);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Branched propagation not bi-directional", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: badReset }));
- const tree = createTree(() => ({
- nodes: [[a, b, c]],
- resetPropagation: branchedResetPropagation,
- branches: [{ startNode: c, endNode: b }]
- }));
- tree.reset(b);
- expect(shouldNotReset.value).toBe(false);
- });
-});