From 192d1dbedb66092486619d8962acf57b7a7505cc Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 13:57:07 +0200 Subject: [PATCH 1/7] Initial take on belt optimization --- src/js/core/config.js | 2 +- src/js/game/belt_path.js | 638 +++++++++++++++++++++++++ src/js/game/components/belt.js | 24 +- src/js/game/components/item_ejector.js | 13 + src/js/game/core.js | 4 + src/js/game/hud/hud.js | 3 + src/js/game/hud/parts/debug_changes.js | 2 +- src/js/game/logic.js | 1 + src/js/game/systems/belt.js | 260 +++++++++- 9 files changed, 927 insertions(+), 20 deletions(-) create mode 100644 src/js/game/belt_path.js diff --git a/src/js/core/config.js b/src/js/core/config.js index 6825e762..0018b746 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -51,7 +51,7 @@ export const globalConfig = { // Belt speeds // NOTICE: Update webpack.production.config too! beltSpeedItemsPerSecond: 2, - itemSpacingOnBelts: 0.8, + itemSpacingOnBelts: 0.63, minerSpeedItemsPerSecond: 0, // COMPUTED undergroundBeltMaxTilesByTier: [5, 8], diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js new file mode 100644 index 00000000..04b74d11 --- /dev/null +++ b/src/js/game/belt_path.js @@ -0,0 +1,638 @@ +import { globalConfig } from "../core/config"; +import { DrawParameters } from "../core/draw_parameters"; +import { Vector } from "../core/vector"; +import { BaseItem } from "./base_item"; +import { Entity } from "./entity"; +import { GameRoot } from "./root"; +import { round4Digits, epsilonCompare } from "../core/utils"; +import { Math_min } from "../core/builtins"; +import { createLogger, logSection } from "../core/logging"; + +const logger = createLogger("belt_path"); + +// Helpers for more semantic access into interleaved arrays +const NEXT_ITEM_OFFSET_INDEX = 0; +const ITEM_INDEX = 1; + +/** + * Stores a path of belts, used for optimizing performance + */ +export class BeltPath { + /** + * @param {GameRoot} root + * @param {Array} entityPath + */ + constructor(root, entityPath) { + this.root = root; + + assert(entityPath.length > 0, "invalid entity path"); + this.entityPath = entityPath; + + /** + * Stores the items sorted, and their distance to the previous item (or start) + * Layout: [distanceToNext, item] + * @type {Array<[number, BaseItem]>} + */ + this.items = []; + + /** + * 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; + + // Connect the belts + for (let i = 0; i < this.entityPath.length; ++i) { + this.entityPath[i].components.Belt.assignedPath = this; + } + + this.debug_checkIntegrity("constructor"); + } + + /** + * Helper to throw an error on mismatch + * @param {string} change + * @param {Array} reason + */ + debug_failIntegrity(change, ...reason) { + throw new Error("belt path invalid (" + change + "): " + reason.map(i => "" + i).join(" ")); + } + + /** + * Checks if this path is valid + */ + debug_checkIntegrity(currentChange = "change") { + if (!G_IS_DEV) { + return; + } + + const fail = (...args) => this.debug_failIntegrity(currentChange, ...args); + + // Check for empty path + if (this.entityPath.length === 0) { + return fail("Belt path is empty"); + } + + // Check for mismatching length + const totalLength = this.computeTotalLength(); + if (this.totalLength !== totalLength) { + return this.debug_failIntegrity( + currentChange, + "Total length mismatch, stored =", + this.totalLength, + "but correct is", + totalLength + ); + } + + // Check for misconnected entities + for (let i = 0; i < this.entityPath.length - 1; ++i) { + const entity = this.entityPath[i]; + const followUp = this.root.systemMgr.systems.belt.findFollowUpEntity(entity); + if (!followUp) { + return fail( + "Follow up entity for the", + i, + "-th entity (total length", + this.entityPath.length, + ") was null!" + ); + } + if (followUp !== this.entityPath[i + 1]) { + return fail( + "Follow up entity mismatch, stored is", + this.entityPath[i + 1].uid, + "but real one is", + followUp.uid + ); + } + if (entity.components.Belt.assignedPath !== this) { + return fail( + "Entity with uid", + entity.uid, + "doesn't have this path assigned, but this path contains the entity." + ); + } + } + + // Check for right ejector component and slot + if (this.ejectorComp !== this.entityPath[this.entityPath.length - 1].components.ItemEjector) { + return fail("Stale ejectorComp handle"); + } + if (this.ejectorSlot !== this.ejectorComp.slots[0]) { + return fail("Stale ejector slot handle"); + } + if (!this.ejectorComp) { + return fail("Ejector comp not set"); + } + if (!this.ejectorSlot) { + return fail("Ejector slot not set"); + } + if (this.initialBeltComponent !== this.entityPath[0].components.Belt) { + return fail("Stale initial belt component handle"); + } + + // Check spacing + if (this.spacingToFirstItem > this.totalLength + 0.005) { + return fail( + currentChange, + "spacing to first item (", + this.spacingToFirstItem, + ") is greater than total length (", + this.totalLength, + ")" + ); + } + + // Check distance if empty + if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength)) { + return fail( + currentChange, + "Path is empty but spacing to first item (", + this.spacingToFirstItem, + ") does not equal total length (", + this.totalLength, + ")" + ); + } + + // Check items etc + let currentPos = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + + if (item[NEXT_ITEM_OFFSET_INDEX] < 0 || item[NEXT_ITEM_OFFSET_INDEX] > this.totalLength) { + return fail( + "Item has invalid offset to next item: ", + item[0], + "(total length:", + this.totalLength, + ")" + ); + } + + currentPos += item[0]; + } + + // Check the total sum matches + if (!epsilonCompare(currentPos, this.totalLength)) { + return fail( + "total sum (", + currentPos, + ") of first item spacing (", + this.spacingToFirstItem, + ") and items does not match total length (", + this.totalLength, + ")" + ); + } + } + + /** + * Extends the belt path by the given belt + * @param {Entity} entity + */ + extendOnEnd(entity) { + logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin); + + const beltComp = entity.components.Belt; + + // If the last belt has something on its ejector, put that into the path first + const pendingItem = this.ejectorComp.takeSlotItem(0); + if (pendingItem) { + // Ok, so we have a pending item + logger.log("Taking pending item and putting it back on the path"); + this.items.push([0, pendingItem]); + } + + // Append the entity + this.entityPath.push(entity); + + // Extend the path length + const additionalLength = beltComp.getEffectiveLengthTiles(); + this.totalLength += additionalLength; + logger.log(" Extended total length by", additionalLength, "to", this.totalLength); + + // If we have no item, just update the distance to the first item + if (this.items.length === 0) { + this.spacingToFirstItem = this.totalLength; + logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + } else { + // Otherwise, update the next-distance of the last item + const lastItem = this.items[this.items.length - 1]; + logger.log( + " Extended spacing of last item from", + lastItem[NEXT_ITEM_OFFSET_INDEX], + "to", + lastItem[NEXT_ITEM_OFFSET_INDEX] + additionalLength + ); + lastItem[NEXT_ITEM_OFFSET_INDEX] += additionalLength; + } + + // Update handles + this.ejectorComp = entity.components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + + // Assign reference + beltComp.assignedPath = this; + + this.debug_checkIntegrity("extend-on-end"); + } + + /** + * Extends the path with the given entity on the beginning + * @param {Entity} entity + */ + extendOnBeginning(entity) { + const beltComp = entity.components.Belt; + + logger.log("Extending the path on the beginning"); + + // All items on that belt are simply lost (for now) + + const length = beltComp.getEffectiveLengthTiles(); + + // Extend the length of this path + this.totalLength += length; + + // Simply adjust the first item spacing cuz we have no items contained + this.spacingToFirstItem += length; + + // Set handles and append entity + beltComp.assignedPath = this; + this.initialBeltComponent = this.entityPath[0].components.Belt; + this.entityPath.unshift(entity); + + this.debug_checkIntegrity("extend-on-begin"); + } + + /** + * Splits this path at the given entity by removing it, and + * returning the new secondary paht + * @param {Entity} entity + * @returns {BeltPath} + */ + deleteEntityOnPathSplitIntoTwo(entity) { + logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin); + + // First, find where the current path ends + const beltComp = entity.components.Belt; + beltComp.assignedPath = null; + + const entityLength = beltComp.getEffectiveLengthTiles(); + assert(this.entityPath.indexOf(entity) >= 0, "Entity not contained for split"); + assert(this.entityPath.indexOf(entity) !== 0, "Entity is first"); + assert(this.entityPath.indexOf(entity) !== this.entityPath.length - 1, "Entity is last"); + + let firstPathEntityCount = 0; + let firstPathLength = 0; + let firstPathEndEntity = null; + + for (let i = 0; i < this.entityPath.length; ++i) { + const otherEntity = this.entityPath[i]; + if (otherEntity === entity) { + logger.log("Found entity at", i, "of length", firstPathLength); + break; + } + + ++firstPathEntityCount; + firstPathEndEntity = otherEntity; + firstPathLength += otherEntity.components.Belt.getEffectiveLengthTiles(); + } + + logger.log( + "First path ends at", + firstPathLength, + "and entity", + firstPathEndEntity.components.StaticMapEntity.origin, + "and has", + firstPathEntityCount, + "entities" + ); + + // Compute length of second path + const secondPathLength = this.totalLength - firstPathLength - entityLength; + const secondPathStart = firstPathLength + entityLength; + const secondEntities = this.entityPath.splice(firstPathEntityCount + 1); + logger.log( + "Second path starts at", + secondPathStart, + "and has a length of ", + secondPathLength, + "with", + secondEntities.length, + "entities" + ); + + // Remove the last item + this.entityPath.pop(); + + logger.log("Splitting", this.items.length, "items"); + logger.log( + "Old items are", + this.items.map(i => i[NEXT_ITEM_OFFSET_INDEX]) + ); + + // Create second path + const secondPath = new BeltPath(this.root, secondEntities); + + // Remove all items which are no longer relevant and transfer them to the second path + let itemPos = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + const distanceToNext = item[NEXT_ITEM_OFFSET_INDEX]; + + logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); + + // Check if this item is past the first path + if (itemPos >= firstPathLength) { + // Remove it from the first path + this.items.splice(i, 1); + i -= 1; + logger.log(" Removed item from first path since its no longer contained @", itemPos); + + // Check if its on the second path (otherwise its on the removed belt and simply lost) + if (itemPos >= secondPathStart) { + // Put item on second path + secondPath.items.push([distanceToNext, item[ITEM_INDEX]]); + logger.log( + " Put item to second path @", + itemPos, + "with distance to next =", + distanceToNext + ); + + // If it was the first item, adjust the distance to the first item + if (secondPath.items.length === 1) { + logger.log(" Sinc it was the first, set sapcing of first to", itemPos); + secondPath.spacingToFirstItem = itemPos - secondPathStart; + } + } else { + logger.log(" Item was on the removed belt, so its gone - forever!"); + } + } else { + // Seems this item is on the first path (so all good), so just make sure it doesn't + // have a nextDistance which is bigger than the total path length + const clampedDistanceToNext = Math_min(itemPos + distanceToNext, firstPathLength) - itemPos; + if (clampedDistanceToNext < distanceToNext) { + logger.log( + "Correcting next distance (first path) from", + distanceToNext, + "to", + clampedDistanceToNext + ); + item[NEXT_ITEM_OFFSET_INDEX] = clampedDistanceToNext; + } + } + + // Advance items + itemPos += distanceToNext; + } + + logger.log( + "New items are", + this.items.map(i => i[0]) + ); + + logger.log( + "And second path items are", + secondPath.items.map(i => i[0]) + ); + + // Adjust our total length + this.totalLength = firstPathLength; + + // Make sure that if we are empty, we set our first distance properly + if (this.items.length === 0) { + this.spacingToFirstItem = this.totalLength; + } + + // Set new ejector and acceptor handles + this.ejectorComp = firstPathEndEntity.components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + + this.debug_checkIntegrity("split-two-first"); + secondPath.debug_checkIntegrity("split-two-second"); + + return secondPath; + } + + /** + * Extends the path by the given other path + * @param {BeltPath} otherPath + */ + extendByPath(otherPath) { + const entities = otherPath.entityPath; + logger.log("Extending path by other path, starting to add entities"); + const oldLength = this.totalLength; + const oldLastItem = this.items[this.items.length - 1]; + + for (let i = 0; i < entities.length; ++i) { + this.extendOnEnd(entities[i]); + } + + logger.log(" Transferring new items:", otherPath.items); + + // Check if we have no items and thus need to adjust the spacing + if (this.items.length === 0) { + // This one is easy - Since our first path is empty, we can just + // set the spacing to the first one to the whole first part length + // and add the spacing on the second path (Which might be the whole second part + // length if its entirely empty, too) + this.spacingToFirstItem = this.totalLength + otherPath.spacingToFirstItem; + logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + + // Simply copy over all items + for (let i = 0; i < otherPath.items.length; ++i) { + const item = otherPath.items[0]; + this.items.push([item[0], item[1]]); + } + } else { + console.error("TODO4"); + + // Adjust the distance from our last item to the first item of the second path. + // First, find the absolute position of the first item: + let itemPosition = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + itemPosition += this.items[i][0]; + } + } + + this.debug_checkIntegrity("extend-by-path"); + } + + /** + * Computes the total length of the path + * @returns {number} + */ + computeTotalLength() { + let length = 0; + for (let i = 0; i < this.entityPath.length; ++i) { + length += this.entityPath[i].components.Belt.getEffectiveLengthTiles(); + } + return length; + } + + /** + * Performs one tick + */ + update() { + this.debug_checkIntegrity("pre-update"); + const firstBeltItems = this.initialBeltComponent.sortedItems; + const transferItemAndProgress = firstBeltItems[0]; + + // Check if the first belt took a new item + if (transferItemAndProgress) { + const transferItem = transferItemAndProgress[1]; + + if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { + // Can take new item + firstBeltItems.splice(0, 1); + + this.items.unshift([this.spacingToFirstItem, transferItem]); + this.spacingToFirstItem = 0; + } + } + + // 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; + } + + let minimumDistance = this.ejectorSlot.item ? globalConfig.itemSpacingOnBelts : 0; + + // Try to reduce spacing + let remainingAmount = beltSpeed; + for (let i = this.items.length - 1; i >= 0; --i) { + const nextDistanceAndItem = this.items[i]; + const minimumSpacing = minimumDistance; + + const takeAway = Math.max(0, Math.min(remainingAmount, nextDistanceAndItem[0] - minimumSpacing)); + + remainingAmount -= takeAway; + nextDistanceAndItem[0] -= takeAway; + + this.spacingToFirstItem += takeAway; + if (remainingAmount === 0.0) { + break; + } + + minimumDistance = globalConfig.itemSpacingOnBelts; + } + + const lastItem = this.items[this.items.length - 1]; + if (lastItem && lastItem[0] === 0.0) { + // Take over + if (this.ejectorComp.tryEject(0, lastItem[1])) { + this.items.pop(); + } + } + + this.debug_checkIntegrity("post-update"); + } + + /** + * Computes a world space position from the given progress + * @param {number} progress + * @returns {Vector} + */ + computePositionFromProgress(progress) { + let currentLength = 0; + + // floating point issuses .. + assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress); + + for (let i = 0; i < this.entityPath.length; ++i) { + const beltComp = this.entityPath[i].components.Belt; + const localLength = beltComp.getEffectiveLengthTiles(); + + if (currentLength + localLength >= progress || i === this.entityPath.length - 1) { + // Min required here due to floating point issues + const localProgress = Math_min(1.0, progress - currentLength); + + assert(localProgress >= 0.0, "Invalid local progress: " + localProgress); + const localSpace = beltComp.transformBeltToLocalSpace(localProgress); + return this.entityPath[i].components.StaticMapEntity.localTileToWorld(localSpace); + } + currentLength += localLength; + } + + assert(false, "invalid progress: " + progress + " (max: " + this.totalLength + ")"); + } + + /** + * + * @param {DrawParameters} parameters + */ + drawDebug(parameters) { + parameters.context.fillStyle = "#d79a25"; + parameters.context.strokeStyle = "#d79a25"; + parameters.context.beginPath(); + + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + const pos = entity.components.StaticMapEntity; + const worldPos = pos.origin.toWorldSpaceCenterOfTile(); + + if (i === 0) { + parameters.context.moveTo(worldPos.x, worldPos.y); + } else { + parameters.context.lineTo(worldPos.x, worldPos.y); + } + } + parameters.context.stroke(); + + // Items + let progress = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const nextDistanceAndItem = this.items[i]; + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "#268e4d"; + parameters.context.beginRoundedRect(worldPos.x - 5, worldPos.y - 5, 10, 10, 3); + parameters.context.fill(); + parameters.context.font = "6px GameFont"; + parameters.context.fillStyle = "#111"; + parameters.context.fillText( + "" + round4Digits(nextDistanceAndItem[0]), + worldPos.x + 5, + worldPos.y + 2 + ); + progress += nextDistanceAndItem[0]; + nextDistanceAndItem[1].draw(worldPos.x, worldPos.y, parameters, 10); + } + + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + parameters.context.fillStyle = "#d79a25"; + const pos = entity.components.StaticMapEntity; + const worldPos = pos.origin.toWorldSpaceCenterOfTile(); + parameters.context.beginCircle(worldPos.x, worldPos.y, i === 0 ? 5 : 3); + parameters.context.fill(); + } + + for (let progress = 0; progress <= this.totalLength + 0.01; progress += 0.2) { + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "red"; + parameters.context.beginCircle(worldPos.x, worldPos.y, 1); + parameters.context.fill(); + } + + const firstItemIndicator = this.computePositionFromProgress( + this.spacingToFirstItem + ).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "purple"; + parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2); + } +} diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index a9be5c99..4d5fa16c 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -6,6 +6,9 @@ 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"; + +export const curvedBeltLength = /* Math_PI / 4 */ 0.78; export class BeltComponent extends Component { static getId() { @@ -39,6 +42,20 @@ export class BeltComponent extends Component { /** @type {Entity} */ this.followUpCache = null; + + /** + * The path this belt is contained in, not serialized + * @type {BeltPath} + */ + this.assignedPath = null; + } + + /** + * Returns the effective length of this belt in tile space + * @returns {number} + */ + getEffectiveLengthTiles() { + return this.direction === enumDirection.top ? 1.0 : curvedBeltLength; } /** @@ -50,14 +67,17 @@ export class BeltComponent extends Component { transformBeltToLocalSpace(progress) { switch (this.direction) { case enumDirection.top: + assert(progress <= 1.02, "Invalid progress: " + progress); return new Vector(0, 0.5 - progress); case enumDirection.right: { - const arcProgress = progress * 0.5 * Math_PI; + assert(progress <= curvedBeltLength + 0.02, "Invalid progress 2: " + progress); + const arcProgress = (progress / curvedBeltLength) * 0.5 * Math_PI; return new Vector(0.5 - 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress)); } case enumDirection.left: { - const arcProgress = progress * 0.5 * Math_PI; + assert(progress <= curvedBeltLength + 0.02, "Invalid progress 3: " + progress); + const arcProgress = (progress / curvedBeltLength) * 0.5 * Math_PI; return new Vector(-0.5 + 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress)); } default: diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index e78faa36..7ae97724 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -194,4 +194,17 @@ export class ItemEjectorComponent extends Component { this.slots[slotIndex].progress = this.instantEject ? 1 : 0; return true; } + + /** + * Clears the given slot and returns the item it had + * @param {number} slotIndex + * @returns {BaseItem|null} + */ + takeSlotItem(slotIndex) { + const slot = this.slots[slotIndex]; + const item = slot.item; + slot.item = null; + slot.progress = 0.0; + return item; + } } diff --git a/src/js/game/core.js b/src/js/game/core.js index 203e005b..e16a57a8 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -418,6 +418,10 @@ export class GameCore { root.map.drawStaticEntityDebugOverlays(params); } + if (G_IS_DEV && globalConfig.debug.renderBeltPaths) { + systems.belt.drawBeltPathDebug(params); + } + // END OF GAME CONTENT // ----- diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index f9c28d93..d7c2de51 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -74,8 +74,11 @@ export class GameHUD { shapeViewer: new HUDShapeViewer(this.root), wiresOverlay: new HUDWiresOverlay(this.root), + // Typing hints + /* typehints:start */ /** @type {HUDChangesDebugger} */ changesDebugger: null, + /* typehints:end */ }; this.signals = { diff --git a/src/js/game/hud/parts/debug_changes.js b/src/js/game/hud/parts/debug_changes.js index 46ad5234..1502afa2 100644 --- a/src/js/game/hud/parts/debug_changes.js +++ b/src/js/game/hud/parts/debug_changes.js @@ -27,7 +27,7 @@ export class HUDChangesDebugger extends BaseHUDPart { * @param {string} fillColor Color to display (Hex) * @param {number=} timeToDisplay How long to display the change */ - renderChange(label, area, fillColor, timeToDisplay = 2) { + renderChange(label, area, fillColor, timeToDisplay = 0.3) { this.changes.push({ label, area: area.clone(), diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 5c1bbf66..71c709dc 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -227,6 +227,7 @@ export class GameLogic { } this.root.map.removeStaticEntity(building); this.root.entityMgr.destroyEntity(building); + this.root.entityMgr.processDestroyList(); return true; } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 5e4ac7f0..04220352 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -1,21 +1,18 @@ -import { Math_sqrt, Math_max } from "../../core/builtins"; +import { Math_sqrt } from "../../core/builtins"; import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; -import { gMetaBuildingRegistry } from "../../core/global_registries"; import { Loader } from "../../core/loader"; import { createLogger } from "../../core/logging"; -import { Rectangle } from "../../core/rectangle"; import { AtlasSprite } from "../../core/sprites"; -import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../../core/vector"; -import { MetaBeltBaseBuilding } from "../buildings/belt_base"; +import { enumDirection, enumDirectionToVector, enumInvertedDirections } from "../../core/vector"; +import { BeltPath } from "../belt_path"; import { BeltComponent } from "../components/belt"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunkView } from "../map_chunk_view"; -import { defaultBuildingVariant } from "../meta_building"; +import { fastArrayDeleteValue } from "../../core/utils"; export const BELT_ANIM_COUNT = 28; -const SQRT_2 = Math_sqrt(2); const logger = createLogger("belt"); @@ -52,10 +49,17 @@ export class BeltSystem extends GameSystemWithFilter { this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); 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 {Rectangle} */ + // this.areaToRecompute = null; + + /** @type {Array} */ + this.beltPaths = []; + + this.recomputePaths = true; } /** @@ -72,10 +76,8 @@ export class BeltSystem extends GameSystemWithFilter { return; } - if (entity.components.Belt) { - this.cacheNeedsUpdate = true; - } - + // this.recomputePaths = true; + /* const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); // Compute affected area @@ -85,6 +87,8 @@ export class BeltSystem extends GameSystemWithFilter { // Store if anything got changed, if so we need to queue a recompute let anythingChanged = false; + anythingChanged = true; // TODO / FIXME + for (let x = affectedArea.x; x < affectedArea.right(); ++x) { for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { if (!originalRect.containsPoint(x, y)) { @@ -121,6 +125,105 @@ export class BeltSystem extends GameSystemWithFilter { logger.log("Queuing recompute:", this.areaToRecompute); } } + + // FIXME + this.areaToRecompute = new Rectangle(-1000, -1000, 2000, 2000); + */ + } + + /** + * Called when an entity got destroyed + * @param {Entity} entity + */ + onEntityDestroyed(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + console.log("DESTROY"); + + const assignedPath = entity.components.Belt.assignedPath; + assert(assignedPath, "Entity has no belt path assigned"); + + // Find from and to entities + const fromEntity = this.findSupplyingEntity(entity); + const toEntity = this.findFollowUpEntity(entity); + + // Check if the belt had a previous belt + if (fromEntity) { + const fromPath = fromEntity.components.Belt.assignedPath; + + // Check if the entity had a followup - belt + if (toEntity) { + const toPath = toEntity.components.Belt.assignedPath; + assert(fromPath === toPath, "Invalid belt path layout (from path != to path)"); + console.log("Remove inbetween"); + + const newPath = fromPath.deleteEntityOnPathSplitIntoTwo(entity); + this.beltPaths.push(newPath); + } else { + // TODO + console.error("TODO 1"); + } + } else { + if (toEntity) { + // TODO + console.error("TODO 2"); + } else { + // TODO + console.error("TODO 3"); + } + } + } + + /** + * Called when an entity got added + * @param {Entity} entity + */ + onEntityAdded(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + console.log("ADD"); + + const fromEntity = this.findSupplyingEntity(entity); + const toEntity = this.findFollowUpEntity(entity); + + console.log("From:", fromEntity, "to:", toEntity); + + // Check if we can add the entity to the previous path + if (fromEntity) { + const fromPath = fromEntity.components.Belt.assignedPath; + fromPath.extendOnEnd(entity); + + // Check if we now can extend the current path by the next path + if (toEntity) { + const toPath = toEntity.components.Belt.assignedPath; + fromPath.extendByPath(toPath); + + // Delete now obsolete path + fastArrayDeleteValue(this.beltPaths, toPath); + } + } else { + if (toEntity) { + // Prepend it to the other path + const toPath = toEntity.components.Belt.assignedPath; + toPath.extendOnBeginning(entity); + } else { + // This is an empty belt path + const path = new BeltPath(this.root, [entity]); + this.beltPaths.push(path); + } + } } draw(parameters) { @@ -165,10 +268,47 @@ export class BeltSystem extends GameSystemWithFilter { return null; } + /** + * Finds the supplying belt for a given belt. Used for building the dependencies + * @param {Entity} entity + */ + findSupplyingEntity(entity) { + const staticComp = entity.components.StaticMapEntity; + + const supplyDirection = staticComp.localDirectionToWorld(enumDirection.bottom); + const supplyVector = enumDirectionToVector[supplyDirection]; + + const supplyTile = staticComp.origin.add(supplyVector); + const supplyEntity = this.root.map.getTileContent(supplyTile); + + // Check if theres a belt at the tile we point to + if (supplyEntity) { + const supplyBeltComp = supplyEntity.components.Belt; + if (supplyBeltComp) { + const supplyStatic = supplyEntity.components.StaticMapEntity; + const supplyEjector = supplyEntity.components.ItemEjector; + + // Check if the belt accepts items from our direction + const ejectorSlots = supplyEjector.slots; + for (let i = 0; i < ejectorSlots.length; ++i) { + const slot = ejectorSlots[i]; + const localDirection = supplyStatic.localDirectionToWorld(slot.direction); + if (enumInvertedDirections[localDirection] === supplyDirection) { + return supplyEntity; + } + } + } + } + + return null; + } + /** * Recomputes the belt cache */ computeBeltCache() { + this.recomputePaths = false; + /* if (this.areaToRecompute) { logger.log("Updating belt cache by updating area:", this.areaToRecompute); @@ -207,13 +347,88 @@ export class BeltSystem extends GameSystemWithFilter { entity.components.Belt.followUpCache = this.findFollowUpEntity(entity); } } + */ + this.computeBeltPaths(); + } + + /** + * Computes the belt path network + */ + computeBeltPaths() { + const visitedUids = new Set(); + console.log("Computing belt paths"); + + const debugEntity = e => e.components.StaticMapEntity.origin.toString(); + + // const stackToVisit = this.allEntities.slice(); + const result = []; + + const currentPath = null; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + if (visitedUids.has(entity.uid)) { + continue; + } + + // console.log("Starting at", debugEntity(entity)); + // Mark entity as visited + visitedUids.add(entity.uid); + + // Compute path, start with entity and find precedors / successors + const path = [entity]; + + // Find precedors + let prevEntity = this.findSupplyingEntity(entity); + while (prevEntity) { + if (visitedUids.has(prevEntity)) { + break; + } + // console.log(" -> precedor: ", debugEntity(prevEntity)); + path.unshift(prevEntity); + visitedUids.add(prevEntity.uid); + prevEntity = this.findSupplyingEntity(prevEntity); + } + + // Find succedors + let nextEntity = this.findFollowUpEntity(entity); + while (nextEntity) { + if (visitedUids.has(nextEntity)) { + break; + } + + // console.log(" -> succedor: ", debugEntity(nextEntity)); + path.push(nextEntity); + visitedUids.add(nextEntity.uid); + nextEntity = this.findFollowUpEntity(nextEntity); + } + + // console.log( + // "Found path:", + // path.map(e => debugEntity(e)) + // ); + + result.push(new BeltPath(this.root, path)); + + // let prevEntity = this.findSupplyingEntity(srcEntity); + } + + logger.log("Found", this.beltPaths.length, "belt paths"); + this.beltPaths = result; } update() { - if (this.areaToRecompute) { + if (this.recomputePaths) { this.computeBeltCache(); } + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].update(); + } + + return; + /* + // Divide by item spacing on belts since we use throughput and not speed let beltSpeed = this.root.hubGoals.getBeltBaseSpeed() * @@ -238,7 +453,7 @@ export class BeltSystem extends GameSystemWithFilter { const ejectorComp = entity.components.ItemEjector; let maxProgress = 1; - /* PERFORMANCE OPTIMIZATION */ + // PERFORMANCE OPTIMIZATION // Original: // const isCurrentlyEjecting = ejectorComp.isAnySlotEjecting(); // Replaced (Since belts always have just one slot): @@ -300,7 +515,7 @@ export class BeltSystem extends GameSystemWithFilter { } else { // Try to give this item to a new belt - /* PERFORMANCE OPTIMIZATION */ + // PERFORMANCE OPTIMIZATION // Original: // const freeSlot = ejectorComp.getFirstFreeSlot(); @@ -326,6 +541,7 @@ export class BeltSystem extends GameSystemWithFilter { } } } + */ } /** @@ -372,6 +588,7 @@ export class BeltSystem extends GameSystemWithFilter { * @param {Entity} entity */ drawEntityItems(parameters, entity) { + /* const beltComp = entity.components.Belt; const staticComp = entity.components.StaticMapEntity; @@ -401,5 +618,16 @@ export class BeltSystem extends GameSystemWithFilter { parameters ); } + */ + } + + /** + * Draws the belt parameters + * @param {DrawParameters} parameters + */ + drawBeltPathDebug(parameters) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].drawDebug(parameters); + } } } From 9ce912dbdd5875a6c30ca669d315b1e8dad0b04d Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 16:31:36 +0200 Subject: [PATCH 2/7] Second take on belt performance --- src/js/game/belt_path.js | 332 ++++++++++++++++++++++++++++++------ src/js/game/systems/belt.js | 65 +++++-- 2 files changed, 332 insertions(+), 65 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 04b74d11..0ab7299f 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1,18 +1,18 @@ +import { Math_min } from "../core/builtins"; import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; +import { createLogger } from "../core/logging"; +import { epsilonCompare, round4Digits } from "../core/utils"; import { Vector } from "../core/vector"; import { BaseItem } from "./base_item"; import { Entity } from "./entity"; import { GameRoot } from "./root"; -import { round4Digits, epsilonCompare } from "../core/utils"; -import { Math_min } from "../core/builtins"; -import { createLogger, logSection } from "../core/logging"; const logger = createLogger("belt_path"); // Helpers for more semantic access into interleaved arrays -const NEXT_ITEM_OFFSET_INDEX = 0; -const ITEM_INDEX = 1; +const _nextDistance = 0; +const _item = 1; /** * Stores a path of belts, used for optimizing performance @@ -82,7 +82,7 @@ export class BeltPath { // Check for mismatching length const totalLength = this.computeTotalLength(); - if (this.totalLength !== totalLength) { + if (!epsilonCompare(this.totalLength, totalLength)) { return this.debug_failIntegrity( currentChange, "Total length mismatch, stored =", @@ -95,6 +95,10 @@ export class BeltPath { // Check for misconnected entities for (let i = 0; i < this.entityPath.length - 1; ++i) { const entity = this.entityPath[i]; + if (entity.destroyed) { + return fail("Reference to destroyed entity " + entity.uid); + } + const followUp = this.root.systemMgr.systems.belt.findFollowUpEntity(entity); if (!followUp) { return fail( @@ -168,17 +172,17 @@ export class BeltPath { for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; - if (item[NEXT_ITEM_OFFSET_INDEX] < 0 || item[NEXT_ITEM_OFFSET_INDEX] > this.totalLength) { + if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength) { return fail( "Item has invalid offset to next item: ", - item[0], + item[_nextDistance], "(total length:", this.totalLength, ")" ); } - currentPos += item[0]; + currentPos += item[_nextDistance]; } // Check the total sum matches @@ -190,7 +194,7 @@ export class BeltPath { this.spacingToFirstItem, ") and items does not match total length (", this.totalLength, - ")" + ") -> items: " + this.items.map(i => i[_nextDistance]).join("|") ); } } @@ -229,11 +233,11 @@ export class BeltPath { const lastItem = this.items[this.items.length - 1]; logger.log( " Extended spacing of last item from", - lastItem[NEXT_ITEM_OFFSET_INDEX], + lastItem[_nextDistance], "to", - lastItem[NEXT_ITEM_OFFSET_INDEX] + additionalLength + lastItem[_nextDistance] + additionalLength ); - lastItem[NEXT_ITEM_OFFSET_INDEX] += additionalLength; + lastItem[_nextDistance] += additionalLength; } // Update handles @@ -267,8 +271,8 @@ export class BeltPath { // Set handles and append entity beltComp.assignedPath = this; - this.initialBeltComponent = this.entityPath[0].components.Belt; this.entityPath.unshift(entity); + this.initialBeltComponent = this.entityPath[0].components.Belt; this.debug_checkIntegrity("extend-on-begin"); } @@ -337,7 +341,7 @@ export class BeltPath { logger.log("Splitting", this.items.length, "items"); logger.log( "Old items are", - this.items.map(i => i[NEXT_ITEM_OFFSET_INDEX]) + this.items.map(i => i[_nextDistance]) ); // Create second path @@ -347,7 +351,7 @@ export class BeltPath { let itemPos = this.spacingToFirstItem; for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; - const distanceToNext = item[NEXT_ITEM_OFFSET_INDEX]; + const distanceToNext = item[_nextDistance]; logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); @@ -361,7 +365,7 @@ export class BeltPath { // Check if its on the second path (otherwise its on the removed belt and simply lost) if (itemPos >= secondPathStart) { // Put item on second path - secondPath.items.push([distanceToNext, item[ITEM_INDEX]]); + secondPath.items.push([distanceToNext, item[_item]]); logger.log( " Put item to second path @", itemPos, @@ -388,7 +392,7 @@ export class BeltPath { "to", clampedDistanceToNext ); - item[NEXT_ITEM_OFFSET_INDEX] = clampedDistanceToNext; + item[_nextDistance] = clampedDistanceToNext; } } @@ -398,12 +402,12 @@ export class BeltPath { logger.log( "New items are", - this.items.map(i => i[0]) + this.items.map(i => i[_nextDistance]) ); logger.log( "And second path items are", - secondPath.items.map(i => i[0]) + secondPath.items.map(i => i[_nextDistance]) ); // Adjust our total length @@ -424,45 +428,268 @@ export class BeltPath { return secondPath; } + /** + * Deletes the last entity + * @param {Entity} entity + */ + deleteEntityOnEnd(entity) { + assert(this.entityPath[this.entityPath.length - 1] === entity, "Not the last entity actually"); + + // Ok, first remove the entity + const beltComp = entity.components.Belt; + const beltLength = beltComp.getEffectiveLengthTiles(); + + logger.log( + "Deleting last entity on path with length", + this.entityPath.length, + "(reducing", + this.totalLength, + " by", + beltLength, + ")" + ); + this.totalLength -= beltLength; + this.entityPath.pop(); + + logger.log(" New path has length of", this.totalLength, "with", this.entityPath.length, "entities"); + + // This is just for sanity + beltComp.assignedPath = null; + + // Clean up items + if (this.items.length === 0) { + // Simple case with no items, just update the first item spacing + this.spacingToFirstItem = this.totalLength; + } else { + // Ok, make sure we simply drop all items which are no longer contained + let itemOffset = this.spacingToFirstItem; + let lastItemOffset = itemOffset; + + logger.log(" Adjusting", this.items.length, "items"); + + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + + // Get rid of items past this path + if (itemOffset >= this.totalLength) { + logger.log("Dropping item (current index=", i, ")"); + this.items.splice(i, 1); + i -= 1; + continue; + } + + logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]); + lastItemOffset = itemOffset; + itemOffset += item[_nextDistance]; + } + + // If we still have an item, make sure the last item matches + if (this.items.length > 0) { + // We can easily compute the next distance since we know where the last item is now + const lastDistance = this.totalLength - lastItemOffset; + assert( + lastDistance >= 0.0, + "Last item distance mismatch: " + + lastDistance + + " -> Total length was " + + this.totalLength + + " and lastItemOffset was " + + lastItemOffset + ); + + logger.log( + "Adjusted distance of last item: it is at", + lastItemOffset, + "so it has a distance of", + lastDistance, + "to the end (", + this.totalLength, + ")" + ); + this.items[this.items.length - 1][_nextDistance] = lastDistance; + } else { + logger.log(" Removed all items so we'll update spacing to total length"); + + // We removed all items so update our spacing + this.spacingToFirstItem = this.totalLength; + } + } + + // Update handles + this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; + + this.debug_checkIntegrity("delete-on-end"); + } + + /** + * Deletes the entity of the start of the path + * @see deleteEntityOnEnd + * @param {Entity} entity + */ + deleteEntityOnStart(entity) { + assert(entity === this.entityPath[0], "Not actually the start entity"); + + // Ok, first remove the entity + const beltComp = entity.components.Belt; + const beltLength = beltComp.getEffectiveLengthTiles(); + + logger.log( + "Deleting first entity on path with length", + this.entityPath.length, + "(reducing", + this.totalLength, + " by", + beltLength, + ")" + ); + this.totalLength -= beltLength; + this.entityPath.shift(); + + logger.log(" New path has length of", this.totalLength, "with", this.entityPath.length, "entities"); + + // This is just for sanity + beltComp.assignedPath = null; + + // Clean up items + if (this.items.length === 0) { + // Simple case with no items, just update the first item spacing + this.spacingToFirstItem = this.totalLength; + } else { + // Simple case, we had no item on the beginning -> all good + if (this.spacingToFirstItem >= beltLength) { + logger.log( + " No item on the first place, so we can just adjust the spacing (spacing=", + this.spacingToFirstItem, + ") removed =", + beltLength + ); + this.spacingToFirstItem -= beltLength; + } else { + // Welp, okay we need to drop all items which are < beltLength and adjust + // the other item offsets as well + + logger.log( + " We have at least one item in the beginning, drop those and adjust spacing (first item @", + this.spacingToFirstItem, + ") since we removed", + beltLength, + "length from path" + ); + logger.log( + " Items:", + this.items.map(i => i[_nextDistance]) + ); + + // Find offset to first item + let itemOffset = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + if (itemOffset <= beltLength) { + logger.log( + " -> Dropping item with index", + i, + "at", + itemOffset, + "since it was on the removed belt" + ); + // This item must be dropped + this.items.splice(i, 1); + i -= 1; + itemOffset += item[_nextDistance]; + continue; + } else { + // This item can be kept, thus its the first we know + break; + } + } + + if (this.items.length > 0) { + logger.log( + " Offset of first non-dropped item was at:", + itemOffset, + "-> setting spacing to it (total length=", + this.totalLength, + ")" + ); + + this.spacingToFirstItem = itemOffset - beltLength; + assert( + this.spacingToFirstItem >= 0.0, + "Invalid spacing after delete on start: " + this.spacingToFirstItem + ); + } else { + logger.log(" We dropped all items, simply set spacing to total length"); + // We dropped all items, simple one + this.spacingToFirstItem = this.totalLength; + } + } + } + + // Update handles + this.initialBeltComponent = this.entityPath[0].components.Belt; + + this.debug_checkIntegrity("delete-on-start"); + } + /** * Extends the path by the given other path * @param {BeltPath} otherPath */ extendByPath(otherPath) { + assert(otherPath !== this, "Circular path dependency"); + const entities = otherPath.entityPath; logger.log("Extending path by other path, starting to add entities"); + const oldLength = this.totalLength; - const oldLastItem = this.items[this.items.length - 1]; + logger.log(" Adding", entities.length, "new entities, current length =", this.totalLength); + + // First, append entities for (let i = 0; i < entities.length; ++i) { - this.extendOnEnd(entities[i]); + const entity = entities[i]; + const beltComp = entity.components.Belt; + + // Add to path and update references + this.entityPath.push(entity); + beltComp.assignedPath = this; + + // Update our length + const additionalLength = beltComp.getEffectiveLengthTiles(); + this.totalLength += additionalLength; } - logger.log(" Transferring new items:", otherPath.items); + logger.log(" Path is now", this.entityPath.length, "entities and has a length of", this.totalLength); - // Check if we have no items and thus need to adjust the spacing - if (this.items.length === 0) { - // This one is easy - Since our first path is empty, we can just - // set the spacing to the first one to the whole first part length - // and add the spacing on the second path (Which might be the whole second part - // length if its entirely empty, too) - this.spacingToFirstItem = this.totalLength + otherPath.spacingToFirstItem; - logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + // Update handles + this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; + this.ejectorSlot = this.ejectorComp.slots[0]; - // Simply copy over all items - for (let i = 0; i < otherPath.items.length; ++i) { - const item = otherPath.items[0]; - this.items.push([item[0], item[1]]); - } + // Now, update the distance of our last item + if (this.items.length !== 0) { + const lastItem = this.items[this.items.length - 1]; + lastItem[_nextDistance] += otherPath.spacingToFirstItem; + logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now"); } else { - console.error("TODO4"); + // Seems we have no items, update our first item distance + this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem; + logger.log( + " We had no items, so our new spacing to first is old length (", + oldLength, + ") plus others spacing to first (", + otherPath.spacingToFirstItem, + ") =", + this.spacingToFirstItem + ); + } - // Adjust the distance from our last item to the first item of the second path. - // First, find the absolute position of the first item: - let itemPosition = this.spacingToFirstItem; - for (let i = 0; i < this.items.length; ++i) { - itemPosition += this.items[i][0]; - } + logger.log(" Pushing", otherPath.items.length, "items from other path"); + + // Aaand push the other paths items + for (let i = 0; i < otherPath.items.length; ++i) { + const item = otherPath.items[i]; + this.items.push([item[_nextDistance], item[_item]]); } this.debug_checkIntegrity("extend-by-path"); @@ -490,7 +717,7 @@ export class BeltPath { // Check if the first belt took a new item if (transferItemAndProgress) { - const transferItem = transferItemAndProgress[1]; + const transferItem = transferItemAndProgress[_item]; if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { // Can take new item @@ -519,10 +746,13 @@ export class BeltPath { const nextDistanceAndItem = this.items[i]; const minimumSpacing = minimumDistance; - const takeAway = Math.max(0, Math.min(remainingAmount, nextDistanceAndItem[0] - minimumSpacing)); + const takeAway = Math.max( + 0, + Math.min(remainingAmount, nextDistanceAndItem[_nextDistance] - minimumSpacing) + ); remainingAmount -= takeAway; - nextDistanceAndItem[0] -= takeAway; + nextDistanceAndItem[_nextDistance] -= takeAway; this.spacingToFirstItem += takeAway; if (remainingAmount === 0.0) { @@ -533,9 +763,9 @@ export class BeltPath { } const lastItem = this.items[this.items.length - 1]; - if (lastItem && lastItem[0] === 0.0) { + if (lastItem && lastItem[_nextDistance] === 0) { // Take over - if (this.ejectorComp.tryEject(0, lastItem[1])) { + if (this.ejectorComp.tryEject(0, lastItem[_item])) { this.items.pop(); } } @@ -605,12 +835,12 @@ export class BeltPath { parameters.context.font = "6px GameFont"; parameters.context.fillStyle = "#111"; parameters.context.fillText( - "" + round4Digits(nextDistanceAndItem[0]), + "" + round4Digits(nextDistanceAndItem[_nextDistance]), worldPos.x + 5, worldPos.y + 2 ); - progress += nextDistanceAndItem[0]; - nextDistanceAndItem[1].draw(worldPos.x, worldPos.y, parameters, 10); + progress += nextDistanceAndItem[_nextDistance]; + nextDistanceAndItem[_item].draw(worldPos.x, worldPos.y, parameters, 10); } for (let i = 0; i < this.entityPath.length; ++i) { diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 04220352..ed4e4311 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -144,8 +144,6 @@ export class BeltSystem extends GameSystemWithFilter { return; } - console.log("DESTROY"); - const assignedPath = entity.components.Belt.assignedPath; assert(assignedPath, "Entity has no belt path assigned"); @@ -161,23 +159,25 @@ export class BeltSystem extends GameSystemWithFilter { if (toEntity) { const toPath = toEntity.components.Belt.assignedPath; assert(fromPath === toPath, "Invalid belt path layout (from path != to path)"); - console.log("Remove inbetween"); const newPath = fromPath.deleteEntityOnPathSplitIntoTwo(entity); this.beltPaths.push(newPath); } else { - // TODO - console.error("TODO 1"); + fromPath.deleteEntityOnEnd(entity); } } else { if (toEntity) { - // TODO - console.error("TODO 2"); + // We need to remove the entity from the beginning of the other path + const toPath = toEntity.components.Belt.assignedPath; + toPath.deleteEntityOnStart(entity); } else { - // TODO - console.error("TODO 3"); + // This is a single entity path, easy to do + const path = entity.components.Belt.assignedPath; + fastArrayDeleteValue(this.beltPaths, path); } } + + this.verifyBeltPaths(); } /** @@ -208,10 +208,15 @@ export class BeltSystem extends GameSystemWithFilter { // Check if we now can extend the current path by the next path if (toEntity) { const toPath = toEntity.components.Belt.assignedPath; - fromPath.extendByPath(toPath); - // Delete now obsolete path - fastArrayDeleteValue(this.beltPaths, toPath); + if (fromPath === toPath) { + // This is a circular dependency -> Ignore + } else { + fromPath.extendByPath(toPath); + + // Delete now obsolete path + fastArrayDeleteValue(this.beltPaths, toPath); + } } } else { if (toEntity) { @@ -224,12 +229,36 @@ export class BeltSystem extends GameSystemWithFilter { this.beltPaths.push(path); } } + + this.verifyBeltPaths(); } draw(parameters) { this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this)); } + /** + * Verifies all belt paths + */ + verifyBeltPaths() { + if (G_IS_DEV) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].debug_checkIntegrity("general-verify"); + } + + const belts = this.root.entityMgr.getAllWithComponent(BeltComponent); + for (let i = 0; i < belts.length; ++i) { + const path = belts[i].components.Belt.assignedPath; + if (!path) { + throw new Error("Belt has no path: " + belts[i].uid); + } + if (this.beltPaths.indexOf(path) < 0) { + throw new Error("Path of entity not contained: " + belts[i].uid); + } + } + } + } + /** * Finds the follow up entity for a given belt. Used for building the dependencies * @param {Entity} entity @@ -378,9 +407,11 @@ export class BeltSystem extends GameSystemWithFilter { // Compute path, start with entity and find precedors / successors const path = [entity]; + let maxIter = 9999; + // Find precedors let prevEntity = this.findSupplyingEntity(entity); - while (prevEntity) { + while (prevEntity && --maxIter > 0) { if (visitedUids.has(prevEntity)) { break; } @@ -392,7 +423,7 @@ export class BeltSystem extends GameSystemWithFilter { // Find succedors let nextEntity = this.findFollowUpEntity(entity); - while (nextEntity) { + while (nextEntity && --maxIter > 0) { if (visitedUids.has(nextEntity)) { break; } @@ -403,6 +434,8 @@ export class BeltSystem extends GameSystemWithFilter { nextEntity = this.findFollowUpEntity(nextEntity); } + assert(maxIter !== 0, "Ran out of iterations"); + // console.log( // "Found path:", // path.map(e => debugEntity(e)) @@ -422,10 +455,14 @@ export class BeltSystem extends GameSystemWithFilter { this.computeBeltCache(); } + this.verifyBeltPaths(); + for (let i = 0; i < this.beltPaths.length; ++i) { this.beltPaths[i].update(); } + this.verifyBeltPaths(); + return; /* From a71c0b80398b58c7cc3191f40906a555967b1ec5 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 17:02:52 +0200 Subject: [PATCH 3/7] Take 3 on the belt performance (+ tslint fixes) --- src/js/core/modal_dialog_forms.js | 2 +- src/js/core/polyfills.js | 1 + src/js/core/utils.js | 2 - src/js/game/belt_path.js | 345 +++++++++++++--------- src/js/game/{hud/parts => }/blueprint.js | 34 +-- src/js/game/hud/parts/blueprint_placer.js | 2 +- src/js/game/logic.js | 2 +- src/js/game/systems/belt.js | 80 ++--- 8 files changed, 248 insertions(+), 220 deletions(-) rename src/js/game/{hud/parts => }/blueprint.js (85%) diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index 4d1c9f97..1ded9a8b 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -19,7 +19,7 @@ export class FormElement { abstract; } - focus(parent) {} + focus() {} isValid() { return true; diff --git a/src/js/core/polyfills.js b/src/js/core/polyfills.js index e5efca1d..145b4c82 100644 --- a/src/js/core/polyfills.js +++ b/src/js/core/polyfills.js @@ -72,6 +72,7 @@ function objectPolyfills() { } if (!Object.entries) { + // @ts-ignore Object.entries = function entries(O) { return reduce( keys(O), diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 23368317..3d4e524c 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -405,8 +405,6 @@ export function findNiceValue(num) { return Math_round(niceValue * 100) / 100; } -window.fn = findNiceValue; - /** * Finds a nice integer value * @see findNiceValue diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 0ab7299f..48d6a93e 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -14,6 +14,8 @@ const logger = createLogger("belt_path"); const _nextDistance = 0; const _item = 1; +const DEBUG = G_IS_DEV && false; + /** * Stores a path of belts, used for optimizing performance */ @@ -204,7 +206,7 @@ export class BeltPath { * @param {Entity} entity */ extendOnEnd(entity) { - logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin); + DEBUG && logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin); const beltComp = entity.components.Belt; @@ -212,7 +214,7 @@ export class BeltPath { const pendingItem = this.ejectorComp.takeSlotItem(0); if (pendingItem) { // Ok, so we have a pending item - logger.log("Taking pending item and putting it back on the path"); + DEBUG && logger.log("Taking pending item and putting it back on the path"); this.items.push([0, pendingItem]); } @@ -222,21 +224,22 @@ export class BeltPath { // Extend the path length const additionalLength = beltComp.getEffectiveLengthTiles(); this.totalLength += additionalLength; - logger.log(" Extended total length by", additionalLength, "to", this.totalLength); + DEBUG && logger.log(" Extended total length by", additionalLength, "to", this.totalLength); // If we have no item, just update the distance to the first item if (this.items.length === 0) { this.spacingToFirstItem = this.totalLength; - logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + DEBUG && logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); } else { // Otherwise, update the next-distance of the last item const lastItem = this.items[this.items.length - 1]; - logger.log( - " Extended spacing of last item from", - lastItem[_nextDistance], - "to", - lastItem[_nextDistance] + additionalLength - ); + DEBUG && + logger.log( + " Extended spacing of last item from", + lastItem[_nextDistance], + "to", + lastItem[_nextDistance] + additionalLength + ); lastItem[_nextDistance] += additionalLength; } @@ -257,7 +260,7 @@ export class BeltPath { extendOnBeginning(entity) { const beltComp = entity.components.Belt; - logger.log("Extending the path on the beginning"); + DEBUG && logger.log("Extending the path on the beginning"); // All items on that belt are simply lost (for now) @@ -277,6 +280,24 @@ export class BeltPath { this.debug_checkIntegrity("extend-on-begin"); } + /** + * Returns if the given entity is the end entity of the path + * @param {Entity} entity + * @returns {boolean} + */ + isEndEntity(entity) { + return this.entityPath[this.entityPath.length - 1] === entity; + } + + /** + * Returns if the given entity is the start entity of the path + * @param {Entity} entity + * @returns {boolean} + */ + isStartEntity(entity) { + return this.entityPath[0] === entity; + } + /** * Splits this path at the given entity by removing it, and * returning the new secondary paht @@ -284,7 +305,7 @@ export class BeltPath { * @returns {BeltPath} */ deleteEntityOnPathSplitIntoTwo(entity) { - logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin); + DEBUG && logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin); // First, find where the current path ends const beltComp = entity.components.Belt; @@ -302,7 +323,7 @@ export class BeltPath { for (let i = 0; i < this.entityPath.length; ++i) { const otherEntity = this.entityPath[i]; if (otherEntity === entity) { - logger.log("Found entity at", i, "of length", firstPathLength); + DEBUG && logger.log("Found entity at", i, "of length", firstPathLength); break; } @@ -311,38 +332,41 @@ export class BeltPath { firstPathLength += otherEntity.components.Belt.getEffectiveLengthTiles(); } - logger.log( - "First path ends at", - firstPathLength, - "and entity", - firstPathEndEntity.components.StaticMapEntity.origin, - "and has", - firstPathEntityCount, - "entities" - ); + DEBUG && + logger.log( + "First path ends at", + firstPathLength, + "and entity", + firstPathEndEntity.components.StaticMapEntity.origin, + "and has", + firstPathEntityCount, + "entities" + ); // Compute length of second path const secondPathLength = this.totalLength - firstPathLength - entityLength; const secondPathStart = firstPathLength + entityLength; const secondEntities = this.entityPath.splice(firstPathEntityCount + 1); - logger.log( - "Second path starts at", - secondPathStart, - "and has a length of ", - secondPathLength, - "with", - secondEntities.length, - "entities" - ); + DEBUG && + logger.log( + "Second path starts at", + secondPathStart, + "and has a length of ", + secondPathLength, + "with", + secondEntities.length, + "entities" + ); // Remove the last item this.entityPath.pop(); - logger.log("Splitting", this.items.length, "items"); - logger.log( - "Old items are", - this.items.map(i => i[_nextDistance]) - ); + DEBUG && logger.log("Splitting", this.items.length, "items"); + DEBUG && + logger.log( + "Old items are", + this.items.map(i => i[_nextDistance]) + ); // Create second path const secondPath = new BeltPath(this.root, secondEntities); @@ -353,45 +377,48 @@ export class BeltPath { const item = this.items[i]; const distanceToNext = item[_nextDistance]; - logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); + DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); // Check if this item is past the first path if (itemPos >= firstPathLength) { // Remove it from the first path this.items.splice(i, 1); i -= 1; - logger.log(" Removed item from first path since its no longer contained @", itemPos); + DEBUG && + logger.log(" Removed item from first path since its no longer contained @", itemPos); // Check if its on the second path (otherwise its on the removed belt and simply lost) if (itemPos >= secondPathStart) { // Put item on second path secondPath.items.push([distanceToNext, item[_item]]); - logger.log( - " Put item to second path @", - itemPos, - "with distance to next =", - distanceToNext - ); + DEBUG && + logger.log( + " Put item to second path @", + itemPos, + "with distance to next =", + distanceToNext + ); // If it was the first item, adjust the distance to the first item if (secondPath.items.length === 1) { - logger.log(" Sinc it was the first, set sapcing of first to", itemPos); + DEBUG && logger.log(" Sinc it was the first, set sapcing of first to", itemPos); secondPath.spacingToFirstItem = itemPos - secondPathStart; } } else { - logger.log(" Item was on the removed belt, so its gone - forever!"); + DEBUG && logger.log(" Item was on the removed belt, so its gone - forever!"); } } else { // Seems this item is on the first path (so all good), so just make sure it doesn't // have a nextDistance which is bigger than the total path length const clampedDistanceToNext = Math_min(itemPos + distanceToNext, firstPathLength) - itemPos; if (clampedDistanceToNext < distanceToNext) { - logger.log( - "Correcting next distance (first path) from", - distanceToNext, - "to", - clampedDistanceToNext - ); + DEBUG && + logger.log( + "Correcting next distance (first path) from", + distanceToNext, + "to", + clampedDistanceToNext + ); item[_nextDistance] = clampedDistanceToNext; } } @@ -400,15 +427,17 @@ export class BeltPath { itemPos += distanceToNext; } - logger.log( - "New items are", - this.items.map(i => i[_nextDistance]) - ); + DEBUG && + logger.log( + "New items are", + this.items.map(i => i[_nextDistance]) + ); - logger.log( - "And second path items are", - secondPath.items.map(i => i[_nextDistance]) - ); + DEBUG && + logger.log( + "And second path items are", + secondPath.items.map(i => i[_nextDistance]) + ); // Adjust our total length this.totalLength = firstPathLength; @@ -433,25 +462,36 @@ export class BeltPath { * @param {Entity} entity */ deleteEntityOnEnd(entity) { - assert(this.entityPath[this.entityPath.length - 1] === entity, "Not the last entity actually"); + assert( + this.entityPath[this.entityPath.length - 1] === entity, + "Not actually the last entity (instead " + this.entityPath.indexOf(entity) + ")" + ); // Ok, first remove the entity const beltComp = entity.components.Belt; const beltLength = beltComp.getEffectiveLengthTiles(); - logger.log( - "Deleting last entity on path with length", - this.entityPath.length, - "(reducing", - this.totalLength, - " by", - beltLength, - ")" - ); + DEBUG && + logger.log( + "Deleting last entity on path with length", + this.entityPath.length, + "(reducing", + this.totalLength, + " by", + beltLength, + ")" + ); this.totalLength -= beltLength; this.entityPath.pop(); - logger.log(" New path has length of", this.totalLength, "with", this.entityPath.length, "entities"); + DEBUG && + logger.log( + " New path has length of", + this.totalLength, + "with", + this.entityPath.length, + "entities" + ); // This is just for sanity beltComp.assignedPath = null; @@ -465,20 +505,20 @@ export class BeltPath { let itemOffset = this.spacingToFirstItem; let lastItemOffset = itemOffset; - logger.log(" Adjusting", this.items.length, "items"); + DEBUG && logger.log(" Adjusting", this.items.length, "items"); for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; // Get rid of items past this path if (itemOffset >= this.totalLength) { - logger.log("Dropping item (current index=", i, ")"); + DEBUG && logger.log("Dropping item (current index=", i, ")"); this.items.splice(i, 1); i -= 1; continue; } - logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]); + DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]); lastItemOffset = itemOffset; itemOffset += item[_nextDistance]; } @@ -497,18 +537,19 @@ export class BeltPath { lastItemOffset ); - logger.log( - "Adjusted distance of last item: it is at", - lastItemOffset, - "so it has a distance of", - lastDistance, - "to the end (", - this.totalLength, - ")" - ); + DEBUG && + logger.log( + "Adjusted distance of last item: it is at", + lastItemOffset, + "so it has a distance of", + lastDistance, + "to the end (", + this.totalLength, + ")" + ); this.items[this.items.length - 1][_nextDistance] = lastDistance; } else { - logger.log(" Removed all items so we'll update spacing to total length"); + DEBUG && logger.log(" Removed all items so we'll update spacing to total length"); // We removed all items so update our spacing this.spacingToFirstItem = this.totalLength; @@ -528,25 +569,36 @@ export class BeltPath { * @param {Entity} entity */ deleteEntityOnStart(entity) { - assert(entity === this.entityPath[0], "Not actually the start entity"); + assert( + entity === this.entityPath[0], + "Not actually the start entity (instead " + this.entityPath.indexOf(entity) + ")" + ); // Ok, first remove the entity const beltComp = entity.components.Belt; const beltLength = beltComp.getEffectiveLengthTiles(); - logger.log( - "Deleting first entity on path with length", - this.entityPath.length, - "(reducing", - this.totalLength, - " by", - beltLength, - ")" - ); + DEBUG && + logger.log( + "Deleting first entity on path with length", + this.entityPath.length, + "(reducing", + this.totalLength, + " by", + beltLength, + ")" + ); this.totalLength -= beltLength; this.entityPath.shift(); - logger.log(" New path has length of", this.totalLength, "with", this.entityPath.length, "entities"); + DEBUG && + logger.log( + " New path has length of", + this.totalLength, + "with", + this.entityPath.length, + "entities" + ); // This is just for sanity beltComp.assignedPath = null; @@ -558,41 +610,45 @@ export class BeltPath { } else { // Simple case, we had no item on the beginning -> all good if (this.spacingToFirstItem >= beltLength) { - logger.log( - " No item on the first place, so we can just adjust the spacing (spacing=", - this.spacingToFirstItem, - ") removed =", - beltLength - ); + DEBUG && + logger.log( + " No item on the first place, so we can just adjust the spacing (spacing=", + this.spacingToFirstItem, + ") removed =", + beltLength + ); this.spacingToFirstItem -= beltLength; } else { // Welp, okay we need to drop all items which are < beltLength and adjust // the other item offsets as well - logger.log( - " We have at least one item in the beginning, drop those and adjust spacing (first item @", - this.spacingToFirstItem, - ") since we removed", - beltLength, - "length from path" - ); - logger.log( - " Items:", - this.items.map(i => i[_nextDistance]) - ); + DEBUG && + logger.log( + " We have at least one item in the beginning, drop those and adjust spacing (first item @", + this.spacingToFirstItem, + ") since we removed", + beltLength, + "length from path" + ); + DEBUG && + logger.log( + " Items:", + this.items.map(i => i[_nextDistance]) + ); // Find offset to first item let itemOffset = this.spacingToFirstItem; for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; if (itemOffset <= beltLength) { - logger.log( - " -> Dropping item with index", - i, - "at", - itemOffset, - "since it was on the removed belt" - ); + DEBUG && + logger.log( + " -> Dropping item with index", + i, + "at", + itemOffset, + "since it was on the removed belt" + ); // This item must be dropped this.items.splice(i, 1); i -= 1; @@ -605,13 +661,14 @@ export class BeltPath { } if (this.items.length > 0) { - logger.log( - " Offset of first non-dropped item was at:", - itemOffset, - "-> setting spacing to it (total length=", - this.totalLength, - ")" - ); + DEBUG && + logger.log( + " Offset of first non-dropped item was at:", + itemOffset, + "-> setting spacing to it (total length=", + this.totalLength, + ")" + ); this.spacingToFirstItem = itemOffset - beltLength; assert( @@ -619,7 +676,7 @@ export class BeltPath { "Invalid spacing after delete on start: " + this.spacingToFirstItem ); } else { - logger.log(" We dropped all items, simply set spacing to total length"); + DEBUG && logger.log(" We dropped all items, simply set spacing to total length"); // We dropped all items, simple one this.spacingToFirstItem = this.totalLength; } @@ -640,11 +697,11 @@ export class BeltPath { assert(otherPath !== this, "Circular path dependency"); const entities = otherPath.entityPath; - logger.log("Extending path by other path, starting to add entities"); + DEBUG && logger.log("Extending path by other path, starting to add entities"); const oldLength = this.totalLength; - logger.log(" Adding", entities.length, "new entities, current length =", this.totalLength); + DEBUG && logger.log(" Adding", entities.length, "new entities, current length =", this.totalLength); // First, append entities for (let i = 0; i < entities.length; ++i) { @@ -660,7 +717,13 @@ export class BeltPath { this.totalLength += additionalLength; } - logger.log(" Path is now", this.entityPath.length, "entities and has a length of", this.totalLength); + DEBUG && + logger.log( + " Path is now", + this.entityPath.length, + "entities and has a length of", + this.totalLength + ); // Update handles this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; @@ -670,21 +733,23 @@ export class BeltPath { if (this.items.length !== 0) { const lastItem = this.items[this.items.length - 1]; lastItem[_nextDistance] += otherPath.spacingToFirstItem; - logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now"); + DEBUG && + logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now"); } else { // Seems we have no items, update our first item distance this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem; - logger.log( - " We had no items, so our new spacing to first is old length (", - oldLength, - ") plus others spacing to first (", - otherPath.spacingToFirstItem, - ") =", - this.spacingToFirstItem - ); + DEBUG && + logger.log( + " We had no items, so our new spacing to first is old length (", + oldLength, + ") plus others spacing to first (", + otherPath.spacingToFirstItem, + ") =", + this.spacingToFirstItem + ); } - logger.log(" Pushing", otherPath.items.length, "items from other path"); + DEBUG && logger.log(" Pushing", otherPath.items.length, "items from other path"); // Aaand push the other paths items for (let i = 0; i < otherPath.items.length; ++i) { diff --git a/src/js/game/hud/parts/blueprint.js b/src/js/game/blueprint.js similarity index 85% rename from src/js/game/hud/parts/blueprint.js rename to src/js/game/blueprint.js index c53163d9..d923b2e1 100644 --- a/src/js/game/hud/parts/blueprint.js +++ b/src/js/game/blueprint.js @@ -1,13 +1,13 @@ -import { DrawParameters } from "../../../core/draw_parameters"; -import { Loader } from "../../../core/loader"; -import { createLogger } from "../../../core/logging"; -import { Vector } from "../../../core/vector"; -import { Entity } from "../../entity"; -import { GameRoot } from "../../root"; -import { findNiceIntegerValue } from "../../../core/utils"; -import { Math_pow } from "../../../core/builtins"; -import { blueprintShape } from "../../upgrades"; -import { globalConfig } from "../../../core/config"; +import { DrawParameters } from "../core/draw_parameters"; +import { Loader } from "../core/loader"; +import { createLogger } from "../core/logging"; +import { Vector } from "../core/vector"; +import { Entity } from "./entity"; +import { GameRoot } from "./root"; +import { findNiceIntegerValue } from "../core/utils"; +import { Math_pow } from "../core/builtins"; +import { blueprintShape } from "./upgrades"; +import { globalConfig } from "../core/config"; const logger = createLogger("blueprint"); @@ -176,7 +176,6 @@ export class Blueprint { tryPlace(root, tile) { return root.logic.performBulkOperation(() => { let anyPlaced = false; - const beltsToRegisterLater = []; for (let i = 0; i < this.entities.length; ++i) { let placeable = true; const entity = this.entities[i]; @@ -217,21 +216,10 @@ export class Blueprint { root.map.placeStaticEntity(clone); - // Registering a belt immediately triggers a recalculation of surrounding belt - // directions, which is no good when not all belts have been placed. To resolve - // this, only register belts after all entities have been placed. - if (!clone.components.Belt) { - root.entityMgr.registerEntity(clone); - } else { - beltsToRegisterLater.push(clone); - } + root.entityMgr.registerEntity(clone); anyPlaced = true; } } - - for (let i = 0; i < beltsToRegisterLater.length; i++) { - root.entityMgr.registerEntity(beltsToRegisterLater[i]); - } return anyPlaced; }); } diff --git a/src/js/game/hud/parts/blueprint_placer.js b/src/js/game/hud/parts/blueprint_placer.js index c98fbf2d..6b2af42e 100644 --- a/src/js/game/hud/parts/blueprint_placer.js +++ b/src/js/game/hud/parts/blueprint_placer.js @@ -9,7 +9,7 @@ import { KEYMAPPINGS } from "../../key_action_mapper"; import { blueprintShape } from "../../upgrades"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { Blueprint } from "./blueprint"; +import { Blueprint } from "../../blueprint"; import { SOUNDS } from "../../../platform/sound"; export class HUDBlueprintPlacer extends BaseHUDPart { diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 71c709dc..79caf38b 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -196,7 +196,7 @@ export class GameLogic { * @param {function} operation */ performBulkOperation(operation) { - logger.log("Running bulk operation ..."); + logger.warn("Running bulk operation ..."); assert(!this.root.bulkOperationRunning, "Can not run two bulk operations twice"); this.root.bulkOperationRunning = true; const now = performanceNow(); diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index ed4e4311..92de413b 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -146,38 +146,35 @@ export class BeltSystem extends GameSystemWithFilter { const assignedPath = entity.components.Belt.assignedPath; assert(assignedPath, "Entity has no belt path assigned"); + this.deleteEntityFromPath(assignedPath, entity); + this.verifyBeltPaths(); + } - // Find from and to entities - const fromEntity = this.findSupplyingEntity(entity); - const toEntity = this.findFollowUpEntity(entity); - - // Check if the belt had a previous belt - if (fromEntity) { - const fromPath = fromEntity.components.Belt.assignedPath; - - // Check if the entity had a followup - belt - if (toEntity) { - const toPath = toEntity.components.Belt.assignedPath; - assert(fromPath === toPath, "Invalid belt path layout (from path != to path)"); + /** + * Attempts to delete the belt from its current path + * @param {BeltPath} path + * @param {Entity} entity + */ + deleteEntityFromPath(path, entity) { + if (path.entityPath.length === 1) { + // This is a single entity path, easy to do, simply erase whole path + fastArrayDeleteValue(this.beltPaths, path); + return; + } - const newPath = fromPath.deleteEntityOnPathSplitIntoTwo(entity); - this.beltPaths.push(newPath); - } else { - fromPath.deleteEntityOnEnd(entity); - } + // Notice: Since there might be circular references, it is important to check + // which role the entity has + if (path.isStartEntity(entity)) { + // We tried to delete the start + path.deleteEntityOnStart(entity); + } else if (path.isEndEntity(entity)) { + // We tried to delete the end + path.deleteEntityOnEnd(entity); } else { - if (toEntity) { - // We need to remove the entity from the beginning of the other path - const toPath = toEntity.components.Belt.assignedPath; - toPath.deleteEntityOnStart(entity); - } else { - // This is a single entity path, easy to do - const path = entity.components.Belt.assignedPath; - fastArrayDeleteValue(this.beltPaths, path); - } + // We tried to delete something inbetween + const newPath = path.deleteEntityOnPathSplitIntoTwo(entity); + this.beltPaths.push(newPath); } - - this.verifyBeltPaths(); } /** @@ -193,13 +190,9 @@ export class BeltSystem extends GameSystemWithFilter { return; } - console.log("ADD"); - const fromEntity = this.findSupplyingEntity(entity); const toEntity = this.findFollowUpEntity(entity); - console.log("From:", fromEntity, "to:", toEntity); - // Check if we can add the entity to the previous path if (fromEntity) { const fromPath = fromEntity.components.Belt.assignedPath; @@ -385,22 +378,15 @@ export class BeltSystem extends GameSystemWithFilter { */ computeBeltPaths() { const visitedUids = new Set(); - console.log("Computing belt paths"); - const debugEntity = e => e.components.StaticMapEntity.origin.toString(); - - // const stackToVisit = this.allEntities.slice(); const result = []; - const currentPath = null; - for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; if (visitedUids.has(entity.uid)) { continue; } - // console.log("Starting at", debugEntity(entity)); // Mark entity as visited visitedUids.add(entity.uid); @@ -412,10 +398,9 @@ export class BeltSystem extends GameSystemWithFilter { // Find precedors let prevEntity = this.findSupplyingEntity(entity); while (prevEntity && --maxIter > 0) { - if (visitedUids.has(prevEntity)) { + if (visitedUids.has(prevEntity.uid)) { break; } - // console.log(" -> precedor: ", debugEntity(prevEntity)); path.unshift(prevEntity); visitedUids.add(prevEntity.uid); prevEntity = this.findSupplyingEntity(prevEntity); @@ -424,26 +409,17 @@ export class BeltSystem extends GameSystemWithFilter { // Find succedors let nextEntity = this.findFollowUpEntity(entity); while (nextEntity && --maxIter > 0) { - if (visitedUids.has(nextEntity)) { + if (visitedUids.has(nextEntity.uid)) { break; } - // console.log(" -> succedor: ", debugEntity(nextEntity)); path.push(nextEntity); visitedUids.add(nextEntity.uid); nextEntity = this.findFollowUpEntity(nextEntity); } - assert(maxIter !== 0, "Ran out of iterations"); - - // console.log( - // "Found path:", - // path.map(e => debugEntity(e)) - // ); - + assert(maxIter > 1, "Ran out of iterations"); result.push(new BeltPath(this.root, path)); - - // let prevEntity = this.findSupplyingEntity(srcEntity); } logger.log("Found", this.beltPaths.length, "belt paths"); From e594b6a4a7864750c30fcf80eeb8d0ee9887f57a Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 17:28:19 +0200 Subject: [PATCH 4/7] Add belt rendering (very slow for now) --- src/js/game/belt_path.js | 54 ++++++++++++++++++++--------- src/js/game/systems/belt.js | 4 ++- src/js/game/systems/item_ejector.js | 6 ++-- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 48d6a93e..0de39526 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -58,6 +58,27 @@ export class BeltPath { this.debug_checkIntegrity("constructor"); } + /** + * Returns whether this path can accept a new item + * @returns {boolean} + */ + canAcceptItem() { + return this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts; + } + + /** + * Tries to accept the item + * @param {BaseItem} item + */ + tryAcceptItem(item) { + if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { + this.items.unshift([this.spacingToFirstItem, item]); + this.spacingToFirstItem = 0; + return true; + } + return false; + } + /** * Helper to throw an error on mismatch * @param {string} change @@ -174,7 +195,7 @@ export class BeltPath { for (let i = 0; i < this.items.length; ++i) { const item = this.items[i]; - if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength) { + if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) { return fail( "Item has invalid offset to next item: ", item[_nextDistance], @@ -777,21 +798,6 @@ export class BeltPath { */ update() { this.debug_checkIntegrity("pre-update"); - const firstBeltItems = this.initialBeltComponent.sortedItems; - const transferItemAndProgress = firstBeltItems[0]; - - // Check if the first belt took a new item - if (transferItemAndProgress) { - const transferItem = transferItemAndProgress[_item]; - - if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { - // Can take new item - firstBeltItems.splice(0, 1); - - this.items.unshift([this.spacingToFirstItem, transferItem]); - this.spacingToFirstItem = 0; - } - } // Divide by item spacing on belts since we use throughput and not speed let beltSpeed = @@ -930,4 +936,20 @@ export class BeltPath { parameters.context.fillStyle = "purple"; parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2); } + + /** + * Draws the path + * @param {DrawParameters} parameters + */ + draw(parameters) { + let progress = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const nextDistanceAndItem = this.items[i]; + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + if (parameters.visibleRect.containsCircle(worldPos.x, worldPos.y, 10)) { + nextDistanceAndItem[_item].draw(worldPos.x, worldPos.y, parameters); + } + progress += nextDistanceAndItem[_nextDistance]; + } + } } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 92de413b..04d66b2b 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -227,7 +227,9 @@ export class BeltSystem extends GameSystemWithFilter { } draw(parameters) { - this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this)); + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].draw(parameters); + } } /** diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index d6597ecd..b5da836a 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -239,9 +239,9 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const beltComp = receiver.components.Belt; if (beltComp) { - // Ayy, its a belt! - if (beltComp.canAcceptItem()) { - beltComp.takeItem(item); + const path = beltComp.assignedPath; + assert(path, "belt has no path"); + if (path.tryAcceptItem(item)) { return true; } } From 857b79cac031f558a550d20f1977a04fde1e67de Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 17:44:11 +0200 Subject: [PATCH 5/7] Improve rendering performance by caching bounds of paths --- src/js/game/belt_path.js | 63 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 0de39526..ac6a74ab 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1,4 +1,4 @@ -import { Math_min } from "../core/builtins"; +import { Math_min, Math_max } from "../core/builtins"; import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; import { createLogger } from "../core/logging"; @@ -7,6 +7,7 @@ import { Vector } from "../core/vector"; import { BaseItem } from "./base_item"; import { Entity } from "./entity"; import { GameRoot } from "./root"; +import { Rectangle } from "../core/rectangle"; const logger = createLogger("belt_path"); @@ -50,6 +51,12 @@ export class BeltPath { 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; @@ -79,6 +86,20 @@ export class BeltPath { return false; } + /** + * Computes the tile bounds of the path + * @returns {Rectangle} + */ + computeBounds() { + let bounds = this.entityPath[0].components.StaticMapEntity.getTileSpaceBounds(); + for (let i = 1; i < this.entityPath.length; ++i) { + const staticComp = this.entityPath[i].components.StaticMapEntity; + const otherBounds = staticComp.getTileSpaceBounds(); + bounds = bounds.getUnion(otherBounds); + } + return bounds.allScaled(globalConfig.tileSize); + } + /** * Helper to throw an error on mismatch * @param {string} change @@ -92,7 +113,7 @@ export class BeltPath { * Checks if this path is valid */ debug_checkIntegrity(currentChange = "change") { - if (!G_IS_DEV) { + if (!G_IS_DEV || !DEBUG) { return; } @@ -220,6 +241,12 @@ export class BeltPath { ") -> items: " + this.items.map(i => i[_nextDistance]).join("|") ); } + + // Check bounds + const actualBounds = this.computeBounds(); + if (!actualBounds.equalsEpsilon(this.worldBounds, 0.01)) { + return fail("Bounds are stale"); + } } /** @@ -271,6 +298,9 @@ export class BeltPath { // Assign reference beltComp.assignedPath = this; + // Update bounds + this.worldBounds = this.computeBounds(); + this.debug_checkIntegrity("extend-on-end"); } @@ -298,6 +328,9 @@ export class BeltPath { this.entityPath.unshift(entity); this.initialBeltComponent = this.entityPath[0].components.Belt; + // Update bounds + this.worldBounds = this.computeBounds(); + this.debug_checkIntegrity("extend-on-begin"); } @@ -472,6 +505,9 @@ export class BeltPath { this.ejectorComp = firstPathEndEntity.components.ItemEjector; this.ejectorSlot = this.ejectorComp.slots[0]; + // Update bounds + this.worldBounds = this.computeBounds(); + this.debug_checkIntegrity("split-two-first"); secondPath.debug_checkIntegrity("split-two-second"); @@ -581,6 +617,9 @@ export class BeltPath { this.ejectorComp = this.entityPath[this.entityPath.length - 1].components.ItemEjector; this.ejectorSlot = this.ejectorComp.slots[0]; + // Update bounds + this.worldBounds = this.computeBounds(); + this.debug_checkIntegrity("delete-on-end"); } @@ -707,6 +746,9 @@ export class BeltPath { // Update handles this.initialBeltComponent = this.entityPath[0].components.Belt; + // Update bounds + this.worldBounds = this.computeBounds(); + this.debug_checkIntegrity("delete-on-start"); } @@ -778,6 +820,9 @@ export class BeltPath { this.items.push([item[_nextDistance], item[_item]]); } + // Update bounds + this.worldBounds = this.computeBounds(); + this.debug_checkIntegrity("extend-by-path"); } @@ -817,16 +862,16 @@ export class BeltPath { const nextDistanceAndItem = this.items[i]; const minimumSpacing = minimumDistance; - const takeAway = Math.max( + const takeAway = Math_max( 0, - Math.min(remainingAmount, nextDistanceAndItem[_nextDistance] - minimumSpacing) + Math_min(remainingAmount, nextDistanceAndItem[_nextDistance] - minimumSpacing) ); remainingAmount -= takeAway; nextDistanceAndItem[_nextDistance] -= takeAway; this.spacingToFirstItem += takeAway; - if (remainingAmount === 0.0) { + if (remainingAmount < 0.01) { break; } @@ -878,6 +923,10 @@ export class BeltPath { * @param {DrawParameters} parameters */ drawDebug(parameters) { + if (!parameters.visibleRect.containsRect(this.worldBounds)) { + return; + } + parameters.context.fillStyle = "#d79a25"; parameters.context.strokeStyle = "#d79a25"; parameters.context.beginPath(); @@ -942,6 +991,10 @@ export class BeltPath { * @param {DrawParameters} parameters */ draw(parameters) { + if (!parameters.visibleRect.containsRect(this.worldBounds)) { + return; + } + let progress = this.spacingToFirstItem; for (let i = 0; i < this.items.length; ++i) { const nextDistanceAndItem = this.items[i]; From 9a6029279d4b11c042f312c79422a39ff410eda6 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 17:46:12 +0200 Subject: [PATCH 6/7] Disable some logging --- src/js/changelog.js | 3 ++- src/js/game/systems/belt.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/changelog.js b/src/js/changelog.js index d4751f5b..29b5a28d 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,8 +1,9 @@ export const CHANGELOG = [ { version: "1.1.18", - date: "24.06.2020", + date: "26.06.2020", entries: [ + "Huge belt performance improvements - up to 50% increase", "Preparations for the wires update", "Update belt placement performance on huge factories (by Phlosioneer)", "Allow clicking on variants to select them", diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 04d66b2b..456aefd7 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -236,7 +236,7 @@ export class BeltSystem extends GameSystemWithFilter { * Verifies all belt paths */ verifyBeltPaths() { - if (G_IS_DEV) { + if (G_IS_DEV && false) { for (let i = 0; i < this.beltPaths.length; ++i) { this.beltPaths[i].debug_checkIntegrity("general-verify"); } From 42c569d91f3898f20c69ce86cb6fb833b6a8f863 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 26 Jun 2020 18:24:02 +0200 Subject: [PATCH 7/7] Implement saving and restoring belt paths --- src/js/game/belt_path.js | 102 +++++-- 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, 179 insertions(+), 292 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,36 +22,57 @@ 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 {Array} entityPath + * @param {Object} data + * @returns {BeltPath|string} */ - constructor(root, entityPath) { - this.root = root; + 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; + } - assert(entityPath.length > 0, "invalid entity path"); - this.entityPath = entityPath; + // Compute other properties + fakeObject.init(false); - /** - * Stores the items sorted, and their distance to the previous item (or start) - * Layout: [distanceToNext, item] - * @type {Array<[number, BaseItem]>} - */ - this.items = []; - - /** - * Stores the spacing to the first item - */ + 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(); - this.spacingToFirstItem = this.totalLength; + + if (computeSpacing) { + this.spacingToFirstItem = this.totalLength; + } /** * Current bounds of this path @@ -61,6 +84,31 @@ export class BeltPath { 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"); + this.entityPath = entityPath; + + /** + * Stores the items sorted, and their distance to the previous item (or start) + * Layout: [distanceToNext, item] + * @type {Array<[number, BaseItem]>} + */ + this.items = []; + + /** + * Stores the spacing to the first item + */ + + 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 +}