#region Copyright & License Information /* * Copyright 2007-2015 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. For more information, * see COPYING. */ #endregion using System; using System.Linq; using OpenRA.Activities; using OpenRA.Traits; namespace OpenRA.Mods.Common.Activities { public abstract class Enter : Activity { public enum ReserveStatus { None, TooFar, Pending, Ready } enum State { ApproachingOrEntering, Inside, Exiting, Done } readonly IMove move; readonly int maxTries = 0; readonly bool targetCenter; public Target Target { get { return target; } } 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, bool targetCenter = false) { this.move = self.Trait(); this.target = Target.FromActor(target); this.maxTries = maxTries; this.targetCenter = targetCenter; } // 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 null; } // 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); } // 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, targetCenter ? Target.FromPos(target.CenterPosition) : target); // 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) return CanceledTick(self); // Check target validity if not exiting or done if (nextState != State.Done && (target.Type != TargetType.Actor || !target.IsValidFor(self))) AbortOrExit(self); // If no current activity, tick next activity if (inner == null && FindAndTransitionToNextState(self) == State.Done) return CanceledTick(self); // Run inner activity/InsideTick inner = inner == this ? InsideTick(self) : Util.RunActivity(self, inner); return this; } } }