From e48a77b67a777fe89ed1adc749e1314a9c86189a Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Fri, 16 Jan 2026 19:33:25 +0000 Subject: [PATCH] Fix a desync in HierarchicalPathFinder. In a multiplayer game with bots running, only the host client runs bot logic. This logic must not alter the world state, or the host will desync from the other clients which do not run the bot logic. The bot logic can interact with the pathfinder. This can eventually flow into HierarchicalPathFinder.BuildGrid which calls AbstractCellForLocalCells to determine the abstract cells within the grid. This method chooses an abstract cell closest to the middle, but there can be multiple cells tied for this. The input comes from a local path search, whose ordering depends on current cell costs. Previously, this ordering could would influence which of the equally close cells we would choose. In this case we'd choose the first in the ordering. On non-host clients which don't run the bot logic, they may not build the grid until a later tick. If the cell costs have changed the the local path search would still explore the same set of cells, but the ordering may be different. With the previous behaviour, they could choose a different abstract cell from the equally close cells. This results in a diverged state where their abstract graph differs subtly from the host abstract graph. Eventually, a unit will path search in a manner where this state divergence causes it to take a different path, which will be detected as a desync. To fix the issue, we ensure we pick the same abstract cell no matter the ordering of the list, by inserting an arbitrary tiebreaker rule. This means the differing ordering of the explored cells from the local path search is still possible, but the ordering no longer influences the abstract cell we choose as a result. This keeps the state of the abstract graph in sync across the clients, and thus prevents units from making different pathfinding decisions and desyncing. --- OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs index 28d7bca440..341d8a07e4 100644 --- a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs +++ b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs @@ -390,12 +390,16 @@ namespace OpenRA.Mods.Common.Pathfinder // Make sure the abstract cell is one of the available local cells. // This ensures each abstract cell we generate is unique. // We'll choose an abstract node as close to the middle of the region as possible. + // If there are multiple equally close cells, choose the leftmost/topmost (arbitrary tiebreaker). + // This is important so we always choose the same abstract cell, no matter the ordering of the list. var abstractCell = desired; var distance = int.MaxValue; foreach (var cell in cells) { var newDistance = (cell - desired).LengthSquared; - if (distance > newDistance) + if (distance > newDistance || + (distance == newDistance && abstractCell.X > cell.X) || + (distance == newDistance && abstractCell.X == cell.X && abstractCell.Y > cell.Y)) { distance = newDistance; abstractCell = cell; @@ -416,6 +420,8 @@ namespace OpenRA.Mods.Common.Pathfinder using (var search = GetLocalPathSearch( null, [src], src, customCost, null, BlockedByActor.None, false, grid, 100, null, false, null)) { + // Determinism: The order of visited cells from ExpandAll can be perturbed by cell cost changes. + // Processing of this list must not take any implicit dependencies on its ordering. var localCellsInRegion = search.ExpandAll(); var abstractCell = AbstractCellForLocalCells(localCellsInRegion, gridLayer); accessibleCells.ExceptWith(localCellsInRegion);