#region Copyright & License Information /* * Copyright 2007-2020 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.Activities; using OpenRA.Mods.Common.Pathfinder; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Activities { public class Move : Activity { static readonly List NoPath = new List(); readonly Mobile mobile; readonly WDist nearEnough; readonly Func> getPath; readonly Actor ignoreActor; readonly Color? targetLineColor; static readonly BlockedByActor[] PathSearchOrder = { BlockedByActor.All, BlockedByActor.Immovable, BlockedByActor.Stationary, BlockedByActor.None }; List path; CPos? destination; // For dealing with blockers bool hasWaited; int waitTicksRemaining; // To work around queued activity issues while minimizing changes to legacy behaviour bool evaluateNearestMovableCell; // Scriptable move order // Ignores lane bias and nearby units public Move(Actor self, CPos destination, Color? targetLineColor = null) { // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; getPath = check => { List path; using (var search = PathSearch.FromPoint(self.World, mobile.Locomotor, self, mobile.ToCell, destination, check) .WithoutLaneBias()) path = mobile.Pathfinder.FindPath(search); return path; }; this.destination = destination; this.targetLineColor = targetLineColor; nearEnough = WDist.Zero; } public Move(Actor self, CPos destination, WDist nearEnough, Actor ignoreActor = null, bool evaluateNearestMovableCell = false, Color? targetLineColor = null) { // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; getPath = check => { if (!this.destination.HasValue) return NoPath; return mobile.Pathfinder.FindUnitPath(mobile.ToCell, this.destination.Value, self, ignoreActor, check); }; // Note: Will be recalculated from OnFirstRun if evaluateNearestMovableCell is true this.destination = destination; this.nearEnough = nearEnough; this.ignoreActor = ignoreActor; this.evaluateNearestMovableCell = evaluateNearestMovableCell; this.targetLineColor = targetLineColor; } public Move(Actor self, CPos destination, SubCell subCell, WDist nearEnough, Color? targetLineColor = null) { // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; getPath = check => mobile.Pathfinder.FindUnitPathToRange( mobile.FromCell, subCell, self.World.Map.CenterOfSubCell(destination, subCell), nearEnough, self, check); this.destination = destination; this.nearEnough = nearEnough; this.targetLineColor = targetLineColor; } public Move(Actor self, Target target, WDist range, Color? targetLineColor = null) { // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; getPath = check => { if (!target.IsValidFor(self)) return NoPath; return mobile.Pathfinder.FindUnitPathToRange( mobile.ToCell, mobile.ToSubCell, target.CenterPosition, range, self, check); }; destination = null; nearEnough = range; this.targetLineColor = targetLineColor; } public Move(Actor self, Func> getPath, Color? targetLineColor = null) { // PERF: Because we can be sure that OccupiesSpace is Mobile here, we can save some performance by avoiding querying for the trait. mobile = (Mobile)self.OccupiesSpace; this.getPath = getPath; destination = null; nearEnough = WDist.Zero; this.targetLineColor = targetLineColor; } static int HashList(List xs) { var hash = 0; var n = 0; foreach (var x in xs) hash += n++ * x.GetHashCode(); return hash; } List EvalPath(BlockedByActor check) { var path = getPath(check).TakeWhile(a => a != mobile.ToCell).ToList(); return path; } protected override void OnFirstRun(Actor self) { if (evaluateNearestMovableCell && destination.HasValue) { var movableDestination = mobile.NearestMoveableCell(destination.Value); destination = mobile.CanEnterCell(movableDestination, check: BlockedByActor.Immovable) ? movableDestination : (CPos?)null; } // TODO: Change this to BlockedByActor.Stationary after improving the local avoidance behaviour foreach (var check in PathSearchOrder) { path = EvalPath(check); if (path.Count > 0) return; } } public override bool Tick(Actor self) { mobile.TurnToMove = false; if (IsCanceling && mobile.CanStayInCell(mobile.ToCell)) { path?.Clear(); return true; } if (mobile.IsTraitDisabled || mobile.IsTraitPaused) return false; if (destination == mobile.ToCell) return true; if (path.Count == 0) { destination = mobile.ToCell; return false; } destination = path[0]; var nextCell = PopPath(self); if (nextCell == null) return false; var firstFacing = self.World.Map.FacingBetween(mobile.FromCell, nextCell.Value.Cell, mobile.Facing); if (firstFacing != mobile.Facing) { path.Add(nextCell.Value.Cell); QueueChild(new Turn(self, firstFacing)); mobile.TurnToMove = true; return false; } mobile.SetLocation(mobile.FromCell, mobile.FromSubCell, nextCell.Value.Cell, nextCell.Value.SubCell); var map = self.World.Map; var from = (mobile.FromCell.Layer == 0 ? map.CenterOfCell(mobile.FromCell) : self.World.GetCustomMovementLayers()[mobile.FromCell.Layer].CenterOfCell(mobile.FromCell)) + map.Grid.OffsetOfSubCell(mobile.FromSubCell); var to = Util.BetweenCells(self.World, mobile.FromCell, mobile.ToCell) + (map.Grid.OffsetOfSubCell(mobile.FromSubCell) + map.Grid.OffsetOfSubCell(mobile.ToSubCell)) / 2; QueueChild(new MoveFirstHalf(this, from, to, mobile.Facing, mobile.Facing, 0)); return false; } (CPos Cell, SubCell SubCell)? PopPath(Actor self) { if (path.Count == 0) return null; var nextCell = path[path.Count - 1]; // Something else might have moved us, so the path is no longer valid. if (!Util.AreAdjacentCells(mobile.ToCell, nextCell)) { path = EvalPath(BlockedByActor.Immovable); return null; } var containsTemporaryBlocker = WorldUtils.ContainsTemporaryBlocker(self.World, nextCell, self); // Next cell in the move is blocked by another actor if (containsTemporaryBlocker || !mobile.CanEnterCell(nextCell, ignoreActor)) { // Are we close enough? var cellRange = nearEnough.Length / 1024; if (!containsTemporaryBlocker && (mobile.ToCell - destination.Value).LengthSquared <= cellRange * cellRange && mobile.CanStayInCell(mobile.ToCell)) { // Apply some simple checks to avoid giving up in cases where we can be confident that // nudging/waiting/repathing should produce better results. // Avoid fighting over the destination cell if (path.Count < 2) { path.Clear(); return null; } // We can reasonably assume that the blocker is friendly and has a similar locomotor type. // If there is a free cell next to the blocker that is a similar or closer distance to the // destination then we can probably nudge or path around it. var blockerDistSq = (nextCell - destination.Value).LengthSquared; var nudgeOrRepath = CVec.Directions .Select(d => nextCell + d) .Any(c => c != self.Location && (c - destination.Value).LengthSquared <= blockerDistSq && mobile.CanEnterCell(c, ignoreActor)); if (!nudgeOrRepath) { path.Clear(); return null; } } // There is no point in waiting for the other actor to move if it is incapable of moving. if (!mobile.CanEnterCell(nextCell, ignoreActor, BlockedByActor.Immovable)) { path = EvalPath(BlockedByActor.Immovable); return null; } // See if they will move self.NotifyBlocker(nextCell); // Wait a bit to see if they leave if (!hasWaited) { waitTicksRemaining = mobile.Info.LocomotorInfo.WaitAverage; hasWaited = true; return null; } if (--waitTicksRemaining >= 0) return null; hasWaited = false; // If the blocking actors are already leaving, wait a little longer instead of repathing if (CellIsEvacuating(self, nextCell)) return null; // Calculate a new path mobile.RemoveInfluence(); var newPath = EvalPath(BlockedByActor.All); mobile.AddInfluence(); if (newPath.Count != 0) { path = newPath; var newCell = path[path.Count - 1]; path.RemoveAt(path.Count - 1); return (newCell, mobile.GetAvailableSubCell(nextCell, mobile.FromSubCell, ignoreActor)); } else if (mobile.IsBlocking) { // If there is no way around the blocker and blocker will not move and we are blocking others, back up to let others pass. var newCell = mobile.GetAdjacentCell(nextCell); if (newCell != null) { if ((nextCell - newCell).Value.LengthSquared > 2) path.Add(mobile.ToCell); return (newCell.Value, mobile.GetAvailableSubCell(newCell.Value, mobile.FromSubCell, ignoreActor)); } } return null; } hasWaited = false; path.RemoveAt(path.Count - 1); return (nextCell, mobile.GetAvailableSubCell(nextCell, mobile.FromSubCell, ignoreActor)); } protected override void OnLastRun(Actor self) { path = null; } bool CellIsEvacuating(Actor self, CPos cell) { foreach (var actor in self.World.ActorMap.GetActorsAt(cell)) { var move = actor.OccupiesSpace as Mobile; if (move == null || !move.IsTraitEnabled() || !move.IsLeaving()) return false; } return true; } public override void Cancel(Actor self, bool keepQueue = false) { Cancel(self, keepQueue, false); } public void Cancel(Actor self, bool keepQueue, bool forceClearPath) { // We need to clear the path here in order to prevent MovePart queueing new instances of itself // when the unit is making a turn. if (path != null && (forceClearPath || mobile.CanStayInCell(mobile.ToCell))) path.Clear(); base.Cancel(self, keepQueue); } public override IEnumerable GetTargets(Actor self) { if (path != null) return Enumerable.Reverse(path).Select(c => Target.FromCell(self.World, c)); if (destination != null) return new Target[] { Target.FromCell(self.World, destination.Value) }; return Target.None; } public override IEnumerable TargetLineNodes(Actor self) { // destination might be initialized with null, but will be set in a subsequent tick if (targetLineColor != null && destination != null) yield return new TargetLineNode(Target.FromCell(self.World, destination.Value), targetLineColor.Value); } abstract class MovePart : Activity { protected readonly Move Move; protected readonly WPos From, To; protected readonly WAngle FromFacing, ToFacing; protected readonly bool EnableArc; protected readonly WPos ArcCenter; protected readonly int ArcFromLength; protected readonly WAngle ArcFromAngle; protected readonly int ArcToLength; protected readonly WAngle ArcToAngle; protected readonly int Distance; protected int progress; public MovePart(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFacing, int carryoverProgress) { Move = move; From = from; To = to; FromFacing = fromFacing; ToFacing = toFacing; progress = carryoverProgress; Distance = (to - from).Length; IsInterruptible = false; // See comments in Move.Cancel() // Calculate an elliptical arc that joins from and to var delta = (fromFacing - toFacing).Angle; if (delta != 0 && delta != 512) { // The center of rotation is where the normal vectors cross var u = new WVec(1024, 0, 0).Rotate(WRot.FromYaw(fromFacing)); var v = new WVec(1024, 0, 0).Rotate(WRot.FromYaw(toFacing)); // Make sure that u and v aren't parallel, which may happen due to rounding // in WVec.Rotate if delta is close but not necessarily equal to 0 or 512 if (v.X * u.Y != v.Y * u.X) { var w = from - to; var s = (v.Y * w.X - v.X * w.Y) * 1024 / (v.X * u.Y - v.Y * u.X); var x = from.X + s * u.X / 1024; var y = from.Y + s * u.Y / 1024; ArcCenter = new WPos(x, y, 0); ArcFromLength = (ArcCenter - from).HorizontalLength; ArcFromAngle = (ArcCenter - from).Yaw; ArcToLength = (ArcCenter - to).HorizontalLength; ArcToAngle = (ArcCenter - to).Yaw; EnableArc = true; } } } public override bool Tick(Actor self) { var mobile = Move.mobile; progress += mobile.MovementSpeedForCell(self, mobile.ToCell); if (progress >= Distance) { progress = Distance; mobile.SetCenterPosition(self, To); mobile.Facing = ToFacing; Queue(OnComplete(self, mobile, Move)); return true; } WPos pos; if (EnableArc) { var angle = WAngle.Lerp(ArcFromAngle, ArcToAngle, progress, Distance); var length = int2.Lerp(ArcFromLength, ArcToLength, progress, Distance); var height = int2.Lerp(From.Z, To.Z, progress, Distance); pos = ArcCenter + new WVec(0, length, height).Rotate(WRot.FromYaw(angle)); } else pos = WPos.Lerp(From, To, progress, Distance); if (self.Location.Layer == 0) pos -= new WVec(WDist.Zero, WDist.Zero, self.World.Map.DistanceAboveTerrain(pos)); mobile.SetCenterPosition(self, pos); mobile.Facing = WAngle.Lerp(FromFacing, ToFacing, progress, Distance); return false; } protected abstract MovePart OnComplete(Actor self, Mobile mobile, Move parent); public override IEnumerable GetTargets(Actor self) { return Move.GetTargets(self); } } class MoveFirstHalf : MovePart { public MoveFirstHalf(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFacing, int carryoverProgress) : base(move, from, to, fromFacing, toFacing, carryoverProgress) { } static bool IsTurn(Mobile mobile, CPos nextCell, Map map) { // Some actors with a limited number of sprite facings should never move along curved trajectories. if (mobile.Info.AlwaysTurnInPlace) return false; // Tight U-turns should be done in place instead of making silly looking loops. var nextFacing = map.FacingBetween(nextCell, mobile.ToCell, mobile.Facing); var currentFacing = map.FacingBetween(mobile.ToCell, mobile.FromCell, mobile.Facing); var delta = (nextFacing - currentFacing).Angle; return delta != 0 && (delta < 384 || delta > 640); } protected override MovePart OnComplete(Actor self, Mobile mobile, Move parent) { var map = self.World.Map; var fromSubcellOffset = map.Grid.OffsetOfSubCell(mobile.FromSubCell); var toSubcellOffset = map.Grid.OffsetOfSubCell(mobile.ToSubCell); var nextCell = parent.PopPath(self); if (nextCell != null) { if (!mobile.IsTraitPaused && !mobile.IsTraitDisabled && IsTurn(mobile, nextCell.Value.Cell, map)) { var nextSubcellOffset = map.Grid.OffsetOfSubCell(nextCell.Value.SubCell); var ret = new MoveFirstHalf( Move, Util.BetweenCells(self.World, mobile.FromCell, mobile.ToCell) + (fromSubcellOffset + toSubcellOffset) / 2, Util.BetweenCells(self.World, mobile.ToCell, nextCell.Value.Cell) + (toSubcellOffset + nextSubcellOffset) / 2, mobile.Facing, map.FacingBetween(mobile.ToCell, nextCell.Value.Cell, mobile.Facing), progress - Distance); mobile.FinishedMoving(self); mobile.SetLocation(mobile.ToCell, mobile.ToSubCell, nextCell.Value.Cell, nextCell.Value.SubCell); return ret; } parent.path.Add(nextCell.Value.Cell); } var toPos = mobile.ToCell.Layer == 0 ? map.CenterOfCell(mobile.ToCell) : self.World.GetCustomMovementLayers()[mobile.ToCell.Layer].CenterOfCell(mobile.ToCell); var ret2 = new MoveSecondHalf( Move, Util.BetweenCells(self.World, mobile.FromCell, mobile.ToCell) + (fromSubcellOffset + toSubcellOffset) / 2, toPos + toSubcellOffset, mobile.Facing, mobile.Facing, progress - Distance); mobile.EnteringCell(self); mobile.SetLocation(mobile.ToCell, mobile.ToSubCell, mobile.ToCell, mobile.ToSubCell); return ret2; } } class MoveSecondHalf : MovePart { public MoveSecondHalf(Move move, WPos from, WPos to, WAngle fromFacing, WAngle toFacing, int carryoverProgress) : base(move, from, to, fromFacing, toFacing, carryoverProgress) { } protected override MovePart OnComplete(Actor self, Mobile mobile, Move parent) { mobile.SetPosition(self, mobile.ToCell); return null; } } } }