1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Collections.Generic; 14 using System.Linq; 15 using OpenRA.Graphics; 16 using OpenRA.Primitives; 17 using OpenRA.Traits; 18 19 namespace OpenRA.Activities 20 { 21 public enum ActivityState { Queued, Active, Canceling, Done } 22 23 public class TargetLineNode 24 { 25 public readonly Target Target; 26 public readonly Color Color; 27 public readonly Sprite Tile; 28 TargetLineNode(Target target, Color color, Sprite tile = null)29 public TargetLineNode(Target target, Color color, Sprite tile = null) 30 { 31 // Note: Not all activities are drawable. In that case, pass Target.Invalid as target, 32 // if "yield break" in TargetLineNode(Actor self) is not feasible. 33 Target = target; 34 Color = color; 35 Tile = tile; 36 } 37 } 38 39 /* 40 * Things to be aware of when writing activities: 41 * 42 * - Use "return true" at least once somewhere in the tick method. 43 * - Do not "reuse" activity objects (by queuing them as next or child, for example) that have already started running. 44 * Queue a new instance instead. 45 * - Avoid calling actor.CancelActivity(). It is almost always a bug. Call activity.Cancel() instead. 46 * - Do not evaluate dynamic state (an actor's location, health, conditions, etc.) in the activity's constructor, 47 * as that might change before the activity gets to tick for the first time. Use the OnFirstRun() method instead. 48 */ 49 public abstract class Activity : IActivityInterface 50 { 51 public ActivityState State { get; private set; } 52 53 Activity childActivity; 54 protected Activity ChildActivity 55 { 56 get { return SkipDoneActivities(childActivity); } 57 private set { childActivity = value; } 58 } 59 60 Activity nextActivity; 61 public Activity NextActivity 62 { 63 get { return SkipDoneActivities(nextActivity); } 64 private set { nextActivity = value; } 65 } 66 SkipDoneActivities(Activity first)67 internal static Activity SkipDoneActivities(Activity first) 68 { 69 // If first.Cancel() was called while it was queued (i.e. before it first ticked), its state will be Done 70 // rather than Queued (the activity system guarantees that it cannot be Active or Canceling). 71 // An unknown number of ticks may have elapsed between the Cancel() call and now, 72 // so we cannot make any assumptions on the value of first.NextActivity. 73 // We must not return first (ticking it would be bogus), but returning null would potentially 74 // drop valid activities queued after it. Walk the queue until we find a valid activity or 75 // (more likely) run out of activities. 76 while (first != null && first.State == ActivityState.Done) 77 first = first.NextActivity; 78 79 return first; 80 } 81 82 public bool IsInterruptible { get; protected set; } 83 public bool ChildHasPriority { get; protected set; } 84 public bool IsCanceling { get { return State == ActivityState.Canceling; } } 85 bool finishing; 86 bool firstRunCompleted; 87 bool lastRun; 88 Activity()89 public Activity() 90 { 91 IsInterruptible = true; 92 ChildHasPriority = true; 93 } 94 TickOuter(Actor self)95 public Activity TickOuter(Actor self) 96 { 97 if (State == ActivityState.Done) 98 throw new InvalidOperationException("Actor {0} attempted to tick activity {1} after it had already completed.".F(self, GetType())); 99 100 if (State == ActivityState.Queued) 101 { 102 OnFirstRun(self); 103 firstRunCompleted = true; 104 State = ActivityState.Active; 105 } 106 107 if (!firstRunCompleted) 108 throw new InvalidOperationException("Actor {0} attempted to tick activity {1} before running its OnFirstRun method.".F(self, GetType())); 109 110 // Only run the parent tick when the child is done. 111 // We must always let the child finish on its own before continuing. 112 if (ChildHasPriority) 113 { 114 lastRun = TickChild(self) && (finishing || Tick(self)); 115 finishing |= lastRun; 116 } 117 118 // The parent determines whether the child gets a chance at ticking. 119 else 120 lastRun = Tick(self); 121 122 // Avoid a single tick delay if the childactivity was just queued. 123 if (ChildActivity != null && ChildActivity.State == ActivityState.Queued) 124 { 125 if (ChildHasPriority) 126 lastRun = TickChild(self) && finishing; 127 else 128 TickChild(self); 129 } 130 131 if (lastRun) 132 { 133 State = ActivityState.Done; 134 OnLastRun(self); 135 return NextActivity; 136 } 137 138 return this; 139 } 140 TickChild(Actor self)141 protected bool TickChild(Actor self) 142 { 143 ChildActivity = ActivityUtils.RunActivity(self, ChildActivity); 144 return ChildActivity == null; 145 } 146 147 /// <summary> 148 /// Called every tick to run activity logic. Returns false if the activity should 149 /// remain active, or true if it is complete. Cancelled activities must ensure they 150 /// return the actor to a consistent state before returning true. 151 /// 152 /// Child activities can be queued using QueueChild, and these will be ticked 153 /// instead of the parent while they are active. Activities that need to run logic 154 /// in parallel with child activities should set ChildHasPriority to false and 155 /// manually call TickChildren. 156 /// 157 /// Queuing one or more child activities and returning true is valid, and causes 158 /// the activity to be completed immediately (without ticking again) once the 159 /// children have completed. 160 /// </summary> Tick(Actor self)161 public virtual bool Tick(Actor self) 162 { 163 return true; 164 } 165 166 /// <summary> 167 /// Runs once immediately before the first Tick() execution. 168 /// </summary> OnFirstRun(Actor self)169 protected virtual void OnFirstRun(Actor self) { } 170 171 /// <summary> 172 /// Runs once immediately after the last Tick() execution. 173 /// </summary> OnLastRun(Actor self)174 protected virtual void OnLastRun(Actor self) { } 175 176 /// <summary> 177 /// Runs once on Actor.Dispose() (through OnActorDisposeOuter) and can be used to perform activity clean-up on actor death/disposal, 178 /// for example by force-triggering OnLastRun (which would otherwise be skipped). 179 /// </summary> OnActorDispose(Actor self)180 protected virtual void OnActorDispose(Actor self) { } 181 182 /// <summary> 183 /// Runs once on Actor.Dispose(). 184 /// Main purpose is to ensure ChildActivity.OnActorDispose runs as well (which isn't otherwise accessible due to protection level). 185 /// </summary> OnActorDisposeOuter(Actor self)186 internal void OnActorDisposeOuter(Actor self) 187 { 188 if (ChildActivity != null) 189 ChildActivity.OnActorDisposeOuter(self); 190 191 OnActorDispose(self); 192 } 193 Cancel(Actor self, bool keepQueue = false)194 public virtual void Cancel(Actor self, bool keepQueue = false) 195 { 196 if (!keepQueue) 197 NextActivity = null; 198 199 if (!IsInterruptible) 200 return; 201 202 if (ChildActivity != null) 203 ChildActivity.Cancel(self); 204 205 // Directly mark activities that are queued and therefore didn't run yet as done 206 State = State == ActivityState.Queued ? ActivityState.Done : ActivityState.Canceling; 207 } 208 Queue(Activity activity)209 public void Queue(Activity activity) 210 { 211 if (NextActivity != null) 212 NextActivity.Queue(activity); 213 else 214 NextActivity = activity; 215 } 216 QueueChild(Activity activity)217 public void QueueChild(Activity activity) 218 { 219 if (ChildActivity != null) 220 ChildActivity.Queue(activity); 221 else 222 ChildActivity = activity; 223 } 224 225 /// <summary> 226 /// Prints the activity tree, starting from the top or optionally from a given origin. 227 /// 228 /// Call this method from any place that's called during a tick, such as the Tick() method itself or 229 /// the Before(First|Last)Run() methods. The origin activity will be marked in the output. 230 /// </summary> 231 /// <param name="self">The actor performing this activity.</param> 232 /// <param name="origin">Activity from which to start traversing, and which to mark. If null, mark the calling activity, and start traversal from the top.</param> 233 /// <param name="level">Initial level of indentation.</param> PrintActivityTree(Actor self, Activity origin = null, int level = 0)234 protected void PrintActivityTree(Actor self, Activity origin = null, int level = 0) 235 { 236 if (origin == null) 237 self.CurrentActivity.PrintActivityTree(self, this); 238 else 239 { 240 Console.Write(new string(' ', level * 2)); 241 if (origin == this) 242 Console.Write("*"); 243 244 Console.WriteLine(GetType().ToString().Split('.').Last()); 245 246 if (ChildActivity != null) 247 ChildActivity.PrintActivityTree(self, origin, level + 1); 248 249 if (NextActivity != null) 250 NextActivity.PrintActivityTree(self, origin, level); 251 } 252 } 253 GetTargets(Actor self)254 public virtual IEnumerable<Target> GetTargets(Actor self) 255 { 256 yield break; 257 } 258 TargetLineNodes(Actor self)259 public virtual IEnumerable<TargetLineNode> TargetLineNodes(Actor self) 260 { 261 yield break; 262 } 263 DebugLabelComponents()264 public IEnumerable<string> DebugLabelComponents() 265 { 266 var act = this; 267 while (act != null) 268 { 269 yield return act.GetType().Name; 270 act = act.ChildActivity; 271 } 272 } 273 274 public IEnumerable<T> ActivitiesImplementing<T>(bool includeChildren = true) where T : IActivityInterface 275 { 276 if (includeChildren && ChildActivity != null) 277 foreach (var a in ChildActivity.ActivitiesImplementing<T>()) 278 yield return a; 279 280 if (this is T) 281 yield return (T)(object)this; 282 283 if (NextActivity != null) 284 foreach (var a in NextActivity.ActivitiesImplementing<T>()) 285 yield return a; 286 } 287 } 288 } 289