Save encoding (maybe compression, remains to be seen for bigger saves)
This commit is contained in:
parent
61b4f4b06d
commit
921e74fb88
4 changed files with 466 additions and 7 deletions
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
|
@ -1,9 +1,11 @@
|
|||
/// <reference types="svelte" />
|
||||
|
||||
import type { LZString } from "./shark/LZString";
|
||||
import type { SharkGame } from "./shark/SharkGame";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SharkGame: SharkGame;
|
||||
LZString: LZString;
|
||||
}
|
||||
}
|
||||
|
|
435
src/shark/LZString.ts
Normal file
435
src/shark/LZString.ts
Normal file
|
@ -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<string, number> {
|
||||
const dict: Record<string, number> = {};
|
||||
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<string, number> = {};
|
||||
const contextDictionaryToCreate: Record<string, boolean> = {};
|
||||
|
||||
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;
|
|
@ -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<typeof Settings["getCurrent"]>;
|
||||
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<unknown> {
|
||||
const localSave = localStorage.getItem(SaveHandler.saveName);
|
||||
const localSave = LZString.decompressFromBase64(
|
||||
localStorage.getItem(SaveHandler.saveName)
|
||||
);
|
||||
const loadedSave = JSON.parse(localSave ?? "{}");
|
||||
const saveVersion = loadedSave.version ?? 0;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Reference in a new issue