Fix reversed path searches from inaccessible locations.
The Harvester trait and MoveAdjacentTo activity called the pathfinder but had a single source and multiple targets. The pathfinder interface only allows for the opposite: multiple sources and a single target. To work around this they would swap the inputs. This works in most cases but not all cases. One aspect of asymmetry is that an actor may move out of an inaccessible source cell, but not onto an inaccessible target cell. Searches that involved an inaccessible source cell and that applied this swapping method would therefore fail to return a path, when a valid path was possible. Although a rare case, once good way to reproduce is to use a production building that spawns actors on inaccessible cells around it, such as the RA naval yard. A move order uses the pathfinder correctly and the unit will move out. Using a force attack causes the unit to use the broken "swapped" mechanism in MoveAdjacentTo and it will be stuck. This asymmetry has been longstanding but the pathfinding infrastructure only sporadically accounted for it. It is now documented and applied consistently. Create a new overload on the pathfinder trait that allows a single source and multiple targets, so callers have an overload that does what they need and won't be tempted to swap the positions and run into this issue. Internally, this requires us to teach Locomotor to ignore the self actor when performing movement cost checks for these "in reverse" searches so the unit doesn't consider the cell blocked by itself.
This commit is contained in:
committed by
Paul Chote
parent
e4ba9733fe
commit
4ec5a4b34a
@@ -46,7 +46,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
var graph = new MapPathGraph(LayerPoolForWorld(world), locomotor, self, world, check, customCost, ignoreActor, laneBias, false);
|
||||
var search = new PathSearch(graph, loc => 0, 0, targetPredicate, recorder);
|
||||
|
||||
AddInitialCells(world, locomotor, froms, customCost, search);
|
||||
AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, false, search);
|
||||
|
||||
return search;
|
||||
}
|
||||
@@ -71,11 +71,22 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
heuristic ??= DefaultCostEstimator(locomotor, target);
|
||||
var search = new PathSearch(graph, heuristic, heuristicWeightPercentage, loc => loc == target, recorder);
|
||||
|
||||
AddInitialCells(world, locomotor, froms, customCost, search);
|
||||
AddInitialCells(world, locomotor, self, froms, check, customCost, ignoreActor, inReverse, search);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a cell is a valid pathfinding location.
|
||||
/// <list type="bullet">
|
||||
/// <item>It is in the world.</item>
|
||||
/// <item>It is either on the ground layer (0) or on an *enabled* custom movement layer.</item>
|
||||
/// <item>It has not been excluded by the <paramref name="customCost"/>.</item>
|
||||
/// </list>
|
||||
/// If required, follow this with a call to
|
||||
/// <see cref="Locomotor.MovementCostToEnterCell(Actor, CPos, CPos, BlockedByActor, Actor, bool)"/> to
|
||||
/// determine if the cell is accessible.
|
||||
/// </summary>
|
||||
public static bool CellAllowsMovement(World world, Locomotor locomotor, CPos cell, Func<CPos, int> customCost)
|
||||
{
|
||||
return world.Map.Contains(cell) &&
|
||||
@@ -83,10 +94,18 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
(customCost == null || customCost(cell) != PathGraph.PathCostForInvalidPath);
|
||||
}
|
||||
|
||||
static void AddInitialCells(World world, Locomotor locomotor, IEnumerable<CPos> froms, Func<CPos, int> customCost, PathSearch search)
|
||||
static void AddInitialCells(World world, Locomotor locomotor, Actor self, IEnumerable<CPos> froms,
|
||||
BlockedByActor check, Func<CPos, int> customCost, Actor ignoreActor, bool inReverse, PathSearch search)
|
||||
{
|
||||
// A source cell is allowed to have an unreachable movement cost.
|
||||
// Therefore we don't need to check if the cell is accessible, only that it allows movement.
|
||||
// *Unless* the search is being done in reverse, in this case the source is really a target,
|
||||
// and a target is required to have a reachable cost.
|
||||
// We also need to ignore self, so we don't consider the location blocked by ourselves!
|
||||
foreach (var sl in froms)
|
||||
if (CellAllowsMovement(world, locomotor, sl, customCost))
|
||||
if (CellAllowsMovement(world, locomotor, sl, customCost) &&
|
||||
(!inReverse || locomotor.MovementCostToEnterCell(self, sl, check, ignoreActor, true)
|
||||
!= PathGraph.MovementCostForUnreachableCell))
|
||||
search.AddInitialCell(sl, customCost);
|
||||
}
|
||||
|
||||
@@ -229,7 +248,7 @@ namespace OpenRA.Mods.Common.Pathfinder
|
||||
var currentInfo = Graph[currentMinNode];
|
||||
Graph[currentMinNode] = new CellInfo(CellStatus.Closed, currentInfo.CostSoFar, currentInfo.EstimatedTotalCost, currentInfo.PreviousNode);
|
||||
|
||||
foreach (var connection in Graph.GetConnections(currentMinNode))
|
||||
foreach (var connection in Graph.GetConnections(currentMinNode, TargetPredicate))
|
||||
{
|
||||
// Calculate the cost up to that point
|
||||
var costSoFarToNeighbor = currentInfo.CostSoFar + connection.Cost;
|
||||
|
||||
Reference in New Issue
Block a user