1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Initial commit

This commit is contained in:
Tobias Springer
2020-05-09 16:45:23 +02:00
commit 93c6ea683d
304 changed files with 56031 additions and 0 deletions

237
src/js/savegame/savegame.js Normal file
View File

@@ -0,0 +1,237 @@
/* typehints:start */
import { Application } from "../application";
import { GameRoot } from "../game/root";
/* typehints:end */
import { ReadWriteProxy } from "../core/read_write_proxy";
import { ExplainedResult } from "../core/explained_result";
import { SavegameSerializer } from "./savegame_serializer";
import { BaseSavegameInterface } from "./savegame_interface";
import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config";
import { SavegameInterface_V1000 } from "./schemas/1000";
import { getSavegameInterface } from "./savegame_interface_registry";
const logger = createLogger("savegame");
export class Savegame extends ReadWriteProxy {
/**
*
* @param {Application} app
* @param {object} param0
* @param {string} param0.internalId
* @param {import("./savegame_manager").SavegameMetadata} param0.metaDataRef Handle to the meta data
*/
constructor(app, { internalId, metaDataRef }) {
super(app, "savegame-" + internalId + ".bin");
this.internalId = internalId;
this.metaDataRef = metaDataRef;
/** @type {SavegameData} */
this.currentData = this.getDefaultData();
}
//////// RW Proxy Impl //////////
/**
* @returns {number}
*/
static getCurrentVersion() {
return 1015;
}
/**
* @returns {typeof BaseSavegameInterface}
*/
static getReaderClass() {
return SavegameInterface_V1000;
}
/**
* @returns {number}
*/
getCurrentVersion() {
return /** @type {typeof Savegame} */ (this.constructor).getCurrentVersion();
}
/**
* Returns the savegames default data
* @returns {SavegameData}
*/
getDefaultData() {
return {
version: this.getCurrentVersion(),
dump: null,
stats: {
buildingsPlaced: 0,
},
lastUpdate: Date.now(),
};
}
/**
* Migrates the savegames data
* @param {SavegameData} data
*/
migrate(data) {
// if (data.version === 1014) {
// if (data.dump) {
// const reader = new SavegameInterface_V1015(fakeLogger, data);
// reader.migrateFrom1014();
// }
// data.version = 1015;
// }
return ExplainedResult.good();
}
/**
* Verifies the savegames data
* @param {SavegameData} data
*/
verify(data) {
if (!data.dump) {
// Well, guess that works
return ExplainedResult.good();
}
if (!this.getDumpReaderForExternalData(data).validate()) {
return ExplainedResult.bad("dump-reader-failed-validation");
}
return ExplainedResult.good();
}
//////// Subclasses interface ////////
/**
* Returns if this game can be saved on disc
* @returns {boolean}
*/
isSaveable() {
return true;
}
/**
* Returns the statistics of the savegame
* @returns {SavegameStats}
*/
getStatistics() {
return this.currentData.stats;
}
/**
* Returns the *real* last update of the savegame, not the one of the metadata
* which could also be the servers one
*/
getRealLastUpdate() {
return this.currentData.lastUpdate;
}
/**
* Returns if this game has a serialized game dump
*/
hasGameDump() {
return !!this.currentData.dump;
}
/**
* Returns the current game dump
* @returns {SerializedGame}
*/
getCurrentDump() {
return this.currentData.dump;
}
/**
* Returns a reader to access the data
* @returns {BaseSavegameInterface}
*/
getDumpReader() {
if (!this.currentData.dump) {
logger.warn("Getting reader on null-savegame dump");
}
const cls = /** @type {typeof Savegame} */ (this.constructor).getReaderClass();
return new cls(this.currentData);
}
/**
* Returns a reader to access external data
* @returns {BaseSavegameInterface}
*/
getDumpReaderForExternalData(data) {
assert(data.version, "External data contains no version");
return getSavegameInterface(data);
}
///////// Public Interface ///////////
/**
* Updates the last update field so we can send the savegame to the server,
* WITHOUT Saving!
*/
setLastUpdate(time) {
this.currentData.lastUpdate = time;
}
/**
*
* @param {GameRoot} root
*/
updateData(root) {
// Construct a new serializer
const serializer = new SavegameSerializer();
// let timer = performanceNow();
const dump = serializer.generateDumpFromGameRoot(root);
if (!dump) {
return false;
}
// let duration = performanceNow() - timer;
// console.log("TOOK", duration, "ms to generate dump:", dump);
const shadowData = Object.assign({}, this.currentData);
shadowData.dump = dump;
shadowData.lastUpdate = new Date().getTime();
shadowData.version = this.getCurrentVersion();
const reader = this.getDumpReaderForExternalData(shadowData);
// Validate (not in prod though)
if (!G_IS_RELEASE) {
const validationResult = reader.validate();
if (!validationResult) {
return false;
}
}
// Save data
this.currentData = shadowData;
}
/**
* Writes the savegame as well as its metadata
*/
writeSavegameAndMetadata() {
return this.writeAsync().then(() => this.saveMetadata());
}
/**
* Updates the savegames metadata
*/
saveMetadata() {
const reader = this.getDumpReader();
this.metaDataRef.lastUpdate = new Date().getTime();
this.metaDataRef.version = this.getCurrentVersion();
return this.app.savegameMgr.writeAsync();
}
/**
* @see ReadWriteProxy.writeAsync
* @returns {Promise<any>}
*/
writeAsync() {
if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
return Promise.resolve();
}
return super.writeAsync();
}
}

View File

@@ -0,0 +1,108 @@
import { createLogger } from "../core/logging";
const Ajv = require("ajv");
const ajv = new Ajv({
allErrors: false,
uniqueItems: false,
unicode: false,
nullable: false,
});
const validators = {};
const logger = createLogger("savegame_interface");
export class BaseSavegameInterface {
/**
* Returns the interfaces version
*/
getVersion() {
throw new Error("Implement get version");
}
/**
* Returns the uncached json schema
* @returns {object}
*/
getSchemaUncached() {
throw new Error("Implement get schema");
return {};
}
getValidator() {
const version = this.getVersion();
if (validators[version]) {
return validators[version];
}
logger.log("Compiling schema for savegame version", version);
const schema = this.getSchemaUncached();
try {
validators[version] = ajv.compile(schema);
} catch (ex) {
logger.error("SCHEMA FOR", this.getVersion(), "IS INVALID!");
logger.error(ex);
throw new Error("Invalid schema for version " + version);
}
return validators[version];
}
/**
* Constructs an new interface for the given savegame
* @param {any} data
*/
constructor(data) {
this.data = data;
}
/**
* Validates the data
* @returns {boolean}
*/
validate() {
const validator = this.getValidator();
if (!validator(this.data)) {
logger.error(
"Savegame failed validation! ErrorText:",
ajv.errorsText(validator.errors),
"RawErrors:",
validator.errors
);
return false;
}
return true;
}
///// INTERFACE (Override when the schema changes) /////
/**
* Returns the time of last update
* @returns {number}
*/
readLastUpdate() {
return this.data.lastUpdate;
}
/**
* Returns the ingame time in seconds
* @returns {number}
*/
readIngameTimeSeconds() {
return this.data.dump.time.timeSeconds;
}
/**
//////// ANTICHEAT ///////
/**
* Detects cheats in the savegmae - returns false if the game looks cheated
*/
performAnticheatCheck() {
// TODO
return true;
}
}

View File

@@ -0,0 +1,35 @@
import { BaseSavegameInterface } from "./savegame_interface";
import { SavegameInterface_V1000 } from "./schemas/1000";
import { createLogger } from "../core/logging";
/** @type {Object.<number, typeof BaseSavegameInterface>} */
const interfaces = {
1000: SavegameInterface_V1000,
};
const logger = createLogger("savegame_interface_registry");
/**
* Returns if the given savegame has any supported interface
* @param {any} savegame
* @returns {BaseSavegameInterface|null}
*/
export function getSavegameInterface(savegame) {
if (!savegame || !savegame.version) {
logger.warn("Savegame does not contain a valid version (undefined)");
return null;
}
const version = savegame.version;
if (!Number.isInteger(version)) {
logger.warn("Savegame does not contain a valid version (non-integer):", version);
return null;
}
const interfaceClass = interfaces[version];
if (!interfaceClass) {
logger.warn("Version", version, "has no implemented interface!");
return null;
}
return new interfaceClass(savegame);
}

View File

@@ -0,0 +1,211 @@
import { ExplainedResult } from "../core/explained_result";
import { createLogger } from "../core/logging";
import { ReadWriteProxy } from "../core/read_write_proxy";
import { globalConfig } from "../core/config";
import { Savegame } from "./savegame";
import { Math_floor } from "../core/builtins";
const logger = createLogger("savegame_manager");
const Rusha = require("rusha");
/** @enum {string} */
export const enumLocalSavegameStatus = {
offline: "offline",
synced: "synced",
};
/**
* @typedef {{
* lastUpdate: number,
* version: number,
* internalId: string
* }} SavegameMetadata
*
* @typedef {{
* version: number,
* savegames: Array<SavegameMetadata>
* }} SavegamesData
*/
export class SavegameManager extends ReadWriteProxy {
constructor(app) {
super(app, "savegames.bin");
/** @type {SavegamesData} */
this.currentData = this.getDefaultData();
}
// RW Proxy Impl
/**
* @returns {SavegamesData}
*/
getDefaultData() {
return {
version: this.getCurrentVersion(),
savegames: [],
};
}
getCurrentVersion() {
return 1000;
}
/**
* @returns {SavegamesData}
*/
getCurrentData() {
return super.getCurrentData();
}
verify(data) {
// TODO / FIXME!!!!
return ExplainedResult.good();
}
/**
*
* @param {SavegamesData} data
*/
migrate(data) {
return ExplainedResult.good();
}
// End rw proxy
/**
* @returns {Array<SavegameMetadata>}
*/
getSavegamesMetaData() {
return this.currentData.savegames;
}
/**
*
* @param {string} internalId
* @returns {Savegame}
*/
getSavegameById(internalId) {
const metadata = this.getGameMetaDataByInternalId(internalId);
if (!metadata) {
return null;
}
return new Savegame(this.app, { internalId, metaDataRef: metadata });
}
/**
* Deletes a savegame
* @param {SavegameMetadata} game
*/
deleteSavegame(game) {
const handle = new Savegame(this.app, {
internalId: game.internalId,
metaDataRef: game,
});
return handle.deleteAsync().then(() => {
for (let i = 0; i < this.currentData.savegames.length; ++i) {
const potentialGame = this.currentData.savegames[i];
if (potentialGame.internalId === handle.internalId) {
this.currentData.savegames.splice(i, 1);
break;
}
}
return this.writeAsync();
});
}
/**
* Returns a given games metadata by id
* @param {string} id
* @returns {SavegameMetadata}
*/
getGameMetaDataByInternalId(id) {
for (let i = 0; i < this.currentData.savegames.length; ++i) {
const data = this.currentData.savegames[i];
if (data.internalId === id) {
return data;
}
}
logger.error("Savegame internal id not found:", id);
return null;
}
/**
* Creates a new savegame
* @returns {Savegame}
*/
createNewSavegame() {
const id = this.generateInternalId();
const metaData = /** @type {SavegameMetadata} */ ({
lastUpdate: Date.now(),
version: Savegame.getCurrentVersion(),
internalId: id,
});
this.currentData.savegames.push(metaData);
this.sortSavegames();
return new Savegame(this.app, {
internalId: id,
metaDataRef: metaData,
});
}
/**
* Sorts all savegames by their creation time descending
* @returns {Promise<any>}
*/
sortSavegames() {
this.currentData.savegames.sort((a, b) => b.lastUpdate - a.lastUpdate);
let promiseChain = Promise.resolve();
while (this.currentData.savegames.length > 100) {
const toRemove = this.currentData.savegames.pop();
// Try to remove the savegame since its no longer available
const game = new Savegame(this.app, {
internalId: toRemove.internalId,
metaDataRef: toRemove,
});
promiseChain = promiseChain
.then(() => game.deleteAsync())
.then(
() => {},
err => {
logger.error(this, "Failed to remove old savegame:", toRemove, ":", err);
}
);
}
return promiseChain;
}
/**
* Helper method to generate a new internal savegame id
*/
generateInternalId() {
const timestamp = ("" + Math_floor(Date.now() / 1000.0 - 1565641619)).padStart(10, "0");
return (
timestamp +
"." +
Rusha.createHash()
.update(Date.now() + "/" + Math.random())
.digest("hex")
);
}
// End
initialize() {
// First read, then directly write to ensure we have the latest data
// @ts-ignore
return this.readAsync().then(() => {
if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
return Promise.resolve();
}
return this.writeAsync();
});
}
}

View File

@@ -0,0 +1,216 @@
/* typehints:start */
import { Component } from "../game/component";
import { GameRoot } from "../game/root";
/* typehints:end */
import { JSON_stringify } from "../core/builtins";
import { ExplainedResult } from "../core/explained_result";
import { createLogger } from "../core/logging";
// import { BuildingComponent } from "../components/impl/building";
import { gComponentRegistry } from "../core/global_registries";
import { SerializerInternal } from "./serializer_internal";
const logger = createLogger("savegame_serializer");
/**
* Allows to serialize a savegame
*/
export class SavegameSerializer {
constructor() {
this.internal = new SerializerInternal();
}
/**
* Serializes the game root into a dump
* @param {GameRoot} root
* @param {boolean=} sanityChecks Whether to check for validity
* @returns {SerializedGame}
*/
generateDumpFromGameRoot(root, sanityChecks = true) {
// Finalize particles before saving (Like granting destroy indicator rewards)
// root.particleMgr.finalizeBeforeSave();
// root.uiParticleMgr.finalizeBeforeSave();
// Now store generic savegame payload
const data = /** @type {SerializedGame} */ ({
camera: root.camera.serialize(),
time: root.time.serialize(),
entityMgr: root.entityMgr.serialize(),
entities: {},
});
// 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);
if (!G_IS_RELEASE) {
if (sanityChecks) {
// Sanity check
const sanity = this.verifyLogicalErrors(data);
if (!sanity.result) {
logger.error("Created invalid savegame:", sanity.reason, "savegame:", data);
return null;
}
}
}
return data;
}
/**
* Verifies if there are logical errors in the savegame
* @param {SerializedGame} savegame
* @returns {ExplainedResult}
*/
verifyLogicalErrors(savegame) {
if (!savegame.entities) {
return ExplainedResult.bad("Savegame has no entities");
}
const seenUids = [];
// Check for duplicate UIDS
for (const entityListId in savegame.entities) {
for (let i = 0; i < savegame.entities[entityListId].length; ++i) {
const list = savegame.entities[entityListId][i];
for (let k = 0; k < list.length; ++k) {
const entity = list[k];
const uid = entity.uid;
if (!Number.isInteger(uid)) {
return ExplainedResult.bad("Entity has invalid uid: " + uid);
}
if (seenUids.indexOf(uid) >= 0) {
return ExplainedResult.bad("Duplicate uid " + uid);
}
seenUids.push(uid);
// Verify components
if (!entity.components) {
return ExplainedResult.bad(
"Entity is missing key 'components': " + JSON_stringify(entity)
);
}
const components = entity.components;
for (const componentId in components) {
// Verify component data
const componentData = components[componentId];
const componentClass = gComponentRegistry.findById(componentId);
// Check component id is known
if (!componentClass) {
return ExplainedResult.bad("Unknown component id: " + componentId);
}
// Check component data is ok
const componentVerifyError = /** @type {typeof Component} */ (componentClass).verify(
componentData
);
if (componentVerifyError) {
return ExplainedResult.bad(
"Component " + componentId + " has invalid data: " + componentVerifyError
);
}
}
}
}
}
return ExplainedResult.good();
}
/**
* Tries to load the savegame from a given dump
* @param {SerializedGame} savegame
* @param {GameRoot} root
* @returns {ExplainedResult}
*/
deserialize(savegame, root) {
// Sanity
const verifyResult = this.verifyLogicalErrors(savegame);
if (!verifyResult.result) {
return ExplainedResult.bad(verifyResult.reason);
}
let errorReason = null;
// entities
errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr);
// resources
errorReason =
errorReason ||
this.internal.deserializeEntityArrayFixedType(
root,
savegame.entities.resources,
this.internal.deserializeResource
);
// buildings
errorReason =
errorReason ||
this.internal.deserializeEntityArray(
root,
savegame.entities.buildings,
this.internal.deserializeBuilding
);
// other stuff
errorReason = errorReason || root.time.deserialize(savegame.time);
errorReason = errorReason || root.camera.deserialize(savegame.camera);
// Check for errors
if (errorReason) {
return ExplainedResult.bad(errorReason);
}
return ExplainedResult.good();
}
/////////// MIGRATION HELPERS ///////////
/**
* Performs a function on each component (useful to add / remove / alter properties for migration)
* @param {SerializedGame} savegame
* @param {typeof Component} componentHandle
* @param {function} modifier
*/
migration_migrateComponent(savegame, componentHandle, modifier) {
const targetId = componentHandle.getId();
for (const entityListId in savegame.entities) {
for (let i = 0; i < savegame.entities[entityListId].length; ++i) {
const list = savegame.entities[entityListId][i];
for (let k = 0; k < list.length; ++k) {
const entity = list[k];
const components = entity.components;
if (components[targetId]) {
modifier(components[targetId]);
}
}
}
}
}
/**
* Performs an operation on each object which is a PooledObject (usually Projectiles). Useful to
* perform migrations
* @param {Array<any>} pools
* @param {string} targetClassKey
* @param {function} modifier
*/
migration_migrateGenericObjectPool(pools, targetClassKey, modifier) {
for (let i = 0; i < pools.length; ++i) {
const pool = pools[i];
if (pool.key === targetClassKey) {
const entries = pool.data.entries;
for (const uid in entries) {
modifier(entries[uid]);
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* @typedef {{
* buildingsPlaced: number
* }} SavegameStats
*/
/**
* @typedef {{
* x: number,
* y: number,
* uid: number,
* key: string
* }} SerializedMapResource
*/
/**
* @typedef {{
* camera: any,
* time: any,
* entityMgr: any,
* entities: {
* resources: Array<SerializedMapResource>,
* buildings: Array<any>
* }
* }} SerializedGame
*/
/**
* @typedef {{
* version: number,
* dump: SerializedGame,
* stats: SavegameStats,
* lastUpdate: number
* }} SavegameData
*/

View File

@@ -0,0 +1,13 @@
import { BaseSavegameInterface } from "../savegame_interface.js";
const schema = require("./1000.json");
export class SavegameInterface_V1000 extends BaseSavegameInterface {
getVersion() {
return 1000;
}
getSchemaUncached() {
return schema;
}
}

View File

@@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}

View File

@@ -0,0 +1,330 @@
import { JSON_stringify } from "../core/builtins";
import {
BaseDataType,
TypeArray,
TypeBoolean,
TypeClass,
TypeClassId,
TypeEntity,
TypeEntityWeakref,
TypeEnum,
TypeFixedClass,
TypeInteger,
TypeKeyValueMap,
TypeMetaClass,
TypeNullable,
TypeNumber,
TypePair,
TypePositiveInteger,
TypePositiveNumber,
TypeString,
TypeVector,
TypeClassFromMetaclass,
TypeClassData,
} from "./serialization_data_types";
import { createLogger } from "../core/logging";
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(),
/**
* @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 {Array<string>} values
*/
enum(values) {
return new TypeEnum(values);
},
/**
* @param {FactoryTemplate<*>} registry
*/
obj(registry) {
return new TypeClass(registry);
},
/**
* @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 {SingletonFactoryTemplate<*>} innerType
*/
classRef(registry) {
return new TypeMetaClass(registry);
},
/**
* @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>} 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} */
serialize() {
return serializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema()
);
}
/** @returns {string|void} */
deserialize(data) {
return deserializeSchema(
this,
/** @type {typeof BasicSerializableObject} */ (this.constructor).getCachedSchema(),
data
);
}
/** @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: " + JSON_stringify(schema) + " / " + key);
}
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
* @returns {string|void} String error code or nothing on success
*/
export function deserializeSchema(obj, schema, data, baseclassErrorResult = null) {
if (baseclassErrorResult) {
return baseclassErrorResult;
}
if (!data) {
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);
if (errorStatus) {
error(
"serialization",
"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)) {
error("verify", "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)) {
error("verify", "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) {
error("verify", 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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,180 @@
/* typehints:start */
import { GameRoot } from "../game/root";
/* typehints:end */
import { Vector } from "../core/vector";
import { createLogger } from "../core/logging";
import { gMetaBuildingRegistry } from "../core/global_registries";
import { Entity } from "../game/entity";
const logger = createLogger("serializer_internal");
// Internal serializer methods
export class SerializerInternal {
constructor() {}
/**
* Serializes an array of entities
* @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];
if (!entity.queuedForDestroy && !entity.destroyed) {
serialized.push(entity.serialize());
}
}
return serialized;
}
/**
*
* @param {GameRoot} root
* @param {Array<any>} array
* @param {function(GameRoot, { $: string, data: object }):string|void} deserializerMethod
* @returns {string|void}
*/
deserializeEntityArray(root, array, deserializerMethod) {
for (let i = 0; i < array.length; ++i) {
const errorState = deserializerMethod.call(this, root, array[i]);
if (errorState) {
return errorState;
}
}
return null;
}
/**
*
* @param {GameRoot} root
* @param {Array<any>} array
* @param {function(GameRoot, object):string|void} deserializerMethod
* @returns {string|void}
*/
deserializeEntityArrayFixedType(root, array, deserializerMethod) {
for (let i = 0; i < array.length; ++i) {
const errorState = deserializerMethod.call(this, root, array[i]);
if (errorState) {
return errorState;
}
}
return null;
}
/**
* Deserializes a building
* @param {GameRoot} root
* @param {{ $: string, data: any }} payload
*/
deserializeBuilding(root, payload) {
const data = payload.data;
const id = payload.$;
if (!gMetaBuildingRegistry.hasId(id)) {
return "Metaclass not found for building: '" + id + "'";
}
const meta = gMetaBuildingRegistry.findById(id);
if (!meta) {
return "Metaclass not found for building: '" + id + "'";
}
const tile = new Vector(data.x, data.y).toTileSpace();
const instance = root.logic.internalPlaceBuildingLocalClientOnly({
tile: tile,
metaBuilding: meta,
uid: data.uid,
});
// Apply component specific properties
const errorStatus = this.deserializeComponents(instance, data.components);
if (errorStatus) {
return errorStatus;
}
// Apply enhancements
instance.updateEnhancements();
}
/**
* Deserializes a blueprint
* @param {GameRoot} root
* @param {any} data
* @returns {string|void}
*/
deserializeBlueprint(root, data) {
const id = data.meta;
const metaClass = gMetaBuildingRegistry.findById(id);
if (!metaClass) {
return "Metaclass not found for blueprint: '" + id + "'";
}
const tile = new Vector(data.x, data.y).toTileSpace();
const instance = root.logic.internalPlaceBlueprintLocalClientOnly({
tile: tile,
metaBuilding: metaClass,
uid: data.uid,
});
return this.deserializeComponents(instance, data.components);
}
/////// COMPONENTS ////
/**
* Deserializes components of an entity
* @param {Entity} entity
* @param {Object.<string, any>} data
* @returns {string|void}
*/
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);
if (errorStatus) {
return errorStatus;
}
}
}
/**
* Deserializes a resource
* @param {GameRoot} root
* @param {object} data
* @returns {string|void}
*/
deserializeResource(root, data) {
const id = data.key;
const instance = new MapResource(root, this.neutralFaction, id);
root.logic.internalPlaceMapEntityLocalClientOnly(
new Vector(data.x, data.y).toTileSpace(),
instance,
data.uid
);
}
}