From 42c569d91f3898f20c69ce86cb6fb833b6a8f863 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 18:24:02 +0200 Subject: [PATCH] Implement saving and restoring belt paths --- src/js/game/belt_path.js | 106 ++++++-- src/js/game/components/belt.js | 57 +--- src/js/game/hud/parts/color_blind_helper.js | 6 +- src/js/game/systems/belt.js | 253 +++--------------- src/js/savegame/savegame.js | 8 +- .../savegame/savegame_interface_registry.js | 2 + src/js/savegame/savegame_serializer.js | 2 + src/js/savegame/savegame_typedefs.js | 7 +- src/js/savegame/schemas/1005.js | 29 ++ src/js/savegame/schemas/1005.json | 5 + 10 files changed, 181 insertions(+), 294 deletions(-) create mode 100644 src/js/savegame/schemas/1005.js create mode 100644 src/js/savegame/schemas/1005.json diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index ac6a74ab..71d268ff 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -8,6 +8,8 @@ import { BaseItem } from "./base_item"; import { Entity } from "./entity"; import { GameRoot } from "./root"; import { Rectangle } from "../core/rectangle"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { gItemRegistry } from "../core/global_registries"; const logger = createLogger("belt_path"); @@ -20,12 +22,76 @@ const DEBUG = G_IS_DEV && false; /** * Stores a path of belts, used for optimizing performance */ -export class BeltPath { +export class BeltPath extends BasicSerializableObject { + static getId() { + return "BeltPath"; + } + + static getSchema() { + return { + entityPath: types.array(types.entity), + items: types.array(types.pair(types.ufloat, types.obj(gItemRegistry))), + spacingToFirstItem: types.ufloat, + }; + } + + /** + * Creates a path from a serialized object + * @param {GameRoot} root + * @param {Object} data + * @returns {BeltPath|string} + */ + static fromSerialized(root, data) { + // Create fake object which looks like a belt path but skips the constructor + const fakeObject = /** @type {BeltPath} */ (Object.create(BeltPath.prototype)); + fakeObject.root = root; + + // Deserialize the data + const errorCodeDeserialize = fakeObject.deserialize(data); + if (errorCodeDeserialize) { + return errorCodeDeserialize; + } + + // Compute other properties + fakeObject.init(false); + + return fakeObject; + } + + /** + * Initializes the path by computing the properties which are not saved + * @param {boolean} computeSpacing Whether to also compute the spacing + */ + init(computeSpacing = true) { + // Find acceptor and ejector + this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + this.initialBeltComponent = this.entityPath[0].components.Belt; + + this.totalLength = this.computeTotalLength(); + + if (computeSpacing) { + this.spacingToFirstItem = this.totalLength; + } + + /** + * Current bounds of this path + * @type {Rectangle} + */ + this.worldBounds = this.computeBounds(); + + // Connect the belts + for (let i = 0; i < this.entityPath.length; ++i) { + this.entityPath[i].components.Belt.assignedPath = this; + } + } + /** * @param {GameRoot} root * @param {Array} entityPath */ constructor(root, entityPath) { + super(); this.root = root; assert(entityPath.length > 0, "invalid entity path"); @@ -42,25 +108,7 @@ export class BeltPath { * Stores the spacing to the first item */ - // Find acceptor and ejector - - this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; - this.ejectorSlot = this.ejectorComp.slots[0]; - this.initialBeltComponent = this.entityPath[0].components.Belt; - - this.totalLength = this.computeTotalLength(); - this.spacingToFirstItem = this.totalLength; - - /** - * Current bounds of this path - * @type {Rectangle} - */ - this.worldBounds = this.computeBounds(); - - // Connect the belts - for (let i = 0; i < this.entityPath.length; ++i) { - this.entityPath[i].components.Belt.assignedPath = this; - } + this.init(); this.debug_checkIntegrity("constructor"); } @@ -86,6 +134,16 @@ export class BeltPath { return false; } + /** + * SLOW / Tries to find the item closest to the given tile + * @param {Vector} tile + * @returns {BaseItem|null} + */ + findItemAtTile(tile) { + // TODO: This breaks color blind mode otherwise + return null; + } + /** * Computes the tile bounds of the path * @returns {Rectangle} @@ -113,7 +171,7 @@ export class BeltPath { * Checks if this path is valid */ debug_checkIntegrity(currentChange = "change") { - if (!G_IS_DEV || !DEBUG) { + if (!G_IS_DEV) { return; } @@ -126,7 +184,7 @@ export class BeltPath { // Check for mismatching length const totalLength = this.computeTotalLength(); - if (!epsilonCompare(this.totalLength, totalLength)) { + if (!epsilonCompare(this.totalLength, totalLength, 0.01)) { return this.debug_failIntegrity( currentChange, "Total length mismatch, stored =", @@ -200,7 +258,7 @@ export class BeltPath { } // Check distance if empty - if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength)) { + if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength, 0.01)) { return fail( currentChange, "Path is empty but spacing to first item (", @@ -230,7 +288,7 @@ export class BeltPath { } // Check the total sum matches - if (!epsilonCompare(currentPos, this.totalLength)) { + if (!epsilonCompare(currentPos, this.totalLength, 0.01)) { return fail( "total sum (", currentPos, diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index 4d5fa16c..75ba27d5 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -1,12 +1,9 @@ -import { Component } from "../component"; +import { Math_cos, Math_PI, Math_sin } from "../../core/builtins"; +import { enumDirection, Vector } from "../../core/vector"; import { types } from "../../savegame/serialization"; -import { gItemRegistry } from "../../core/global_registries"; -import { BaseItem } from "../base_item"; -import { Vector, enumDirection } from "../../core/vector"; -import { Math_PI, Math_sin, Math_cos } from "../../core/builtins"; -import { globalConfig } from "../../core/config"; -import { Entity } from "../entity"; import { BeltPath } from "../belt_path"; +import { Component } from "../component"; +import { Entity } from "../entity"; export const curvedBeltLength = /* Math_PI / 4 */ 0.78; @@ -19,7 +16,6 @@ export class BeltComponent extends Component { // The followUpCache field is not serialized. return { direction: types.string, - sortedItems: types.array(types.pair(types.float, types.obj(gItemRegistry))), }; } @@ -37,9 +33,6 @@ export class BeltComponent extends Component { this.direction = direction; - /** @type {Array<[number, BaseItem]>} */ - this.sortedItems = []; - /** @type {Entity} */ this.followUpCache = null; @@ -85,46 +78,4 @@ export class BeltComponent extends Component { return new Vector(0, 0); } } - - /** - * Returns if the belt can currently accept an item from the given direction - */ - canAcceptItem() { - const firstItem = this.sortedItems[0]; - if (!firstItem) { - return true; - } - - return firstItem[0] > globalConfig.itemSpacingOnBelts; - } - - /** - * Pushes a new item to the belt - * @param {BaseItem} item - */ - takeItem(item, leftoverProgress = 0.0) { - if (G_IS_DEV) { - assert( - this.sortedItems.length === 0 || - leftoverProgress <= this.sortedItems[0][0] - globalConfig.itemSpacingOnBelts + 0.001, - "Invalid leftover: " + - leftoverProgress + - " items are " + - this.sortedItems.map(item => item[0]) - ); - assert(leftoverProgress < 1.0, "Invalid leftover: " + leftoverProgress); - } - this.sortedItems.unshift([leftoverProgress, item]); - } - - /** - * Returns how much space there is to the first item - */ - getDistanceToFirstItemCenter() { - const firstItem = this.sortedItems[0]; - if (!firstItem) { - return 1; - } - return firstItem[0]; - } } diff --git a/src/js/game/hud/parts/color_blind_helper.js b/src/js/game/hud/parts/color_blind_helper.js index 4e6a0229..7e79fa1e 100644 --- a/src/js/game/hud/parts/color_blind_helper.js +++ b/src/js/game/hud/parts/color_blind_helper.js @@ -48,9 +48,9 @@ export class HUDColorBlindHelper extends BaseHUDPart { // Check if the belt has a color item if (beltComp) { - const firstItem = beltComp.sortedItems[0]; - if (firstItem && firstItem[1] instanceof ColorItem) { - return firstItem[1].color; + const item = beltComp.assignedPath.findItemAtTile(tile); + if (item && item instanceof ColorItem) { + return item.color; } } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 456aefd7..638351f5 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -51,15 +51,51 @@ export class BeltSystem extends GameSystemWithFilter { this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this); this.root.signals.entityAdded.add(this.onEntityAdded, this); - this.root.signals.postLoadHook.add(this.computeBeltCache, this); // /** @type {Rectangle} */ // this.areaToRecompute = null; /** @type {Array} */ this.beltPaths = []; + } - this.recomputePaths = true; + /** + * Serializes all belt paths + */ + serializePaths() { + let data = []; + for (let i = 0; i < this.beltPaths.length; ++i) { + data.push(this.beltPaths[i].serialize()); + } + return data; + } + + /** + * Deserializes all belt paths + * @param {Array} data + */ + deserializePaths(data) { + if (!Array.isArray(data)) { + return "Belt paths are not an array: " + typeof data; + } + + for (let i = 0; i < data.length; ++i) { + const path = BeltPath.fromSerialized(this.root, data[i]); + if (!(path instanceof BeltPath)) { + return "Failed to create path from belt data: " + path; + } + + this.beltPaths.push(path); + } + + if (this.beltPaths.length === 0) { + logger.warn("Recomputing belt paths (most likely the savegame is old)"); + this.recomputeAllBeltPaths(); + } else { + logger.warn("Restored", this.beltPaths.length, "belt paths"); + } + + this.verifyBeltPaths(); } /** @@ -76,7 +112,6 @@ export class BeltSystem extends GameSystemWithFilter { return; } - // this.recomputePaths = true; /* const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); @@ -236,7 +271,7 @@ export class BeltSystem extends GameSystemWithFilter { * Verifies all belt paths */ verifyBeltPaths() { - if (G_IS_DEV && false) { + if (G_IS_DEV && true) { for (let i = 0; i < this.beltPaths.length; ++i) { this.beltPaths[i].debug_checkIntegrity("general-verify"); } @@ -327,58 +362,11 @@ export class BeltSystem extends GameSystemWithFilter { return null; } - /** - * Recomputes the belt cache - */ - computeBeltCache() { - this.recomputePaths = false; - /* - if (this.areaToRecompute) { - logger.log("Updating belt cache by updating area:", this.areaToRecompute); - - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange( - "belt-area", - this.areaToRecompute, - "#00fff6" - ); - } - - for (let x = this.areaToRecompute.x; x < this.areaToRecompute.right(); ++x) { - for (let y = this.areaToRecompute.y; y < this.areaToRecompute.bottom(); ++y) { - const tile = this.root.map.getTileContentXY(x, y); - if (tile && tile.components.Belt) { - tile.components.Belt.followUpCache = this.findFollowUpEntity(tile); - } - } - } - - // Reset stale areas afterwards - this.areaToRecompute = null; - } else { - logger.log("Doing full belt recompute"); - - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange( - "", - new Rectangle(-1000, -1000, 2000, 2000), - "#00fff6" - ); - } - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - entity.components.Belt.followUpCache = this.findFollowUpEntity(entity); - } - } - */ - this.computeBeltPaths(); - } - /** * Computes the belt path network */ - computeBeltPaths() { + recomputeAllBeltPaths() { + logger.warn("Recomputing all belt paths"); const visitedUids = new Set(); const result = []; @@ -429,10 +417,6 @@ export class BeltSystem extends GameSystemWithFilter { } update() { - if (this.recomputePaths) { - this.computeBeltCache(); - } - this.verifyBeltPaths(); for (let i = 0; i < this.beltPaths.length; ++i) { @@ -440,123 +424,6 @@ export class BeltSystem extends GameSystemWithFilter { } this.verifyBeltPaths(); - - return; - /* - - // Divide by item spacing on belts since we use throughput and not speed - let beltSpeed = - this.root.hubGoals.getBeltBaseSpeed() * - this.root.dynamicTickrate.deltaSeconds * - globalConfig.itemSpacingOnBelts; - - if (G_IS_DEV && globalConfig.debug.instantBelts) { - beltSpeed *= 100; - } - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - - const beltComp = entity.components.Belt; - const items = beltComp.sortedItems; - - if (items.length === 0) { - // Fast out for performance - continue; - } - - const ejectorComp = entity.components.ItemEjector; - let maxProgress = 1; - - // PERFORMANCE OPTIMIZATION - // Original: - // const isCurrentlyEjecting = ejectorComp.isAnySlotEjecting(); - // Replaced (Since belts always have just one slot): - const ejectorSlot = ejectorComp.slots[0]; - const isCurrentlyEjecting = ejectorSlot.item; - - // When ejecting, we can not go further than the item spacing since it - // will be on the corner - if (isCurrentlyEjecting) { - maxProgress = 1 - globalConfig.itemSpacingOnBelts; - } else { - // Otherwise our progress depends on the follow up - if (beltComp.followUpCache) { - const spacingOnBelt = beltComp.followUpCache.components.Belt.getDistanceToFirstItemCenter(); - maxProgress = Math.min(2, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt); - - // Useful check, but hurts performance - // assert(maxProgress >= 0.0, "max progress < 0 (I) (" + maxProgress + ")"); - } - } - - let speedMultiplier = 1; - if (beltComp.direction !== enumDirection.top) { - // Curved belts are shorter, thus being quicker (Looks weird otherwise) - speedMultiplier = SQRT_2; - } - - // How much offset we add when transferring to a new belt - // This substracts one tick because the belt will be updated directly - // afterwards anyways - const takeoverOffset = 1.0 + beltSpeed * speedMultiplier; - - // Not really nice. haven't found the reason for this yet. - if (items.length > 2 / globalConfig.itemSpacingOnBelts) { - beltComp.sortedItems = []; - } - - for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) { - const progressAndItem = items[itemIndex]; - - progressAndItem[0] = Math.min(maxProgress, progressAndItem[0] + speedMultiplier * beltSpeed); - assert(progressAndItem[0] >= 0, "Bad progress: " + progressAndItem[0]); - - if (progressAndItem[0] >= 1.0) { - if (beltComp.followUpCache) { - const followUpBelt = beltComp.followUpCache.components.Belt; - if (followUpBelt.canAcceptItem()) { - followUpBelt.takeItem( - progressAndItem[1], - Math_max(0, progressAndItem[0] - takeoverOffset) - ); - items.splice(itemIndex, 1); - } else { - // Well, we couldn't really take it to a follow up belt, keep it at - // max progress - progressAndItem[0] = 1.0; - maxProgress = 1 - globalConfig.itemSpacingOnBelts; - } - } else { - // Try to give this item to a new belt - - // PERFORMANCE OPTIMIZATION - - // Original: - // const freeSlot = ejectorComp.getFirstFreeSlot(); - - // Replaced - if (ejectorSlot.item) { - // So, we don't have a free slot - damned! - progressAndItem[0] = 1.0; - maxProgress = 1 - globalConfig.itemSpacingOnBelts; - } else { - // We got a free slot, remove this item and keep it on the ejector slot - if (!ejectorComp.tryEject(0, progressAndItem[1])) { - assert(false, "Ejection failed"); - } - items.splice(itemIndex, 1); - - // NOTICE: Do not override max progress here at all, this leads to issues - } - } - } else { - // We just moved this item forward, so determine the maximum progress of other items - maxProgress = Math.max(0, progressAndItem[0] - globalConfig.itemSpacingOnBelts); - } - } - } - */ } /** @@ -598,44 +465,6 @@ export class BeltSystem extends GameSystemWithFilter { 1; } - /** - * @param {DrawParameters} parameters - * @param {Entity} entity - */ - drawEntityItems(parameters, entity) { - /* - const beltComp = entity.components.Belt; - const staticComp = entity.components.StaticMapEntity; - - const items = beltComp.sortedItems; - - if (items.length === 0) { - // Fast out for performance - return; - } - - if (!staticComp.shouldBeDrawn(parameters)) { - return; - } - - for (let i = 0; i < items.length; ++i) { - const itemAndProgress = items[i]; - - // Nice would be const [pos, item] = itemAndPos; but that gets polyfilled and is super slow then - const progress = itemAndProgress[0]; - const item = itemAndProgress[1]; - - const position = staticComp.applyRotationToVector(beltComp.transformBeltToLocalSpace(progress)); - - item.draw( - (staticComp.origin.x + position.x + 0.5) * globalConfig.tileSize, - (staticComp.origin.y + position.y + 0.5) * globalConfig.tileSize, - parameters - ); - } - */ - } - /** * Draws the belt parameters * @param {DrawParameters} parameters diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 359a48b5..d5395dce 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -14,6 +14,7 @@ import { SavegameInterface_V1001 } from "./schemas/1001"; import { SavegameInterface_V1002 } from "./schemas/1002"; import { SavegameInterface_V1003 } from "./schemas/1003"; import { SavegameInterface_V1004 } from "./schemas/1004"; +import { SavegameInterface_V1005 } from "./schemas/1005"; const logger = createLogger("savegame"); @@ -45,7 +46,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1004; + return 1005; } /** @@ -104,6 +105,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1004; } + if (data.version === 1004) { + SavegameInterface_V1005.migrate1004to1005(data); + data.version = 1005; + } + return ExplainedResult.good(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 6144ca62..fb1df52f 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -5,6 +5,7 @@ import { SavegameInterface_V1001 } from "./schemas/1001"; import { SavegameInterface_V1002 } from "./schemas/1002"; import { SavegameInterface_V1003 } from "./schemas/1003"; import { SavegameInterface_V1004 } from "./schemas/1004"; +import { SavegameInterface_V1005 } from "./schemas/1005"; /** @type {Object.} */ export const savegameInterfaces = { @@ -13,6 +14,7 @@ export const savegameInterfaces = { 1002: SavegameInterface_V1002, 1003: SavegameInterface_V1003, 1004: SavegameInterface_V1004, + 1005: SavegameInterface_V1005, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 52a59528..59675668 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -40,6 +40,7 @@ export class SavegameSerializer { hubGoals: root.hubGoals.serialize(), pinnedShapes: root.hud.parts.pinnedShapes.serialize(), waypoints: root.hud.parts.waypoints.serialize(), + beltPaths: root.systemMgr.systems.belt.serializePaths(), }; data.entities = this.internal.serializeEntityArray(root.entityMgr.entities); @@ -140,6 +141,7 @@ export class SavegameSerializer { errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); + errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); // Check for errors if (errorReason) { diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index 6211150f..642865cd 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -5,6 +5,10 @@ import { Entity } from "../game/entity"; * }} SavegameStats */ +/** + * + */ + /** * @typedef {{ * camera: any, @@ -14,7 +18,8 @@ import { Entity } from "../game/entity"; * hubGoals: any, * pinnedShapes: any, * waypoints: any, - * entities: Array + * entities: Array, + * beltPaths: Array * }} SerializedGame */ diff --git a/src/js/savegame/schemas/1005.js b/src/js/savegame/schemas/1005.js new file mode 100644 index 00000000..f86a280d --- /dev/null +++ b/src/js/savegame/schemas/1005.js @@ -0,0 +1,29 @@ +import { createLogger } from "../../core/logging.js"; +import { SavegameInterface_V1004 } from "./1004.js"; + +const schema = require("./1005.json"); +const logger = createLogger("savegame_interface/1005"); + +export class SavegameInterface_V1005 extends SavegameInterface_V1004 { + getVersion() { + return 1005; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1004to1005(data) { + logger.log("Migrating 1004 to 1005"); + const dump = data.dump; + if (!dump) { + return true; + } + + // just reset belt paths for now + dump.beltPaths = []; + } +} diff --git a/src/js/savegame/schemas/1005.json b/src/js/savegame/schemas/1005.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1005.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +}