From 23f3f8d90c3e1186d4bb9af2f2707cc607cba488 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Thu, 20 Jul 2023 18:36:47 +0100 Subject: [PATCH] Add helper methods to locate actors that can be reached via a path. Previously, the ClosestTo and PositionClosestTo existed to perform a simple distance based check to choose the closest location from a choice of locations to a single other location. For some functions this is sufficient, but for many functions we want to then move between the locations. If the location selected is in fact unreachable (e.g. on another island) then we would not want to consider it. We now introduce ClosestToIgnoringPath for checks where we don't care about a path existing, e.g. weapons hitting nearby targets. When we do care about paths, we introduce ClosestToWithPathFrom and ClosestToWithPathTo which will check that a path exists. The PathFrom check will make sure one of the actors from the list can make it to the single target location. The PathTo check will make sure the single actor can make it to one of the target locations. This difference allows us to specify which actor will be doing the moving. This is important as a path might exists for one actor, but not another. Consider two islands with a hovercraft on one and a tank on the other. The hovercraft can path to the tank, but the tank cannot path to the hovercraft. We also introduce WithPathFrom and WithPathTo. These will perform filtering by checking for valid paths, but won't select the closest location. By employing the new methods that filter for paths, we fix various behaviour that would cause actors to get confused. Imagine an islands map, by checking for paths we ensure logic will locate reachable locations on the island, rather than considering a location on a nearby island that is physically closer but unreachable. This fixes AI squad automation, and other automated behaviours such as rearming. --- OpenRA.Game/Exts.cs | 5 + OpenRA.Game/WorldUtils.cs | 44 +++- OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs | 2 +- OpenRA.Mods.Common/AIUtils.cs | 6 - .../Activities/Air/ReturnToBase.cs | 2 +- OpenRA.Mods.Common/Activities/Enter.cs | 2 +- OpenRA.Mods.Common/Activities/Hunt.cs | 2 +- OpenRA.Mods.Common/Activities/LayMines.cs | 7 +- .../Activities/Move/LocalMoveIntoTarget.cs | 15 +- OpenRA.Mods.Common/Projectiles/AreaBeam.cs | 2 +- OpenRA.Mods.Common/Projectiles/LaserZap.cs | 2 +- OpenRA.Mods.Common/Projectiles/Missile.cs | 2 +- OpenRA.Mods.Common/Traits/Armament.cs | 2 +- .../Traits/Attack/AttackBase.cs | 2 +- OpenRA.Mods.Common/Traits/AutoCarryall.cs | 2 +- OpenRA.Mods.Common/Traits/AutoCrusher.cs | 2 +- .../BotModuleLogic/BaseBuilderQueueManager.cs | 5 +- .../BotModules/CaptureManagerBotModule.cs | 18 +- .../BotModules/SquadManagerBotModule.cs | 171 +++++++++--- .../Traits/BotModules/Squads/Squad.cs | 99 +++++-- .../BotModules/Squads/States/AirStates.cs | 15 +- .../BotModules/Squads/States/GroundStates.cs | 69 +++-- .../BotModules/Squads/States/NavyStates.cs | 247 ------------------ .../Squads/States/ProtectionStates.cs | 14 +- .../Traits/Buildings/RallyPoint.cs | 6 +- OpenRA.Mods.Common/Traits/CaptureManager.cs | 2 +- OpenRA.Mods.Common/WorldExtensions.cs | 179 +++++++++++++ OpenRA.Mods.D2k/Traits/Sandworm.cs | 3 + 28 files changed, 527 insertions(+), 400 deletions(-) delete mode 100644 OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs diff --git a/OpenRA.Game/Exts.cs b/OpenRA.Game/Exts.cs index 26c38fa702..3c613bec29 100644 --- a/OpenRA.Game/Exts.cs +++ b/OpenRA.Game/Exts.cs @@ -355,6 +355,11 @@ namespace OpenRA return number * 46341 / 32768; } + public static int MultiplyBySqrtTwoOverTwo(int number) + { + return (int)(number * 23170L / 32768L); + } + public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor) { var quotient = Math.DivRem(dividend, divisor, out var remainder); diff --git a/OpenRA.Game/WorldUtils.cs b/OpenRA.Game/WorldUtils.cs index 604fcd1259..483b777c4b 100644 --- a/OpenRA.Game/WorldUtils.cs +++ b/OpenRA.Game/WorldUtils.cs @@ -19,19 +19,51 @@ namespace OpenRA { public static class WorldUtils { - public static Actor ClosestTo(this IEnumerable actors, Actor a) + /// + /// From the given , select the one nearest the given by + /// comparing their . No check is done to see if a path exists. + /// + public static Actor ClosestToIgnoringPath(this IEnumerable actors, Actor actor) { - return actors.ClosestTo(a.CenterPosition); + return actors.ClosestToIgnoringPath(actor.CenterPosition); } - public static Actor ClosestTo(this IEnumerable actors, WPos pos) + /// + /// From the given , select the one nearest the given by + /// comparing the . No check is done to see if a path exists. + /// + public static Actor ClosestToIgnoringPath(this IEnumerable actors, WPos position) { - return actors.MinByOrDefault(a => (a.CenterPosition - pos).LengthSquared); + return actors.MinByOrDefault(a => (a.CenterPosition - position).LengthSquared); } - public static WPos PositionClosestTo(this IEnumerable positions, WPos pos) + /// + /// From the given that can be projected to , + /// select the one nearest the given by + /// comparing their . No check is done to see if a path exists. + /// + public static T ClosestToIgnoringPath(IEnumerable items, Func selector, Actor actor) { - return positions.MinByOrDefault(p => (p - pos).LengthSquared); + return ClosestToIgnoringPath(items, selector, actor.CenterPosition); + } + + /// + /// From the given that can be projected to , + /// select the one nearest the given by + /// comparing the . No check is done to see if a path exists. + /// + public static T ClosestToIgnoringPath(IEnumerable items, Func selector, WPos position) + { + return items.MinByOrDefault(x => (selector(x).CenterPosition - position).LengthSquared); + } + + /// + /// From the given , select the one nearest the given . + /// No check is done to see if a path exists, as an actor is required for that. + /// + public static WPos ClosestToIgnoringPath(this IEnumerable positions, WPos position) + { + return positions.MinByOrDefault(p => (p - position).LengthSquared); } public static IEnumerable FindActorsInCircle(this World world, WPos origin, WDist r) diff --git a/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs b/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs index a2eb89352e..19d61d79fd 100644 --- a/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs +++ b/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs @@ -79,7 +79,7 @@ namespace OpenRA.Mods.Cnc.Projectiles // Zap tracks target if (info.TrackTarget && args.GuidedTarget.IsValidFor(args.SourceActor)) - target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source); + target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source); if (damageDuration-- > 0) args.Weapon.Impact(Target.FromPos(target), new WarheadArgs(args)); diff --git a/OpenRA.Mods.Common/AIUtils.cs b/OpenRA.Mods.Common/AIUtils.cs index 09804b9330..ec90bed688 100644 --- a/OpenRA.Mods.Common/AIUtils.cs +++ b/OpenRA.Mods.Common/AIUtils.cs @@ -63,12 +63,6 @@ namespace OpenRA.Mods.Common .Count(a => a.Owner == owner && buildings.Contains(a.Info.Name)); } - public static List FindEnemiesByCommonName(HashSet commonNames, Player player) - { - return player.World.Actors.Where(a => !a.IsDead && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy && - commonNames.Contains(a.Info.Name)).ToList(); - } - public static ActorInfo GetInfoByCommonName(HashSet names, Player owner) { return owner.World.Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(owner.World.LocalRandom).Value; diff --git a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs index cd0443b39e..924ae1ee56 100644 --- a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs +++ b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs @@ -47,7 +47,7 @@ namespace OpenRA.Mods.Common.Activities && a.Owner == self.Owner && rearmInfo.RearmActors.Contains(a.Info.Name) && (!unreservedOnly || Reservable.IsAvailableFor(a, self))) - .ClosestTo(self); + .ClosestToWithPathFrom(self); } bool ShouldLandAtBuilding(Actor self, Actor dest) diff --git a/OpenRA.Mods.Common/Activities/Enter.cs b/OpenRA.Mods.Common/Activities/Enter.cs index 925a9bed47..da84b55710 100644 --- a/OpenRA.Mods.Common/Activities/Enter.cs +++ b/OpenRA.Mods.Common/Activities/Enter.cs @@ -126,7 +126,7 @@ namespace OpenRA.Mods.Common.Activities case EnterState.Entering: { // Check that we reached the requested position - var targetPos = target.Positions.PositionClosestTo(self.CenterPosition); + var targetPos = target.Positions.ClosestToWithPathFrom(self); if (!IsCanceling && self.CenterPosition == targetPos && target.Type == TargetType.Actor) OnEnterComplete(self, target.Actor); diff --git a/OpenRA.Mods.Common/Activities/Hunt.cs b/OpenRA.Mods.Common/Activities/Hunt.cs index e4d02b8370..219e2fc3b2 100644 --- a/OpenRA.Mods.Common/Activities/Hunt.cs +++ b/OpenRA.Mods.Common/Activities/Hunt.cs @@ -36,7 +36,7 @@ namespace OpenRA.Mods.Common.Activities if (IsCanceling) return true; - var targetActor = targets.ClosestTo(self); + var targetActor = targets.ClosestToWithPathFrom(self); if (targetActor == null) return false; diff --git a/OpenRA.Mods.Common/Activities/LayMines.cs b/OpenRA.Mods.Common/Activities/LayMines.cs index e473b2efb8..fb5740af16 100644 --- a/OpenRA.Mods.Common/Activities/LayMines.cs +++ b/OpenRA.Mods.Common/Activities/LayMines.cs @@ -88,8 +88,11 @@ namespace OpenRA.Mods.Common.Activities if (rearmableInfo != null && ammoPools.Any(p => p.Info.Name == minelayer.Info.AmmoPoolName && !p.HasAmmo)) { // Rearm (and possibly repair) at rearm building, then back out here to refill the minefield some more - rearmTarget = self.World.Actors.Where(a => self.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally && rearmableInfo.RearmActors.Contains(a.Info.Name)) - .ClosestTo(self); + rearmTarget = self.World.Actors + .Where(a => + self.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally + && rearmableInfo.RearmActors.Contains(a.Info.Name)) + .ClosestToWithPathFrom(self); if (rearmTarget == null) return true; diff --git a/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs b/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs index d673f8203a..a3958e1854 100644 --- a/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs +++ b/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Activities readonly Target target; readonly Color? targetLineColor; readonly WDist targetMovementThreshold; - WPos targetStartPos; + WPos? targetStartPos; public LocalMoveIntoTarget(Actor self, in Target target, WDist targetMovementThreshold, Color? targetLineColor = null) { @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Activities protected override void OnFirstRun(Actor self) { - targetStartPos = target.Positions.PositionClosestTo(self.CenterPosition); + targetStartPos = target.Positions.ClosestToWithPathFrom(self); } public override bool Tick(Actor self) @@ -47,14 +47,17 @@ namespace OpenRA.Mods.Common.Activities return false; var currentPos = self.CenterPosition; - var targetPos = target.Positions.PositionClosestTo(currentPos); + var targetPos = target.Positions.ClosestToWithPathFrom(self); + + if (targetStartPos == null || targetPos == null) + return true; // Give up if the target has moved too far - if (targetMovementThreshold > WDist.Zero && (targetPos - targetStartPos).LengthSquared > targetMovementThreshold.LengthSquared) + if (targetMovementThreshold > WDist.Zero && (targetPos.Value - targetStartPos.Value).LengthSquared > targetMovementThreshold.LengthSquared) return true; // Turn if required - var delta = targetPos - currentPos; + var delta = targetPos.Value - currentPos; var facing = delta.HorizontalLengthSquared != 0 ? delta.Yaw : mobile.Facing; if (facing != mobile.Facing) { @@ -66,7 +69,7 @@ namespace OpenRA.Mods.Common.Activities var speed = mobile.MovementSpeedForCell(self.Location); if (delta.LengthSquared <= speed * speed) { - mobile.SetCenterPosition(self, targetPos); + mobile.SetCenterPosition(self, targetPos.Value); return true; } diff --git a/OpenRA.Mods.Common/Projectiles/AreaBeam.cs b/OpenRA.Mods.Common/Projectiles/AreaBeam.cs index 3576b7f9cb..32955c4b64 100644 --- a/OpenRA.Mods.Common/Projectiles/AreaBeam.cs +++ b/OpenRA.Mods.Common/Projectiles/AreaBeam.cs @@ -164,7 +164,7 @@ namespace OpenRA.Mods.Common.Projectiles if (args.GuidedTarget.IsValidFor(args.SourceActor)) { - var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source); + var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source); var targetDistance = new WDist((guidedTargetPos - args.Source).Length); // Only continue tracking target if it's within weapon range + diff --git a/OpenRA.Mods.Common/Projectiles/LaserZap.cs b/OpenRA.Mods.Common/Projectiles/LaserZap.cs index 01d32ea4fc..5f578ece9b 100644 --- a/OpenRA.Mods.Common/Projectiles/LaserZap.cs +++ b/OpenRA.Mods.Common/Projectiles/LaserZap.cs @@ -155,7 +155,7 @@ namespace OpenRA.Mods.Common.Projectiles // Beam tracks target if (info.TrackTarget && args.GuidedTarget.IsValidFor(args.SourceActor)) - target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(source); + target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(source); // Check for blocking actors if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, source, target, info.Width, out var blockedPos)) diff --git a/OpenRA.Mods.Common/Projectiles/Missile.cs b/OpenRA.Mods.Common/Projectiles/Missile.cs index 3e9cdbd341..cefe57b86e 100644 --- a/OpenRA.Mods.Common/Projectiles/Missile.cs +++ b/OpenRA.Mods.Common/Projectiles/Missile.cs @@ -849,7 +849,7 @@ namespace OpenRA.Mods.Common.Projectiles // Check if target position should be updated (actor visible & locked on) var newTarPos = targetPosition; if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn) - newTarPos = (args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source)) + newTarPos = (args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source)) + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); // Compute target's predicted velocity vector (assuming uniform circular motion) diff --git a/OpenRA.Mods.Common/Traits/Armament.cs b/OpenRA.Mods.Common/Traits/Armament.cs index cdc33bcc1a..e402375c50 100644 --- a/OpenRA.Mods.Common/Traits/Armament.cs +++ b/OpenRA.Mods.Common/Traits/Armament.cs @@ -282,7 +282,7 @@ namespace OpenRA.Mods.Common.Traits WAngle MuzzleFacing() => MuzzleOrientation(self, barrel).Yaw; var muzzleOrientation = WRot.FromYaw(MuzzleFacing()); - var passiveTarget = Weapon.TargetActorCenter ? target.CenterPosition : target.Positions.PositionClosestTo(MuzzlePosition()); + var passiveTarget = Weapon.TargetActorCenter ? target.CenterPosition : target.Positions.ClosestToIgnoringPath(MuzzlePosition()); var initialOffset = Weapon.FirstBurstTargetOffset; if (initialOffset != WVec.Zero) { diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs index 8e8bdee8d0..a96473eaf9 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs @@ -247,7 +247,7 @@ namespace OpenRA.Mods.Common.Traits public virtual WPos GetTargetPosition(WPos pos, in Target target) { - return HasAnyValidWeapons(target, true) ? target.CenterPosition : target.Positions.PositionClosestTo(pos); + return HasAnyValidWeapons(target, true) ? target.CenterPosition : target.Positions.ClosestToIgnoringPath(pos); } public WDist GetMinimumRange() diff --git a/OpenRA.Mods.Common/Traits/AutoCarryall.cs b/OpenRA.Mods.Common/Traits/AutoCarryall.cs index 66c54c6e6a..6449f5bb72 100644 --- a/OpenRA.Mods.Common/Traits/AutoCarryall.cs +++ b/OpenRA.Mods.Common/Traits/AutoCarryall.cs @@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.Traits var carriers = self.World.ActorsHavingTrait(c => !Busy(self) && c.EnableAutoCarry) .Where(a => a.Owner == self.Owner && a.IsInWorld); - return carriers.ClosestTo(candidateCargo) == self; + return carriers.ClosestToWithPathTo(candidateCargo) == self; } void FindCarryableForTransport(Actor self) diff --git a/OpenRA.Mods.Common/Traits/AutoCrusher.cs b/OpenRA.Mods.Common/Traits/AutoCrusher.cs index 4bf5818b38..aa07c4e6fd 100644 --- a/OpenRA.Mods.Common/Traits/AutoCrusher.cs +++ b/OpenRA.Mods.Common/Traits/AutoCrusher.cs @@ -63,7 +63,7 @@ namespace OpenRA.Mods.Common.Traits self.Location != a.Location && a.IsAtGroundLevel() && Info.TargetRelationships.HasRelationship(self.Owner.RelationshipWith(a.Owner)) && a.TraitsImplementing().Any(c => c.CrushableBy(a, self, Info.CrushClasses))) - .ClosestTo(self); // TODO: Make it use shortest pathfinding distance instead + .ClosestToWithPathFrom(self); // TODO: Make it use shortest pathfinding distance instead if (crushableActor == null) return; diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 4285432359..f91f20c44a 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -479,8 +479,9 @@ namespace OpenRA.Mods.Common.Traits case BuildingType.Defense: // Build near the closest enemy structure - var closestEnemy = world.ActorsHavingTrait().Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) - .ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter)); + var closestEnemy = world.ActorsHavingTrait() + .Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) + .ClosestToIgnoringPath(world.Map.CenterOfCell(baseBuilder.DefenseCenter)); var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter; diff --git a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs index 14a208429a..a45dc1dd1d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs @@ -47,7 +47,6 @@ namespace OpenRA.Mods.Common.Traits { readonly World world; readonly Player player; - readonly Func isEnemyUnit; readonly Predicate unitCannotBeOrderedOrIsIdle; readonly int maximumCaptureTargetOptions; int minCaptureDelayTicks; @@ -64,11 +63,6 @@ namespace OpenRA.Mods.Common.Traits if (world.Type == WorldType.Editor) return; - isEnemyUnit = unit => - player.RelationshipWith(unit.Owner) == PlayerRelationship.Enemy - && !unit.Info.HasTraitInfo() - && unit.Info.HasTraitInfo(); - unitCannotBeOrderedOrIsIdle = a => a.Owner != player || a.IsDead || !a.IsInWorld || a.IsIdle; maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); @@ -89,16 +83,6 @@ namespace OpenRA.Mods.Common.Traits } } - internal Actor FindClosestEnemy(WPos pos) - { - return world.Actors.Where(isEnemyUnit).ClosestTo(pos); - } - - internal Actor FindClosestEnemy(WPos pos, WDist radius) - { - return world.FindActorsInCircle(pos, radius).Where(isEnemyUnit).ClosestTo(pos); - } - IEnumerable GetVisibleActorsBelongingToPlayer(Player owner) { foreach (var actor in GetActorsThatCanBeOrderedByPlayer(owner)) @@ -160,7 +144,7 @@ namespace OpenRA.Mods.Common.Traits foreach (var capturer in capturers) { - var targetActor = capturableTargetOptionsList.MinByOrDefault(target => (target.CenterPosition - capturer.Actor.CenterPosition).LengthSquared); + var targetActor = capturableTargetOptionsList.ClosestToWithPathFrom(capturer.Actor); if (targetActor == null) continue; diff --git a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs index fee5de3f6a..e07a0e1f04 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs @@ -118,7 +118,7 @@ namespace OpenRA.Mods.Common.Traits readonly List unitsHangingAroundTheBase = new(); // Units that the bot already knows about. Any unit not on this list needs to be given a role. - readonly List activeUnits = new(); + readonly HashSet activeUnits = new(); public List Squads = new(); @@ -195,22 +195,105 @@ namespace OpenRA.Mods.Common.Traits AssignRolesToIdleUnits(bot); } - internal Actor FindClosestEnemy(WPos pos) + internal static Actor ClosestTo(IEnumerable ownActors, Actor targetActor) { - var units = World.Actors.Where(IsPreferredEnemyUnit).ToList(); - return units.Where(IsNotHiddenUnit).ClosestTo(pos) ?? units.ClosestTo(pos); + // Return actors that can get within weapons range of the target. + // First, let's determine the max weapons range for each of the actors. + var target = Target.FromActor(targetActor); + var ownActorsAndTheirAttackRanges = ownActors + .Select(a => (Actor: a, AttackBases: a.TraitsImplementing().Where(Exts.IsTraitEnabled) + .Where(ab => ab.HasAnyValidWeapons(target)).ToList())) + .Where(x => x.AttackBases.Count > 0) + .Select(x => (x.Actor, Range: x.AttackBases.Max(ab => ab.GetMaximumRangeVersusTarget(target)))) + .ToDictionary(x => x.Actor, x => x.Range); + + // Now determine if each actor can either path directly to the target, + // or if it can path to a nearby location at the edge of its weapon range to the target + // A thorough check would check each position within the circle, but for performance + // we'll only check 8 positions around the edge of the circle. + // We need to account for the weapons range here to account for units such as boats. + // They can't path directly to a land target, + // but might be able to get close enough to shore to attack the target from range. + return ownActorsAndTheirAttackRanges.Keys + .ClosestToWithPathToAny(targetActor.World, a => + { + var range = ownActorsAndTheirAttackRanges[a].Length; + var rangeDiag = Exts.MultiplyBySqrtTwoOverTwo(range); + return new[] + { + targetActor.CenterPosition, + targetActor.CenterPosition + new WVec(range, 0, 0), + targetActor.CenterPosition + new WVec(-range, 0, 0), + targetActor.CenterPosition + new WVec(0, range, 0), + targetActor.CenterPosition + new WVec(0, -range, 0), + targetActor.CenterPosition + new WVec(rangeDiag, rangeDiag, 0), + targetActor.CenterPosition + new WVec(-rangeDiag, rangeDiag, 0), + targetActor.CenterPosition + new WVec(-rangeDiag, -rangeDiag, 0), + targetActor.CenterPosition + new WVec(rangeDiag, -rangeDiag, 0), + }; + }); } - internal Actor FindClosestEnemy(WPos pos, WDist radius) + internal IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(IEnumerable actors, Actor sourceActor) { - return World.FindActorsInCircle(pos, radius).Where(a => IsPreferredEnemyUnit(a) && IsNotHiddenUnit(a)).ClosestTo(pos); + // Check units are in fact enemies and not hidden. + // Then check which are in weapons range of the source. + var activeAttackBases = sourceActor.TraitsImplementing().Where(Exts.IsTraitEnabled).ToArray(); + var enemiesAndSourceAttackRanges = actors + .Where(a => IsPreferredEnemyUnit(a) && IsNotHiddenUnit(a)) + .Select(a => (Actor: a, AttackBases: activeAttackBases.Where(ab => ab.HasAnyValidWeapons(Target.FromActor(a))).ToList())) + .Where(x => x.AttackBases.Count > 0) + .Select(x => (x.Actor, Range: x.AttackBases.Max(ab => ab.GetMaximumRangeVersusTarget(Target.FromActor(x.Actor))))) + .ToDictionary(x => x.Actor, x => x.Range); + + // Now determine if the source actor can path directly to the target, + // or if it can path to a nearby location at the edge of its weapon range to the target + // A thorough check would check each position within the circle, but for performance + // we'll only check 8 positions around the edge of the circle. + // We need to account for the weapons range here to account for units such as boats. + // They can't path directly to a land target, + // but might be able to get close enough to shore to attack the target from range. + return enemiesAndSourceAttackRanges.Keys + .WithPathFrom(sourceActor, a => + { + var range = enemiesAndSourceAttackRanges[a].Length; + var rangeDiag = Exts.MultiplyBySqrtTwoOverTwo(range); + return new[] + { + WVec.Zero, + new WVec(range, 0, 0), + new WVec(-range, 0, 0), + new WVec(0, range, 0), + new WVec(0, -range, 0), + new WVec(rangeDiag, rangeDiag, 0), + new WVec(-rangeDiag, rangeDiag, 0), + new WVec(-rangeDiag, -rangeDiag, 0), + new WVec(rangeDiag, -rangeDiag, 0), + }; + }) + .Select(x => (x.Actor, x.ReachableOffsets.MinBy(o => o.LengthSquared))); + } + + internal (Actor Actor, WVec Offset) FindClosestEnemy(Actor sourceActor) + { + return FindClosestEnemy(World.Actors, sourceActor); + } + + internal (Actor Actor, WVec Offset) FindClosestEnemy(Actor sourceActor, WDist radius) + { + return FindClosestEnemy(World.FindActorsInCircle(sourceActor.CenterPosition, radius), sourceActor); + } + + (Actor Actor, WVec Offset) FindClosestEnemy(IEnumerable actors, Actor sourceActor) + { + return WorldUtils.ClosestToIgnoringPath(FindEnemies(actors, sourceActor), x => x.Actor, sourceActor); } void CleanSquads() { - Squads.RemoveAll(s => !s.IsValid); foreach (var s in Squads) - s.Units.RemoveAll(unitCannotBeOrdered); + s.Units.RemoveWhere(unitCannotBeOrdered); + Squads.RemoveAll(s => !s.IsValid); } // HACK: Use of this function requires that there is one squad of this type. @@ -219,18 +302,28 @@ namespace OpenRA.Mods.Common.Traits return Squads.FirstOrDefault(s => s.Type == type); } - Squad RegisterNewSquad(IBot bot, SquadType type, Actor target = null) + Squad RegisterNewSquad(IBot bot, SquadType type, (Actor Actor, WVec Offset) target = default) { var ret = new Squad(bot, this, type, target); Squads.Add(ret); return ret; } + internal void UnregisterSquad(Squad squad) + { + activeUnits.ExceptWith(squad.Units); + squad.Units.Clear(); + + // CleanSquads will remove the squad from the Squads list. + // We can't do that here as this is designed to be called from within Squad.Update + // and thus would mutate the Squads list we are iterating over. + } + void AssignRolesToIdleUnits(IBot bot) { CleanSquads(); - activeUnits.RemoveAll(unitCannotBeOrdered); + activeUnits.RemoveWhere(unitCannotBeOrdered); unitsHangingAroundTheBase.RemoveAll(unitCannotBeOrdered); foreach (var n in notifyIdleBaseUnits) n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); @@ -305,8 +398,7 @@ namespace OpenRA.Mods.Common.Traits { var attackForce = RegisterNewSquad(bot, SquadType.Assault); - foreach (var a in unitsHangingAroundTheBase) - attackForce.Units.Add(a); + attackForce.Units.UnionWith(unitsHangingAroundTheBase); unitsHangingAroundTheBase.Clear(); foreach (var n in notifyIdleBaseUnits) @@ -316,29 +408,45 @@ namespace OpenRA.Mods.Common.Traits void TryToRushAttack(IBot bot) { - var allEnemyBaseBuilder = AIUtils.FindEnemiesByCommonName(Info.ConstructionYardTypes, Player); - var ownUnits = activeUnits - .Where(unit => unit.IsIdle && unit.Info.HasTraitInfo() - && !Info.AirUnitsTypes.Contains(unit.Info.Name) && !Info.NavalUnitsTypes.Contains(unit.Info.Name) && !Info.ExcludeFromSquadsTypes.Contains(unit.Info.Name)).ToList(); + .Where(unit => + unit.IsIdle + && unit.Info.HasTraitInfo() + && !Info.AirUnitsTypes.Contains(unit.Info.Name) + && !Info.NavalUnitsTypes.Contains(unit.Info.Name) + && !Info.ExcludeFromSquadsTypes.Contains(unit.Info.Name)) + .ToList(); + + if (ownUnits.Count < Info.SquadSize) + return; + + var allEnemyBaseBuilder = FindEnemies( + World.Actors.Where(a => Info.ConstructionYardTypes.Contains(a.Info.Name)), + ownUnits.First()) + .ToList(); if (allEnemyBaseBuilder.Count == 0 || ownUnits.Count < Info.SquadSize) return; - foreach (var b in allEnemyBaseBuilder) + foreach (var enemyBaseBuilder in allEnemyBaseBuilder) { // Don't rush enemy aircraft! - var enemies = World.FindActorsInCircle(b.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) - .Where(unit => IsPreferredEnemyUnit(unit) && unit.Info.HasTraitInfo() && !Info.AirUnitsTypes.Contains(unit.Info.Name) && !Info.NavalUnitsTypes.Contains(unit.Info.Name)).ToList(); + var enemies = FindEnemies( + World.FindActorsInCircle(enemyBaseBuilder.Actor.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) + .Where(unit => + unit.Info.HasTraitInfo() + && !Info.AirUnitsTypes.Contains(unit.Info.Name) + && !Info.NavalUnitsTypes.Contains(unit.Info.Name)), + ownUnits.First()) + .ToList(); - if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies)) + if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies.Select(x => x.Actor).ToList())) { - var target = enemies.Count > 0 ? enemies.Random(World.LocalRandom) : b; + var target = enemies.Count > 0 ? enemies.Random(World.LocalRandom) : enemyBaseBuilder; var rush = GetSquadOfType(SquadType.Rush); rush ??= RegisterNewSquad(bot, SquadType.Rush, target); - foreach (var a3 in ownUnits) - rush.Units.Add(a3); + rush.Units.UnionWith(ownUnits); return; } @@ -348,18 +456,21 @@ namespace OpenRA.Mods.Common.Traits void ProtectOwn(IBot bot, Actor attacker) { var protectSq = GetSquadOfType(SquadType.Protection); - protectSq ??= RegisterNewSquad(bot, SquadType.Protection, attacker); + protectSq ??= RegisterNewSquad(bot, SquadType.Protection, (attacker, WVec.Zero)); - if (!protectSq.IsTargetValid) - protectSq.TargetActor = attacker; + if (protectSq.IsValid && !protectSq.IsTargetValid()) + protectSq.SetActorToTarget((attacker, WVec.Zero)); if (!protectSq.IsValid) { var ownUnits = World.FindActorsInCircle(World.Map.CenterOfCell(GetRandomBaseCenter()), WDist.FromCells(Info.ProtectUnitScanRadius)) - .Where(unit => unit.Owner == Player && !Info.ProtectionTypes.Contains(unit.Info.Name) && unit.Info.HasTraitInfo()); + .Where(unit => + unit.Owner == Player + && !Info.ProtectionTypes.Contains(unit.Info.Name) + && unit.Info.HasTraitInfo()) + .WithPathTo(World, attacker.CenterPosition); - foreach (var a in ownUnits) - protectSq.Units.Add(a); + protectSq.Units.UnionWith(ownUnits); } } @@ -429,7 +540,7 @@ namespace OpenRA.Mods.Common.Traits if (activeUnitsNode != null) { activeUnits.Clear(); - activeUnits.AddRange(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value.Value) + activeUnits.UnionWith(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value.Value) .Select(a => self.World.GetActorById(a)).Where(a => a != null)); } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs index fa7f55b701..d7d9cae52d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs @@ -20,34 +20,44 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads public class Squad { - public List Units = new(); + public HashSet Units = new(); public SquadType Type; internal IBot Bot; internal World World; internal SquadManagerBotModule SquadManager; internal MersenneTwister Random; - - internal Target Target; internal StateMachine FuzzyStateMachine; - public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type) - : this(bot, squadManager, type, null) { } + /// + /// Target location to attack. This will be either the targeted actor, + /// or a position close to that actor sufficient to get within weapons range. + /// + internal Target Target { get; set; } - public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, Actor target) + /// + /// Actor that is targeted, for any actor based checks. Use for a targeting location. + /// + internal Actor TargetActor; + + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type) + : this(bot, squadManager, type, default) { } + + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, (Actor Actor, WVec Offset) target) { Bot = bot; SquadManager = squadManager; World = bot.Player.PlayerActor.World; Random = World.LocalRandom; Type = type; - Target = Target.FromActor(target); + SetActorToTarget(target); FuzzyStateMachine = new StateMachine(); switch (type) { case SquadType.Assault: case SquadType.Rush: + case SquadType.Naval: FuzzyStateMachine.ChangeState(this, new GroundUnitsIdleState(), true); break; case SquadType.Air: @@ -56,9 +66,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads case SquadType.Protection: FuzzyStateMachine.ChangeState(this, new UnitsForProtectionIdleState(), true); break; - case SquadType.Naval: - FuzzyStateMachine.ChangeState(this, new NavyUnitsIdleState(), true); - break; } } @@ -70,15 +77,50 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads public bool IsValid => Units.Count > 0; - public Actor TargetActor + public void SetActorToTarget((Actor Actor, WVec Offset) target) { - get => Target.Actor; - set => Target = Target.FromActor(value); + TargetActor = target.Actor; + if (TargetActor == null) + { + Target = Target.Invalid; + return; + } + + if (target.Offset == WVec.Zero) + Target = Target.FromActor(TargetActor); + else + Target = Target.FromPos(TargetActor.CenterPosition + target.Offset); } - public bool IsTargetValid => Target.IsValidFor(Units.FirstOrDefault()) && !Target.Actor.Info.HasTraitInfo(); + /// + /// Checks the target is still valid, and updates the location if it is still valid. + /// + public bool IsTargetValid() + { + var valid = + TargetActor != null && + TargetActor.IsInWorld && + TargetActor.IsTargetableBy(Units.FirstOrDefault()) && + !TargetActor.Info.HasTraitInfo(); + if (!valid) + return false; - public bool IsTargetVisible => TargetActor.CanBeViewedByPlayer(Bot.Player); + // Refresh the target location. + // If the actor moved out of reach then we'll mark it invalid. + // e.g. a ship targeting a land unit that moves inland out of weapons range. + // or the target crossed a bridge which is then destroyed. + // If it is still in range but we have to target a nearby location, we can update that location. + // e.g. a ship targeting a land unit, but the land unit moved north. + // We need to update our location to move north as well. + // If we can reach the actor directly, we'll just target it directly. + var target = SquadManager.FindEnemies(new[] { TargetActor }, Units.First()).FirstOrDefault(); + SetActorToTarget(target); + return target.Actor != null; + } + + public bool IsTargetVisible => + TargetActor != null && + TargetActor.CanBeViewedByPlayer(Bot.Player); public WPos CenterPosition { get { return Units.Select(u => u.CenterPosition).Average(); } } @@ -87,10 +129,14 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads var nodes = new List() { new MiniYamlNode("Type", FieldSaver.FormatValue(Type)), - new MiniYamlNode("Units", FieldSaver.FormatValue(Units.Select(a => a.ActorID).ToArray())), + new MiniYamlNode("Units", FieldSaver.FormatValue(Units.Select(a => a.ActorID).ToArray())) }; - if (Target.Type == TargetType.Actor) - nodes.Add(new MiniYamlNode("Target", FieldSaver.FormatValue(Target.Actor.ActorID))); + + if (Target != Target.Invalid) + { + nodes.Add(new MiniYamlNode("ActorToTarget", FieldSaver.FormatValue(TargetActor.ActorID))); + nodes.Add(new MiniYamlNode("TargetOffset", FieldSaver.FormatValue(Target.CenterPosition - TargetActor.CenterPosition))); + } return new MiniYaml("", nodes); } @@ -98,21 +144,26 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads public static Squad Deserialize(IBot bot, SquadManagerBotModule squadManager, MiniYaml yaml) { var type = SquadType.Rush; - Actor targetActor = null; + var target = ((Actor)null, WVec.Zero); var typeNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Type"); if (typeNode != null) type = FieldLoader.GetValue("Type", typeNode.Value.Value); - var targetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Target"); - if (targetNode != null) - targetActor = squadManager.World.GetActorById(FieldLoader.GetValue("ActiveUnits", targetNode.Value.Value)); + var actorToTargetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "ActorToTarget"); + var targetOffsetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "TargetOffset"); + if (actorToTargetNode != null && targetOffsetNode != null) + { + var actorToTarget = squadManager.World.GetActorById(FieldLoader.GetValue("ActorToTarget", actorToTargetNode.Value.Value)); + var targetOffset = FieldLoader.GetValue("TargetOffset", targetOffsetNode.Value.Value); + target = (actorToTarget, targetOffset); + } - var squad = new Squad(bot, squadManager, type, targetActor); + var squad = new Squad(bot, squadManager, type, target); var unitsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Units"); if (unitsNode != null) - squad.Units.AddRange(FieldLoader.GetValue("Units", unitsNode.Value.Value) + squad.Units.UnionWith(FieldLoader.GetValue("Units", unitsNode.Value.Value) .Select(a => squadManager.World.GetActorById(a))); return squad; diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs index b23423c219..6819d1232f 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs @@ -134,7 +134,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (e == null) return; - owner.TargetActor = e; + owner.SetActorToTarget((e, WVec.Zero)); owner.FuzzyStateMachine.ChangeState(owner, new AirAttackState(), true); } @@ -150,20 +150,19 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var a = owner.Units.Random(owner.Random); - var closestEnemy = owner.SquadManager.FindClosestEnemy(a.CenterPosition); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else + var closestEnemy = owner.SquadManager.FindClosestEnemy(a); + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; } } - if (!NearToPosSafely(owner, owner.TargetActor.CenterPosition)) + if (!NearToPosSafely(owner, owner.Target.CenterPosition)) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; @@ -188,7 +187,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } if (CanAttackTarget(a, owner.TargetActor)) - owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); + owner.Bot.QueueOrder(new Order("Attack", a, owner.Target, false)); } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs index 796224a701..dceb78972c 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs @@ -9,6 +9,7 @@ */ #endregion +using System.Collections.Generic; using System.Linq; using OpenRA.Traits; @@ -21,9 +22,21 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); } - protected static Actor FindClosestEnemy(Squad owner) + protected static (Actor Actor, WVec Offset) FindClosestEnemy(Squad owner) { - return owner.SquadManager.FindClosestEnemy(owner.Units.First().CenterPosition); + return owner.SquadManager.FindClosestEnemy(owner.Units.First()); + } + + protected static IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable actors) + { + return owner.SquadManager.FindEnemies( + actors, + owner.Units.First()); + } + + protected static Actor ClosestToEnemy(Squad owner) + { + return SquadManagerBotModule.ClosestTo(owner.Units, owner.TargetActor); } } @@ -36,24 +49,26 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy == null) + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) return; - - owner.TargetActor = closestEnemy; } - var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); + var enemyUnits = + FindEnemies(owner, + owner.World.FindActorsInCircle(owner.Target.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius))) + .Select(x => x.Actor) + .ToList(); if (enemyUnits.Count == 0) return; if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) { - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); // We have gathered sufficient units. Attack the nearest enemy unit. owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState(), true); @@ -78,19 +93,18 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); return; } } - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); + var leader = ClosestToEnemy(owner); if (leader == null) return; @@ -129,16 +143,14 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } else { - var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit); - var target = enemies.ClosestTo(leader.CenterPosition); - if (target != null) + var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)); + if (target.Actor != null) { - owner.TargetActor = target; + owner.SetActorToTarget(target); owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackState(), true); } else - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); } if (ShouldFlee(owner)) @@ -161,22 +173,21 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); return; } } - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); - if (leader.Location != lastLeaderLocation) + var leader = ClosestToEnemy(owner); + if (leader?.Location != lastLeaderLocation) { - lastLeaderLocation = leader.Location; + lastLeaderLocation = leader?.Location; lastUpdatedTick = owner.World.WorldTick; } @@ -197,7 +208,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads foreach (var a in owner.Units) if (!BusyAttack(a)) - owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); + owner.Bot.QueueOrder(new Order("AttackMove", a, owner.Target, false)); if (ShouldFlee(owner)) owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); @@ -219,6 +230,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); } - public void Deactivate(Squad owner) { owner.Units.Clear(); } + public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs deleted file mode 100644 index 8c706545e1..0000000000 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs +++ /dev/null @@ -1,247 +0,0 @@ -#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.Linq; -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.Traits.BotModules.Squads -{ - abstract class NavyStateBase : StateBase - { - protected virtual bool ShouldFlee(Squad owner) - { - return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); - } - - protected static Actor FindClosestEnemy(Squad owner) - { - var first = owner.Units.First(); - - // Navy squad AI can exploit enemy naval production to find path, if any. - // (Way better than finding a nearest target which is likely to be on Ground) - // You might be tempted to move these lookups into Activate() but that causes null reference exception. - var mobile = first.Trait(); - - var navalProductions = owner.World.ActorsHavingTrait().Where(a - => owner.SquadManager.Info.NavalProductionTypes.Contains(a.Info.Name) - && mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, first.Location, a.Location) - && a.AppearsHostileTo(first)); - - var nearest = navalProductions.ClosestTo(first); - if (nearest != null) - { - // Return nearest when it is FAR enough. - // If the naval production is within MaxBaseRadius, it implies that - // this squad is close to enemy territory and they should expect a naval combat; - // closest enemy makes more sense in that case. - if ((nearest.Location - first.Location).LengthSquared > owner.SquadManager.Info.MaxBaseRadius * owner.SquadManager.Info.MaxBaseRadius) - return nearest; - } - - return owner.SquadManager.FindClosestEnemy(first.CenterPosition); - } - } - - sealed class NavyUnitsIdleState : NavyStateBase, IState - { - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - if (!owner.IsTargetValid) - { - var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy == null) - return; - - owner.TargetActor = closestEnemy; - } - - var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); - - if (enemyUnits.Count == 0) - return; - - if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) - { - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); - - // We have gathered sufficient units. Attack the nearest enemy unit. - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackMoveState(), true); - } - else - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - } - - public void Deactivate(Squad owner) { } - } - - sealed class NavyUnitsAttackMoveState : NavyStateBase, IState - { - int lastUpdatedTick; - CPos? lastLeaderLocation; - Actor lastTarget; - - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - if (!owner.IsTargetValid) - { - var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - return; - } - } - - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); - if (leader == null) - return; - - if (leader.Location != lastLeaderLocation) - { - lastLeaderLocation = leader.Location; - lastUpdatedTick = owner.World.WorldTick; - } - - if (owner.TargetActor != lastTarget) - { - lastTarget = owner.TargetActor; - lastUpdatedTick = owner.World.WorldTick; - } - - // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds - // This works around the squad being stuck trying to attack-move to a location - // that they cannot path to, generating expensive pathfinding calls each tick. - if (owner.World.WorldTick > lastUpdatedTick + 63) - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); - return; - } - - var ownUnits = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Units.Count) / 3) - .Where(a => a.Owner == owner.Units.First().Owner && owner.Units.Contains(a)).ToHashSet(); - - if (ownUnits.Count < owner.Units.Count) - { - // Since units have different movement speeds, they get separated while approaching the target. - // Let them regroup into tighter formation. - owner.Bot.QueueOrder(new Order("Stop", leader, false)); - - var units = owner.Units.Where(a => !ownUnits.Contains(a)).ToArray(); - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: units)); - } - else - { - var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit); - var target = enemies.ClosestTo(leader.CenterPosition); - if (target != null) - { - owner.TargetActor = target; - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackState(), true); - } - else - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); - } - - if (ShouldFlee(owner)) - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - } - - public void Deactivate(Squad owner) { } - } - - sealed class NavyUnitsAttackState : NavyStateBase, IState - { - int lastUpdatedTick; - CPos? lastLeaderLocation; - Actor lastTarget; - - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - if (!owner.IsTargetValid) - { - var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - return; - } - } - - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); - if (leader.Location != lastLeaderLocation) - { - lastLeaderLocation = leader.Location; - lastUpdatedTick = owner.World.WorldTick; - } - - if (owner.TargetActor != lastTarget) - { - lastTarget = owner.TargetActor; - lastUpdatedTick = owner.World.WorldTick; - } - - // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds - // This works around the squad being stuck trying to attack-move to a location - // that they cannot path to, generating expensive pathfinding calls each tick. - if (owner.World.WorldTick > lastUpdatedTick + 63) - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); - return; - } - - foreach (var a in owner.Units) - if (!BusyAttack(a)) - owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); - - if (ShouldFlee(owner)) - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - } - - public void Deactivate(Squad owner) { } - } - - sealed class NavyUnitsFleeState : NavyStateBase, IState - { - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - GoToRandomOwnBuilding(owner); - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); - } - - public void Deactivate(Squad owner) { owner.Units.Clear(); } - } -} diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs index a5998990ec..f6433bff8d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs @@ -9,7 +9,7 @@ */ #endregion -using OpenRA.Traits; +using System.Linq; namespace OpenRA.Mods.Common.Traits.BotModules.Squads { @@ -32,11 +32,11 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { - owner.TargetActor = owner.SquadManager.FindClosestEnemy(owner.CenterPosition, WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); - - if (owner.TargetActor == null) + var target = owner.SquadManager.FindClosestEnemy(owner.Units.First(), WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); + owner.SetActorToTarget(target); + if (target.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), true); return; @@ -55,7 +55,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads Backoff--; } else - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); } public void Deactivate(Squad owner) { } @@ -74,6 +74,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionIdleState(), true); } - public void Deactivate(Squad owner) { owner.Units.Clear(); } + public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs b/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs index 1a8dd6f5a4..cace318dae 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs @@ -183,10 +183,8 @@ namespace OpenRA.Mods.Common.Traits if (modifiers.HasModifier(TargetModifiers.ForceAttack) && !string.IsNullOrEmpty(info.ForceSetType)) { var closest = self.World.Selection.Actors - .Select(a => (a, a.TraitOrDefault())) - .Where(x => x.RallyPoint != null && x.RallyPoint.Info.ForceSetType == info.ForceSetType) - .OrderBy(x => (location - x.Actor.Location).LengthSquared) - .FirstOrDefault().Actor; + .Where(a => a.TraitOrDefault()?.Info.ForceSetType == info.ForceSetType) + .ClosestToWithPathTo(self.World, target.CenterPosition); ForceSet = closest == self; } diff --git a/OpenRA.Mods.Common/Traits/CaptureManager.cs b/OpenRA.Mods.Common/Traits/CaptureManager.cs index 624226a8eb..8a6cde17e4 100644 --- a/OpenRA.Mods.Common/Traits/CaptureManager.cs +++ b/OpenRA.Mods.Common/Traits/CaptureManager.cs @@ -226,7 +226,7 @@ namespace OpenRA.Mods.Common.Traits currentTargetTotal = captures.Info.CaptureDelay; if (move != null && captures.Info.ConsumedByCapture) { - var pos = target.GetTargetablePositions().PositionClosestTo(self.CenterPosition); + var pos = target.GetTargetablePositions().ClosestToIgnoringPath(self.CenterPosition); currentTargetTotal += move.EstimatedMoveDuration(self, self.CenterPosition, pos); } diff --git a/OpenRA.Mods.Common/WorldExtensions.cs b/OpenRA.Mods.Common/WorldExtensions.cs index b5d0791adf..77aff2e5e7 100644 --- a/OpenRA.Mods.Common/WorldExtensions.cs +++ b/OpenRA.Mods.Common/WorldExtensions.cs @@ -11,12 +11,191 @@ using System; using System.Collections.Generic; +using System.Linq; using OpenRA.Mods.Common.Traits; namespace OpenRA.Mods.Common { public static class WorldExtensions { + /// + /// Filters by only returning those that can be reached as the target of a path from + /// . Only terrain is taken into account, i.e. as if + /// was given. + /// is used to define locations around each actor in + /// of which one must be reachable. + /// + public static IEnumerable<(Actor Actor, WVec[] ReachableOffsets)> WithPathFrom(this IEnumerable actors, Actor sourceActor, Func targetOffsets) + { + if (sourceActor.Info.HasTraitInfo()) + return actors.Select(a => (a, targetOffsets(a))); + var mobile = sourceActor.TraitOrDefault(); + if (mobile == null) + return Enumerable.Empty<(Actor Actor, WVec[] ReachableOffsets)>(); + + var pathFinder = sourceActor.World.WorldActor.Trait(); + var locomotor = mobile.Locomotor; + var map = sourceActor.World.Map; + return actors + .Select(a => + { + return (a, targetOffsets(a).Where(offset => + pathFinder.PathExistsForLocomotor( + mobile.Locomotor, + map.CellContaining(sourceActor.CenterPosition), + map.CellContaining(a.CenterPosition + offset))) + .ToArray()); + }) + .Where(x => x.ReachableOffsets.Length > 0); + } + + /// + /// Filters by only returning those that can be reached as the target of a path from + /// . Only terrain is taken into account, i.e. as if + /// was given. + /// + public static IEnumerable WithPathFrom(this IEnumerable actors, Actor sourceActor) + { + return actors.WithPathFrom(sourceActor, _ => new[] { WVec.Zero }).Select(x => x.Actor); + } + + /// + /// Of that can be reached as the target of a path from + /// , returns the nearest by comparing their . + /// Only terrain is taken into account, i.e. as if was given. + /// is used to define locations around each actor in + /// of which one must be reachable. + /// + public static Actor ClosestToWithPathFrom(this IEnumerable actors, Actor sourceActor, Func targetOffsets = null) + { + return actors + .WithPathFrom(sourceActor, targetOffsets ?? (_ => new[] { WVec.Zero })) + .Select(x => x.Actor) + .ClosestToIgnoringPath(sourceActor); + } + + /// + /// Of that can be reached as the target of a path from + /// , returns the nearest by comparing the . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static WPos? ClosestToWithPathFrom(this IEnumerable positions, Actor sourceActor) + { + if (sourceActor.Info.HasTraitInfo()) + return positions.ClosestToIgnoringPath(sourceActor.CenterPosition); + var mobile = sourceActor.TraitOrDefault(); + if (mobile == null) + return null; + + var pathFinder = sourceActor.World.WorldActor.Trait(); + var locomotor = mobile.Locomotor; + var map = sourceActor.World.Map; + return positions + .Where(p => pathFinder.PathExistsForLocomotor( + locomotor, + map.CellContaining(sourceActor.CenterPosition), + map.CellContaining(p))) + .ClosestToIgnoringPath(sourceActor.CenterPosition); + } + + /// + /// Filters by only returning those where the can + /// be reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if + /// was given. + /// + public static IEnumerable WithPathTo(this IEnumerable actors, World world, WPos targetPosition) + { + var pathFinder = world.WorldActor.Trait(); + var map = world.Map; + return actors + .Where(a => + { + if (a.Info.HasTraitInfo()) + return true; + var mobile = a.TraitOrDefault(); + if (mobile == null) + return false; + return pathFinder.PathExistsForLocomotor( + mobile.Locomotor, + map.CellContaining(targetPosition), + map.CellContaining(a.CenterPosition)); + }); + } + + /// + /// Filters by only returning those where any of the + /// can be reached as the target of a path from the actor. + /// Returns the reachable target positions for each actor. + /// Only terrain is taken into account, i.e. as if was given. + /// + public static IEnumerable<(Actor Actor, WPos[] ReachablePositions)> WithPathToAny( + this IEnumerable actors, World world, Func targetPositions) + { + var pathFinder = world.WorldActor.Trait(); + var map = world.Map; + return actors + .Select(a => + { + if (a.Info.HasTraitInfo()) + return (a, targetPositions(a).ToArray()); + var mobile = a.TraitOrDefault(); + if (mobile == null) + return (a, Array.Empty()); + return (a, targetPositions(a).Where(targetPosition => + pathFinder.PathExistsForLocomotor( + mobile.Locomotor, + map.CellContaining(targetPosition), + map.CellContaining(a.CenterPosition))) + .ToArray()); + }) + .Where(x => x.ReachablePositions.Length > 0); + } + + /// + /// Filters by only returning those where the can be + /// reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if + /// was given. + /// + public static IEnumerable WithPathTo(this IEnumerable actors, Actor targetActor) + { + return actors.WithPathTo(targetActor.World, targetActor.CenterPosition); + } + + /// + /// Of where the can be reached as the target of a + /// path from the actor, returns the nearest by comparing the . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static Actor ClosestToWithPathTo(this IEnumerable actors, World world, WPos targetPosition) + { + return actors + .WithPathTo(world, targetPosition) + .ClosestToIgnoringPath(targetPosition); + } + + /// + /// Of where any of the can be reached as the + /// target of a path from the actor, returns the nearest by comparing the . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static Actor ClosestToWithPathToAny(this IEnumerable actors, World world, Func targetPositions) + { + return actors + .WithPathToAny(world, targetPositions) + .MinByOrDefault(x => x.ReachablePositions.Min(pos => (x.Actor.CenterPosition - pos).LengthSquared)) + .Actor; + } + + /// + /// Of where the can be reached as the target of a + /// path from the actor, returns the nearest by comparing their . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static Actor ClosestToWithPathTo(this IEnumerable actors, Actor targetActor) + { + return actors.ClosestToWithPathTo(targetActor.World, targetActor.CenterPosition); + } + /// /// Finds all the actors of which their health radius is intersected by a line (with a definable width) between two points. /// diff --git a/OpenRA.Mods.D2k/Traits/Sandworm.cs b/OpenRA.Mods.D2k/Traits/Sandworm.cs index e43ae3316f..b223b53a68 100644 --- a/OpenRA.Mods.D2k/Traits/Sandworm.cs +++ b/OpenRA.Mods.D2k/Traits/Sandworm.cs @@ -10,6 +10,7 @@ #endregion using System.Linq; +using OpenRA.Mods.Common; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; @@ -83,6 +84,7 @@ namespace OpenRA.Mods.D2k.Traits // If close enough, we don't care about other actors. var target = self.World.FindActorsInCircle(self.CenterPosition, WormInfo.IgnoreNoiseAttackRange) + .WithPathFrom(self) .Select(t => Target.FromActor(t)) .FirstOrDefault(t => attackTrait.HasAnyValidWeapons(t)); @@ -101,6 +103,7 @@ namespace OpenRA.Mods.D2k.Traits } var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, WormInfo.MaxSearchRadius) + .WithPathFrom(self) .Where(IsValidTarget).SelectMany(a => a.TraitsImplementing()); var noiseDirection = actorsInRange.Aggregate(WVec.Zero, (a, b) => a + b.AttractionAtPosition(self.CenterPosition));