mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-03-02 03:39:21 +00:00
Initial support for saving games
This commit is contained in:
@@ -11,6 +11,8 @@ import { createLogger } from "../core/logging";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { SavegameInterface_V1000 } from "./schemas/1000";
|
||||
import { getSavegameInterface } from "./savegame_interface_registry";
|
||||
import { compressObject } from "./savegame_compressor";
|
||||
import { compressX64 } from "../core/lzstring";
|
||||
|
||||
const logger = createLogger("savegame");
|
||||
|
||||
@@ -37,7 +39,7 @@ export class Savegame extends ReadWriteProxy {
|
||||
* @returns {number}
|
||||
*/
|
||||
static getCurrentVersion() {
|
||||
return 1015;
|
||||
return 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +131,7 @@ export class Savegame extends ReadWriteProxy {
|
||||
* Returns if this game has a serialized game dump
|
||||
*/
|
||||
hasGameDump() {
|
||||
return !!this.currentData.dump;
|
||||
return !!this.currentData.dump && this.currentData.dump.entities.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,6 +187,12 @@ export class Savegame extends ReadWriteProxy {
|
||||
if (!dump) {
|
||||
return false;
|
||||
}
|
||||
const parsed = JSON.stringify(compressObject(dump));
|
||||
const compressed = compressX64(parsed);
|
||||
|
||||
console.log("Regular: ", Math.round(parsed.length / 1024.0), "KB");
|
||||
console.log("Compressed: ", Math.round(compressed.length / 1024.0), "KB");
|
||||
|
||||
// let duration = performanceNow() - timer;
|
||||
// console.log("TOOK", duration, "ms to generate dump:", dump);
|
||||
|
||||
|
||||
134
src/js/savegame/savegame_compressor.js
Normal file
134
src/js/savegame/savegame_compressor.js
Normal file
@@ -0,0 +1,134 @@
|
||||
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;
|
||||
|
||||
if (compressionCache[i]) {
|
||||
return compressionCache[i];
|
||||
}
|
||||
let result = "";
|
||||
do {
|
||||
result += charmap[i % charmap.length];
|
||||
i = Math.floor(i / charmap.length);
|
||||
} while (i > 0);
|
||||
return (compressionCache[i] = 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
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
let result = {};
|
||||
for (const key in obj) {
|
||||
let index = keys.indexOf(key);
|
||||
if (index < 0) {
|
||||
keys.push(key);
|
||||
index = keys.length - 1;
|
||||
}
|
||||
const value = obj[key];
|
||||
result[compressInt(index)] = compressObjectInternal(value, keys, values);
|
||||
}
|
||||
return result;
|
||||
} else if (typeof obj === "string") {
|
||||
let index = values.indexOf(obj);
|
||||
if (index < 0) {
|
||||
values.push(obj);
|
||||
index = values.length - 1;
|
||||
}
|
||||
return compressInt(index);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function compressObject(obj) {
|
||||
if (G_IS_DEV) {
|
||||
return obj;
|
||||
}
|
||||
const keys = [];
|
||||
const values = [];
|
||||
const data = compressObjectInternal(obj, keys, values);
|
||||
return {
|
||||
keys,
|
||||
values,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
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") {
|
||||
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;
|
||||
}
|
||||
|
||||
export function decompressObject(obj) {
|
||||
if (G_IS_DEV) {
|
||||
return obj;
|
||||
}
|
||||
const keys = obj.keys;
|
||||
const values = obj.values;
|
||||
const result = decompressObjectInternal(obj.data, keys, values);
|
||||
return result;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class BaseSavegameInterface {
|
||||
//////// ANTICHEAT ///////
|
||||
|
||||
/**
|
||||
* Detects cheats in the savegmae - returns false if the game looks cheated
|
||||
* Detects cheats in the savegame - returns false if the game looks cheated
|
||||
*/
|
||||
performAnticheatCheck() {
|
||||
// TODO
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SavegameSerializer {
|
||||
* Serializes the game root into a dump
|
||||
* @param {GameRoot} root
|
||||
* @param {boolean=} sanityChecks Whether to check for validity
|
||||
* @returns {SerializedGame}
|
||||
* @returns {object}
|
||||
*/
|
||||
generateDumpFromGameRoot(root, sanityChecks = true) {
|
||||
// Finalize particles before saving (Like granting destroy indicator rewards)
|
||||
@@ -32,21 +32,15 @@ export class SavegameSerializer {
|
||||
// root.uiParticleMgr.finalizeBeforeSave();
|
||||
|
||||
// Now store generic savegame payload
|
||||
const data = /** @type {SerializedGame} */ ({
|
||||
const data = {
|
||||
camera: root.camera.serialize(),
|
||||
time: root.time.serialize(),
|
||||
map: root.map.serialize(),
|
||||
entityMgr: root.entityMgr.serialize(),
|
||||
entities: {},
|
||||
});
|
||||
hubGoals: root.hubGoals.serialize(),
|
||||
};
|
||||
|
||||
// Serialize all types of entities
|
||||
const serializeEntities = component =>
|
||||
this.internal.serializeEntityArray(root.entityMgr.getAllWithComponent(component));
|
||||
const serializeEntitiesFixed = component =>
|
||||
this.internal.serializeEntityArrayFixedType(root.entityMgr.getAllWithComponent(component));
|
||||
|
||||
// data.entities.resources = serializeEntitiesFixed(RawMaterialComponent);
|
||||
// data.entities.buildings = serializeEntities(BuildingComponent);
|
||||
data.entities = this.internal.serializeEntityArray(root.entityMgr.entities);
|
||||
|
||||
if (!G_IS_RELEASE) {
|
||||
if (sanityChecks) {
|
||||
@@ -58,13 +52,12 @@ export class SavegameSerializer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if there are logical errors in the savegame
|
||||
* @param {SerializedGame} savegame
|
||||
* @param {object} savegame
|
||||
* @returns {ExplainedResult}
|
||||
*/
|
||||
verifyLogicalErrors(savegame) {
|
||||
@@ -138,12 +131,12 @@ export class SavegameSerializer {
|
||||
|
||||
let errorReason = null;
|
||||
|
||||
// entities
|
||||
errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr);
|
||||
|
||||
// other stuff
|
||||
errorReason = errorReason || root.time.deserialize(savegame.time);
|
||||
errorReason = errorReason || root.camera.deserialize(savegame.camera);
|
||||
errorReason = errorReason || root.map.deserialize(savegame.map);
|
||||
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals);
|
||||
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);
|
||||
|
||||
// Check for errors
|
||||
if (errorReason) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TypeVector,
|
||||
TypeClassFromMetaclass,
|
||||
TypeClassData,
|
||||
TypeStructuredObject,
|
||||
} from "./serialization_data_types";
|
||||
import { createLogger } from "../core/logging";
|
||||
|
||||
@@ -61,7 +62,7 @@ export const types = {
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array<string>} values
|
||||
* @param {Object<string, any>} values
|
||||
*/
|
||||
enum(values) {
|
||||
return new TypeEnum(values);
|
||||
@@ -102,6 +103,13 @@ export const types = {
|
||||
return new TypeMetaClass(registry);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object.<string, BaseDataType>} descriptor
|
||||
*/
|
||||
structured(descriptor) {
|
||||
return new TypeStructuredObject(descriptor);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {BaseDataType} a
|
||||
* @param {BaseDataType} b
|
||||
@@ -215,7 +223,7 @@ export function serializeSchema(obj, schema, mergeWith = {}) {
|
||||
);
|
||||
}
|
||||
if (!schema[key]) {
|
||||
assert(false, "Invalid schema: " + JSON_stringify(schema) + " / " + key);
|
||||
assert(false, "Invalid schema (bad key '" + key + "'): " + JSON_stringify(schema));
|
||||
}
|
||||
|
||||
if (G_IS_DEV) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BasicSerializableObject } from "./serialization";
|
||||
/* typehints:end */
|
||||
|
||||
import { Vector } from "../core/vector";
|
||||
import { round4Digits, schemaObject } from "../core/utils";
|
||||
import { round4Digits, schemaObject, accessNestedPropertyReverse } from "../core/utils";
|
||||
import { JSON_stringify } from "../core/builtins";
|
||||
|
||||
export const globalJsonSchemaDefs = {};
|
||||
@@ -458,11 +458,11 @@ export class TypePositiveNumber extends BaseDataType {
|
||||
|
||||
export class TypeEnum extends BaseDataType {
|
||||
/**
|
||||
* @param {Array<string>} availableValues
|
||||
* @param {Object.<string, any>} enumeration
|
||||
*/
|
||||
constructor(availableValues = []) {
|
||||
constructor(enumeration = {}) {
|
||||
super();
|
||||
this.availableValues = availableValues;
|
||||
this.availableValues = Object.keys(enumeration);
|
||||
}
|
||||
|
||||
serialize(value) {
|
||||
@@ -664,7 +664,7 @@ export class TypeClass extends BaseDataType {
|
||||
}
|
||||
|
||||
if (!this.registry.hasId(value.$)) {
|
||||
return "Invalid class id: " + value.$;
|
||||
return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,7 +709,7 @@ export class TypeClassData extends BaseDataType {
|
||||
* @returns {string|void} String error code or null on success
|
||||
*/
|
||||
deserialize(value, targetObject, targetKey, root) {
|
||||
assert(false, "can not deserialize class data");
|
||||
assert(false, "can not deserialize class data of type " + this.registry.getId());
|
||||
}
|
||||
|
||||
verifySerializedValue(value) {
|
||||
@@ -785,7 +785,7 @@ export class TypeClassFromMetaclass extends BaseDataType {
|
||||
}
|
||||
|
||||
if (!this.registry.hasId(value.$)) {
|
||||
return "Invalid class id: " + value.$;
|
||||
return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,7 +841,7 @@ export class TypeMetaClass extends BaseDataType {
|
||||
}
|
||||
|
||||
if (!this.registry.hasId(value)) {
|
||||
return "Invalid class id: " + value;
|
||||
return "Invalid class id: " + value + " (factory is " + this.registry.getId() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,12 +1100,11 @@ export class TypePair extends BaseDataType {
|
||||
deserialize(value, targetObject, targetKey, root) {
|
||||
const result = [undefined, undefined];
|
||||
|
||||
let errorCode = this.type1.deserialize(value, result, 0, root);
|
||||
let errorCode = this.type1.deserialize(value[0], result, 0, root);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
errorCode = this.type2.deserialize(value, result, 1, root);
|
||||
errorCode = this.type2.deserialize(value[1], result, 1, root);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -1202,3 +1201,79 @@ export class TypeNullable extends BaseDataType {
|
||||
return "nullable." + this.wrapped.getCacheKey();
|
||||
}
|
||||
}
|
||||
|
||||
export class TypeStructuredObject extends BaseDataType {
|
||||
/**
|
||||
* @param {Object.<string, BaseDataType>} descriptor
|
||||
*/
|
||||
constructor(descriptor) {
|
||||
super();
|
||||
this.descriptor = descriptor;
|
||||
}
|
||||
|
||||
serialize(value) {
|
||||
assert(typeof value === "object", "not an object");
|
||||
let result = {};
|
||||
for (const key in this.descriptor) {
|
||||
// assert(value.hasOwnProperty(key), "Serialization: Object does not have", key, "property!");
|
||||
result[key] = this.descriptor[key].serialize(value[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BaseDataType.deserialize
|
||||
* @param {any} value
|
||||
* @param {GameRoot} root
|
||||
* @param {object} targetObject
|
||||
* @param {string|number} targetKey
|
||||
* @returns {string|void} String error code or null on success
|
||||
*/
|
||||
deserialize(value, targetObject, targetKey, root) {
|
||||
let result = {};
|
||||
for (const key in value) {
|
||||
const valueType = this.descriptor[key];
|
||||
const errorCode = valueType.deserializeWithVerify(value[key], result, key, root);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
targetObject[targetKey] = result;
|
||||
}
|
||||
|
||||
getAsJsonSchemaUncached() {
|
||||
let properties = {};
|
||||
for (const key in this.descriptor) {
|
||||
properties[key] = this.descriptor[key].getAsJsonSchema();
|
||||
}
|
||||
|
||||
return {
|
||||
type: "object",
|
||||
required: Object.keys(this.descriptor),
|
||||
properties,
|
||||
};
|
||||
}
|
||||
|
||||
verifySerializedValue(value) {
|
||||
if (typeof value !== "object") {
|
||||
return "structured object is not an object";
|
||||
}
|
||||
for (const key in this.descriptor) {
|
||||
if (!value.hasOwnProperty(key)) {
|
||||
return "structured object is missing key " + key;
|
||||
}
|
||||
const subError = this.descriptor[key].verifySerializedValue(value[key]);
|
||||
if (subError) {
|
||||
return "structured object::" + subError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCacheKey() {
|
||||
let props = [];
|
||||
for (const key in this.descriptor) {
|
||||
props.push(key + "=" + this.descriptor[key].getCacheKey());
|
||||
}
|
||||
return "structured[" + props.join(",") + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
import { GameRoot } from "../game/root";
|
||||
/* typehints:end */
|
||||
|
||||
import { Vector } from "../core/vector";
|
||||
import { gComponentRegistry } from "../core/global_registries";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { gMetaBuildingRegistry } from "../core/global_registries";
|
||||
import { Entity } from "../game/entity";
|
||||
import { MapResourcesSystem } from "../game/systems/map_resources";
|
||||
|
||||
const logger = createLogger("serializer_internal");
|
||||
|
||||
// Internal serializer methods
|
||||
export class SerializerInternal {
|
||||
@@ -19,24 +15,6 @@ export class SerializerInternal {
|
||||
* @param {Array<Entity>} array
|
||||
*/
|
||||
serializeEntityArray(array) {
|
||||
const serialized = [];
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
const entity = array[i];
|
||||
if (!entity.queuedForDestroy && !entity.destroyed) {
|
||||
serialized.push({
|
||||
$: entity.getMetaclass().getId(),
|
||||
data: entity.serialize(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an array of entities where we know the type of
|
||||
* @param {Array<Entity>} array
|
||||
*/
|
||||
serializeEntityArrayFixedType(array) {
|
||||
const serialized = [];
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
const entity = array[i];
|
||||
@@ -51,12 +29,11 @@ export class SerializerInternal {
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
* @param {Array<any>} array
|
||||
* @param {function(GameRoot, { $: string, data: object }):string|void} deserializerMethod
|
||||
* @returns {string|void}
|
||||
*/
|
||||
deserializeEntityArray(root, array, deserializerMethod) {
|
||||
deserializeEntityArray(root, array) {
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
const errorState = deserializerMethod.call(this, root, array[i]);
|
||||
const errorState = this.deserializeEntity(root, array[i]);
|
||||
if (errorState) {
|
||||
return errorState;
|
||||
}
|
||||
@@ -67,18 +44,17 @@ export class SerializerInternal {
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
* @param {Array<any>} array
|
||||
* @param {function(GameRoot, object):string|void} deserializerMethod
|
||||
* @returns {string|void}
|
||||
* @param {Entity} payload
|
||||
*/
|
||||
deserializeEntityArrayFixedType(root, array, deserializerMethod) {
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
const errorState = deserializerMethod.call(this, root, array[i]);
|
||||
if (errorState) {
|
||||
return errorState;
|
||||
}
|
||||
deserializeEntity(root, payload) {
|
||||
const entity = new Entity(null);
|
||||
this.deserializeComponents(entity, payload.components);
|
||||
|
||||
root.entityMgr.registerEntity(entity, payload.uid);
|
||||
|
||||
if (entity.components.StaticMapEntity) {
|
||||
root.map.placeStaticEntity(entity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/////// COMPONENTS ////
|
||||
@@ -91,17 +67,10 @@ export class SerializerInternal {
|
||||
*/
|
||||
deserializeComponents(entity, data) {
|
||||
for (const componentId in data) {
|
||||
const componentHandle = entity.components[componentId];
|
||||
if (!componentHandle) {
|
||||
logger.warn(
|
||||
"Loading outdated savegame, where entity had component",
|
||||
componentId,
|
||||
"but now no longer has"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const componentData = data[componentId];
|
||||
const errorStatus = componentHandle.deserialize(componentData);
|
||||
const componentClass = gComponentRegistry.findById(componentId);
|
||||
const componentHandle = new componentClass({});
|
||||
entity.addComponent(componentHandle);
|
||||
const errorStatus = componentHandle.deserialize(data[componentId]);
|
||||
if (errorStatus) {
|
||||
return errorStatus;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user