diff --git a/src/global.d.ts b/src/global.d.ts
index 74ad4c4..698febf 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -1,9 +1,11 @@
///
+import type { LZString } from "./shark/LZString";
import type { SharkGame } from "./shark/SharkGame";
declare global {
interface Window {
SharkGame: SharkGame;
+ LZString: LZString;
}
}
diff --git a/src/shark/LZString.ts b/src/shark/LZString.ts
new file mode 100644
index 0000000..ba57e7d
--- /dev/null
+++ b/src/shark/LZString.ts
@@ -0,0 +1,435 @@
+import { StaticClass } from "./StaticClass";
+
+export class LZString extends StaticClass {
+ static readonly #KeyStrBase64 =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+ static readonly #KeyStrBase64Dict = LZString.#createBaseDict(
+ LZString.#KeyStrBase64
+ );
+
+ static #createBaseDict(alphabet: string): Record {
+ const dict: Record = {};
+ for (let i = 0; i < alphabet.length; i++) {
+ dict[alphabet[i]] = i;
+ }
+ return dict;
+ }
+
+ static compressToBase64(input: string): string {
+ const result = LZString.#compress(input, 6, (a) =>
+ LZString.#KeyStrBase64.charAt(a)
+ );
+ return result + "=".repeat((4 - (result.length % 4)) % 4);
+ }
+ static decompressFromBase64(input: string | null): string | null {
+ if (input === null || input === "") return null;
+ return LZString.#decompress(
+ input.length,
+ 32,
+ (index) => LZString.#KeyStrBase64Dict[input.charAt(index)]
+ );
+ }
+
+ static compress(uncompressed: string): string {
+ return LZString.#compress(uncompressed);
+ }
+ static #compress(uncompressed: string): string;
+ static #compress(
+ uncompressed: string,
+ bitsPerChar: number,
+ getCharFromInt: (index: number) => string
+ ): string;
+ static #compress(
+ uncompressed: string,
+ bitsPerChar = 16,
+ getCharFromNumber = (code: number) => String.fromCharCode(code)
+ ): string {
+ const contextDictionary: Record = {};
+ const contextDictionaryToCreate: Record = {};
+
+ let contextDictSize = 3;
+ let contextW = "";
+ let contextEnlargeIn = 2; // Compensate for the first entry which should not count
+ let contextNumBits = 2;
+ let contextData = "";
+ let contextDataVal = 0;
+ let contextDataPosition = 0;
+
+ let value: number;
+
+ for (const contextC of uncompressed) {
+ if (!Object.keys(contextDictionary).includes(contextC)) {
+ contextDictionary[contextC] = contextDictSize++;
+ contextDictionaryToCreate[contextC] = true;
+ }
+
+ const contextWC = contextW + contextC;
+ if (Object.keys(contextDictionary).includes(contextWC)) {
+ contextW = contextWC;
+ } else {
+ if (Object.keys(contextDictionaryToCreate).includes(contextW)) {
+ if (contextW.charCodeAt(0) < 256) {
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal <<= 1;
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ }
+ value = contextW.charCodeAt(0);
+ for (let i = 0; i < 8; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+ } else {
+ value = 1;
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal = (contextDataVal << 1) | value;
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value = 0;
+ }
+ value = contextW.charCodeAt(0);
+
+ for (let i = 0; i < 16; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+ }
+ contextEnlargeIn--;
+ if (contextEnlargeIn === 0) {
+ contextEnlargeIn = 2 ** contextNumBits;
+ contextNumBits++;
+ }
+ delete contextDictionaryToCreate[contextW];
+ } else {
+ value = contextDictionary[contextW];
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+ }
+ contextEnlargeIn--;
+ if (contextEnlargeIn === 0) {
+ contextEnlargeIn = 2 ** contextNumBits;
+ contextNumBits++;
+ }
+ contextDictionary[contextWC] = contextDictSize++;
+ contextW = contextC;
+ }
+ }
+
+ if (contextW !== "") {
+ if (Object.keys(contextDictionaryToCreate).includes(contextW)) {
+ if (contextW.charCodeAt(0) < 256) {
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal = contextDataVal << 1;
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ }
+ value = contextW.charCodeAt(0);
+ for (let i = 0; i < 8; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+ } else {
+ value = 1;
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal = (contextDataVal << 1) | value;
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value = 0;
+ }
+
+ value = contextW.charCodeAt(0);
+ for (let i = 0; i < 16; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+ }
+ contextEnlargeIn--;
+ if (contextEnlargeIn === 0) {
+ contextEnlargeIn = 2 ** contextNumBits;
+ contextNumBits++;
+ }
+ delete contextDictionaryToCreate[contextW];
+ } else {
+ value = contextDictionary[contextW];
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+ }
+ contextEnlargeIn--;
+ if (contextEnlargeIn === 0) {
+ contextNumBits++;
+ }
+ }
+ value = 2;
+ for (let i = 0; i < contextNumBits; i++) {
+ contextDataVal = (contextDataVal << 1) | (value & 1);
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextDataPosition = 0;
+ contextData += getCharFromNumber(contextDataVal);
+ contextDataVal = 0;
+ } else {
+ contextDataPosition++;
+ }
+ value >>= 1;
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ contextDataVal <<= 1;
+ if (contextDataPosition === bitsPerChar - 1) {
+ contextData += getCharFromNumber(contextDataVal);
+ break;
+ } else {
+ contextDataPosition++;
+ }
+ }
+
+ return contextData;
+ }
+
+ static decompress(compressed: string | null): string | null {
+ if (compressed === null || compressed === "") return null;
+ return LZString.#decompress(compressed.length, 32768, (index) =>
+ compressed.charCodeAt(index)
+ );
+ }
+ static #decompress(
+ length: number,
+ resetValue: number,
+ getNextValue: (index: number) => number
+ ): string | null {
+ const dictionary: (number | string)[] = [];
+
+ let enlargeIn = 4;
+ let dictSize = 4;
+ let numBits = 3;
+ let entry = "";
+ const result = [];
+
+ const data = {
+ val: getNextValue(0),
+ position: resetValue,
+ index: 1,
+ };
+
+ for (let i = 0; i < 3; i++) {
+ dictionary[i] = i;
+ }
+
+ let bits = 0;
+ let maxPower = 2 ** 2;
+ let power = 1;
+
+ let resb: number;
+ while (power !== maxPower) {
+ resb = data.val & data.position;
+ data.position >>= 1;
+ if (data.position === 0) {
+ data.position = resetValue;
+ data.val = getNextValue(data.index++);
+ }
+ bits |= resb > 0 ? power : 0;
+ power <<= 1;
+ }
+
+ let c = "";
+ switch (bits) {
+ case 0:
+ bits = 0;
+ maxPower = 2 ** 8;
+ power = 1;
+ while (power !== maxPower) {
+ resb = data.val & data.position;
+ data.position >>= 1;
+ if (data.position === 0) {
+ data.position = resetValue;
+ data.val = getNextValue(data.index++);
+ }
+ bits |= (resb > 0 ? 1 : 0) * power;
+ power <<= 1;
+ }
+ c = String.fromCharCode(bits);
+ break;
+ case 1:
+ bits = 0;
+ maxPower = 2 ** 16;
+ power = 1;
+ while (power != maxPower) {
+ resb = data.val & data.position;
+ data.position >>= 1;
+ if (data.position == 0) {
+ data.position = resetValue;
+ data.val = getNextValue(data.index++);
+ }
+ bits |= (resb > 0 ? 1 : 0) * power;
+ power <<= 1;
+ }
+ c = String.fromCharCode(bits);
+ break;
+ case 2:
+ return "";
+ }
+ dictionary[3] = c;
+ let w = c;
+ result.push(c);
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (data.index > length) {
+ return "";
+ }
+
+ bits = 0;
+ maxPower = 2 ** numBits;
+ power = 1;
+
+ while (power !== maxPower) {
+ resb = data.val & data.position;
+ data.position >>= 1;
+ if (data.position === 0) {
+ data.position = resetValue;
+ data.val = getNextValue(data.index++);
+ }
+ bits |= resb > 0 ? power : 0;
+ power <<= 1;
+ }
+
+ let c2;
+ switch ((c2 = bits)) {
+ case 0:
+ bits = 0;
+ maxPower = 2 ** 8;
+ power = 1;
+ while (power !== maxPower) {
+ resb = data.val & data.position;
+ data.position >>= 1;
+ if (data.position === 0) {
+ data.position = resetValue;
+ data.val = getNextValue(data.index++);
+ }
+ bits |= (resb > 0 ? 1 : 0) * power;
+ power <<= 1;
+ }
+
+ dictionary[dictSize++] = String.fromCharCode(bits);
+ c2 = dictSize - 1;
+ enlargeIn--;
+ break;
+ case 1:
+ bits = 0;
+ maxPower = 2 ** 16;
+ power = 1;
+ while (power !== maxPower) {
+ resb = data.val & data.position;
+ data.position >>= 1;
+ if (data.position === 0) {
+ data.position = resetValue;
+ data.val = getNextValue(data.index++);
+ }
+ bits |= (resb > 0 ? 1 : 0) * power;
+ power <<= 1;
+ }
+ dictionary[dictSize++] = String.fromCharCode(bits);
+ c2 = dictSize - 1;
+ enlargeIn--;
+ break;
+ case 2:
+ return result.join("");
+ }
+
+ if (enlargeIn == 0) {
+ enlargeIn = Math.pow(2, numBits);
+ numBits++;
+ }
+
+ if (dictionary[c2]) {
+ entry = dictionary[c2] as string;
+ } else {
+ if (c2 === dictSize) {
+ entry = w + w.charAt(0);
+ } else {
+ return null;
+ }
+ }
+ result.push(entry);
+
+ // Add w+entry[0] to the dictionary.
+ dictionary[dictSize++] = w + entry.charAt(0);
+ enlargeIn--;
+
+ w = entry;
+
+ if (enlargeIn == 0) {
+ enlargeIn = Math.pow(2, numBits);
+ numBits++;
+ }
+ }
+ }
+}
+
+window.LZString = LZString;
diff --git a/src/shark/SaveHandler.ts b/src/shark/SaveHandler.ts
index afb146d..86d9fbb 100644
--- a/src/shark/SaveHandler.ts
+++ b/src/shark/SaveHandler.ts
@@ -1,8 +1,8 @@
import type { Message, MessageType } from "./Message";
-import type { Settings } from "./Settings";
import type { SharkGame } from "./SharkGame";
import { StaticClass } from "./StaticClass";
import type { TabHandler } from "./TabHandler";
+import { LZString } from "./LZString";
const __EMPTY_OBJECT = {};
type Version0Save = typeof __EMPTY_OBJECT;
@@ -13,7 +13,7 @@ type Version1Save = Version0Save & {
type: MessageType;
}[];
selectedTab: keyof typeof TabHandler["AllTabs"];
- settings: ReturnType;
+ settings: Record<`${string};${string}`, unknown>;
};
type CurrentVersionSave = Version1Save;
@@ -40,10 +40,21 @@ export class SaveHandler extends StaticClass {
);
const selectedTabName = allTabsEntries[selectedTabIndex][0];
+ const currentSettings = game.Settings.getSaveable();
+ const saveSettings: CurrentVersionSave["settings"] = {};
+ for (const [settingId, setting] of Object.entries(currentSettings) as [
+ keyof typeof currentSettings,
+ typeof currentSettings[keyof typeof currentSettings]
+ ][]) {
+ if (setting.current !== setting.default) {
+ saveSettings[settingId] = setting.current;
+ }
+ }
+
const save: CurrentVersionSave = {
version: 1,
selectedTab: selectedTabName,
- settings: game.Settings.getCurrent(),
+ settings: saveSettings,
messages: messages.map((message) => {
return {
message: message.message,
@@ -51,13 +62,24 @@ export class SaveHandler extends StaticClass {
};
}),
};
- const encodedSave = JSON.stringify(save);
+ const stringifiedSave = JSON.stringify(save);
+ const encodedSave = LZString.compressToBase64(stringifiedSave);
+ console.debug(
+ `${new Date(
+ Date.now()
+ ).toISOString()} - saving ${stringifiedSave} - encoded to ${
+ Math.round((encodedSave.length / stringifiedSave.length) * 100 * 100) /
+ 100
+ }% size`
+ );
localStorage.setItem(SaveHandler.saveName, encodedSave);
}
static async load(game: typeof SharkGame): Promise {
- const localSave = localStorage.getItem(SaveHandler.saveName);
+ const localSave = LZString.decompressFromBase64(
+ localStorage.getItem(SaveHandler.saveName)
+ );
const loadedSave = JSON.parse(localSave ?? "{}");
const saveVersion = loadedSave.version ?? 0;
diff --git a/src/shark/Settings.ts b/src/shark/Settings.ts
index ac8b8a2..cad8433 100644
--- a/src/shark/Settings.ts
+++ b/src/shark/Settings.ts
@@ -162,12 +162,12 @@ export class Settings extends StaticClass {
static getSettings(): SettingsRecord {
return Object.seal(Object.assign({}, Settings.#settings));
}
- static getCurrent(): Record<`${string};${string}`, Setting> {
+ static getSaveable(): Record<`${string};${string}`, Setting> {
const current = this.getSettings();
const result: Record<`${string};${string}`, Setting> = {};
for (const [categoryName, category] of Object.entries(current)) {
for (const [settingName, setting] of Object.entries(category)) {
- result[`${categoryName};${settingName}`] = setting.current;
+ result[`${categoryName};${settingName}`] = setting;
}
}
return result;