mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 01:38:13 +00:00
Fix cluster deletion (#3185)
## Description: When a train station is removed, the clusters are recomputed. However the cluster recomputation code has not been changed from the original rail network implementation, which was a tree. The deletion code made assumptions that are not true anymore since we introduced loops in the network. As a result the cluster recomputation was very inefficient, although the data was correct. Changes: - Fix clusters computation when a structure is deleted - Structures are frequently deleted in bulk: atom/hydro/MIRV. Re-computing the clusters when a single structure is deleted would be inefficient because the recomputed cluster would probably need to be recomputed again in the same tick. Instead, when a structure is deleted, flag the cluster as "dirty", and recompute all the dirty clusters once per tick only. Previous performances (hydro over a dense area): <img width="700" height="160" alt="image" src="https://github.com/user-attachments/assets/cd3ceb42-6d5f-4ad1-b35a-f8e5e0513821" /> Now: <img width="450" height="269" alt="image" src="https://github.com/user-attachments/assets/55dec3b9-8619-4a6c-9a16-f5368fe40da1" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { placeName } from "../client/graphics/NameBoxCalculator";
|
||||
import { getConfig } from "./configuration/ConfigLoader";
|
||||
import { Executor } from "./execution/ExecutionManager";
|
||||
import { RecomputeRailClusterExecution } from "./execution/RecomputeRailClusterExecution";
|
||||
import { WinCheckExecution } from "./execution/WinCheckExecution";
|
||||
import {
|
||||
AllPlayers,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
PlayerInfo,
|
||||
PlayerProfile,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
import { createGame } from "./game/GameImpl";
|
||||
import { TileRef } from "./game/GameMap";
|
||||
@@ -105,6 +107,11 @@ export class GameRunner {
|
||||
this.game.addExecution(...this.execManager.nationExecutions());
|
||||
}
|
||||
this.game.addExecution(new WinCheckExecution());
|
||||
if (!this.game.config().isUnitDisabled(UnitType.Factory)) {
|
||||
this.game.addExecution(
|
||||
new RecomputeRailClusterExecution(this.game.railNetwork()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public addTurn(turn: Turn): void {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Execution, Game } from "../game/Game";
|
||||
import { RailNetwork } from "../game/RailNetwork";
|
||||
|
||||
export class RecomputeRailClusterExecution implements Execution {
|
||||
constructor(private railNetwork: RailNetwork) {}
|
||||
|
||||
isActive(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {}
|
||||
|
||||
tick(ticks: number): void {
|
||||
this.railNetwork.recomputeClusters();
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ export interface RailNetwork {
|
||||
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
|
||||
stationManager(): StationManager;
|
||||
overlappingRailroads(tile: TileRef): number[];
|
||||
recomputeClusters(): void;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
private gridCellSize: number = 4;
|
||||
private railGrid: RailSpatialGrid;
|
||||
private nextId: number = 0;
|
||||
private dirtyClusters = new Set<Cluster>();
|
||||
|
||||
constructor(
|
||||
private game: Game,
|
||||
@@ -106,26 +107,48 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
}
|
||||
}
|
||||
|
||||
recomputeClusters() {
|
||||
if (this.dirtyClusters.size === 0) return;
|
||||
|
||||
for (const cluster of this.dirtyClusters) {
|
||||
const allOriginalStations = new Set(cluster.stations);
|
||||
while (allOriginalStations.size > 0) {
|
||||
const nextStation = allOriginalStations.values().next().value;
|
||||
const allConnectedStations = this.computeCluster(nextStation);
|
||||
// Filter stations that are connected to the current cluster
|
||||
for (const connectedStation of allConnectedStations) {
|
||||
allOriginalStations.delete(connectedStation);
|
||||
}
|
||||
// Those stations were disconnected: new cluster
|
||||
if (allOriginalStations.size > 0) {
|
||||
const newCluster = new Cluster();
|
||||
// Switching their cluster will automatically remove them from their current cluster
|
||||
newCluster.addStations(allConnectedStations);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dirtyClusters.clear();
|
||||
}
|
||||
|
||||
removeStation(unit: Unit): void {
|
||||
const station = this._stationManager.findStation(unit);
|
||||
if (!station) return;
|
||||
|
||||
const neighbors = station.neighbors();
|
||||
this.disconnectFromNetwork(station);
|
||||
this._stationManager.removeStation(station);
|
||||
station.unit.setTrainStation(false);
|
||||
|
||||
const cluster = station.getCluster();
|
||||
if (!cluster) return;
|
||||
if (neighbors.length === 1) {
|
||||
cluster.removeStation(station);
|
||||
} else if (neighbors.length > 1) {
|
||||
for (const neighbor of neighbors) {
|
||||
const stations = this.computeCluster(neighbor);
|
||||
const newCluster = new Cluster();
|
||||
newCluster.addStations(stations);
|
||||
}
|
||||
|
||||
cluster.removeStation(station);
|
||||
if (cluster.size() === 0) {
|
||||
this.deleteCluster(cluster);
|
||||
this.dirtyClusters.delete(cluster);
|
||||
return;
|
||||
}
|
||||
station.unit.setTrainStation(false);
|
||||
|
||||
this.dirtyClusters.add(cluster);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,10 +281,6 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
this.railGrid.unregister(rail);
|
||||
}
|
||||
station.clearRailroads();
|
||||
const cluster = station.getCluster();
|
||||
if (cluster !== null && cluster.size() === 1) {
|
||||
this.deleteCluster(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
private deleteCluster(cluster: Cluster) {
|
||||
|
||||
@@ -53,7 +53,7 @@ export class TrainStation {
|
||||
id: number = -1; // assigned by StationManager
|
||||
private readonly stopHandlers: Partial<Record<UnitType, TrainStopHandler>> =
|
||||
{};
|
||||
private cluster: Cluster | null;
|
||||
private cluster: Cluster | null = null;
|
||||
private railroads: Set<Railroad> = new Set();
|
||||
// Quick lookup from neighboring station to connecting railroad
|
||||
private railroadByNeighbor: Map<TrainStation, Railroad> = new Map();
|
||||
@@ -129,6 +129,10 @@ export class TrainStation {
|
||||
}
|
||||
|
||||
setCluster(cluster: Cluster | null) {
|
||||
// Properly disconnect cluster if it's already set
|
||||
if (this.cluster !== null) {
|
||||
this.cluster.removeStation(this);
|
||||
}
|
||||
this.cluster = cluster;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user