diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 4fcbbb48..f873e6e9 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -3,6 +3,7 @@ import { Component } from "../component"; import { globalConfig } from "../../core/config"; import { types } from "../../savegame/serialization"; import { gItemRegistry } from "../../core/global_registries"; +import { Entity } from "../entity"; /** @enum {string} */ export const enumUndergroundBeltMode = { @@ -10,6 +11,13 @@ export const enumUndergroundBeltMode = { receiver: "receiver", }; +/** + * @typedef {{ + * entity: Entity, + * distance: number + * }} LinkedUndergroundBelt + */ + export class UndergroundBeltComponent extends Component { static getId() { return "UndergroundBelt"; @@ -52,6 +60,13 @@ export class UndergroundBeltComponent extends Component { * @type {Array<[BaseItem, number]>} Format is [Item, remaining seconds until transfer/ejection] */ this.pendingItems = []; + + /** + * The linked entity, used to speed up performance. This contains either + * the entrance or exit depending on the tunnel type + * @type {LinkedUndergroundBelt} + */ + this.cachedLinkedEntity = null; } /** diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 93a5dce2..c18ce2e9 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -62,7 +62,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { "#fe50a6" ); } - this.recomputeAreaCache(this.areaToRecompute); + this.recomputeAreaCache(); this.areaToRecompute = null; } else { logger.log("Full cache recompute"); @@ -83,10 +83,10 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } /** - * - * @param {Rectangle} area + * Recomputes the cache in the given area */ - recomputeAreaCache(area) { + recomputeAreaCache() { + const area = this.areaToRecompute; let entryCount = 0; logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h); diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index a4512e18..7e97181f 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -1,16 +1,20 @@ import { globalConfig } from "../../core/config"; import { Loader } from "../../core/loader"; +import { createLogger } from "../../core/logging"; +import { Rectangle } from "../../core/rectangle"; import { + enumAngleToDirection, enumDirection, enumDirectionToAngle, enumDirectionToVector, - Vector, - enumAngleToDirection, enumInvertedDirections, } from "../../core/vector"; import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; +import { fastArrayDelete } from "../../core/utils"; + +const logger = createLogger("tunnels"); export class UndergroundBeltSystem extends GameSystemWithFilter { constructor(root) { @@ -25,30 +29,40 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { ), }; - this.root.signals.entityManuallyPlaced.add(this.onEntityPlaced, this); + this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this); + + /** + * @type {Rectangle} + */ + this.areaToRecompute = null; + + this.root.signals.entityAdded.add(this.onEntityChanged, this); + this.root.signals.entityDestroyed.add(this.onEntityChanged, this); } - update() { - const delta = this.root.dynamicTickrate.deltaSeconds; + /** + * Called when an entity got added or removed + * @param {Entity} entity + */ + onEntityChanged(entity) { + if (!this.root.gameInitialized) { + return; + } + const undergroundComp = entity.components.UndergroundBelt; + if (!undergroundComp) { + return; + } - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const undergroundComp = entity.components.UndergroundBelt; - const pendingItems = undergroundComp.pendingItems; + const affectedArea = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections( + globalConfig.undergroundBeltMaxTilesByTier[ + globalConfig.undergroundBeltMaxTilesByTier.length - 1 + ] + 1 + ); - // Decrease remaining time of all items in belt - for (let k = 0; k < pendingItems.length; ++k) { - const item = pendingItems[k]; - item[1] = Math.max(0, item[1] - delta); - if (G_IS_DEV && globalConfig.debug.instantBelts) { - item[1] = 0; - } - } - if (undergroundComp.mode === enumUndergroundBeltMode.sender) { - this.handleSender(entity); - } else { - this.handleReceiver(entity); - } + if (this.areaToRecompute) { + this.areaToRecompute = this.areaToRecompute.getUnion(affectedArea); + } else { + this.areaToRecompute = affectedArea; } } @@ -56,7 +70,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { * Callback when an entity got placed, used to remove belts between underground belts * @param {Entity} entity */ - onEntityPlaced(entity) { + onEntityManuallyPlaced(entity) { if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) { // Smart-place disabled return; @@ -207,63 +221,157 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { } } + /** + * Recomputes the cache in the given area, invalidating all entries there + */ + recomputeArea() { + const area = this.areaToRecompute; + logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h); + if (G_IS_DEV && globalConfig.debug.renderChanges) { + this.root.hud.parts.changesDebugger.renderChange("tunnels", this.areaToRecompute, "#fc03be"); + } + + for (let x = area.x; x < area.right(); ++x) { + for (let y = area.y; y < area.bottom(); ++y) { + const entity = this.root.map.getTileContentXY(x, y); + if (!entity) { + continue; + } + const undergroundComp = entity.components.UndergroundBelt; + if (!undergroundComp) { + continue; + } + + undergroundComp.cachedLinkedEntity = null; + } + } + } + + update() { + if (this.areaToRecompute) { + this.recomputeArea(); + this.areaToRecompute = null; + } + + const delta = this.root.dynamicTickrate.deltaSeconds; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const undergroundComp = entity.components.UndergroundBelt; + const pendingItems = undergroundComp.pendingItems; + + // Decrease remaining time of all items in belt + for (let k = 0; k < pendingItems.length; ++k) { + const item = pendingItems[k]; + item[1] = Math.max(0, item[1] - delta); + if (G_IS_DEV && globalConfig.debug.instantBelts) { + item[1] = 0; + } + } + if (undergroundComp.mode === enumUndergroundBeltMode.sender) { + this.handleSender(entity); + } else { + this.handleReceiver(entity); + } + } + } + + /** + * Finds the receiver for a given sender + * @param {Entity} entity + * @returns {import("../components/underground_belt").LinkedUndergroundBelt} + */ + findRecieverForSender(entity) { + const staticComp = entity.components.StaticMapEntity; + const undergroundComp = entity.components.UndergroundBelt; + const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); + const searchVector = enumDirectionToVector[searchDirection]; + const targetRotation = enumDirectionToAngle[searchDirection]; + let currentTile = staticComp.origin; + + // Search in the direction of the tunnel + for ( + let searchOffset = 0; + searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; + ++searchOffset + ) { + currentTile = currentTile.add(searchVector); + + const potentialReceiver = this.root.map.getTileContent(currentTile); + if (!potentialReceiver) { + // Empty tile + continue; + } + const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt; + if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) { + // Not a tunnel, or not on the same tier + continue; + } + + if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) { + // Not a receiver + continue; + } + + const receiverStaticComp = potentialReceiver.components.StaticMapEntity; + if (receiverStaticComp.rotation !== targetRotation) { + // Wrong rotation + continue; + } + + return { entity: potentialReceiver, distance: searchOffset }; + } + + // None found + return { entity: null, distance: 0 }; + } + /** * * @param {Entity} entity */ handleSender(entity) { - const staticComp = entity.components.StaticMapEntity; const undergroundComp = entity.components.UndergroundBelt; - // Check if we have any item + // Find the current receiver + let receiver = undergroundComp.cachedLinkedEntity; + if (!receiver) { + // We don't have a receiver, compute it + receiver = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity); + if (G_IS_DEV && globalConfig.debug.renderChanges) { + this.root.hud.parts.changesDebugger.renderChange( + "sender", + entity.components.StaticMapEntity.getTileSpaceBounds(), + "#fc03be" + ); + } + } + + if (!receiver.entity) { + // If there is no connection to a receiver, ignore this one + return; + } + + // Check if we have any item if (undergroundComp.pendingItems.length > 0) { + assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); const nextItemAndDuration = undergroundComp.pendingItems[0]; const remainingTime = nextItemAndDuration[1]; const nextItem = nextItemAndDuration[0]; + // Check if the item is ready to be emitted if (remainingTime === 0) { - // Try to find a receiver - const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); - const searchVector = enumDirectionToVector[searchDirection]; - const targetRotation = enumDirectionToAngle[searchDirection]; - - let currentTile = staticComp.origin; - - for ( - let searchOffset = 0; - searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; - ++searchOffset + // Check if the receiver can accept it + if ( + receiver.entity.components.UndergroundBelt.tryAcceptTunneledItem( + nextItem, + receiver.distance, + this.root.hubGoals.getUndergroundBeltBaseSpeed() + ) ) { - currentTile = currentTile.add(searchVector); - - const contents = this.root.map.getTileContent(currentTile); - if (contents) { - const receiverUndergroundComp = contents.components.UndergroundBelt; - if ( - receiverUndergroundComp && - receiverUndergroundComp.tier === undergroundComp.tier - ) { - const receiverStaticComp = contents.components.StaticMapEntity; - if (receiverStaticComp.rotation === targetRotation) { - if (receiverUndergroundComp.mode === enumUndergroundBeltMode.receiver) { - // Try to pass over the item to the receiver - if ( - receiverUndergroundComp.tryAcceptTunneledItem( - nextItem, - searchOffset, - this.root.hubGoals.getUndergroundBeltBaseSpeed() - ) - ) { - undergroundComp.pendingItems = []; - } - } - - // When we hit some underground belt, always stop, no matter what - break; - } - } - } + // Drop this item + fastArrayDelete(undergroundComp.pendingItems, 0); } } } @@ -276,7 +384,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { handleReceiver(entity) { const undergroundComp = entity.components.UndergroundBelt; - // Try to eject items, we only check the first one cuz its sorted by remaining time + // Try to eject items, we only check the first one because it is sorted by remaining time const items = undergroundComp.pendingItems; if (items.length > 0) { const nextItemAndDuration = undergroundComp.pendingItems[0];