#region Copyright & License Information /* * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) * 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; using System.Linq; using OpenRA.Mods.Common.Traits; namespace OpenRA.Mods.Common.Pathfinder { /// /// A dense pathfinding graph that implements the ability to cost and get connections for cells, /// and supports . Allows searching over a dense grid of cells. /// Derived classes are required to provide backing storage for the pathfinding information. /// abstract class DensePathGraph : IPathGraph { const int LaneBiasCost = 1; protected readonly ICustomMovementLayer[] CustomMovementLayers; readonly int customMovementLayersEnabledForLocomotor; readonly Locomotor locomotor; readonly Actor actor; readonly World world; readonly BlockedByActor check; readonly Func customCost; readonly Actor ignoreActor; readonly bool laneBias; readonly bool inReverse; readonly bool checkTerrainHeight; protected DensePathGraph(Locomotor locomotor, Actor actor, World world, BlockedByActor check, Func customCost, Actor ignoreActor, bool laneBias, bool inReverse) { CustomMovementLayers = world.GetCustomMovementLayers(); customMovementLayersEnabledForLocomotor = CustomMovementLayers.Count(cml => cml != null && cml.EnabledForLocomotor(locomotor.Info)); this.locomotor = locomotor; this.world = world; this.actor = actor; this.check = check; this.customCost = customCost; this.ignoreActor = ignoreActor; this.laneBias = laneBias; this.inReverse = inReverse; checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0; } public abstract CellInfo this[CPos node] { get; set; } /// /// Determines if a candidate neighbouring position is /// allowable to be returned in a . /// /// The candidate cell. This might not lie within map bounds. protected virtual bool IsValidNeighbor(CPos neighbor) { return true; } // Sets of neighbors for each incoming direction. These exclude the neighbors which are guaranteed // to be reached more cheaply by a path through our parent cell which does not include the current cell. // For horizontal/vertical directions, the set is the three cells 'ahead'. For diagonal directions, the set // is the three cells ahead, plus the two cells to the side. Effectively, these are the cells left over // if you ignore the ones reachable from the parent cell. // We can do this because for any cell in range of both the current and parent location, // if we can reach it from one we are guaranteed to be able to reach it from the other. static readonly CVec[][] DirectedNeighbors = { new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(-1, 0), new CVec(-1, 1) }, // TL new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1) }, // T new[] { new CVec(-1, -1), new CVec(0, -1), new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, // TR new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1) }, // L CVec.Directions, new[] { new CVec(1, -1), new CVec(1, 0), new CVec(1, 1) }, // R new[] { new CVec(-1, -1), new CVec(-1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // BL new[] { new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // B new[] { new CVec(1, -1), new CVec(1, 0), new CVec(-1, 1), new CVec(0, 1), new CVec(1, 1) }, // BR }; // With height discontinuities between the parent and current cell, we cannot optimize the possible neighbors. // It is no longer true that for any cell in range of both the current and parent location, // if we can reach it from one we are guaranteed to be able to reach it from the other. // This is because a height discontinuity may have prevented the parent location from reaching, // but our current cell on a new height may be able to reach as the height difference may be small enough. // Therefore, we can only exclude the parent cell in each set of directions. static readonly CVec[][] DirectedNeighborsConservative = { CVec.Directions.Exclude(new CVec(1, 1)).ToArray(), // TL CVec.Directions.Exclude(new CVec(0, 1)).ToArray(), // T CVec.Directions.Exclude(new CVec(-1, 1)).ToArray(), // TR CVec.Directions.Exclude(new CVec(1, 0)).ToArray(), // L CVec.Directions, CVec.Directions.Exclude(new CVec(-1, 0)).ToArray(), // R CVec.Directions.Exclude(new CVec(1, -1)).ToArray(), // BL CVec.Directions.Exclude(new CVec(0, -1)).ToArray(), // B CVec.Directions.Exclude(new CVec(-1, -1)).ToArray(), // BR }; public List GetConnections(CPos position) { var layer = position.Layer; var info = this[position]; var previousNode = info.PreviousNode; var dx = position.X - previousNode.X; var dy = position.Y - previousNode.Y; var index = dy * 3 + dx + 4; var heightLayer = world.Map.Height; var directions = (checkTerrainHeight && layer == 0 && previousNode.Layer == 0 && heightLayer[position] != heightLayer[previousNode] ? DirectedNeighborsConservative : DirectedNeighbors)[index]; var validNeighbors = new List(directions.Length + (layer == 0 ? customMovementLayersEnabledForLocomotor : 1)); for (var i = 0; i < directions.Length; i++) { var dir = directions[i]; var neighbor = position + dir; if (!IsValidNeighbor(neighbor)) continue; var pathCost = GetPathCostToNode(position, neighbor, dir); if (pathCost != PathGraph.PathCostForInvalidPath && this[neighbor].Status != CellStatus.Closed) validNeighbors.Add(new GraphConnection(neighbor, pathCost)); } if (layer == 0) { foreach (var cml in CustomMovementLayers) { if (cml == null || !cml.EnabledForLocomotor(locomotor.Info)) continue; var layerPosition = new CPos(position.X, position.Y, cml.Index); if (!IsValidNeighbor(layerPosition)) continue; var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition); if (entryCost != PathGraph.MovementCostForUnreachableCell && CanEnterNode(position, layerPosition) && this[layerPosition].Status != CellStatus.Closed) validNeighbors.Add(new GraphConnection(layerPosition, entryCost)); } } else { var groundPosition = new CPos(position.X, position.Y, 0); if (IsValidNeighbor(groundPosition)) { var exitCost = CustomMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition); if (exitCost != PathGraph.MovementCostForUnreachableCell && CanEnterNode(position, groundPosition) && this[groundPosition].Status != CellStatus.Closed) validNeighbors.Add(new GraphConnection(groundPosition, exitCost)); } } return validNeighbors; } bool CanEnterNode(CPos srcNode, CPos destNode) { return locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor) != PathGraph.MovementCostForUnreachableCell; } int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction) { var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor); if (movementCost != PathGraph.MovementCostForUnreachableCell) return CalculateCellPathCost(destNode, direction, movementCost); return PathGraph.PathCostForInvalidPath; } int CalculateCellPathCost(CPos neighborCPos, CVec direction, short movementCost) { var cellCost = direction.X * direction.Y != 0 ? Exts.MultiplyBySqrtTwo(movementCost) : movementCost; if (customCost != null) { var customCellCost = customCost(neighborCPos); if (customCellCost == PathGraph.PathCostForInvalidPath) return PathGraph.PathCostForInvalidPath; cellCost += customCellCost; } // Directional bonuses for smoother flow! if (laneBias) { var ux = neighborCPos.X + (inReverse ? 1 : 0) & 1; var uy = neighborCPos.Y + (inReverse ? 1 : 0) & 1; if ((ux == 0 && direction.Y < 0) || (ux == 1 && direction.Y > 0)) cellCost += LaneBiasCost; if ((uy == 0 && direction.X < 0) || (uy == 1 && direction.X > 0)) cellCost += LaneBiasCost; } return cellCost; } protected virtual void Dispose(bool disposing) { } public void Dispose() { Dispose(true); } } }