#region Copyright & License Information
/*
* Copyright 2007-2021 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 supports a search over all cells within a map.
/// It implements the ability to cost and get connections for cells, and supports .
///
sealed class PathGraph : IPathGraph
{
public const int PathCostForInvalidPath = int.MaxValue;
public const short MovementCostForUnreachableCell = short.MaxValue;
const int LaneBiasCost = 1;
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 inReverse;
readonly bool laneBias;
readonly bool checkTerrainHeight;
readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer;
readonly CellLayer[] cellInfoForLayer;
public PathGraph(CellInfoLayerPool layerPool, Locomotor locomotor, Actor actor, World world, BlockedByActor check,
Func customCost, Actor ignoreActor, bool inReverse, bool laneBias)
{
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.inReverse = inReverse;
this.laneBias = laneBias;
checkTerrainHeight = world.Map.Grid.MaximumTerrainHeight > 0;
// As we support a search over the whole map area,
// use the pool to grab the CellInfos we need to track the graph state.
// This allows us to avoid the cost of allocating large arrays constantly.
// PERF: Avoid LINQ
pooledLayer = layerPool.Get();
cellInfoForLayer = new CellLayer[customMovementLayers.Length];
cellInfoForLayer[0] = pooledLayer.GetLayer();
foreach (var cml in customMovementLayers)
if (cml != null && cml.EnabledForLocomotor(locomotor.Info))
cellInfoForLayer[cml.Index] = pooledLayer.GetLayer();
}
// 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;
var pathCost = GetPathCostToNode(position, neighbor, dir);
if (pathCost != 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);
var entryCost = cml.EntryMovementCost(locomotor.Info, layerPosition);
if (entryCost != 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);
var exitCost = customMovementLayers[layer].ExitMovementCost(locomotor.Info, groundPosition);
if (exitCost != 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)
!= MovementCostForUnreachableCell;
}
int GetPathCostToNode(CPos srcNode, CPos destNode, CVec direction)
{
var movementCost = locomotor.MovementCostToEnterCell(actor, srcNode, destNode, check, ignoreActor);
if (movementCost != MovementCostForUnreachableCell)
return CalculateCellPathCost(destNode, direction, movementCost);
return 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 == PathCostForInvalidPath)
return 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;
}
public CellInfo this[CPos pos]
{
get => cellInfoForLayer[pos.Layer][pos];
set => cellInfoForLayer[pos.Layer][pos] = value;
}
public void Dispose()
{
pooledLayer.Dispose();
}
}
}