diff --git a/OpenRA.Mods.RA/Activities/Enter.cs b/OpenRA.Mods.RA/Activities/Enter.cs index ff926a8e3c..9a699b862b 100755 --- a/OpenRA.Mods.RA/Activities/Enter.cs +++ b/OpenRA.Mods.RA/Activities/Enter.cs @@ -8,41 +8,267 @@ */ #endregion +using System; +using System.Collections.Generic; using System.Linq; +using OpenRA.Mods.RA.Move; using OpenRA.Traits; namespace OpenRA.Mods.RA.Activities { public class Enter : Activity { - readonly Target target; - readonly Activity inner; + public enum ReserveStatus { None, TooFar, Pending, Ready } + enum State { ApproachingOrEntering, Inside, Exiting, Done } - public Enter(Actor target, Activity inner) + readonly Activity inside; + readonly IMove move; + readonly int maxTries = 0; + Target target; + State nextState = State.ApproachingOrEntering; // Hint/starting point for next state + bool isEnteringOrInside = false; // Used to know if exiting should be used + WPos savedPos; // Position just before entering + Activity inner; + bool firstApproach = true; + + protected Enter(Actor self, Actor target, int maxTries = 1) + : this(self, target, null) { + this.maxTries = maxTries; + } + + public Enter(Actor self, Actor target, Activity inside) + { + this.move = self.Trait(); this.target = Target.FromActor(target); - this.inner = inner; + this.inside = inside; + } + + // CanEnter(target) should to be true; othwise, Enter may abort. + // Tries counter starts at 1 (reset every tick) + protected virtual bool TryGetAlternateTarget(Actor self, int tries, ref Target target) { return false; } + protected virtual bool CanReserve(Actor self) { return true; } + protected virtual ReserveStatus Reserve(Actor self) + { + return !CanReserve(self) ? ReserveStatus.None : move.CanEnterTargetNow(self, target) ? ReserveStatus.Ready : ReserveStatus.TooFar; + } + + protected virtual void Unreserve(Actor self, bool abort) { } + protected virtual void OnInside(Actor self) { } + + protected bool TryGetAlternateTargetInCircle(Actor self, WRange radius, Action update, Func primaryFilter, Func[] preferenceFilters = null) + { + var radiusSquared = radius.Range * radius.Range; + var diff = new WVec(radius, radius, WRange.Zero); + var candidates = self.World.ActorMap.ActorsInBox(self.CenterPosition - diff, self.CenterPosition + diff) + .Where(primaryFilter).Select(a => new { Actor = a, Ls = (self.CenterPosition - a.CenterPosition).HorizontalLengthSquared }) + .Where(p => p.Ls <= radiusSquared).OrderBy(p => p.Ls).Select(p => p.Actor); + if (preferenceFilters != null) + foreach (var filter in preferenceFilters) + { + var preferredCandidate = candidates.FirstOrDefault(filter); + if (preferredCandidate == null) + continue; + target = Target.FromActor(preferredCandidate); + update(target); + return true; + } + + var candidate = candidates.FirstOrDefault(); + if (candidate == null) + return false; + target = Target.FromActor(candidate); + update(target); + return true; + } + + // Called when inner activity is this and returns inner activity for next tick. + protected virtual Activity InsideTick(Actor self) { return Util.RunActivity(self, inside); } + + // Abort entering and/or leave if necessary + protected virtual void AbortOrExit(Actor self) + { + if (nextState == State.Done) + return; + nextState = isEnteringOrInside ? State.Exiting : State.Done; + if (inner == this) + inner = null; + else if (inner != null) + inner.Cancel(self); + if (isEnteringOrInside) + Unreserve(self, true); + isEnteringOrInside = false; + } + + // Cancel inner activity and mark as done unless already leaving or done + protected void Done(Actor self) + { + if (nextState == State.Done) + return; + nextState = State.Done; + if (inner == this) + inner = null; + else if (inner != null) + inner.Cancel(self); + } + + public override void Cancel(Actor self) + { + AbortOrExit(self); + if (nextState < State.Exiting) + base.Cancel(self); + else + NextActivity = null; + } + + ReserveStatus TryReserveElseTryAlternateReserve(Actor self) + { + for (var tries = 0;;) + switch (Reserve(self)) + { + case ReserveStatus.None: + if (++tries > maxTries || !TryGetAlternateTarget(self, tries, ref target)) + return ReserveStatus.None; + continue; + case ReserveStatus.TooFar: + // Always goto to transport on first approach + if (firstApproach) + { + firstApproach = false; + return ReserveStatus.TooFar; + } + + if (++tries > maxTries) + return ReserveStatus.TooFar; + Target t = target; + if (!TryGetAlternateTarget(self, tries, ref t)) + return ReserveStatus.TooFar; + if ((target.CenterPosition - self.CenterPosition).HorizontalLengthSquared <= (t.CenterPosition - self.CenterPosition).HorizontalLengthSquared) + return ReserveStatus.TooFar; + target = t; + continue; + case ReserveStatus.Pending: + return ReserveStatus.Pending; + case ReserveStatus.Ready: + return ReserveStatus.Ready; + } + } + + State FindAndTransitionToNextState(Actor self) + { + switch (nextState) + { + case State.ApproachingOrEntering: + + // Reserve to enter or approach + isEnteringOrInside = false; + switch (TryReserveElseTryAlternateReserve(self)) + { + case ReserveStatus.None: + return State.Done; // No available target -> abort to next activity + case ReserveStatus.TooFar: + inner = move.MoveToTarget(self, Target.FromPos(target.CenterPosition)); // Approach + return State.ApproachingOrEntering; + case ReserveStatus.Pending: + return State.ApproachingOrEntering; // Retry next tick + case ReserveStatus.Ready: + break; // Reserved target -> start entering target + } + + // Entering + isEnteringOrInside = true; + savedPos = self.CenterPosition; // Save position of self, before entering, for returning on exit + inner = move.MoveIntoTarget(self, target); // Enter + + if (inner != null) + { + nextState = State.Inside; // Should be inside once inner activity is null + return State.ApproachingOrEntering; + } + + // Can enter but there is no activity for it, so go inside without one + goto case State.Inside; + + case State.Inside: + // Might as well teleport into target if there is no MoveIntoTarget activity + if (nextState == State.ApproachingOrEntering) + nextState = State.Inside; + + // Otherwise, try to recover from moving target + else if (target.CenterPosition != self.CenterPosition) + { + nextState = State.ApproachingOrEntering; + Unreserve(self, false); + if (Reserve(self) == ReserveStatus.Ready) + { + inner = move.MoveIntoTarget(self, target); // Enter + if (inner != null) + return State.ApproachingOrEntering; + + nextState = State.ApproachingOrEntering; + goto case State.ApproachingOrEntering; + } + + nextState = State.ApproachingOrEntering; + isEnteringOrInside = false; + inner = move.MoveIntoWorld(self, self.World.Map.CellContaining(savedPos)); + + return State.ApproachingOrEntering; + } + + OnInside(self); + + // Return if Abort(Actor) or Done(self) was called from OnInside. + if (nextState >= State.Exiting) + return State.Inside; + + inner = this; // Start inside activity + nextState = State.Exiting; // Exit once inner activity is null (unless Done(self) is called) + return State.Inside; + + // TODO: Handle target moved while inside or always call done for movable targets and use a separate exit activity + case State.Exiting: + inner = move.MoveIntoWorld(self, self.World.Map.CellContaining(savedPos)); + + // If not successfully exiting, retry on next tick + if (inner == null) + return State.Exiting; + isEnteringOrInside = false; + nextState = State.Done; + return State.Exiting; + + case State.Done: + return State.Done; + } + + return State.Done; // dummy to quiet dumb compiler + } + + Activity CanceledTick(Actor self) + { + if (inner == null) + return Util.RunActivity(self, NextActivity); + inner.Cancel(self); + inner.Queue(NextActivity); + return Util.RunActivity(self, inner); } public override Activity Tick(Actor self) { - if (IsCanceled || !target.IsValidFor(self)) - return NextActivity; + if (IsCanceled) + return CanceledTick(self); - if (target.Type != TargetType.Actor) - return NextActivity; + // Check target validity if not exiting or done + if (nextState != State.Done && (target.Type != TargetType.Actor || !target.IsValidFor(self))) + AbortOrExit(self); - if (!Util.AdjacentCells(self.World, target).Any(c => c == self.Location)) - return Util.SequenceActivities(new MoveAdjacentTo(self, target), this); + // If no current activity, tick next activity + if (inner == null && FindAndTransitionToNextState(self) == State.Done) + return CanceledTick(self); - // Move to the middle of the target, ignoring impassable tiles - var move = self.Trait(); - return Util.SequenceActivities( - move.VisualMove(self, self.CenterPosition, target.CenterPosition), - inner, - move.VisualMove(self, target.CenterPosition, self.CenterPosition), - NextActivity - ); + // Run inner activity/InsideTick + inner = inner == this ? InsideTick(self) : Util.RunActivity(self, inner); + return this; } } } diff --git a/OpenRA.Mods.RA/C4Demolition.cs b/OpenRA.Mods.RA/C4Demolition.cs index 51188b5e86..2e220bd515 100644 --- a/OpenRA.Mods.RA/C4Demolition.cs +++ b/OpenRA.Mods.RA/C4Demolition.cs @@ -76,7 +76,7 @@ namespace OpenRA.Mods.RA self.CancelActivity(); self.SetTargetLine(target, Color.Red); - self.QueueActivity(new Enter(target.Actor, new Demolish( + self.QueueActivity(new Enter(self, target.Actor, new Demolish( target.Actor, info.C4Delay, info.Flashes, info.FlashesDelay, info.FlashInterval, info.FlashDuration))); } diff --git a/OpenRA.Mods.RA/Captures.cs b/OpenRA.Mods.RA/Captures.cs index abd92a7dbe..5b5b9d6e05 100644 --- a/OpenRA.Mods.RA/Captures.cs +++ b/OpenRA.Mods.RA/Captures.cs @@ -75,7 +75,7 @@ namespace OpenRA.Mods.RA self.CancelActivity(); self.SetTargetLine(target, Color.Red); - self.QueueActivity(new Enter(target.Actor, new CaptureActor(target))); + self.QueueActivity(new Enter(self, target.Actor, new CaptureActor(target))); } class CaptureOrderTargeter : UnitOrderTargeter diff --git a/OpenRA.Mods.RA/EngineerRepair.cs b/OpenRA.Mods.RA/EngineerRepair.cs index 1f24b50c78..60c5cdde8b 100644 --- a/OpenRA.Mods.RA/EngineerRepair.cs +++ b/OpenRA.Mods.RA/EngineerRepair.cs @@ -78,7 +78,7 @@ namespace OpenRA.Mods.RA self.CancelActivity(); self.SetTargetLine(target, Color.Yellow); - self.QueueActivity(new Enter(target.Actor, new RepairBuilding(target.Actor))); + self.QueueActivity(new Enter(self, target.Actor, new RepairBuilding(target.Actor))); } class EngineerRepairOrderTargeter : UnitOrderTargeter diff --git a/OpenRA.Mods.RA/Infiltration/Infiltrates.cs b/OpenRA.Mods.RA/Infiltration/Infiltrates.cs index ada7140a79..78d4b97ee1 100644 --- a/OpenRA.Mods.RA/Infiltration/Infiltrates.cs +++ b/OpenRA.Mods.RA/Infiltration/Infiltrates.cs @@ -96,7 +96,7 @@ namespace OpenRA.Mods.RA.Infiltration self.CancelActivity(); self.SetTargetLine(target, Color.Red); - self.QueueActivity(new Enter(target.Actor, new Infiltrate(target.Actor))); + self.QueueActivity(new Enter(self, target.Actor, new Infiltrate(target.Actor))); } } } diff --git a/OpenRA.Mods.RA/RepairsBridges.cs b/OpenRA.Mods.RA/RepairsBridges.cs index f0e8c2302e..b3d8087042 100644 --- a/OpenRA.Mods.RA/RepairsBridges.cs +++ b/OpenRA.Mods.RA/RepairsBridges.cs @@ -59,7 +59,7 @@ namespace OpenRA.Mods.RA self.SetTargetLine(Target.FromOrder(self.World, order), Color.Yellow); self.CancelActivity(); - self.QueueActivity(new Enter(order.TargetActor, new RepairBridge(order.TargetActor))); + self.QueueActivity(new Enter(self, order.TargetActor, new RepairBridge(order.TargetActor))); } } diff --git a/OpenRA.Mods.RA/SupplyTruck.cs b/OpenRA.Mods.RA/SupplyTruck.cs index fc828be078..61b392caf2 100644 --- a/OpenRA.Mods.RA/SupplyTruck.cs +++ b/OpenRA.Mods.RA/SupplyTruck.cs @@ -67,7 +67,7 @@ namespace OpenRA.Mods.RA self.CancelActivity(); self.SetTargetLine(target, Color.Yellow); - self.QueueActivity(new Enter(target.Actor, new DonateSupplies(target.Actor, info.Payload))); + self.QueueActivity(new Enter(self, target.Actor, new DonateSupplies(target.Actor, info.Payload))); } class SupplyTruckOrderTargeter : UnitOrderTargeter