Added support for reading/loading from different encodings

This commit is contained in:
thepaperpilot 2022-04-17 21:15:38 -05:00
parent 36c96a4e44
commit cfe378020a
6 changed files with 163 additions and 43 deletions

27
package-lock.json generated
View file

@ -11,6 +11,7 @@
"@pixi/particle-emitter": "^5.0.4", "@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2", "nanoevents": "^6.0.2",
"pixi.js": "^6.3.0", "pixi.js": "^6.3.0",
"vue": "^3.2.26", "vue": "^3.2.26",
@ -26,6 +27,7 @@
"@jetblack/operator-overloading": "^0.2.0", "@jetblack/operator-overloading": "^0.2.0",
"@rushstack/eslint-patch": "^1.1.0", "@rushstack/eslint-patch": "^1.1.0",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/lz-string": "^1.3.34",
"@vue/babel-plugin-jsx": "^1.1.1", "@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "^5.0.3", "@vue/cli-plugin-babel": "^5.0.3",
"@vue/cli-plugin-eslint": "^5.0.3", "@vue/cli-plugin-eslint": "^5.0.3",
@ -2461,6 +2463,12 @@
"@types/lodash": "*" "@types/lodash": "*"
} }
}, },
"node_modules/@types/lz-string": {
"version": "1.3.34",
"resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
"integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
"dev": true
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -8592,6 +8600,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@ -15010,6 +15026,12 @@
"@types/lodash": "*" "@types/lodash": "*"
} }
}, },
"@types/lz-string": {
"version": "1.3.34",
"resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
"integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
"dev": true
},
"@types/mime": { "@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -19594,6 +19616,11 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
},
"magic-string": { "magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",

View file

@ -12,6 +12,7 @@
"@pixi/particle-emitter": "^5.0.4", "@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2", "nanoevents": "^6.0.2",
"pixi.js": "^6.3.0", "pixi.js": "^6.3.0",
"vue": "^3.2.26", "vue": "^3.2.26",
@ -27,6 +28,7 @@
"@jetblack/operator-overloading": "^0.2.0", "@jetblack/operator-overloading": "^0.2.0",
"@rushstack/eslint-patch": "^1.1.0", "@rushstack/eslint-patch": "^1.1.0",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/lz-string": "^1.3.34",
"@vue/babel-plugin-jsx": "^1.1.1", "@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "^5.0.3", "@vue/cli-plugin-babel": "^5.0.3",
"@vue/cli-plugin-eslint": "^5.0.3", "@vue/cli-plugin-eslint": "^5.0.3",

View file

@ -57,23 +57,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import projInfo from "data/projInfo.json";
import Modal from "components/Modal.vue"; import Modal from "components/Modal.vue";
import player, { PlayerData } from "game/player"; import player, { PlayerData } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import { getUniqueID, loadSave, save, newSave } from "util/save"; import { getUniqueID, loadSave, save, newSave } from "util/save";
import { import { ComponentPublicInstance, computed, nextTick, ref, shallowReactive, watch } from "vue";
ComponentPublicInstance,
computed,
nextTick,
ref,
shallowReactive,
unref,
watch
} from "vue";
import Select from "./fields/Select.vue"; import Select from "./fields/Select.vue";
import Text from "./fields/Text.vue"; import Text from "./fields/Text.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import LZString from "lz-string";
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }; export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
@ -89,21 +83,32 @@ defineExpose({
const importingFailed = ref(false); const importingFailed = ref(false);
const saveToImport = ref(""); const saveToImport = ref("");
watch(saveToImport, save => { watch(saveToImport, importedSave => {
if (save) { if (importedSave) {
nextTick(() => { nextTick(() => {
try { try {
const playerData = JSON.parse(decodeURIComponent(escape(atob(save)))); 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 {
console.warn("Unable to determine preset encoding", importedSave);
importingFailed.value = true;
return;
}
const playerData = JSON.parse(importedSave);
if (typeof playerData !== "object") { if (typeof playerData !== "object") {
importingFailed.value = true; importingFailed.value = true;
return; return;
} }
const id = getUniqueID(); const id = getUniqueID();
playerData.id = id; playerData.id = id;
localStorage.setItem( save(playerData);
id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
saveToImport.value = ""; saveToImport.value = "";
importingFailed.value = false; importingFailed.value = false;
@ -132,14 +137,30 @@ let bank = ref(
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({}); const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
function getCachedSave(id: string) { function getCachedSave(id: string) {
if (cachedSaves[id] == null) { if (cachedSaves[id] == null) {
const save = localStorage.getItem(id); let save = localStorage.getItem(id);
if (save == null) { if (save == null) {
cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id }; cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
} else if (save === "dW5kZWZpbmVk") { } else if (save === "dW5kZWZpbmVk") {
cachedSaves[id] = { error: `Save is undefined`, id }; cachedSaves[id] = { error: `Save is undefined`, id };
} else { } else {
try { try {
cachedSaves[id] = { ...JSON.parse(decodeURIComponent(escape(atob(save)))), id }; 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) { } catch (error) {
cachedSaves[id] = { error, id }; cachedSaves[id] = { error, id };
console.warn( console.warn(
@ -170,7 +191,19 @@ function exportSave(id: string) {
if (player.id === id) { if (player.id === id) {
saveToExport = save(); saveToExport = save();
} else { } else {
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id])))); saveToExport = JSON.stringify(saves.value[id]);
switch (projInfo.saveEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
case "lz":
saveToExport = LZString.compressToUTF16(saveToExport);
break;
case "base64":
saveToExport = btoa(unescape(encodeURIComponent(saveToExport)));
break;
case "plain":
break;
}
} }
// Put on clipboard. Using the clipboard API asks for permissions and stuff // Put on clipboard. Using the clipboard API asks for permissions and stuff
@ -189,10 +222,7 @@ function duplicateSave(id: string) {
} }
const playerData = { ...saves.value[id], id: getUniqueID() }; const playerData = { ...saves.value[id], id: getUniqueID() };
localStorage.setItem( save(playerData as PlayerData);
playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
settings.saves.push(playerData.id); settings.saves.push(playerData.id);
} }
@ -214,12 +244,22 @@ function openSave(id: string) {
} }
function newFromPreset(preset: string) { function newFromPreset(preset: string) {
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset)))); 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 {
console.warn("Unable to determine preset encoding", preset);
return;
}
const playerData = JSON.parse(preset);
playerData.id = getUniqueID(); playerData.id = getUniqueID();
localStorage.setItem( save(playerData as PlayerData);
playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
settings.saves.push(playerData.id); settings.saves.push(playerData.id);
} }
@ -232,7 +272,7 @@ function editSave(id: string, newName: string) {
player.name = newName; player.name = newName;
save(); save();
} else { } else {
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(currSave))))); save(currSave as PlayerData);
cachedSaves[id] = undefined; cachedSaves[id] = undefined;
} }
} }

View file

@ -18,5 +18,6 @@
"maxTickLength": 3600, "maxTickLength": 3600,
"offlineLimit": 1, "offlineLimit": 1,
"enablePausing": true "enablePausing": true,
"saveEncoding": "lz"
} }

View file

@ -2,6 +2,7 @@ import projInfo from "data/projInfo.json";
import { Themes } from "data/themes"; import { Themes } from "data/themes";
import { CoercableComponent } from "features/feature"; import { CoercableComponent } from "features/feature";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import LZString from "lz-string";
import { hardReset } from "util/save"; import { hardReset } from "util/save";
import { reactive, watch } from "vue"; import { reactive, watch } from "vue";
@ -23,20 +24,44 @@ const state = reactive<Partial<Settings>>({
watch( watch(
state, state,
state => state => {
localStorage.setItem( let stringifiedSettings = JSON.stringify(state);
projInfo.id, switch (projInfo.saveEncoding) {
btoa(unescape(encodeURIComponent(JSON.stringify(state)))) default:
), console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
case "lz":
stringifiedSettings = LZString.compressToUTF16(stringifiedSettings);
break;
case "base64":
stringifiedSettings = btoa(unescape(encodeURIComponent(stringifiedSettings)));
break;
case "plain":
break;
}
localStorage.setItem(projInfo.id, stringifiedSettings);
},
{ deep: true } { deep: true }
); );
export default window.settings = state as Settings; export default window.settings = state as Settings;
export function loadSettings(): void { export function loadSettings(): void {
try { try {
const item: string | null = localStorage.getItem(projInfo.id); let item: string | null = localStorage.getItem(projInfo.id);
if (item != null && item !== "") { if (item != null && item !== "") {
const settings = JSON.parse(decodeURIComponent(escape(atob(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 {
console.warn("Unable to determine settings encoding", item);
return;
}
const settings = JSON.parse(item);
if (typeof settings === "object") { if (typeof settings === "object") {
Object.assign(state, settings); Object.assign(state, settings);
} }

View file

@ -2,6 +2,7 @@ import projInfo from "data/projInfo.json";
import player, { Player, PlayerData, stringifySave } from "game/player"; import player, { Player, PlayerData, stringifySave } from "game/player";
import settings, { loadSettings } from "game/settings"; import settings, { loadSettings } from "game/settings";
import { ProxyState } from "./proxies"; import { ProxyState } from "./proxies";
import LZString from "lz-string";
export function setupInitialStore(player: Partial<PlayerData> = {}): Player { export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
return Object.assign( return Object.assign(
@ -23,9 +24,21 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
) as Player; ) as Player;
} }
export function save(): string { export function save(playerData?: PlayerData): string {
const stringifiedSave = btoa(unescape(encodeURIComponent(stringifySave(player[ProxyState])))); let stringifiedSave = stringifySave(playerData ?? player[ProxyState]);
localStorage.setItem(player.id, stringifiedSave); switch (projInfo.saveEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
case "lz":
stringifiedSave = LZString.compressToUTF16(stringifiedSave);
break;
case "base64":
stringifiedSave = btoa(unescape(encodeURIComponent(stringifiedSave)));
break;
case "plain":
break;
}
localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave);
return stringifiedSave; return stringifiedSave;
} }
@ -34,12 +47,24 @@ export async function load(): Promise<void> {
loadSettings(); loadSettings();
try { try {
const save = localStorage.getItem(settings.active); let save = localStorage.getItem(settings.active);
if (save == null) { if (save == null) {
await loadSave(newSave()); await loadSave(newSave());
return; return;
} }
const player = JSON.parse(decodeURIComponent(escape(atob(save)))); 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`;
}
const player = JSON.parse(save);
if (player.modID !== projInfo.id) { if (player.modID !== projInfo.id) {
await loadSave(newSave()); await loadSave(newSave());
return; return;
@ -55,7 +80,7 @@ export async function load(): Promise<void> {
export function newSave(): PlayerData { export function newSave(): PlayerData {
const id = getUniqueID(); const id = getUniqueID();
const player = setupInitialStore({ id }); const player = setupInitialStore({ id });
localStorage.setItem(id, btoa(unescape(encodeURIComponent(stringifySave(player))))); save(player);
settings.saves.push(id); settings.saves.push(id);