Profectus-Demo/src/util/save.ts

207 lines
6.3 KiB
TypeScript
Raw Normal View History

2024-03-17 17:55:41 +00:00
import { fixOldSave, getInitialLayers } from "data/projEntry";
2022-03-04 03:39:48 +00:00
import projInfo from "data/projInfo.json";
2022-07-22 00:23:33 +00:00
import { globalBus } from "game/events";
2024-03-17 17:55:41 +00:00
import { addLayer, layers, removeLayer } from "game/layers";
import type { Player } from "game/player";
2022-06-27 00:17:22 +00:00
import player, { stringifySave } from "game/player";
2022-03-04 03:39:48 +00:00
import settings, { loadSettings } from "game/settings";
import LZString from "lz-string";
2024-02-16 19:17:40 +00:00
import { ref, shallowReactive } from "vue";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
export function setupInitialStore(player: Partial<Player> = {}): Player {
2022-01-25 04:25:34 +00:00
return Object.assign(
{
id: `${projInfo.id}-0`,
name: "Default Save",
tabs: projInfo.initialTabs.slice(),
time: Date.now(),
autosave: true,
offlineProd: true,
offlineTime: 0,
timePlayed: 0,
keepGoing: false,
modID: projInfo.id,
modVersion: projInfo.versionNumber,
2022-01-14 04:25:47 +00:00
layers: {}
},
2022-01-14 04:25:47 +00:00
player
2022-01-25 04:25:34 +00:00
) as Player;
}
export function save(playerData?: Player): string {
const stringifiedSave = LZString.compressToUTF16(stringifySave(playerData ?? player));
localStorage.setItem((playerData ?? player).id, stringifiedSave);
2022-01-14 04:25:47 +00:00
return stringifiedSave;
}
export async function load(): Promise<void> {
// Load global settings
loadSettings();
try {
let save = localStorage.getItem(settings.active);
if (save == null) {
await loadSave(newSave());
return;
}
2024-02-16 19:17:40 +00:00
save = decodeSave(save);
if (save == null) {
throw "Unable to determine save encoding";
}
const player = JSON.parse(save);
if (player.modID !== projInfo.id) {
await loadSave(newSave());
return;
}
2022-01-14 04:25:47 +00:00
player.id = settings.active;
await loadSave(player);
} catch (e) {
2022-01-25 04:23:30 +00:00
console.error("Failed to load save. Falling back to new save.\n", e);
await loadSave(newSave());
}
}
2024-02-16 19:17:40 +00:00
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();
2022-01-25 04:25:34 +00:00
const player = setupInitialStore({ id });
save(player);
settings.saves.push(id);
2022-01-14 04:25:47 +00:00
return player;
}
export function getUniqueID(): string {
let id,
i = 0;
do {
id = `${projInfo.id}-${i++}`;
} while (localStorage.getItem(id) != null);
return id;
}
2022-12-08 05:00:23 +00:00
export const loadingSave = ref(false);
export async function loadSave(playerObj: Partial<Player>): Promise<void> {
2022-01-25 04:23:30 +00:00
console.info("Loading save", playerObj);
2022-12-08 05:00:23 +00:00
loadingSave.value = true;
for (const layer in layers) {
const l = layers[layer];
Feature rewrite - Removed `jsx()` and `JSXFunction`. You can now use `JSX.Element` like any other `Computable` value - `joinJSX` now always requires a joiner. Just pass the array of elements or wrap them in `<>` and `</>` if there's no joiner - Removed `coerceComponent`, `computeComponent`, and `computeOptionalComponent`; just use the `render` function now - It's recommended to now do `<MyComponent />` instead of `<component :is="myComponent" />` - All features no longer take the options as a type parameter, and all generic forms have been removed as a result - Fixed `forceHideGoBack` not being respected - Removed `deepUnref` as now things don't get unreffed before being passed into vue components by default - Moved MarkNode to new wrapper, and removed existing `mark` properties - Moved Tooltip to new wrapper, and made it take an options function instead of raw object - VueFeature component now wraps all vue features, and applies styling, classes, and visibility in the wrapping div. It also adds the Node component so features don't need to - `mergeAdjacent` now works with grids (perhaps should've used scss to reduce the amount of css this took) - `CoercableComponent` renamed to `Renderable` since it should be used with `render` - Replaced `isCoercableComponent` with `isJSXElement` - Replaced `Computable` and `ProcessedComputable` with the vue built-ins `MaybeRefOrGetter` and `MaybeRef` - `convertComputable` renamed to `processGetter` - Also removed `GetComputableTypeWithDefault` and `GetComputableType`, which can similarly be replaced - `dontMerge` is now a property on rows and columns rather than an undocumented css class you'd have to include on every feature within the row or column - Fixed saves manager not being imported in addiction warning component - Created `vueFeatureMixin` for simplifying the vue specific parts of a feature. Passes the component's properties in explicitly and directly from the feature itself - All features should now return an object that includes props typed to omit the options object and satisfies the feature. This will ensure type correctness and pass-through custom properties. (see existing features for more thorough examples of changes) - Replaced decorators with mixins, which won't require casting. Bonus amount decorators converted into generic bonus amount mixin. Removed effect decorator - All `render` functions now return `JSX.Element`. The `JSX` variants (e.g. `renderJSX`) (except `joinJSX`) have been removed - Moved all features that use the clickable component into the clickable folder - Removed `small` property from clickable, since its a single css rule (`min-height: unset`) (you could add a small css class and pass small to any vue feature's classes property, though) - Upgrades now use the clickable component - Added ConversionType symbol - Removed setDefault, just use `??=` - Added isType function that uses a type symbol to check - General cleanup
2024-11-19 14:32:45 +00:00
if (l != null) {
removeLayer(l);
}
}
2022-01-14 04:25:47 +00:00
getInitialLayers(playerObj).forEach(layer => addLayer(layer, playerObj));
2022-01-25 04:25:34 +00:00
playerObj = setupInitialStore(playerObj);
if (
playerObj.offlineProd &&
playerObj.time != null &&
playerObj.time &&
playerObj.devSpeed !== 0
) {
if (playerObj.offlineTime == null) playerObj.offlineTime = 0;
2022-12-03 18:25:58 +00:00
playerObj.offlineTime += Math.min(
playerObj.offlineTime + (Date.now() - playerObj.time) / 1000,
projInfo.offlineLimit * 3600
);
}
2022-01-14 04:25:47 +00:00
playerObj.time = Date.now();
if (playerObj.modVersion !== projInfo.versionNumber) {
2022-01-14 04:25:47 +00:00
fixOldSave(playerObj.modVersion, playerObj);
2022-12-12 05:43:47 +00:00
playerObj.modVersion = projInfo.versionNumber;
}
2022-01-14 04:25:47 +00:00
Object.assign(player, playerObj);
2022-01-25 04:25:34 +00:00
settings.active = player.id;
2022-07-22 00:23:33 +00:00
globalBus.emit("onLoad");
}
2024-02-16 19:17:40 +00:00
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
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();
}
}, 1000);
window.onbeforeunload = () => {
if (player.autosave) {
save();
}
};
2022-07-10 03:09:25 +00:00
declare global {
/**
* Augment the window object so the save, hard reset, and deleteLowerSaves functions can be accessed from the console.
2022-07-10 03:09:25 +00:00
*/
interface Window {
save: VoidFunction;
hardReset: VoidFunction;
deleteLowerSaves: VoidFunction;
2022-07-10 03:09:25 +00:00
}
}
window.save = save;
2021-08-28 16:35:25 +00:00
export const hardReset = (window.hardReset = async () => {
await loadSave(newSave());
2021-08-28 16:35:25 +00:00
});
export const deleteLowerSaves = (window.deleteLowerSaves = () => {
const index = Object.values(settings.saves).indexOf(player.id) + 1;
Object.values(settings.saves)
.slice(index)
.forEach(id => localStorage.removeItem(id));
settings.saves = settings.saves.slice(0, index);
});