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/serialization.js

354 lines
9.9 KiB

import { createLogger } from "../core/logging";
import {
BaseDataType,
TypeArray,
TypeBoolean,
TypeClass,
TypeClassData,
TypeClassFromMetaclass,
TypeClassId,
TypeEntity,
TypeEntityWeakref,
TypeEnum,
TypeFixedClass,
TypeInteger,
TypeKeyValueMap,
TypeMetaClass,
TypeNullable,
TypeNumber,
TypePair,
TypePositiveInteger,
TypePositiveNumber,
TypeString,
TypeStructuredObject,
TypeVector,
TypePositiveIntegerOrString,
} from "./serialization_data_types";
const logger = createLogger("serialization");
// Schema declarations
export const types = {
int: new TypeInteger(),
uint: new TypePositiveInteger(),
float: new TypeNumber(),
ufloat: new TypePositiveNumber(),
string: new TypeString(),
entity: new TypeEntity(),
weakEntityRef: new TypeEntityWeakref(),
vector: new TypeVector(),
tileVector: new TypeVector(),
bool: new TypeBoolean(),
uintOrString: new TypePositiveIntegerOrString(),
/**
* @param {BaseDataType} wrapped
*/
nullable(wrapped) {
return new TypeNullable(wrapped);
},
/**
* @param {FactoryTemplate<*>|SingletonFactoryTemplate<*>} registry
*/
classId(registry) {
return new TypeClassId(registry);
},
/**
* @param {BaseDataType} valueType
* @param {boolean=} includeEmptyValues
*/
keyValueMap(valueType, includeEmptyValues = true) {
return new TypeKeyValueMap(valueType, includeEmptyValues);
},
/**
* @param {Object<string, any>} values
*/
enum(values) {
return new TypeEnum(values);
},
/**
* @param {FactoryTemplate<*>} registry
* @param {(GameRoot, any) => object=} resolver
*/
obj(registry, resolver = null) {
return new TypeClass(registry, resolver);
},
/**
* @param {FactoryTemplate<*>} registry
*/
objData(registry) {
return new TypeClassData(registry);
},
/**
* @param {typeof BasicSerializableObject} cls
*/
knownType(cls) {
return new TypeFixedClass(cls);
},
/**
* @param {BaseDataType} innerType
*/
array(innerType) {
return new TypeArray(innerType);
},
/**
* @param {BaseDataType} innerType
*/
fixedSizeArray(innerType) {
return new TypeArray(innerType, true);
},
/**
* @param {SingletonFactoryTemplate<*>} innerType
*/
classRef(registry) {
return new TypeMetaClass(registry);
},
/**
* @param {Object.<string, BaseDataType>} descriptor
*/
structured(descriptor) {
return new TypeStructuredObject(descriptor);
},
/**
* @param {BaseDataType} a
* @param {BaseDataType} b
*/
pair(a, b) {
return new TypePair(a, b);
},
/**
* @param {typeof BasicSerializableObject} classHandle
* @param {SingletonFactoryTemplate<*>} registry
*/
classWithMetaclass(classHandle, registry) {
return new TypeClassFromMetaclass(classHandle, registry);
},
};
/**
* A full schema declaration
* @typedef {Object.<string, BaseDataType> | object} Schema
*/
const globalSchemaCache = {};
/* dev:start */
const classnamesCache = {};
/* dev:end*/
export class BasicSerializableObject {
/* dev:start */
/**
* Fixes typeof DerivedComponent is not assignable to typeof Component, compiled out
* in non-dev builds
*/
constructor(...args) {}
/* dev:end */
static getId() {
abstract;
}
/**
* Should return the serialization schema
* @returns {Schema}
*/
static getSchema() {
return {};
}
// Implementation
/** @returns {Schema} */
static getCachedSchema() {
const id = this.getId();
/* dev:start */
assert(
classnamesCache[id] === this || classnamesCache[id] === undefined,
"Class name taken twice: " + id + " (from " + this.name + ")"
);
classnamesCache[id] = this;
/* dev:end */
const entry = globalSchemaCache[id];
if (entry) {
return entry;
}
const schema = this.getSchema();
globalSchemaCache[id] = schema;
return schema;
}
/** @returns {object | string | number} */
serialize() {
return serializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema()
);
}
/**
* @param {any} data
* @param {import("./savegame_serializer").GameRoot} root
* @returns {string|void}
*/
deserialize(data, root = null) {
return deserializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema(),
data,
null,
root
);
}
/** @returns {string|void} */
static verify(data) {
return verifySchema(this.getCachedSchema(), data);
}
}
/**
* Serializes an object using the given schema, mergin with the given properties
* @param {object} obj The object to serialize
* @param {Schema} schema The schema to use
* @param {object=} mergeWith Any additional properties to merge with the schema, useful for super calls
* @returns {object} Serialized data object
*/
export function serializeSchema(obj, schema, mergeWith = {}) {
for (const key in schema) {
if (!obj.hasOwnProperty(key)) {
logger.error("Invalid schema, property", key, "does not exist on", obj, "(schema=", schema, ")");
assert(
obj.hasOwnProperty(key),
"serialization: invalid schema, property does not exist on object: " + key
);
}
if (!schema[key]) {
assert(false, "Invalid schema (bad key '" + key + "'): " + JSON.stringify(schema));
}
if (G_IS_DEV) {
try {
mergeWith[key] = schema[key].serialize(obj[key]);
} catch (ex) {
logger.error(
"Serialization of",
obj,
"failed on key '" + key + "' ->",
ex,
"(schema was",
schema,
")"
);
throw ex;
}
} else {
mergeWith[key] = schema[key].serialize(obj[key]);
}
}
return mergeWith;
}
/**
* Deserializes data into an object
* @param {object} obj The object to store the deserialized data into
* @param {Schema} schema The schema to use
* @param {object} data The serialized data
* @param {string|void|null=} baseclassErrorResult Convenience, if this is a string error code, do nothing and return it
* @param {import("../game/root").GameRoot=} root Optional game root reference
* @returns {string|void} String error code or nothing on success
*/
export function deserializeSchema(obj, schema, data, baseclassErrorResult = null, root) {
if (baseclassErrorResult) {
return baseclassErrorResult;
}
if (data === null || typeof data === "undefined") {
logger.error("Got 'NULL' data for", obj, "and schema", schema, "!");
return "Got null data";
}
for (const key in schema) {
if (!data.hasOwnProperty(key)) {
logger.error("Data", data, "does not contain", key, "(schema:", schema, ")");
return "Missing key in schema: " + key + " of class " + obj.constructor.name;
}
if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) {
logger.error("Data", data, "has null value for", key, "(schema:", schema, ")");
return "Non-nullable entry is null: " + key + " of class " + obj.constructor.name;
}
const errorStatus = schema[key].deserializeWithVerify(data[key], obj, key, obj.root || root);
if (errorStatus) {
logger.error(
"Deserialization failed with error '" + errorStatus + "' on object",
obj,
"and key",
key,
"(root? =",
obj.root ? "y" : "n",
")"
);
return errorStatus;
}
}
}
/**
* Verifies stored data using the given schema
* @param {Schema} schema The schema to use
* @param {object} data The data to verify
* @returns {string|void} String error code or nothing on success
*/
export function verifySchema(schema, data) {
for (const key in schema) {
if (!data.hasOwnProperty(key)) {
logger.error("Data", data, "does not contain", key, "(schema:", schema, ")");
return "verify: missing key required by schema in stored data: " + key;
}
if (!schema[key].allowNull() && (data[key] === null || data[key] === undefined)) {
logger.error("Data", data, "has null value for", key, "(schema:", schema, ")");
return "verify: non-nullable entry is null: " + key;
}
const errorStatus = schema[key].verifySerializedValue(data[key]);
if (errorStatus) {
logger.error(errorStatus);
return "verify: " + errorStatus;
}
}
}
/**
* Extends a schema by adding the properties from the new schema to the existing base schema
* @param {Schema} base
* @param {Schema} newOne
* @returns {Schema}
*/
export function extendSchema(base, newOne) {
/** @type {Schema} */
const result = Object.assign({}, base);
for (const key in newOne) {
if (result.hasOwnProperty(key)) {
logger.error("Extend schema got duplicate key:", key);
continue;
}
result[key] = newOne[key];
}
return result;
}