You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tobspr_shapez.io/src/js/savegame/savegame_compressor.js

169 lines
4.4 KiB

const charmap =
"!#%&'()*+,-./:;<=>?@[]^_`{|}~¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿABCDEFGHIJKLMNOPQRSTUVWXYZ";
let compressionCache = {};
let decompressionCache = {};
/**
* Compresses an integer into a tight string representation
* @param {number} i
* @returns {string}
*/
function compressInt(i) {
// Zero value breaks
i += 1;
// save `i` as the cache key
// to avoid it being modified by the
// rest of the function.
const cache_key = i;
if (compressionCache[cache_key]) {
return compressionCache[cache_key];
}
let result = "";
do {
result += charmap[i % charmap.length];
i = Math.floor(i / charmap.length);
} while (i > 0);
return (compressionCache[cache_key] = result);
}
/**
* Decompresses an integer from its tight string representation
* @param {string} s
* @returns {number}
*/
function decompressInt(s) {
if (decompressionCache[s]) {
return decompressionCache[s];
}
s = "" + s;
let result = 0;
for (let i = s.length - 1; i >= 0; --i) {
result = result * charmap.length + charmap.indexOf(s.charAt(i));
}
// Fixes zero value break fix from above
result -= 1;
return (decompressionCache[s] = result);
}
// Sanity
if (G_IS_DEV) {
for (let i = 0; i < 10000; ++i) {
if (decompressInt(compressInt(i)) !== i) {
throw new Error(
"Bad compression for: " +
i +
" compressed: " +
compressInt(i) +
" decompressed: " +
decompressInt(compressInt(i))
);
}
}
}
/**
* @param {any} obj
* @param {Map} keys
* @param {Map} values
* @returns {any[]|object|number|string}
*/
function compressObjectInternal(obj, keys, values) {
if (Array.isArray(obj)) {
let result = [];
for (let i = 0; i < obj.length; ++i) {
result.push(compressObjectInternal(obj[i], keys, values));
}
return result;
} else if (typeof obj === "object" && obj !== null) {
let result = {};
for (const key in obj) {
let index = keys.get(key);
if (index === undefined) {
index = keys.size;
keys.set(key, index);
}
const value = obj[key];
result[compressInt(index)] = compressObjectInternal(value, keys, values);
}
return result;
} else if (typeof obj === "string") {
let index = values.get(obj);
if (index === undefined) {
index = values.size;
values.set(obj, index);
}
return compressInt(index);
}
return obj;
}
/**
* @param {Map} hashMap
* @returns {Array}
*/
function indexMapToArray(hashMap) {
const result = new Array(hashMap.size);
hashMap.forEach((index, key) => {
result[index] = key;
});
return result;
}
/**
* @param {object} obj
*/
export function compressObject(obj) {
const keys = new Map();
const values = new Map();
const data = compressObjectInternal(obj, keys, values);
return {
keys: indexMapToArray(keys),
values: indexMapToArray(values),
data,
};
}
/**
* @param {object} obj
* @param {string[]} keys
* @param {any[]} values
* @returns {object}
*/
function decompressObjectInternal(obj, keys = [], values = []) {
if (Array.isArray(obj)) {
let result = [];
for (let i = 0; i < obj.length; ++i) {
result.push(decompressObjectInternal(obj[i], keys, values));
}
return result;
} else if (typeof obj === "object" && obj !== null) {
let result = {};
for (const key in obj) {
const realIndex = decompressInt(key);
const value = obj[key];
result[keys[realIndex]] = decompressObjectInternal(value, keys, values);
}
return result;
} else if (typeof obj === "string") {
const realIndex = decompressInt(obj);
return values[realIndex];
}
return obj;
}
/**
* @param {object} obj
*/
export function decompressObject(obj) {
if (obj.keys && obj.values && obj.data) {
const keys = obj.keys;
const values = obj.values;
const result = decompressObjectInternal(obj.data, keys, values);
return result;
}
return obj;
}