From 2d45e67bcaa3c94c3ea58a287e44f359ebefb2f6 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sun, 7 Aug 2022 17:21:00 +0100 Subject: [PATCH] Teach HierarchicalPathFinder about Immovable actors. By tracking updates on the ActorMap the HierarchicalPathFinder can be aware of actors moving around the map. We track a subset of immovable actors that always block. These actors can be treated as impassable obstacles just like terrain. When a path needs to be found the abstract path will guide the search around this subset of immovable actors just like it can guide the search around impassable terrain. For path searches that were previously imperformant because some immovable actors created a bottleneck that needed to be routed around, these will now be performant instead. Path searches with bottlenecks created by items such as trees, walls and buildings should see a performance improvement. Bottlenecks created by other units will not benefit. We now maintain two sets of HPFs. One is aware of immovable actors and will be used for path searches that request BlockedByActor.Immovable, BlockedByActor.Stationary and BlockedByActor.All to guide that around the immovable obstacles. The other is aware of terrain only and will be used for searches that request BlockedByActor.None, or if an ignoreActor is provided. A new UI dropdown when using the `/hpf` command will allow switching between the visuals of the two sets. --- OpenRA.Game/Traits/TraitsInterfaces.cs | 1 + .../Pathfinder/HierarchicalPathFinder.cs | 223 ++++++++++++++---- OpenRA.Mods.Common/Traits/World/ActorMap.cs | 12 + .../World/HierarchicalPathFinderOverlay.cs | 7 +- OpenRA.Mods.Common/Traits/World/Locomotor.cs | 6 + OpenRA.Mods.Common/Traits/World/PathFinder.cs | 36 ++- .../HierarchicalPathFinderOverlayLogic.cs | 17 ++ mods/cnc/chrome/ingame.yaml | 13 +- mods/d2k/chrome/ingame-player.yaml | 13 +- mods/ra/chrome/ingame-player.yaml | 13 +- mods/ts/chrome/ingame-player.yaml | 13 +- 11 files changed, 286 insertions(+), 68 deletions(-) diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 12af338740..70d4d02774 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -213,6 +213,7 @@ namespace OpenRA.Traits bool AnyActorsAt(CPos a); bool AnyActorsAt(CPos a, SubCell sub, bool checkTransient = true); bool AnyActorsAt(CPos a, SubCell sub, Func withCondition); + IEnumerable AllActors(); void AddInfluence(Actor self, IOccupySpace ios); void RemoveInfluence(Actor self, IOccupySpace ios); int AddCellTrigger(CPos[] cells, Action onEntry, Action onExit); diff --git a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs index 8b629d7c6c..31959e36d7 100644 --- a/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs +++ b/OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; namespace OpenRA.Mods.Common.Pathfinder { @@ -74,17 +75,23 @@ namespace OpenRA.Mods.Common.Pathfinder /// This implementation is aware of movement costs over terrain given by /// . It is aware of changes to the costs in terrain and able to /// update the abstract graph when this occurs. It is able to search the abstract graph as if - /// had been specified. It is not aware of actors on the map. So blocking actors - /// will not be accounted for in the heuristic. + /// had been specified. If is given in the + /// constructor, the abstract graph will additionally account for a subset of immovable actors using the same rules + /// as . It will be aware of changes + /// to actors on the map and update the abstract graph when this occurs. Other types of blocking actors will not be + /// accounted for in the heuristic. /// /// If the obstacle on the map is from terrain (e.g. a cliff or lake) the heuristic will work well. If the - /// obstacle is from a blocking actor (trees, walls, buildings, units) the heuristic is unaware of these. Therefore - /// the same problem where the search goes in the wrong direction is possible, e.g. through a choke-point that has - /// been walled off. In this scenario the performance benefit will be lost, as the search will have to explore more - /// nodes until it can get around the obstacle. + /// obstacle is from the subset of immovable actors (e.g. trees, walls, buildings) and + /// was given, the heuristic will work well. If the obstacle is from other + /// actors (e.g. units) then the heuristic is unaware of these. Therefore the same problem where the search goes in + /// the wrong direction is possible, e.g. through a choke-point that has units blocking it. In this scenario the + /// performance benefit will be lost, as the search will have to explore more nodes until it can get around the + /// obstacle. /// /// In summary, the reduces the performance impact of path searches that - /// must go around terrain, but does not improve performance of searches that must go around actors. + /// must go around terrain, and some types of actor, but does not improve performance of searches that must go + /// around the remaining types of actor. /// public sealed class HierarchicalPathFinder { @@ -93,8 +100,10 @@ namespace OpenRA.Mods.Common.Pathfinder readonly World world; readonly Locomotor locomotor; + readonly IActorMap actorMap; readonly Func costEstimator; readonly HashSet dirtyGridIndexes = new HashSet(); + readonly HashSet cellsWithBlockingActor; Grid mapBounds; int gridXs; int gridYs; @@ -224,13 +233,34 @@ namespace OpenRA.Mods.Common.Pathfinder } } - public HierarchicalPathFinder(World world, Locomotor locomotor) + public HierarchicalPathFinder(World world, Locomotor locomotor, IActorMap actorMap, BlockedByActor check) { this.world = world; this.locomotor = locomotor; + this.actorMap = actorMap; if (locomotor.Info.TerrainSpeeds.Count == 0) return; + if (check == BlockedByActor.Immovable) + { + // When we account for immovable actors, it depends on the actors on the map. + // When this changes, we must update the cost table. + actorMap.CellUpdated += RequireBlockingRefreshInCell; + + // Determine immovable cells from actors already on the map. + cellsWithBlockingActor = actorMap.AllActors() + .Where(ActorIsBlocking) + .SelectMany(a => + a.OccupiesSpace.OccupiedCells() + .Select(oc => oc.Cell) + .Where(c => ActorCellIsBlocking(a, c))) + .ToHashSet(); + } + else if (check != BlockedByActor.None) + throw new System.ComponentModel.InvalidEnumArgumentException( + $"{nameof(HierarchicalPathFinder)} supports {nameof(BlockedByActor.None)} " + + $"and {nameof(BlockedByActor.Immovable)} only for {nameof(check)}"); + costEstimator = PathSearch.DefaultCostEstimator(locomotor); BuildGrids(); @@ -300,6 +330,12 @@ namespace OpenRA.Mods.Common.Pathfinder { var singleAbstractCellForLayer = new CPos?[customMovementLayers.Length]; var localCellToAbstractCell = new Dictionary(); + + // When accounting for immovable actors, use a custom cost so those cells become invalid paths. + var customCost = cellsWithBlockingActor == null + ? (Func)null + : c => cellsWithBlockingActor.Contains(c) ? PathGraph.PathCostForInvalidPath : 0; + for (byte gridLayer = 0; gridLayer < customMovementLayers.Length; gridLayer++) { if (gridLayer != 0 && @@ -314,7 +350,7 @@ namespace OpenRA.Mods.Common.Pathfinder for (var x = gridX; x < grid.BottomRight.X; x++) { var cell = new CPos(x, y, gridLayer); - if (locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell) + if (CellIsAccessible(cell)) accessibleCells.Add(cell); } } @@ -364,7 +400,7 @@ namespace OpenRA.Mods.Common.Pathfinder { var src = accessibleCells.First(); using (var search = GetLocalPathSearch( - null, new[] { src }, src, null, null, BlockedByActor.None, false, grid, 100)) + null, new[] { src }, src, customCost, null, BlockedByActor.None, false, grid, 100)) { var localCellsInRegion = search.ExpandAll(); var abstractCell = AbstractCellForLocalCells(localCellsInRegion, gridLayer); @@ -420,6 +456,24 @@ namespace OpenRA.Mods.Common.Pathfinder !customMovementLayers[gridLayer].EnabledForLocomotor(locomotor.Info))) continue; + void AddEdgesIfMovementAllowedBetweenCells(CPos cell, CPos candidateCell) + { + if (!MovementAllowedBetweenCells(cell, candidateCell)) + return; + + var gridInfo = gridInfos[GridIndex(cell)]; + var abstractCell = gridInfo.AbstractCellForLocalCell(cell); + if (abstractCell == null) + return; + + var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)]; + var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell); + if (abstractCellAdjacent == null) + return; + + abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value)); + } + // Searches along edges of all grids within a layer. // Checks for the local edge cell if we can traverse to any of the three adjacent cells in the next grid. // Builds connections in the abstract graph when any local cells have connections. @@ -432,28 +486,14 @@ namespace OpenRA.Mods.Common.Pathfinder for (var x = startX; x < startX + GridSize; x += xIncrement) { var cell = new CPos(x, y, gridLayer); - if (locomotor.MovementCostForCell(cell) == PathGraph.MovementCostForUnreachableCell) + if (!CellIsAccessible(cell)) continue; var adjacentCell = cell + adjacentVec; for (var i = -1; i <= 1; i++) { var candidateCell = adjacentCell + i * new CVec(adjacentVec.Y, adjacentVec.X); - if (locomotor.MovementCostToEnterCell(null, cell, candidateCell, BlockedByActor.None, null) != - PathGraph.MovementCostForUnreachableCell) - { - var gridInfo = gridInfos[GridIndex(cell)]; - var abstractCell = gridInfo.AbstractCellForLocalCell(cell); - if (abstractCell == null) - continue; - - var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)]; - var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell); - if (abstractCellAdjacent == null) - continue; - - abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value)); - } + AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell); } } } @@ -479,7 +519,7 @@ namespace OpenRA.Mods.Common.Pathfinder for (var x = gridX; x < gridX + GridSize; x++) { var cell = new CPos(x, y, gridLayer); - if (locomotor.MovementCostForCell(cell) == PathGraph.MovementCostForUnreachableCell) + if (!CellIsAccessible(cell)) continue; CPos candidateCell; @@ -496,21 +536,7 @@ namespace OpenRA.Mods.Common.Pathfinder continue; } - if (locomotor.MovementCostToEnterCell(null, cell, candidateCell, BlockedByActor.None, null) == - PathGraph.MovementCostForUnreachableCell) - continue; - - var gridInfo = gridInfos[GridIndex(cell)]; - var abstractCell = gridInfo.AbstractCellForLocalCell(cell); - if (abstractCell == null) - continue; - - var gridInfoAdjacent = gridInfos[GridIndex(candidateCell)]; - var abstractCellAdjacent = gridInfoAdjacent.AbstractCellForLocalCell(candidateCell); - if (abstractCellAdjacent == null) - continue; - - abstractEdges.Add((abstractCell.Value, abstractCellAdjacent.Value)); + AddEdgesIfMovementAllowedBetweenCells(cell, candidateCell); } } } @@ -544,6 +570,98 @@ namespace OpenRA.Mods.Common.Pathfinder dirtyGridIndexes.Add(GridIndex(cell)); } + bool CellIsAccessible(CPos cell) + { + return locomotor.MovementCostForCell(cell) != PathGraph.MovementCostForUnreachableCell && + (cellsWithBlockingActor == null || !cellsWithBlockingActor.Contains(cell)); + } + + bool MovementAllowedBetweenCells(CPos accessibleSrcCell, CPos destCell) + { + return locomotor.MovementCostToEnterCell( + null, accessibleSrcCell, destCell, BlockedByActor.None, null) != PathGraph.MovementCostForUnreachableCell && + (cellsWithBlockingActor == null || !cellsWithBlockingActor.Contains(destCell)); + } + + /// + /// When actors change for a cell, marks the grid it belongs to as out of date. + /// + void RequireBlockingRefreshInCell(CPos cell) + { + var cellHasBlockingActor = false; + var actors = actorMap.GetActorsAt(cell); + foreach (var actor in actors) + { + if (ActorIsBlocking(actor) && ActorCellIsBlocking(actor, cell)) + { + cellHasBlockingActor = true; + break; + } + } + + if (cellHasBlockingActor) + { + if (cellsWithBlockingActor.Add(cell)) + dirtyGridIndexes.Add(GridIndex(cell)); + } + else + { + if (cellsWithBlockingActor.Remove(cell)) + dirtyGridIndexes.Add(GridIndex(cell)); + } + } + + /// + /// defines immovability based on the mobile trait. The blocking rules + /// in allow units to pass these + /// immovable actors if they are temporary blockers (e.g. gates) or crushable by the locomotor. Since our + /// abstract graph must work for any actor, we have to be conservative and can only consider a subset of the + /// immovable actors in the graph - ones we know cannot be passed by some actors due to these rules. + /// Both this and must be true for a cell to be blocked. + /// + /// This method is dependant on the logic in + /// and + /// . This method must be kept in sync with changes in the locomotor + /// rules. + /// + bool ActorIsBlocking(Actor actor) + { + var mobile = actor.OccupiesSpace as Mobile; + var isMovable = mobile != null && !mobile.IsTraitDisabled && !mobile.IsTraitPaused && !mobile.IsImmovable; + if (isMovable) + return false; + + var isTemporaryBlocker = world.RulesContainTemporaryBlocker && actor.TraitOrDefault() != null; + if (isTemporaryBlocker) + return false; + + var crushables = actor.TraitsImplementing(); + foreach (var crushable in crushables) + if (world.NoPlayersMask != crushable.CrushableBy(actor, locomotor.Info.Crushes)) + return false; + + return true; + } + + /// + /// The blocking rules additionally allow some cells to be considered passable even if the actor is blocking. + /// A cell is passable if the locomotor can share the cell and a subcell is available. It is also passable if + /// it is a transit only cell of a . We cannot consider these cells to be blocked. + /// Both this and must be true for a cell to be blocked. + /// + bool ActorCellIsBlocking(Actor actor, CPos cell) + { + var canShareCell = locomotor.Info.SharesCell && actorMap.HasFreeSubCell(cell); + if (canShareCell) + return false; + + var isTransitOnly = actor.OccupiesSpace is Building building && building.TransitOnlyCells().Contains(cell); + if (isTransitOnly) + return false; + + return true; + } + int GridIndex(CPos cellInGrid) { return @@ -741,7 +859,12 @@ namespace OpenRA.Mods.Common.Pathfinder /// /// Determines if a path exists between source and target. - /// Only terrain is taken into account, i.e. as if was given. + /// When was given, only terrain is taken into account, + /// i.e. as if was used when finding a path. + /// When was given, a subset of immovable actors are also taken into + /// account. If the method returns false, there is definitely no path. If it returns true there could be a + /// path, but it is possible that there is no path because of an immovable actor that does not belong to the + /// subset of actors that can be accounted for. So be careful. /// This would apply for any actor using the same as this . /// public bool PathExists(CPos source, CPos target) @@ -909,8 +1032,16 @@ namespace OpenRA.Mods.Common.Pathfinder // So we don't need to handle an abstract cell lookup failing, or the search failing to expand. // Cells added as initial starting points for the search are filtered out if they aren't reachable. // The search only explores accessible cells from then on. + // If the exceptions here do fire, they indicate a bug. The abstract graph is considering a cell to be + // unreachable, but the local pathfinder thinks it is reachable. We must fix the abstract graph to also + // consider the cell to be reachable. var gridInfo = gridInfos[GridIndex(cell)]; - var abstractCell = gridInfo.AbstractCellForLocalCell(cell).Value; + var maybeAbstractCell = gridInfo.AbstractCellForLocalCell(cell); + if (maybeAbstractCell == null) + throw new Exception( + "The abstract path should never be searched for an unreachable point. " + + "This is a bug. Failed lookup for an abstract cell."); + var abstractCell = maybeAbstractCell.Value; var info = graph[abstractCell]; // Expand the abstract search only if we have yet to get a route to the abstract cell. @@ -918,7 +1049,9 @@ namespace OpenRA.Mods.Common.Pathfinder { abstractSearch.TargetPredicate = c => c == abstractCell; if (!abstractSearch.ExpandToTarget()) - throw new Exception("The abstract path should never be searched for an unreachable point."); + throw new Exception( + "The abstract path should never be searched for an unreachable point. " + + "This is a bug. Failed to route to abstract cell."); info = graph[abstractCell]; } diff --git a/OpenRA.Mods.Common/Traits/World/ActorMap.cs b/OpenRA.Mods.Common/Traits/World/ActorMap.cs index be9d2088e7..e55bd3d432 100644 --- a/OpenRA.Mods.Common/Traits/World/ActorMap.cs +++ b/OpenRA.Mods.Common/Traits/World/ActorMap.cs @@ -403,6 +403,18 @@ namespace OpenRA.Mods.Common.Traits return AnyActorsAt(uv, layer, sub, withCondition); } + public IEnumerable AllActors() + { + foreach (var layer in influence) + { + if (layer == null) + continue; + foreach (var node in layer) + for (var i = node; i != null; i = i.Next) + yield return i.Actor; + } + } + public void AddInfluence(Actor self, IOccupySpace ios) { foreach (var c in ios.OccupiedCells()) diff --git a/OpenRA.Mods.Common/Traits/World/HierarchicalPathFinderOverlay.cs b/OpenRA.Mods.Common/Traits/World/HierarchicalPathFinderOverlay.cs index ded78433ba..3d16ffdb15 100644 --- a/OpenRA.Mods.Common/Traits/World/HierarchicalPathFinderOverlay.cs +++ b/OpenRA.Mods.Common/Traits/World/HierarchicalPathFinderOverlay.cs @@ -48,6 +48,11 @@ namespace OpenRA.Mods.Common.Traits /// public Locomotor Locomotor { get; set; } + /// + /// The blocking check selected in the UI which the overlay will display. + /// + public BlockedByActor Check { get; set; } = BlockedByActor.Immovable; + public HierarchicalPathFinderOverlay(HierarchicalPathFinderOverlayInfo info) { this.info = info; @@ -88,7 +93,7 @@ namespace OpenRA.Mods.Common.Traits : new[] { Locomotor }; foreach (var locomotor in locomotors) { - var (abstractGraph, abstractDomains) = pathFinder.GetOverlayDataForLocomotor(locomotor); + var (abstractGraph, abstractDomains) = pathFinder.GetOverlayDataForLocomotor(locomotor, Check); // Locomotor doesn't allow movement, nothing to display. if (abstractGraph == null || abstractDomains == null) diff --git a/OpenRA.Mods.Common/Traits/World/Locomotor.cs b/OpenRA.Mods.Common/Traits/World/Locomotor.cs index 59f8c866a7..f53ec7fde6 100644 --- a/OpenRA.Mods.Common/Traits/World/Locomotor.cs +++ b/OpenRA.Mods.Common/Traits/World/Locomotor.cs @@ -311,6 +311,9 @@ namespace OpenRA.Mods.Common.Traits return world.ActorMap.FreeSubCell(cell, preferredSubCell); } + /// This logic is replicated in and + /// . If this method is updated please update those as + /// well. bool IsBlockedBy(Actor actor, Actor otherActor, Actor ignoreActor, CPos cell, BlockedByActor check, CellFlag cellFlag) { if (otherActor == ignoreActor) @@ -450,6 +453,9 @@ namespace OpenRA.Mods.Common.Traits } } + /// This logic is replicated in and + /// . If this method is updated please update those as + /// well. void UpdateCellBlocking(CPos cell) { using (new PerfSample("locomotor_cache")) diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index d3e18ba674..d902fadb4f 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -19,8 +19,8 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.World)] - [Desc("Calculates routes for mobile units with locomotors based on the A* search algorithm.", " Attach this to the world actor.")] - public class PathFinderInfo : TraitInfo, Requires + [Desc("Calculates routes for mobile actors with locomotors based on the A* search algorithm.", " Attach this to the world actor.")] + public class PathFinderInfo : TraitInfo, Requires, Requires { public override object Create(ActorInitializer init) { @@ -40,7 +40,8 @@ namespace OpenRA.Mods.Common.Traits readonly World world; PathFinderOverlay pathFinderOverlay; - Dictionary hierarchicalPathFindersByLocomotor; + Dictionary hierarchicalPathFindersBlockedByNoneByLocomotor; + Dictionary hierarchicalPathFindersBlockedByImmovableByLocomotor; public PathFinder(Actor self) { @@ -49,9 +50,10 @@ namespace OpenRA.Mods.Common.Traits public ( IReadOnlyDictionary> AbstractGraph, - IReadOnlyDictionary AbstractDomains) GetOverlayDataForLocomotor(Locomotor locomotor) + IReadOnlyDictionary AbstractDomains) GetOverlayDataForLocomotor( + Locomotor locomotor, BlockedByActor check) { - return hierarchicalPathFindersByLocomotor[locomotor].GetOverlayData(); + return GetHierarchicalPathFinder(locomotor, check, null).GetOverlayData(); } public void WorldLoaded(World w, WorldRenderer wr) @@ -59,9 +61,13 @@ namespace OpenRA.Mods.Common.Traits pathFinderOverlay = world.WorldActor.TraitOrDefault(); // Requires ensures all Locomotors have been initialized. - hierarchicalPathFindersByLocomotor = w.WorldActor.TraitsImplementing().ToDictionary( + var locomotors = w.WorldActor.TraitsImplementing().ToList(); + hierarchicalPathFindersBlockedByNoneByLocomotor = locomotors.ToDictionary( locomotor => locomotor, - locomotor => new HierarchicalPathFinder(world, locomotor)); + locomotor => new HierarchicalPathFinder(world, locomotor, w.ActorMap, BlockedByActor.None)); + hierarchicalPathFindersBlockedByImmovableByLocomotor = locomotors.ToDictionary( + locomotor => locomotor, + locomotor => new HierarchicalPathFinder(world, locomotor, w.ActorMap, BlockedByActor.Immovable)); } /// @@ -110,15 +116,25 @@ namespace OpenRA.Mods.Common.Traits } // Use a hierarchical path search, which performs a guided bidirectional search. - return hierarchicalPathFindersByLocomotor[locomotor].FindPath( + return GetHierarchicalPathFinder(locomotor, check, ignoreActor).FindPath( self, source, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, laneBias, pathFinderOverlay); } // Use a hierarchical path search, which performs a guided unidirectional search. - return hierarchicalPathFindersByLocomotor[locomotor].FindPath( + return GetHierarchicalPathFinder(locomotor, check, ignoreActor).FindPath( self, sourcesList, target, check, DefaultHeuristicWeightPercentage, customCost, ignoreActor, laneBias, pathFinderOverlay); } + HierarchicalPathFinder GetHierarchicalPathFinder(Locomotor locomotor, BlockedByActor check, Actor ignoreActor) + { + // If there is an actor to ignore, we cannot use an HPF that accounts for any blocking actors. + // One of the blocking actors might be the one we need to ignore! + var hpfs = check == BlockedByActor.None || ignoreActor != null + ? hierarchicalPathFindersBlockedByNoneByLocomotor + : hierarchicalPathFindersBlockedByImmovableByLocomotor; + return hpfs[locomotor]; + } + /// /// Calculates a path for the actor from multiple possible sources, whilst searching for an acceptable target. /// Returned path is *reversed* and given target to source. @@ -149,7 +165,7 @@ namespace OpenRA.Mods.Common.Traits /// public bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target) { - return hierarchicalPathFindersByLocomotor[locomotor].PathExists(source, target); + return hierarchicalPathFindersBlockedByNoneByLocomotor[locomotor].PathExists(source, target); } static Locomotor GetActorLocomotor(Actor self) diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/HierarchicalPathFinderOverlayLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/HierarchicalPathFinderOverlayLogic.cs index a022b2ee31..30f0c6bbfe 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/HierarchicalPathFinderOverlayLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/HierarchicalPathFinderOverlayLogic.cs @@ -42,6 +42,23 @@ namespace OpenRA.Mods.Common.Widgets.Logic locomotorSelector.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", locomotors.Length * 30, locomotors, setupItem); }; + + var checks = new[] { BlockedByActor.None, BlockedByActor.Immovable }; + var checkSelector = widget.Get("HPF_OVERLAY_CHECK"); + checkSelector.OnMouseDown = _ => + { + Func setupItem = (option, template) => + { + var item = ScrollItemWidget.Setup( + template, + () => hpfOverlay.Check == option, + () => hpfOverlay.Check = option); + item.Get("LABEL").GetText = () => option.ToString(); + return item; + }; + + checkSelector.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", checks.Length * 30, checks, setupItem); + }; } } } diff --git a/mods/cnc/chrome/ingame.yaml b/mods/cnc/chrome/ingame.yaml index 95e920a893..88244efa31 100644 --- a/mods/cnc/chrome/ingame.yaml +++ b/mods/cnc/chrome/ingame.yaml @@ -1864,14 +1864,21 @@ Container@PLAYER_WIDGETS: Logic: HierarchicalPathFinderOverlayLogic X: WINDOW_RIGHT - WIDTH - 240 Y: 40 - Width: 150 - Height: 25 + Width: 175 + Height: 60 Children: DropDownButton@HPF_OVERLAY_LOCOMOTOR: + Y: PARENT_TOP Width: PARENT_RIGHT - Height: PARENT_BOTTOM + Height: 25 Text: Select Locomotor Font: Regular + DropDownButton@HPF_OVERLAY_CHECK: + Y: PARENT_TOP + 35 + Width: PARENT_RIGHT + Height: 25 + Text: Select BlockedByActor + Font: Regular Background@FMVPLAYER: Width: WINDOW_RIGHT diff --git a/mods/d2k/chrome/ingame-player.yaml b/mods/d2k/chrome/ingame-player.yaml index c6532e20f8..108dcbfcf7 100644 --- a/mods/d2k/chrome/ingame-player.yaml +++ b/mods/d2k/chrome/ingame-player.yaml @@ -645,11 +645,18 @@ Container@PLAYER_WIDGETS: Logic: HierarchicalPathFinderOverlayLogic X: WINDOW_RIGHT - WIDTH - 231 Y: 40 - Width: 150 - Height: 25 + Width: 175 + Height: 60 Children: DropDownButton@HPF_OVERLAY_LOCOMOTOR: + Y: PARENT_TOP Width: PARENT_RIGHT - Height: PARENT_BOTTOM + Height: 25 Text: Select Locomotor Font: Regular + DropDownButton@HPF_OVERLAY_CHECK: + Y: PARENT_TOP + 35 + Width: PARENT_RIGHT + Height: 25 + Text: Select BlockedByActor + Font: Regular diff --git a/mods/ra/chrome/ingame-player.yaml b/mods/ra/chrome/ingame-player.yaml index 0894249fa9..d60270d5c7 100644 --- a/mods/ra/chrome/ingame-player.yaml +++ b/mods/ra/chrome/ingame-player.yaml @@ -653,12 +653,19 @@ Container@PLAYER_WIDGETS: Logic: HierarchicalPathFinderOverlayLogic X: WINDOW_RIGHT - WIDTH - 260 Y: 40 - Width: 150 - Height: 25 + Width: 175 + Height: 60 Children: DropDownButton@HPF_OVERLAY_LOCOMOTOR: + Y: PARENT_TOP Width: PARENT_RIGHT - Height: PARENT_BOTTOM + Height: 25 Text: Select Locomotor Font: Regular + DropDownButton@HPF_OVERLAY_CHECK: + Y: PARENT_TOP + 35 + Width: PARENT_RIGHT + Height: 25 + Text: Select BlockedByActor + Font: Regular diff --git a/mods/ts/chrome/ingame-player.yaml b/mods/ts/chrome/ingame-player.yaml index 202048864c..5ba58b111f 100644 --- a/mods/ts/chrome/ingame-player.yaml +++ b/mods/ts/chrome/ingame-player.yaml @@ -623,11 +623,18 @@ Container@PLAYER_WIDGETS: Logic: HierarchicalPathFinderOverlayLogic X: WINDOW_RIGHT - WIDTH - 245 Y: 40 - Width: 150 - Height: 25 + Width: 175 + Height: 60 Children: DropDownButton@HPF_OVERLAY_LOCOMOTOR: + Y: PARENT_TOP Width: PARENT_RIGHT - Height: PARENT_BOTTOM + Height: 25 Text: Select Locomotor Font: Regular + DropDownButton@HPF_OVERLAY_CHECK: + Y: PARENT_TOP + 35 + Width: PARENT_RIGHT + Height: 25 + Text: Select BlockedByActor + Font: Regular