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.
110 lines
3.5 KiB
C#
110 lines
3.5 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace OpenRA.Mods.Common.Pathfinder
|
|
{
|
|
/// <summary>
|
|
/// Represents a pathfinding graph with nodes and edges.
|
|
/// Nodes are represented as cells, and pathfinding information
|
|
/// in the form of <see cref="CellInfo"/> is attached to each one.
|
|
/// </summary>
|
|
public interface IPathGraph : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Given a source node, returns connections to all reachable destination nodes with their cost.
|
|
/// </summary>
|
|
/// <remarks>PERF: Returns a <see cref="List{T}"/> rather than an <see cref="IEnumerable{T}"/> as enumerating
|
|
/// this efficiently is important for pathfinding performance. Callers should interact with this as an
|
|
/// <see cref="IEnumerable{T}"/> and not mutate the result.</remarks>
|
|
List<GraphConnection> GetConnections(CPos source, Func<CPos, bool> targetPredicate);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the pathfinding information for a given node.
|
|
/// </summary>
|
|
CellInfo this[CPos node] { get; set; }
|
|
}
|
|
|
|
public static class PathGraph
|
|
{
|
|
public const int PathCostForInvalidPath = int.MaxValue;
|
|
public const short MovementCostForUnreachableCell = short.MaxValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a full edge in a graph, giving the cost to traverse between two nodes.
|
|
/// </summary>
|
|
public readonly struct GraphEdge
|
|
{
|
|
public readonly CPos Source;
|
|
public readonly CPos Destination;
|
|
public readonly int Cost;
|
|
|
|
public GraphEdge(CPos source, CPos destination, int cost)
|
|
{
|
|
if (source == destination)
|
|
throw new ArgumentException($"{nameof(source)} and {nameof(destination)} must refer to different cells");
|
|
if (cost < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be negative");
|
|
if (cost == PathGraph.PathCostForInvalidPath)
|
|
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be used for an unreachable path");
|
|
|
|
Source = source;
|
|
Destination = destination;
|
|
Cost = cost;
|
|
}
|
|
|
|
public GraphConnection ToConnection()
|
|
{
|
|
return new GraphConnection(Destination, Cost);
|
|
}
|
|
|
|
public override string ToString() => $"{Source} -> {Destination} = {Cost}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents part of an edge in a graph, giving the cost to traverse to a node.
|
|
/// </summary>
|
|
public readonly struct GraphConnection
|
|
{
|
|
public readonly struct CostComparer : IComparer<GraphConnection>
|
|
{
|
|
public int Compare(GraphConnection x, GraphConnection y)
|
|
{
|
|
return x.Cost.CompareTo(y.Cost);
|
|
}
|
|
}
|
|
|
|
public readonly CPos Destination;
|
|
public readonly int Cost;
|
|
|
|
public GraphConnection(CPos destination, int cost)
|
|
{
|
|
if (cost < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be negative");
|
|
if (cost == PathGraph.PathCostForInvalidPath)
|
|
throw new ArgumentOutOfRangeException(nameof(cost), $"{nameof(cost)} cannot be used for an unreachable path");
|
|
|
|
Destination = destination;
|
|
Cost = cost;
|
|
}
|
|
|
|
public GraphEdge ToEdge(CPos source)
|
|
{
|
|
return new GraphEdge(source, Destination, Cost);
|
|
}
|
|
|
|
public override string ToString() => $"-> {Destination} = {Cost}";
|
|
}
|
|
}
|