Improve (rendering-) performance in DEV mode

This commit is contained in:
tobspr 2020-09-18 19:51:15 +02:00
parent 0238de1260
commit bba29b8a8b
5 changed files with 542 additions and 532 deletions

View File

@ -1,252 +1,243 @@
import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils"; import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils";
import { Component } from "./component"; import { Component } from "./component";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { Entity } from "./entity"; import { Entity } from "./entity";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config";
const logger = createLogger("entity_manager");
const logger = createLogger("entity_manager");
// Manages all entities
// Manages all entities
// NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order
// This is slower but we need it for the street path generation // NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order
// This is slower but we need it for the street path generation
export class EntityManager extends BasicSerializableObject {
constructor(root) { export class EntityManager extends BasicSerializableObject {
super(); constructor(root) {
super();
/** @type {GameRoot} */
this.root = root; /** @type {GameRoot} */
this.root = root;
/** @type {Array<Entity>} */
this.entities = []; /** @type {Array<Entity>} */
this.entities = [];
// We store a seperate list with entities to destroy, since we don't destroy
// them instantly // We store a seperate list with entities to destroy, since we don't destroy
/** @type {Array<Entity>} */ // them instantly
this.destroyList = []; /** @type {Array<Entity>} */
this.destroyList = [];
// Store a map from componentid to entities - This is used by the game system
// for faster processing // Store a map from componentid to entities - This is used by the game system
/** @type {Object.<string, Array<Entity>>} */ // for faster processing
this.componentToEntity = newEmptyMap(); /** @type {Object.<string, Array<Entity>>} */
this.componentToEntity = newEmptyMap();
// Store the next uid to use
this.nextUid = 10000; // Store the next uid to use
} this.nextUid = 10000;
}
static getId() {
return "EntityManager"; static getId() {
} return "EntityManager";
}
static getSchema() {
return { static getSchema() {
nextUid: types.uint, return {
}; nextUid: types.uint,
} };
}
getStatsText() {
return this.entities.length + " entities [" + this.destroyList.length + " to kill]"; getStatsText() {
} return this.entities.length + " entities [" + this.destroyList.length + " to kill]";
}
// Main update
update() { // Main update
this.processDestroyList(); update() {
} this.processDestroyList();
}
/**
* Registers a new entity /**
* @param {Entity} entity * Registers a new entity
* @param {number=} uid Optional predefined uid * @param {Entity} entity
*/ * @param {number=} uid Optional predefined uid
registerEntity(entity, uid = null) { */
assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`); registerEntity(entity, uid = null) {
assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`); if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`);
if (G_IS_DEV && uid !== null) { }
assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid); assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`);
}
if (G_IS_DEV && uid !== null) {
if (uid !== null) { assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid);
assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid); }
}
if (uid !== null) {
this.entities.push(entity); assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid);
}
// Register into the componentToEntity map
for (const componentId in entity.components) { this.entities.push(entity);
if (entity.components[componentId]) {
if (this.componentToEntity[componentId]) { // Register into the componentToEntity map
this.componentToEntity[componentId].push(entity); for (const componentId in entity.components) {
} else { if (entity.components[componentId]) {
this.componentToEntity[componentId] = [entity]; if (this.componentToEntity[componentId]) {
} this.componentToEntity[componentId].push(entity);
} } else {
} this.componentToEntity[componentId] = [entity];
}
// Give each entity a unique id }
entity.uid = uid ? uid : this.generateUid(); }
entity.registered = true;
// Give each entity a unique id
this.root.signals.entityAdded.dispatch(entity); entity.uid = uid ? uid : this.generateUid();
} entity.registered = true;
/** this.root.signals.entityAdded.dispatch(entity);
* Sorts all entitiy lists after a resync }
*/
sortEntityLists() { /**
this.entities.sort((a, b) => a.uid - b.uid); * Generates a new uid
this.destroyList.sort((a, b) => a.uid - b.uid); * @returns {number}
*/
for (const key in this.componentToEntity) { generateUid() {
this.componentToEntity[key].sort((a, b) => a.uid - b.uid); return this.nextUid++;
} }
}
/**
/** * Call to attach a new component after the creation of the entity
* Generates a new uid * @param {Entity} entity
* @returns {number} * @param {Component} component
*/ */
generateUid() { attachDynamicComponent(entity, component) {
return this.nextUid++; entity.addComponent(component, true);
} const componentId = /** @type {typeof Component} */ (component.constructor).getId();
if (this.componentToEntity[componentId]) {
/** this.componentToEntity[componentId].push(entity);
* Call to attach a new component after the creation of the entity } else {
* @param {Entity} entity this.componentToEntity[componentId] = [entity];
* @param {Component} component }
*/ this.root.signals.entityGotNewComponent.dispatch(entity);
attachDynamicComponent(entity, component) { }
entity.addComponent(component, true);
const componentId = /** @type {typeof Component} */ (component.constructor).getId(); /**
if (this.componentToEntity[componentId]) { * Call to remove a component after the creation of the entity
this.componentToEntity[componentId].push(entity); * @param {Entity} entity
} else { * @param {typeof Component} component
this.componentToEntity[componentId] = [entity]; */
} removeDynamicComponent(entity, component) {
this.root.signals.entityGotNewComponent.dispatch(entity); entity.removeComponent(component, true);
} const componentId = /** @type {typeof Component} */ (component.constructor).getId();
/** fastArrayDeleteValue(this.componentToEntity[componentId], entity);
* Call to remove a component after the creation of the entity this.root.signals.entityComponentRemoved.dispatch(entity);
* @param {Entity} entity }
* @param {typeof Component} component
*/ /**
removeDynamicComponent(entity, component) { * Finds an entity buy its uid, kinda slow since it loops over all entities
entity.removeComponent(component, true); * @param {number} uid
const componentId = /** @type {typeof Component} */ (component.constructor).getId(); * @param {boolean=} errorWhenNotFound
* @returns {Entity}
fastArrayDeleteValue(this.componentToEntity[componentId], entity); */
this.root.signals.entityComponentRemoved.dispatch(entity); findByUid(uid, errorWhenNotFound = true) {
} const arr = this.entities;
for (let i = 0, len = arr.length; i < len; ++i) {
/** const entity = arr[i];
* Finds an entity buy its uid, kinda slow since it loops over all entities if (entity.uid === uid) {
* @param {number} uid if (entity.queuedForDestroy || entity.destroyed) {
* @param {boolean=} errorWhenNotFound if (errorWhenNotFound) {
* @returns {Entity} logger.warn("Entity with UID", uid, "not found (destroyed)");
*/ }
findByUid(uid, errorWhenNotFound = true) { return null;
const arr = this.entities; }
for (let i = 0, len = arr.length; i < len; ++i) { return entity;
const entity = arr[i]; }
if (entity.uid === uid) { }
if (entity.queuedForDestroy || entity.destroyed) { if (errorWhenNotFound) {
if (errorWhenNotFound) { logger.warn("Entity with UID", uid, "not found");
logger.warn("Entity with UID", uid, "not found (destroyed)"); }
} return null;
return null; }
}
return entity; /**
} * Returns all entities having the given component
} * @param {typeof Component} componentHandle
if (errorWhenNotFound) { * @returns {Array<Entity>} entities
logger.warn("Entity with UID", uid, "not found"); */
} getAllWithComponent(componentHandle) {
return null; return this.componentToEntity[componentHandle.getId()] || [];
} }
/** /**
* Returns all entities having the given component * Return all of a given class. This is SLOW!
* @param {typeof Component} componentHandle * @param {object} entityClass
* @returns {Array<Entity>} entities * @returns {Array<Entity>} entities
*/ */
getAllWithComponent(componentHandle) { getAllOfClass(entityClass) {
return this.componentToEntity[componentHandle.getId()] || []; // FIXME: Slow
} const result = [];
for (let i = 0; i < this.entities.length; ++i) {
/** const entity = this.entities[i];
* Return all of a given class. This is SLOW! if (entity instanceof entityClass) {
* @param {object} entityClass result.push(entity);
* @returns {Array<Entity>} entities }
*/ }
getAllOfClass(entityClass) { return result;
// FIXME: Slow }
const result = [];
for (let i = 0; i < this.entities.length; ++i) { /**
const entity = this.entities[i]; * Unregisters all components of an entity from the component to entity mapping
if (entity instanceof entityClass) { * @param {Entity} entity
result.push(entity); */
} unregisterEntityComponents(entity) {
} for (const componentId in entity.components) {
return result; if (entity.components[componentId]) {
} arrayDeleteValue(this.componentToEntity[componentId], entity);
}
/** }
* Unregisters all components of an entity from the component to entity mapping }
* @param {Entity} entity
*/ // Processes the entities to destroy and actually destroys them
unregisterEntityComponents(entity) { /* eslint-disable max-statements */
for (const componentId in entity.components) { processDestroyList() {
if (entity.components[componentId]) { for (let i = 0; i < this.destroyList.length; ++i) {
arrayDeleteValue(this.componentToEntity[componentId], entity); const entity = this.destroyList[i];
}
} // Remove from entities list
} arrayDeleteValue(this.entities, entity);
// Processes the entities to destroy and actually destroys them // Remove from componentToEntity list
/* eslint-disable max-statements */ this.unregisterEntityComponents(entity);
processDestroyList() {
for (let i = 0; i < this.destroyList.length; ++i) { entity.registered = false;
const entity = this.destroyList[i]; entity.internalDestroyCallback();
// Remove from entities list this.root.signals.entityDestroyed.dispatch(entity);
arrayDeleteValue(this.entities, entity); }
// Remove from componentToEntity list this.destroyList = [];
this.unregisterEntityComponents(entity); }
entity.registered = false; /**
entity.internalDestroyCallback(); * Queues an entity for destruction
* @param {Entity} entity
this.root.signals.entityDestroyed.dispatch(entity); */
} destroyEntity(entity) {
if (entity.destroyed) {
this.destroyList = []; logger.error("Tried to destroy already destroyed entity:", entity.uid);
} return;
}
/**
* Queues an entity for destruction if (entity.queuedForDestroy) {
* @param {Entity} entity logger.error("Trying to destroy entity which is already queued for destroy!", entity.uid);
*/ return;
destroyEntity(entity) { }
if (entity.destroyed) {
logger.error("Tried to destroy already destroyed entity:", entity.uid); if (this.destroyList.indexOf(entity) < 0) {
return; this.destroyList.push(entity);
} entity.queuedForDestroy = true;
this.root.signals.entityQueuedForDestroy.dispatch(entity);
if (entity.queuedForDestroy) { } else {
logger.error("Trying to destroy entity which is already queued for destroy!", entity.uid); assert(false, "Trying to destroy entity twice");
return; }
} }
}
if (this.destroyList.indexOf(entity) < 0) {
this.destroyList.push(entity);
entity.queuedForDestroy = true;
this.root.signals.entityQueuedForDestroy.dispatch(entity);
} else {
assert(false, "Trying to destroy entity twice");
}
}
}

View File

@ -1,131 +1,136 @@
/* typehints:start */ /* typehints:start */
import { Component } from "./component"; import { Component } from "./component";
import { Entity } from "./entity"; import { Entity } from "./entity";
/* typehints:end */ /* typehints:end */
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { GameSystem } from "./game_system"; import { GameSystem } from "./game_system";
import { arrayDelete, arrayDeleteValue } from "../core/utils"; import { arrayDelete, arrayDeleteValue } from "../core/utils";
import { globalConfig } from "../core/config";
export class GameSystemWithFilter extends GameSystem {
/** export class GameSystemWithFilter extends GameSystem {
* Constructs a new game system with the given component filter. It will process /**
* all entities which have *all* of the passed components * Constructs a new game system with the given component filter. It will process
* @param {GameRoot} root * all entities which have *all* of the passed components
* @param {Array<typeof Component>} requiredComponents * @param {GameRoot} root
*/ * @param {Array<typeof Component>} requiredComponents
constructor(root, requiredComponents) { */
super(root); constructor(root, requiredComponents) {
this.requiredComponents = requiredComponents; super(root);
this.requiredComponentIds = requiredComponents.map(component => component.getId()); this.requiredComponents = requiredComponents;
this.requiredComponentIds = requiredComponents.map(component => component.getId());
/**
* All entities which match the current components /**
* @type {Array<Entity>} * All entities which match the current components
*/ * @type {Array<Entity>}
this.allEntities = []; */
this.allEntities = [];
this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this);
this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this); this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this);
this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this); this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this);
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this);
this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this);
this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
this.root.signals.bulkOperationFinished.add(this.refreshCaches, this); this.root.signals.postLoadHook.add(this.internalPostLoadHook, this);
} this.root.signals.bulkOperationFinished.add(this.refreshCaches, this);
}
/**
* @param {Entity} entity /**
*/ * @param {Entity} entity
internalPushEntityIfMatching(entity) { */
for (let i = 0; i < this.requiredComponentIds.length; ++i) { internalPushEntityIfMatching(entity) {
if (!entity.components[this.requiredComponentIds[i]]) { for (let i = 0; i < this.requiredComponentIds.length; ++i) {
return; if (!entity.components[this.requiredComponentIds[i]]) {
} return;
} }
}
assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity);
this.internalRegisterEntity(entity); // This is slow!
} if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity);
/** }
*
* @param {Entity} entity this.internalRegisterEntity(entity);
*/ }
internalCheckEntityAfterComponentRemoval(entity) {
if (this.allEntities.indexOf(entity) < 0) { /**
// Entity wasn't interesting anyways *
return; * @param {Entity} entity
} */
internalCheckEntityAfterComponentRemoval(entity) {
for (let i = 0; i < this.requiredComponentIds.length; ++i) { if (this.allEntities.indexOf(entity) < 0) {
if (!entity.components[this.requiredComponentIds[i]]) { // Entity wasn't interesting anyways
// Entity is not interesting anymore return;
arrayDeleteValue(this.allEntities, entity); }
}
} for (let i = 0; i < this.requiredComponentIds.length; ++i) {
} if (!entity.components[this.requiredComponentIds[i]]) {
// Entity is not interesting anymore
/** arrayDeleteValue(this.allEntities, entity);
* }
* @param {Entity} entity }
*/ }
internalReconsiderEntityToAdd(entity) {
for (let i = 0; i < this.requiredComponentIds.length; ++i) { /**
if (!entity.components[this.requiredComponentIds[i]]) { *
return; * @param {Entity} entity
} */
} internalReconsiderEntityToAdd(entity) {
if (this.allEntities.indexOf(entity) >= 0) { for (let i = 0; i < this.requiredComponentIds.length; ++i) {
return; if (!entity.components[this.requiredComponentIds[i]]) {
} return;
this.internalRegisterEntity(entity); }
} }
if (this.allEntities.indexOf(entity) >= 0) {
refreshCaches() { return;
this.allEntities.sort((a, b) => a.uid - b.uid); }
this.internalRegisterEntity(entity);
// Remove all entities which are queued for destroy }
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; refreshCaches() {
if (entity.queuedForDestroy || entity.destroyed) { this.allEntities.sort((a, b) => a.uid - b.uid);
this.allEntities.splice(i, 1);
} // Remove all entities which are queued for destroy
} for (let i = 0; i < this.allEntities.length; ++i) {
} const entity = this.allEntities[i];
if (entity.queuedForDestroy || entity.destroyed) {
/** this.allEntities.splice(i, 1);
* Recomputes all target entities after the game has loaded }
*/ }
internalPostLoadHook() { }
this.refreshCaches();
} /**
* Recomputes all target entities after the game has loaded
/** */
* internalPostLoadHook() {
* @param {Entity} entity this.refreshCaches();
*/ }
internalRegisterEntity(entity) {
this.allEntities.push(entity); /**
*
if (this.root.gameInitialized && !this.root.bulkOperationRunning) { * @param {Entity} entity
// Sort entities by uid so behaviour is predictable */
this.allEntities.sort((a, b) => a.uid - b.uid); internalRegisterEntity(entity) {
} this.allEntities.push(entity);
}
if (this.root.gameInitialized && !this.root.bulkOperationRunning) {
/** // Sort entities by uid so behaviour is predictable
* this.allEntities.sort((a, b) => a.uid - b.uid);
* @param {Entity} entity }
*/ }
internalPopEntityIfMatching(entity) {
if (this.root.bulkOperationRunning) { /**
// We do this in refreshCaches afterwards *
return; * @param {Entity} entity
} */
const index = this.allEntities.indexOf(entity); internalPopEntityIfMatching(entity) {
if (index >= 0) { if (this.root.bulkOperationRunning) {
arrayDelete(this.allEntities, index); // We do this in refreshCaches afterwards
} return;
} }
} const index = this.allEntities.indexOf(entity);
if (index >= 0) {
arrayDelete(this.allEntities, index);
}
}
}

View File

@ -9,14 +9,30 @@ import { MapChunkView } from "../map_chunk_view";
export class ItemAcceptorSystem extends GameSystemWithFilter { export class ItemAcceptorSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
super(root, [ItemAcceptorComponent]); super(root, [ItemAcceptorComponent]);
// Well ... it's better to be verbose I guess?
this.accumulatedTicksWhileInMapOverview = 0;
} }
update() { update() {
// This system doesn't render anything while in map overview,
// so simply accumulate ticks
if (this.root.camera.getIsMapOverlayActive()) {
++this.accumulatedTicksWhileInMapOverview;
return;
}
// Compute how much ticks we missed
const numTicks = 1 + this.accumulatedTicksWhileInMapOverview;
const progress = const progress =
this.root.dynamicTickrate.deltaSeconds * this.root.dynamicTickrate.deltaSeconds *
2 * 2 *
this.root.hubGoals.getBeltBaseSpeed() * this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts; // * 2 because its only a half tile globalConfig.itemSpacingOnBelts * // * 2 because its only a half tile
numTicks;
// Reset accumulated ticks
this.accumulatedTicksWhileInMapOverview = 0;
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];

View File

@ -149,8 +149,6 @@ export class WiredPinsSystem extends GameSystemWithFilter {
} }
} }
update() {}
/** /**
* Draws a given entity * Draws a given entity
* @param {DrawParameters} parameters * @param {DrawParameters} parameters

View File

@ -1,146 +1,146 @@
import { ExplainedResult } from "../core/explained_result"; import { ExplainedResult } from "../core/explained_result";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { gComponentRegistry } from "../core/global_registries"; import { gComponentRegistry } from "../core/global_registries";
import { SerializerInternal } from "./serializer_internal"; import { SerializerInternal } from "./serializer_internal";
/** /**
* @typedef {import("../game/component").Component} Component * @typedef {import("../game/component").Component} Component
* @typedef {import("../game/component").StaticComponent} StaticComponent * @typedef {import("../game/component").StaticComponent} StaticComponent
* @typedef {import("../game/entity").Entity} Entity * @typedef {import("../game/entity").Entity} Entity
* @typedef {import("../game/root").GameRoot} GameRoot * @typedef {import("../game/root").GameRoot} GameRoot
* @typedef {import("../savegame/savegame_typedefs").SerializedGame} SerializedGame * @typedef {import("../savegame/savegame_typedefs").SerializedGame} SerializedGame
*/ */
const logger = createLogger("savegame_serializer"); const logger = createLogger("savegame_serializer");
/** /**
* Serializes a savegame * Serializes a savegame
*/ */
export class SavegameSerializer { export class SavegameSerializer {
constructor() { constructor() {
this.internal = new SerializerInternal(); this.internal = new SerializerInternal();
} }
/** /**
* Serializes the game root into a dump * Serializes the game root into a dump
* @param {GameRoot} root * @param {GameRoot} root
* @param {boolean=} sanityChecks Whether to check for validity * @param {boolean=} sanityChecks Whether to check for validity
* @returns {object} * @returns {object}
*/ */
generateDumpFromGameRoot(root, sanityChecks = true) { generateDumpFromGameRoot(root, sanityChecks = true) {
/** @type {SerializedGame} */ /** @type {SerializedGame} */
const data = { const data = {
camera: root.camera.serialize(), camera: root.camera.serialize(),
time: root.time.serialize(), time: root.time.serialize(),
map: root.map.serialize(), map: root.map.serialize(),
entityMgr: root.entityMgr.serialize(), entityMgr: root.entityMgr.serialize(),
hubGoals: root.hubGoals.serialize(), hubGoals: root.hubGoals.serialize(),
pinnedShapes: root.hud.parts.pinnedShapes.serialize(), pinnedShapes: root.hud.parts.pinnedShapes.serialize(),
waypoints: root.hud.parts.waypoints.serialize(), waypoints: root.hud.parts.waypoints.serialize(),
entities: this.internal.serializeEntityArray(root.entityMgr.entities), entities: this.internal.serializeEntityArray(root.entityMgr.entities),
beltPaths: root.systemMgr.systems.belt.serializePaths(), beltPaths: root.systemMgr.systems.belt.serializePaths(),
}; };
if (!G_IS_RELEASE) { if (G_IS_DEV) {
if (sanityChecks) { if (sanityChecks) {
// Sanity check // Sanity check
const sanity = this.verifyLogicalErrors(data); const sanity = this.verifyLogicalErrors(data);
if (!sanity.result) { if (!sanity.result) {
logger.error("Created invalid savegame:", sanity.reason, "savegame:", data); logger.error("Created invalid savegame:", sanity.reason, "savegame:", data);
return null; return null;
} }
} }
} }
return data; return data;
} }
/** /**
* Verifies if there are logical errors in the savegame * Verifies if there are logical errors in the savegame
* @param {SerializedGame} savegame * @param {SerializedGame} savegame
* @returns {ExplainedResult} * @returns {ExplainedResult}
*/ */
verifyLogicalErrors(savegame) { verifyLogicalErrors(savegame) {
if (!savegame.entities) { if (!savegame.entities) {
return ExplainedResult.bad("Savegame has no entities"); return ExplainedResult.bad("Savegame has no entities");
} }
const seenUids = []; const seenUids = new Set();
// Check for duplicate UIDS // Check for duplicate UIDS
for (let i = 0; i < savegame.entities.length; ++i) { for (let i = 0; i < savegame.entities.length; ++i) {
/** @type {Entity} */ /** @type {Entity} */
const entity = savegame.entities[i]; const entity = savegame.entities[i];
const uid = entity.uid; const uid = entity.uid;
if (!Number.isInteger(uid)) { if (!Number.isInteger(uid)) {
return ExplainedResult.bad("Entity has invalid uid: " + uid); return ExplainedResult.bad("Entity has invalid uid: " + uid);
} }
if (seenUids.indexOf(uid) >= 0) { if (seenUids.has(uid)) {
return ExplainedResult.bad("Duplicate uid " + uid); return ExplainedResult.bad("Duplicate uid " + uid);
} }
seenUids.push(uid); seenUids.add(uid);
// Verify components // Verify components
if (!entity.components) { if (!entity.components) {
return ExplainedResult.bad("Entity is missing key 'components': " + JSON.stringify(entity)); return ExplainedResult.bad("Entity is missing key 'components': " + JSON.stringify(entity));
} }
const components = entity.components; const components = entity.components;
for (const componentId in components) { for (const componentId in components) {
const componentClass = gComponentRegistry.findById(componentId); const componentClass = gComponentRegistry.findById(componentId);
// Check component id is known // Check component id is known
if (!componentClass) { if (!componentClass) {
return ExplainedResult.bad("Unknown component id: " + componentId); return ExplainedResult.bad("Unknown component id: " + componentId);
} }
// Verify component data // Verify component data
const componentData = components[componentId]; const componentData = components[componentId];
const componentVerifyError = /** @type {StaticComponent} */ (componentClass).verify( const componentVerifyError = /** @type {StaticComponent} */ (componentClass).verify(
componentData componentData
); );
// Check component data is ok // Check component data is ok
if (componentVerifyError) { if (componentVerifyError) {
return ExplainedResult.bad( return ExplainedResult.bad(
"Component " + componentId + " has invalid data: " + componentVerifyError "Component " + componentId + " has invalid data: " + componentVerifyError
); );
} }
} }
} }
return ExplainedResult.good(); return ExplainedResult.good();
} }
/** /**
* Tries to load the savegame from a given dump * Tries to load the savegame from a given dump
* @param {SerializedGame} savegame * @param {SerializedGame} savegame
* @param {GameRoot} root * @param {GameRoot} root
* @returns {ExplainedResult} * @returns {ExplainedResult}
*/ */
deserialize(savegame, root) { deserialize(savegame, root) {
// Sanity // Sanity
const verifyResult = this.verifyLogicalErrors(savegame); const verifyResult = this.verifyLogicalErrors(savegame);
if (!verifyResult.result) { if (!verifyResult.result) {
return ExplainedResult.bad(verifyResult.reason); return ExplainedResult.bad(verifyResult.reason);
} }
let errorReason = null; let errorReason = null;
errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr); errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr);
errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.time.deserialize(savegame.time);
errorReason = errorReason || root.camera.deserialize(savegame.camera); errorReason = errorReason || root.camera.deserialize(savegame.camera);
errorReason = errorReason || root.map.deserialize(savegame.map); errorReason = errorReason || root.map.deserialize(savegame.map);
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals); errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals);
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);
errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths);
// Check for errors // Check for errors
if (errorReason) { if (errorReason) {
return ExplainedResult.bad(errorReason); return ExplainedResult.bad(errorReason);
} }
return ExplainedResult.good(); return ExplainedResult.good();
} }
} }