Fiber
Fibers Lighweight Cooperative Multitasking
Fiber.cs
1 // Copyright 2018 (C) paul@marrington.net http://www.askowl.net/unity-packages
2 
3 using System;
4 using System.Collections;
5 using System.Text.RegularExpressions;
6 using UnityEngine;
7 
8 namespace Askowl {
9  /// <a href="http://bit.ly/2DF6QHw">lightweight cooperative multi-tasking</a>
10  public partial class Fiber : IDisposable {
11  #region Instantiate
12  ///
13  public Emitter OnComplete;
14 
15  ///
16  public bool Running, Aborted;
17 
18  /// <a href="http://bit.ly/2DDZjbO">Method signature for Do(Action) methods</a>
19  public delegate void Action(Fiber fiber);
20 
21  /// <a href="http://bit.ly/2RAsNy3">Precompile an instance of a fiber command</a>
22  public static Fiber Instance {
23  get {
24  var node = Queue.Waiting.GetRecycledOrNew();
25  Queue.Reactivation(node);
26  var fiber = node.Item;
27  fiber.node = node;
28  fiber.id = ++nextId;
29  return fiber;
30  }
31  }
32 
33  /// <a href="http://bit.ly/2DDvnwP">Cleans up Fiber before it goes into the recycling</a>
34  public void Dispose() {
35  (context as IDisposable)?.Dispose();
36  actions.Dispose();
37  node.Recycle();
38  if (resetOnError) onError = Debug.LogError;
39  }
40 
41  /// <a href="http://bit.ly/2DDvnwP">Prepare a Fiber and place it on the Update queue</a>
42  public static Fiber Start {
43  get {
44  if (controller == null) {
45  controller = Components.Create<FiberController>("FiberController");
46  }
47  var fiber = Instance;
48  fiber.Update = FirstUpdate;
49  fiber.node.MoveTo(Queue.Update);
50  fiber.disposeOnComplete = true;
51  return fiber;
52  }
53  }
54  private static void FirstUpdate(Fiber fiber) { // called on first Update
55  fiber.Go(NextAction);
56  NextAction(fiber);
57  }
58 
59  /// <a href="http://bit.ly/2Rb9oEq">Start a fiber if it is not already running</a>
60  public Fiber Go() => Go(NextAction);
61 
62  /// <a href="http://bit.ly/2Rb9oEq">Start a fiber if it is not already running</a>
63  public Fiber Go(Action updater) {
64  if (Running) return this;
65  if (controller == null) {
66  controller = Components.Create<FiberController>("FiberController");
67  }
68  Update = updater;
69  Running = true;
70  SetAction(actions.Last);
71  node.MoveTo(Queue.Update);
72  return this;
73  }
74  #endregion
75 
76  #region Closures
77  /// <a href="http://bit.ly/2NjSGNX">Interface used by WaitFor(IClosure)</a>
78  public interface IClosure {
79  /// <a href="http://bit.ly/2NjSGNX">A reference to closure.Fiber.OnComplete</a>
81  Fiber Fiber { get; }
82  }
83 
84  /// <a href="http://bit.ly/2NjSGNX">Closure super-class that does all the smarts</a>
85  public abstract class Closure<TS, TTuple> : DelayedCache<Closure<TS, TTuple>>, IClosure
86  where TS : DelayedCache<Closure<TS, TTuple>> {
87  /// <a href="http://bit.ly/2NjSGNX">Scope is available for 10 frames after OnComplete in case it holds response data</a>
88  public TTuple Scope;
89  /// <a href="http://bit.ly/2NjSGNX">Emitter that is fired when the fiber completes all actions</a>
90  public Emitter OnComplete => onComplete;
91  private Emitter onComplete;
92 
93  protected Closure() {
94  fiber = Fiber.Instance;
95  // ReSharper disable once VirtualMemberCallInConstructor
97  Fiber.Do(_ => Dispose());
98  }
99  /// <a href="http://bit.ly/2NjSGNX">Fiber that will run / is running / has run in the context of this closure</a>
100  public Fiber Fiber => fiber;
101  private readonly Fiber fiber;
102 
103  /// <a href="http://bit.ly/2NjSGNX">Add all the steps you need to this override. It is called by the constructor.</a>
104  protected abstract void Activities(Fiber fiberToUpdate);
105 
106  /// <a href="http://bit.ly/2NjSGNX">Calling this static will fetch a prepared fiber, add scope and run it.</a>
107  public static Closure<TS, TTuple> Go(TTuple scope) {
108  var instance = Cache<TS>.Instance as Closure<TS, TTuple>;
109  // ReSharper disable once PossibleNullReferenceException
110  instance.Scope = scope;
111  instance.onComplete = instance.Fiber.OnComplete;
112  instance.Fiber.Go();
113  return instance;
114  }
115  }
116 
117  /// <a href="http://bit.ly/2NjSGNX">Helper that is the same as fiber.WaitFor(closure.OnComplete)</a>
118  public Fiber WaitFor(IClosure closure) => WaitFor(closure.Fiber);
119  #endregion
120 
121  #region Context
122  /// <a href="http://bit.ly/2RUcL2S">Retrieve the context as a class type - null for none or wrong type</a>
123  public T Context<T>() where T : class => context[typeof(T)].Value as T;
124 
125  /// <a href="http://bit.ly/2RUcL2S">Set the context to an instance of a type</a>
126  public Fiber Context<T>(T value) where T : class {
127  (context[typeof(T)].Value as IDisposable)?.Dispose();
128  context.Add(typeof(T), value);
129  return this;
130  }
131  /// <a href="http://bit.ly/2RUcL2S">Retrieve the context as a class type - null for none or wrong type</a>
132  public T Context<T>(string name) where T : class => context[name].Value as T;
133 
134  /// <a href="http://bit.ly/2RUcL2S">Set the context to an instance of a type</a>
135  public Fiber Context<T>(string name, T value) where T : class {
136  (context[name].Value as IDisposable)?.Dispose();
137  context.Add(name, value);
138  return this;
139  }
140  private readonly Map context = Map.Instance;
141  #endregion
142 
143  #region Queues
144  /// <a href="http://bit.ly/2Pqv2Ub">Return Fiber processing to frame Update queue</a>
145  public Fiber OnUpdates => AddAction(_ => node.MoveTo(Queue.Update), "OnUpdates");
146  /// <a href="http://bit.ly/2Pqv2Ub">Move Fiber processing to FixedUpdate queue</a>
147  public Fiber OnFixedUpdates => AddAction(_ => node.MoveTo(Queue.FixedUpdate), "OnFixedUpdates");
148  /// <a href="http://bit.ly/2Pqv2Ub">Move Fiber processing to LateUpdate queue</a>
149  public Fiber OnLateUpdates => AddAction(_ => node.MoveTo(Queue.LateUpdate), "OnLateUpdates");
150 
151  /// <a href="http://bit.ly/2DBVWCe">Abort fiber processing immediately, cleaning up as we go</a>
152  public Fiber Exit() {
153  Aborted = true;
154  action = actions.First;
155  return this;
156  }
157 
158  /// <a href="http://bit.ly/2DBVWCe">Force another fiber to exit immediately</a>
159  public Fiber Exit(Fiber fiber) {
160  AddAction(
161  _ => {
162  fiber.Aborted = true;
163  fiber.action = fiber.actions.First;
164  node.MoveTo(Queue.Update);
165  Update(fiber);
166  });
167  return this;
168  }
169 
170  /// <a href="http://bit.ly/2Rf0dD4">Complete a Fiber.Start statement where needed (no action)</a>
171  public void Finish() { }
172  #endregion
173 
174  #region Blocks and Loops
175  /// <a href="http://bit.ly/2DDvnNl">Loops and Blocks - Begin/End, Begin/Again, Begin-Repeat</a>
176  public Fiber Begin {
177  get {
178  AddAction(_ => blockStack.Push(action?.Previous ?? actions.First), "Begin");
179  AddAction(NextAction);
180  return this;
181  }
182  }
183 
184  /// <a href="http://bit.ly/2PtbezT">Begin/End block - use Break() to create an `if`</a>
185  public Fiber End => AddAction(NextAction).AddAction(_ => blockStack.Pop(), "End");
186 
187  /// <a href="http://bit.ly/2DDvnNl">Begin/Again repeating operations. Use Break() or Exit() to leave</a>
188  public Fiber Again => AddAction(_ => action = blockStack.Top).AddAction(NextAction).End;
189 
190  /// <a href="http://bit.ly/2DDvp7V">Begin/Repeat loop for a specific number of times</a>
191  public Fiber Repeat(int count) {
192  count += 1;
193  int counter = 0;
194  return AddAction(
195  _ => {
196  var begin = blockStack.Top;
197  if ((++counter % count) != 0) action = begin;
198  }, "Repeat").End;
199  }
200 
201  /// <a href="http://bit.ly/2CT634f">Loop until a value function returns true</a>
202  public Fiber Until(Func<Fiber, bool> isTrue) =>
203  AddAction(
204  _ => {
205  var begin = blockStack.Top;
206  if (!isTrue(this)) action = begin;
207  }, "Until").End;
208 
209  /// <a href="http://bit.ly/2RDN05W">Break out of any block if a value function returns true</a>
210  public Fiber BreakIf(Func<Fiber, bool> isBreak) =>
211  AddAction(
212  _ => {
213  if (isBreak(this)) Break();
214  }, "BreakIf");
215 
216  /// <a href="http://bit.ly/2DDvlFd">Break a Begin/End/Repeat/Again block</a>
217  public void Break() {
218  while ((action?.Previous != null) && (action.Previous.Item.Actor != NextAction)) action = action.Previous;
219  }
220 
221  ///
222  public void Break(int after) {
223  Skip(after);
224  Break();
225  }
226 
227  ///
228  public void Skip(int after) {
229  for (int i = 0; (i < after) && (action != null); i++) action = action.Previous;
230  }
231  #endregion
232 
233  #region If Else Then
234  /// <a href="http://bit.ly/2CU6Vp6">Standard If // Else // Then branch</a>
235  public Fiber If(Func<Fiber, bool> isTrue) =>
236  AddAction(
237  _ => {
238  if (!isTrue(this)) Break();
239  }, "If");
240 
241  /// <a href="http://bit.ly/2CU6Vp6">Standard If // Else // Then branch</a>
242  public Fiber Else => AddAction(_ => Break(2), "Else").AddAction(NextAction);
243 
244  /// <a href="http://bit.ly/2CU6Vp6">Standard If // Else // Then branch</a>
245  public Fiber Then => AddAction(NextAction, "Then");
246  #endregion
247 
248  /// <a href="http://bit.ly/2DDZjbO">Business logic activation step</a>
249  public Fiber Do(Action nextAction, string name = null) => AddAction(nextAction, name);
250 
251  #region Fiber Control and Monitoring
252  /// <a href="http://bit.ly/2DB3wgx">Return an IEnumerator to use with a yield in a Coroutine</a>
253  public IEnumerator AsCoroutine() {
254  if (!Running) Go();
255  while (Running) yield return null;
256  }
257 
258  /// <a href="http://bit.ly/2CV0RNn">Wait for another fiber to complete, starting it if needed</a>
259  public Fiber WaitFor(Fiber anotherFiber) {
260  if (anotherFiber == null) return this;
261  if (anotherFiber.onError == globalOnError) anotherFiber.onError = onError;
262  Do(_ => anotherFiber.Go());
263  WaitFor(anotherFiber.OnComplete, "WaitFor(Fiber)");
264  return this;
265  }
266 
267  /// <a href="http://bit.ly/2RWQrpp">Exit later fiber operations if the time supplied is exceeded</a>
268  public Fiber Timeout(float seconds) {
269  secondsTimeout = seconds;
270  if (timeoutFiber == null) timeoutFiber = Instance.WaitFor(_ => secondsTimeout).Exit(this);
271  timeoutFiber.Go();
272  return this;
273  }
274  private float secondsTimeout;
275  private Fiber timeoutFiber;
276 
277  /// <a href="http://bit.ly/2CV0RNn">Wait for another fiber to complete, starting it if needed - value set by return value of a function</a>
278  public Fiber WaitFor(Func<Fiber, Fiber> getFiber) => AddAction(_ => WaitFor(getFiber(this)));
279  #endregion
280 
281  #region Error Management
282  /// <a href="http://bit.ly/2NsjMml">Set a global (app-wide) error catch lambda. All fibers without a local override will come here. The default is to write to the Unity console.</a>
283  public Fiber GlobalOnError(Action<string> actor) {
284  onError = globalOnError = actor;
285  resetOnError = true;
286  return this;
287  }
288  /// <a href="http://bit.ly/2NlxMy3">The catch lambda will be called for any exceptions from this fiber or any fibers called with WaitFor</a>
289  public Fiber OnError(Action<string> actor) {
290  onError = actor;
291  return this;
292  }
293  /// <a href="http://bit.ly/2Nn2xCx">Exceptions in this fiber will cause the fiber to exit</a>
295  get {
296  exitOnError = true;
297  return this;
298  }
299  }
300  /// <a href="http://bit.ly/2NjXMJZ"></a> //#TBD#//
301  public Fiber Error(string message) {
302  onError(message);
303  return this;
304  }
305  /// <a href="http://bit.ly/2NjXMJZ"></a> //#TBD#//
306  public Fiber Error(Func<Fiber, string> messageLambda) {
307  onError(messageLambda(this));
308  return this;
309  }
310  private bool resetOnError;
311  private Action<string> onError = msg => globalOnError(msg);
312  private static Action<string> globalOnError = msg => Debug.LogError($"onError: {msg}");
313  private bool exitOnError;
314  #endregion
315 
316  #region Support
317  /// <a href="http://bit.ly/2DF6QHw">Container for different update queues</a> <inheritdoc />
318  internal class Queue : LinkedList<Fiber> {
319  // Deactivate only used for Start when it is not an infinite loop - in other words hardly ever
320  internal static Action<Node> Deactivation = (node) => {
321  var fiber = node.Item;
322  fiber.actions.Dispose();
323  fiber.blockStack.Dispose();
324  fiber.OnComplete.Dispose();
325  fiber.CancelOnAborted();
326  };
327 
328  internal static void Reactivation(Node node) {
329  var fiber = node.Item;
330  fiber.OnComplete = Emitter.Instance;
331  fiber.actions = Cache<ActionList>.Instance;
332  fiber.AddAction(_ => { }, "Start");
333  fiber.blockStack = Fifo<LinkedList<ActionItem>.Node>.Instance;
334  fiber.Running = fiber.Aborted = fiber.resetOnError = fiber.disposeOnComplete = fiber.exitOnError = false;
335  }
336 
337  internal static readonly Queue Update = new Queue
338  {Name = "Update Fibers", DeactivateItem = Deactivation, ReactivateItem = Reactivation};
339  internal static readonly Queue LateUpdate = new Queue
340  {Name = "Late Update Fibers", DeactivateItem = Deactivation, ReactivateItem = Reactivation};
341  internal static readonly Queue FixedUpdate = new Queue
342  {Name = "Fixed Update Fibers", DeactivateItem = Deactivation, ReactivateItem = Reactivation};
343  internal static readonly Queue Waiting = new Queue
344  {Name = "Fiber Waiting Queue", DeactivateItem = Deactivation, ReactivateItem = Reactivation};
345  }
346  #endregion
347 
348  #region Action
349  private static string ActionName(ActionItem actionItem) {
350  var name = actionItem.Name ?? actionItem.Actor.Method.Name;
351  Match match = getRe.Match(name);
352  for (int i = 0; i < match.Groups.Count; i++) {
353  if (match.Groups[i].Success) name = match.Groups[i].Value;
354  }
355  if (name == "NextAction") name = "_";
356  return name;
357  }
358  private static readonly Regex getRe = new Regex(@"<get_(.*?)>|<.*?>g__(.*)\|\d+|<(.*?)>");
359 
360  private static void NextAction(Fiber fiber) {
361  if (fiber.action?.Previous != null) {
362  try {
363  fiber.SetAction(fiber.action.Previous).Item.Actor(fiber);
364  } catch (Exception e) {
365  fiber.onError(e.ToString());
366  if (fiber.exitOnError) fiber.Exit();
367  }
368  } else {
369  #if UNITY_EDITOR
370  if (fiber.Debugging) fiber.Log($"OnComplete: for {fiber.node}");
371  #endif
372  fiber.OnComplete.Fire();
373  fiber.Running = false;
374  fiber.node.MoveTo(Queue.Waiting);
375  if (fiber.disposeOnComplete) fiber.Dispose();
376  }
377  }
378 
379  private struct ActionItem {
380  public string Name;
381  public Action Actor;
382  }
383 
384  private class ActionList : LinkedList<ActionItem> { }
385 
386  private LinkedList<ActionItem>.Node action;
387  private ActionList actions;
388  private Fifo<LinkedList<ActionItem>.Node> blockStack;
389  private static MonoBehaviour controller;
390  private bool disposeOnComplete;
391  private int id;
392  private static int nextId;
393  private LinkedList<Fiber>.Node node;
394  internal Action Update;
395 
396  private Fiber AddAction(Action newAction, string name = null) {
397  actions.Add(new ActionItem {Name = name, Actor = newAction});
398  return this;
399  }
400 
401  private LinkedList<ActionItem>.Node SetAction(LinkedList<ActionItem>.Node nextAction) {
402  action = nextAction;
403  #if UNITY_EDITOR
404  if (Debugging) Log($"Run: {ActionName(action.Item),10} for {this}");
405  #endif
406  return action;
407  }
408  #endregion
409 
410  #region Debugging Mode
411  /// <a href="http://bit.ly/2DDvmZN">Displays Do() and action events on Unity console</a>
412  public bool Debugging = false;
413 
414  /// <a href="http://bit.ly/2DDvmZN">Return Fiber contents and current state</a><inheritdoc />
415  public override string ToString() => $"Id: {id} // Actions: {ActionNames} // Queue: {node?.Owner}";
416 
417  //BeginAgainExample.IncrementCounter,BeginAgainExample.IncrementCounter,Fiber.<get_End>b__18_0,<>c.<.ctor>b__75_0,BeginAgainExample.IncrementCounter
418  private string ActionNames {
419  get {
420  var array = new string[actions.Count];
421  // ReSharper disable once LocalVariableHidesMember
422  var node = actions.Last;
423 
424  for (var idx = 0; idx < array.Length; node = node.Previous, idx++) {
425  var name = ActionName(node.Item);
426  array[idx] = node == action ? $"[{name}]" : name;
427  }
428  return Csv.ToString(array);
429  }
430  }
431 
432  /// <a href="http://bit.ly/2NjzIHg">Write to the Unity console (optionally as a warning entry)</a>
433  public Fiber Log(string message, bool warning = false) {
434  message = $"{message}\n{this}";
435  if (warning) {
436  Debug.LogWarning(message);
437  } else {
438  Debug.Log(message);
439  }
440  return this;
441  }
442  #endregion
443  }
444 }
Definition: Emitter.cs:6
T Context< T >()
Retrieve the context as a class type - null for none or wrong type
void Finish()
Complete a Fiber.Start statement where needed (no action)
Definition: Fiber.cs:171
Fiber Else
Standard If // Else // Then branch
Definition: Fiber.cs:242
Fiber OnUpdates
Return Fiber processing to frame Update queue
Definition: Fiber.cs:145
Fiber Repeat(int count)
Begin/Repeat loop for a specific number of times
Definition: Fiber.cs:191
Fiber Do(Action nextAction, string name=null)
Business logic activation step
static Fiber Start
Prepare a Fiber and place it on the Update queue
Definition: Fiber.cs:42
bool Debugging
Displays Do() and action events on Unity console
Definition: Fiber.cs:412
Interface used by WaitFor(IClosure)
Definition: Fiber.cs:78
Fiber OnFixedUpdates
Move Fiber processing to FixedUpdate queue
Definition: Fiber.cs:147
Emitter OnComplete
A reference to closure.Fiber.OnComplete
Definition: Fiber.cs:80
void Break()
Break a Begin/End/Repeat/Again block
Definition: Fiber.cs:217
Closure super-class that does all the smarts
Definition: Fiber.cs:85
lightweight cooperative multi-tasking
Definition: Fiber.cs:10
override string ToString()
Return Fiber contents and current state
Fiber Log(string message, bool warning=false)
Write to the Unity console (optionally as a warning entry)
Definition: Fiber.cs:433
static Closure< TS, TTuple > Go(TTuple scope)
Calling this static will fetch a prepared fiber, add scope and run it.
Definition: Fiber.cs:107
Fiber WaitFor(IClosure closure)
Helper that is the same as fiber.WaitFor(closure.OnComplete)
Fiber OnLateUpdates
Move Fiber processing to LateUpdate queue
Definition: Fiber.cs:149
Fiber Exit()
Abort fiber processing immediately, cleaning up as we go
Definition: Fiber.cs:152
Fiber Until(Func< Fiber, bool > isTrue)
Loop until a value function returns true
void Dispose()
Cleans up Fiber before it goes into the recycling
Definition: Fiber.cs:34
Emitter OnComplete
Emitter that is fired when the fiber completes all actions
Definition: Fiber.cs:90
Fiber Error(Func< Fiber, string > messageLambda)
//#TBD#//
Definition: Fiber.cs:306
Fiber Go()
Start a fiber if it is not already running
Fiber WaitFor(Fiber anotherFiber)
Wait for another fiber to complete, starting it if needed
Definition: Fiber.cs:259
Fiber End
Begin/End block - use Break() to create an if
Definition: Fiber.cs:185
Fiber Error(string message)
//#TBD#//
Definition: Fiber.cs:301
Fiber GlobalOnError(Action< string > actor)
Set a global (app-wide) error catch lambda. All fibers without a local override will come here...
Definition: Fiber.cs:283
Fiber Then
Standard If // Else // Then branch
Definition: Fiber.cs:245
Fiber Exit(Fiber fiber)
Force another fiber to exit immediately
Definition: Fiber.cs:159
Fiber ExitOnError
Exceptions in this fiber will cause the fiber to exit
Definition: Fiber.cs:294
Fiber Timeout(float seconds)
Exit later fiber operations if the time supplied is exceeded
Definition: Fiber.cs:268
Fiber BreakIf(Func< Fiber, bool > isBreak)
Break out of any block if a value function returns true
Fiber Again
Begin/Again repeating operations. Use Break() or Exit() to leave
Definition: Fiber.cs:188
abstract void Activities(Fiber fiberToUpdate)
Add all the steps you need to this override. It is called by the constructor.
Fiber Begin
Loops and Blocks - Begin/End, Begin/Again, Begin-Repeat
Definition: Fiber.cs:176
Cached C# Action instances using the observer pattern
Definition: Emitter.cs:8
Fiber If(Func< Fiber, bool > isTrue)
Standard If // Else // Then branch
IEnumerator AsCoroutine()
Return an IEnumerator to use with a yield in a Coroutine
Definition: Fiber.cs:253
static Fiber Instance
Precompile an instance of a fiber command
Definition: Fiber.cs:22
Disposed does not recycle object immediately
Definition: DelayedCache.cs:6
TTuple Scope
Scope is available for 10 frames after OnComplete in case it holds response data
Definition: Fiber.cs:88
delegate void Action(Fiber fiber)
Method signature for Do(Action) methods
Fiber Go(Action updater)
Start a fiber if it is not already running
Definition: Fiber.cs:63
Fiber OnError(Action< string > actor)
The catch lambda will be called for any exceptions from this fiber or any fibers called with WaitFor ...
Definition: Fiber.cs:289