diff --git a/Source/AGS.API/AGS.API.csproj b/Source/AGS.API/AGS.API.csproj index 8ec659603..9d45426d0 100644 --- a/Source/AGS.API/AGS.API.csproj +++ b/Source/AGS.API/AGS.API.csproj @@ -276,6 +276,7 @@ + diff --git a/Source/AGS.API/Misc/Events/ClaimEventToken.cs b/Source/AGS.API/Misc/Events/ClaimEventToken.cs new file mode 100644 index 000000000..3cf7f40c7 --- /dev/null +++ b/Source/AGS.API/Misc/Events/ClaimEventToken.cs @@ -0,0 +1,16 @@ +namespace AGS.API +{ + /// + /// This is a token that can be used when you want to claim an event, so that other subscribers which follow you on the subscriber list will not get that event. + /// + /// + public struct ClaimEventToken + { + /// + /// Gets or sets a value indicating whether this is claimed. + /// If you claim the event (by setting Claimed = true), this event will not be propogated to the rest of the subscribers. + /// + /// true if claimed; otherwise, false. + public bool Claimed { get; set; } + } +} diff --git a/Source/AGS.API/Misc/Events/IBlockingEvent.cs b/Source/AGS.API/Misc/Events/IBlockingEvent.cs index 0504375fb..e130cd349 100644 --- a/Source/AGS.API/Misc/Events/IBlockingEvent.cs +++ b/Source/AGS.API/Misc/Events/IBlockingEvent.cs @@ -3,6 +3,52 @@ namespace AGS.API { + /// + /// This allows you to subscribe to an event with the option of claiming the event, so that other subscribers in the list will not receive the event. + /// + /// + /// + /// myEvent.Subscribe(onEvent1); + /// myEvent.Subscribe(onEvent2); + /// + /// void onEvent1(ref ClaimEventToken token) + /// { + /// token.Claimed = true; //We claimed the event, "onEvent2" will not get called. + /// } + /// + /// + /// + public delegate void ClaimableCallback(ref ClaimEventToken token); + + /// + /// This allows you to subscribe to an event with the option of claiming the event, so that other subscribers in the list will not receive the event. + /// + /// For an example- . + /// + public delegate void ClaimableCallbackWithArgs(TEventArgs args, ref ClaimEventToken token); + + /// + /// In a scenario where an event has multiple subscribers, the callback priority affects which subscriber gets the callback first. + /// High priority subscribers will receive the event before normal priority (the default) subscribers, and low priority will receive the event last. + /// + /// Note: For 2 subscribers with the same callback priority, there's no guarantee regarding who gets the event first. + /// + public enum CallbackPriority : byte + { + /// + /// Low priority- will receive the event last. + /// + Low, + /// + /// Normal priority. + /// + Normal, + /// + /// High priority- will receive the event first. + /// + High, + } + /// /// Represents an event which can be subscribed and invoked synchronously. /// An event is a notification for something that has happened. @@ -21,21 +67,43 @@ public interface IBlockingEvent /// Once subscribed, whenever the event happens this callback will be called. /// /// Callback. - void Subscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void Unsubscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); + + /// + /// Subscribe the specified callback to the event. + /// Once subscribed, whenever the event happens this callback will be called. + /// + /// In addition, this specific overload allows you to claim the event so that subscribers which follow you on the list will not receive the event. + /// For an example- . + /// + /// Callback. + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(ClaimableCallbackWithArgs callback, CallbackPriority priority = CallbackPriority.Normal); + + /// + /// Unsubscribe the specified callback from the event. + /// This will stops notifications to call this callback. + /// + /// Callback. + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(ClaimableCallbackWithArgs callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Asynchronously wait until the event fires and the specific condition applies. /// /// The task to be awaited. /// The condition we are waiting to apply before moving on. - Task WaitUntilAsync(Predicate condition); + /// The callback priority (determines the order in which the subscribers get the events). + Task WaitUntilAsync(Predicate condition, CallbackPriority priority = CallbackPriority.Normal); /// /// Invoke the event synchronously (i.e will wait for all subscribers to process the event before moving on). @@ -62,21 +130,43 @@ public interface IBlockingEvent /// Once subscribed, whenever the event happens this callback will be called. /// /// Callback. - void Subscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); + + /// + /// Unsubscribe the specified callback from the event. + /// This will stops notifications to call this callback. + /// + /// Callback. + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); + + /// + /// Subscribe the specified callback to the event. + /// Once subscribed, whenever the event happens this callback will be called. + /// + /// In addition, this specific overload allows you to claim the event so that subscribers which follow you on the list will not receive the event. + /// For an example- . + /// + /// Callback. + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(ClaimableCallback callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void Unsubscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(ClaimableCallback callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Asynchronously wait until the event fires and the specific condition applies. /// /// The task to be awaited. /// The condition we are waiting to apply before moving on. - Task WaitUntilAsync(Func condition); + /// The callback priority (determines the order in which the subscribers get the events). + Task WaitUntilAsync(Func condition, CallbackPriority priority = CallbackPriority.Normal); /// /// Invoke the event synchronously (i.e will wait for all subscribers to process the event before moving on). diff --git a/Source/AGS.API/Misc/Events/IEvent.cs b/Source/AGS.API/Misc/Events/IEvent.cs index 0d4e0a10b..0855eb6e4 100644 --- a/Source/AGS.API/Misc/Events/IEvent.cs +++ b/Source/AGS.API/Misc/Events/IEvent.cs @@ -21,28 +21,32 @@ public interface IEvent /// Once subscribed, whenever the event happens this callback will be called. /// /// Callback. - void Subscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void Unsubscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Subscribe the specified asynchronous callback to the event. /// Once subscribed, whenever the event happens this callback will be called. /// /// Callback. - void SubscribeToAsync(Func callback); + /// The callback priority (determines the order in which the subscribers get the events). + void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified asynchronous callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void UnsubscribeToAsync(Func callback); + /// The callback priority (determines the order in which the subscribers get the events). + void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Subscribe the specified callback to the event. @@ -50,14 +54,16 @@ public interface IEvent /// This version of Subscribe ignores incoming arguments from the event (if you need the arguments, use the other overload which gets an action of TEventArgs). /// /// Callback. - void Subscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void Unsubscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Subscribe the specified asynchronous callback to the event. @@ -65,21 +71,24 @@ public interface IEvent /// This version of Subscribe ignores incoming arguments from the event (if you need the arguments, use the other overload which gets a func of TEventArgs -> Task). /// /// Callback. - void SubscribeToAsync(Func callback); + /// The callback priority (determines the order in which the subscribers get the events). + void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified asynchronous callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void UnsubscribeToAsync(Func callback); + /// The callback priority (determines the order in which the subscribers get the events). + void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Asynchronously wait until the event fires and the specific condition applies. /// /// The task to be awaited. /// The condition we are waiting to apply before moving on. - Task WaitUntilAsync(Predicate condition); + /// The callback priority (determines the order in which the subscribers get the events). + Task WaitUntilAsync(Predicate condition, CallbackPriority priority = CallbackPriority.Normal); /// /// Invoke the event asynchronously. @@ -106,35 +115,40 @@ public interface IEvent /// Once subscribed, whenever the event happens this callback will be called. /// /// Callback. - void Subscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void Unsubscribe(Action callback); + /// The callback priority (determines the order in which the subscribers get the events). + void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Subscribe the specified asynchronous callback to the event. /// Once subscribed, whenever the event happens this callback will be called. /// /// Callback. - void SubscribeToAsync(Func callback); + /// The callback priority (determines the order in which the subscribers get the events). + void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Unsubscribe the specified asynchronous callback from the event. /// This will stops notifications to call this callback. /// /// Callback. - void UnsubscribeToAsync(Func callback); + /// The callback priority (determines the order in which the subscribers get the events). + void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal); /// /// Asynchronously wait until the event fires and the specific condition applies. /// /// The task to be awaited. /// The condition we are waiting to apply before moving on. - Task WaitUntilAsync(Func condition); + /// The callback priority (determines the order in which the subscribers get the events). + Task WaitUntilAsync(Func condition, CallbackPriority priority = CallbackPriority.Normal); /// /// Invoke the event asynchronously. diff --git a/Source/Engine/AGS.Engine/Misc/Events/AGSEvent.cs b/Source/Engine/AGS.Engine/Misc/Events/AGSEvent.cs index 5d3d02f6a..afe7cf616 100644 --- a/Source/Engine/AGS.Engine/Misc/Events/AGSEvent.cs +++ b/Source/Engine/AGS.Engine/Misc/Events/AGSEvent.cs @@ -6,281 +6,52 @@ namespace AGS.Engine { - public class AGSEvent : IEvent, IBlockingEvent - { - private readonly IConcurrentHashSet _invocationList; - private const int MAX_SUBSCRIPTIONS = 10000; - private int _count; - - public AGSEvent () - { - _invocationList = new AGSConcurrentHashSet(fireListChangedEvent: false); - } - - #region IEvent implementation - - public int SubscribersCount => _count; - - public void Subscribe (Action callback) - { - _count++; - if (_count > MAX_SUBSCRIPTIONS) - { - Debug.WriteLine("Subscribe Overflow!"); - return; - } - _invocationList.Add (new Callback (callback)); - } - - public void Subscribe(Action callback) - { - _count++; - if (_count > MAX_SUBSCRIPTIONS) - { - Debug.WriteLine("Subscribe Overflow!"); - return; - } - _invocationList.Add(new Callback(callback)); - } - - public void Unsubscribe (Action callback) - { - if (_invocationList.Remove(new Callback (callback))) - { - _count--; - } - } - - public void Unsubscribe(Action callback) - { - if (_invocationList.Remove(new Callback(callback))) - { - _count--; - } - } - - public void SubscribeToAsync (Func callback) - { - subscribeToAsync(new Callback (callback)); - } - - public void UnsubscribeToAsync (Func callback) - { - unsubscribeToAsync(new Callback (callback)); - } - - public void SubscribeToAsync(Func callback) - { - subscribeToAsync(new Callback(callback)); - } - - public void UnsubscribeToAsync(Func callback) - { - unsubscribeToAsync(new Callback(callback)); - } - - public async Task WaitUntilAsync(Predicate condition) - { - TaskCompletionSource tcs = new TaskCompletionSource (null); - var callback = new Callback(condition, tcs); - subscribeToAsync(callback); - await tcs.Task; - unsubscribeToAsync(callback); - } - - public async Task InvokeAsync (TEventArgs args) - { - try - { - foreach (var target in _invocationList) - { - if (target.BlockingEvent != null) target.BlockingEvent(args); - else if (target.EmptyBlockingEvent != null) target.EmptyBlockingEvent(); - else if (target.EmptyEvent != null) await target.EmptyEvent(); - else await target.Event (args); - } - } - catch (Exception e) - { - Debug.WriteLine("Exception when invoking an event asynchronously:"); - Debug.WriteLine (e.ToString ()); - throw; - } - } - - public void Invoke (TEventArgs args) - { - try - { - foreach (var target in _invocationList) - { - if (target.BlockingEvent != null) target.BlockingEvent(args); - else if (target.EmptyBlockingEvent != null) target.EmptyBlockingEvent(); - else if (target.EmptyEvent != null) target.EmptyEvent(); - else target.Event(args); - } - } - catch (Exception e) - { - Debug.WriteLine("Exception when invoking an event:"); - Debug.WriteLine (e.ToString ()); - throw; - } - } - - #endregion - - private void subscribeToAsync(Callback callback) - { - _count++; - if (_count > MAX_SUBSCRIPTIONS) - { - Debug.WriteLine("Subscribe Overflow!!"); - return; - } - _invocationList.Add (callback); - } - - private void unsubscribeToAsync(Callback callback) - { - if (_invocationList.Remove(callback)) - { - _count--; - } - } - - private class Callback - { - private readonly Delegate _origObject; - - public Callback(Func callback) - { - _origObject = callback; - Event = callback; - } - - public Callback(Action callback) - { - _origObject = callback; - BlockingEvent = callback; - } - - public Callback(Predicate condition, TaskCompletionSource tcs) - { - _origObject = condition; - Event = convert(condition, tcs); - } - - public Callback(Action callback) - { - _origObject = callback; - EmptyBlockingEvent = callback; - } - - public Callback(Func callback) - { - _origObject = callback; - EmptyEvent = callback; - } - - public Func Event { get; } - public Action BlockingEvent { get; } - public Func EmptyEvent { get; } - public Action EmptyBlockingEvent { get; } - - public override bool Equals(object obj) - { - Callback other = obj as Callback; - if (other == null) return false; - if (other._origObject == _origObject) return true; - if (_origObject.Target != other._origObject.Target) return false; - return getMethodName(_origObject) == getMethodName(other._origObject); - } - - public override int GetHashCode() - { - if (_origObject.Target == null) return getMethodName(_origObject).GetHashCode(); //static method subscriptions - return _origObject.Target.GetHashCode(); - } - - public override string ToString() - { - return $"[Event on {_origObject.Target.ToString()} ({getMethodName(_origObject)})]"; - } - - private string getMethodName(Delegate del) => RuntimeReflectionExtensions.GetMethodInfo(del).Name; - - private Func convert(Predicate condition, TaskCompletionSource tcs) - { - return e => - { - if (!condition(e)) return Task.CompletedTask; - tcs.TrySetResult(null); - return Task.CompletedTask; - }; - } - } - } - public class AGSEvent : IEvent, IBlockingEvent { - private readonly IConcurrentHashSet _invocationList; - private const int MAX_SUBSCRIPTIONS = 10000; - private int _count; + private readonly EventCallbacksCollection _invocationList; public AGSEvent() { - _invocationList = new AGSConcurrentHashSet(fireListChangedEvent: false); + _invocationList = new EventCallbacksCollection(); } #region IEvent implementation - public int SubscribersCount => _count; + public int SubscribersCount => _invocationList.Count; - public void Subscribe(Action callback) - { - _count++; - if (_count > MAX_SUBSCRIPTIONS) - { - Debug.WriteLine("Subscribe Overflow!"); - return; - } - _invocationList.Add(new Callback(callback)); - } + public void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); - public void Unsubscribe(Action callback) - { - if (_invocationList.Remove(new Callback(callback))) - { - _count--; - } - } + public void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); - public void SubscribeToAsync(Func callback) - { - subscribeToAsync(new Callback(callback)); - } + public void Subscribe(ClaimableCallback callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); - public void UnsubscribeToAsync(Func callback) - { - unsubscribeToAsync(new Callback(callback)); - } + public void Unsubscribe(ClaimableCallback callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); + + public void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); + + public void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); - public async Task WaitUntilAsync(Func condition) + public async Task WaitUntilAsync(Func condition, CallbackPriority priority = CallbackPriority.Normal) { TaskCompletionSource tcs = new TaskCompletionSource(null); var callback = new Callback(condition, tcs); - subscribeToAsync(callback); + subscribe(callback, priority); await tcs.Task; - unsubscribeToAsync(callback); + unsubscribe(callback, priority); } public async Task InvokeAsync() { try { + ClaimEventToken token = new ClaimEventToken(); foreach (var target in _invocationList) { + if (target.BlockingEventWithClaimToken != null) + { + target.BlockingEventWithClaimToken(ref token); + if (token.Claimed) return; + } await target.Event(); } } @@ -296,8 +67,14 @@ public void Invoke() { try { + ClaimEventToken token = new ClaimEventToken(); foreach (var target in _invocationList) { + if (target.BlockingEventWithClaimToken != null) + { + target.BlockingEventWithClaimToken(ref token); + if (token.Claimed) return; + } if (target.BlockingEvent != null) target.BlockingEvent(); else target.Event(); } @@ -312,23 +89,14 @@ public void Invoke() #endregion - private void subscribeToAsync(Callback callback) + private void subscribe(Callback callback, CallbackPriority priority) { - _count++; - if (_count > MAX_SUBSCRIPTIONS) - { - Debug.WriteLine("Subscribe Overflow!!"); - return; - } - _invocationList.Add(callback); + _invocationList.Add(callback, priority); } - private void unsubscribeToAsync(Callback callback) + private void unsubscribe(Callback callback, CallbackPriority priority) { - if (_invocationList.Remove(callback)) - { - _count--; - } + _invocationList.Remove(callback, priority); } private class Callback @@ -354,8 +122,15 @@ public Callback(Func condition, TaskCompletionSource tcs) Event = convert(condition, tcs); } + public Callback(ClaimableCallback callback) + { + _origObject = callback; + BlockingEventWithClaimToken = callback; + } + public Func Event { get; } public Action BlockingEvent { get; } + public ClaimableCallback BlockingEventWithClaimToken { get; } public override bool Equals(object obj) { @@ -399,5 +174,4 @@ private Func convert(Func condition, TaskCompletionSource tc } } } -} - +} \ No newline at end of file diff --git a/Source/Engine/AGS.Engine/Misc/Events/AGSEventWithArgs.cs b/Source/Engine/AGS.Engine/Misc/Events/AGSEventWithArgs.cs new file mode 100644 index 000000000..ed6583fc1 --- /dev/null +++ b/Source/Engine/AGS.Engine/Misc/Events/AGSEventWithArgs.cs @@ -0,0 +1,194 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; +using AGS.API; + +namespace AGS.Engine +{ + public class AGSEvent : IEvent, IBlockingEvent + { + private readonly EventCallbacksCollection _invocationList; + + public AGSEvent() + { + _invocationList = new EventCallbacksCollection(); + } + + #region IEvent implementation + + public int SubscribersCount => _invocationList.Count; + + public void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); + + public void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); + + public void Subscribe(ClaimableCallbackWithArgs callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); + + public void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); + + public void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); + + public void Unsubscribe(ClaimableCallbackWithArgs callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); + + public void SubscribeToAsync (Func callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback (callback), priority); + + public void UnsubscribeToAsync (Func callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback (callback), priority); + + public void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) => subscribe(new Callback(callback), priority); + + public void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) => unsubscribe(new Callback(callback), priority); + + public async Task WaitUntilAsync(Predicate condition, CallbackPriority priority = CallbackPriority.Normal) + { + TaskCompletionSource tcs = new TaskCompletionSource (null); + var callback = new Callback(condition, tcs); + subscribe(callback, priority); + await tcs.Task; + unsubscribe(callback, priority); + } + + public async Task InvokeAsync (TEventArgs args) + { + try + { + ClaimEventToken token = new ClaimEventToken(); + foreach (var target in _invocationList) + { + if (target.BlockingEvent != null) target.BlockingEvent(args); + else if (target.BlockingEventWithToken != null) + { + target.BlockingEventWithToken(args, ref token); + if (token.Claimed) return; + } + else if (target.EmptyBlockingEvent != null) target.EmptyBlockingEvent(); + else if (target.EmptyEvent != null) await target.EmptyEvent(); + else await target.Event (args); + } + } + catch (Exception e) + { + Debug.WriteLine("Exception when invoking an event asynchronously:"); + Debug.WriteLine (e.ToString ()); + throw; + } + } + + public void Invoke (TEventArgs args) + { + try + { + ClaimEventToken token = new ClaimEventToken(); + foreach (var target in _invocationList) + { + if (target.BlockingEvent != null) target.BlockingEvent(args); + else if (target.BlockingEventWithToken != null) + { + target.BlockingEventWithToken(args, ref token); + if (token.Claimed) return; + } + else if (target.EmptyBlockingEvent != null) target.EmptyBlockingEvent(); + else if (target.EmptyEvent != null) target.EmptyEvent(); + else target.Event(args); + } + } + catch (Exception e) + { + Debug.WriteLine("Exception when invoking an event:"); + Debug.WriteLine (e.ToString ()); + throw; + } + } + + #endregion + + private void subscribe(Callback callback, CallbackPriority priority) + { + _invocationList.Add(callback, priority); + } + + private void unsubscribe(Callback callback, CallbackPriority priority) + { + _invocationList.Remove(callback, priority); + } + + private class Callback + { + private readonly Delegate _origObject; + + public Callback(Func callback) + { + _origObject = callback; + Event = callback; + } + + public Callback(Action callback) + { + _origObject = callback; + BlockingEvent = callback; + } + + public Callback(Predicate condition, TaskCompletionSource tcs) + { + _origObject = condition; + Event = convert(condition, tcs); + } + + public Callback(Action callback) + { + _origObject = callback; + EmptyBlockingEvent = callback; + } + + public Callback(Func callback) + { + _origObject = callback; + EmptyEvent = callback; + } + + public Callback(ClaimableCallbackWithArgs callback) + { + _origObject = callback; + BlockingEventWithToken = callback; + } + + public Func Event { get; } + public Action BlockingEvent { get; } + public ClaimableCallbackWithArgs BlockingEventWithToken { get; } + public Func EmptyEvent { get; } + public Action EmptyBlockingEvent { get; } + + public override bool Equals(object obj) + { + Callback other = obj as Callback; + if (other == null) return false; + if (other._origObject == _origObject) return true; + if (_origObject.Target != other._origObject.Target) return false; + return getMethodName(_origObject) == getMethodName(other._origObject); + } + + public override int GetHashCode() + { + if (_origObject.Target == null) return getMethodName(_origObject).GetHashCode(); //static method subscriptions + return _origObject.Target.GetHashCode(); + } + + public override string ToString() + { + return $"[Event on {_origObject.Target.ToString()} ({getMethodName(_origObject)})]"; + } + + private string getMethodName(Delegate del) => RuntimeReflectionExtensions.GetMethodInfo(del).Name; + + private Func convert(Predicate condition, TaskCompletionSource tcs) + { + return e => + { + if (!condition(e)) return Task.CompletedTask; + tcs.TrySetResult(null); + return Task.CompletedTask; + }; + } + } + } +} \ No newline at end of file diff --git a/Source/Engine/AGS.Engine/Misc/Events/EventCallbacksCollection.cs b/Source/Engine/AGS.Engine/Misc/Events/EventCallbacksCollection.cs new file mode 100644 index 000000000..952a7cf66 --- /dev/null +++ b/Source/Engine/AGS.Engine/Misc/Events/EventCallbacksCollection.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AGS.API; + +namespace AGS.Engine +{ + public class EventCallbacksCollection : IEnumerable + { + private readonly EventCallbacksSingleList _high, _normal, _low; + + public EventCallbacksCollection() + { + _high = new EventCallbacksSingleList(); + _normal = new EventCallbacksSingleList(); + _low = new EventCallbacksSingleList(); + } + + public int Count => _high.Count + _normal.Count + _low.Count; + + public void Add(Callback callback, CallbackPriority priority) + { + switch (priority) + { + case CallbackPriority.High: + _high.Add(callback); + break; + case CallbackPriority.Normal: + _normal.Add(callback); + break; + case CallbackPriority.Low: + _low.Add(callback); + break; + default: + throw new NotSupportedException($"Unsupported callback priority: ${priority}"); + } + } + + public void Remove(Callback callback, CallbackPriority priority) + { + switch (priority) + { + case CallbackPriority.High: + _high.Remove(callback); + break; + case CallbackPriority.Normal: + _normal.Remove(callback); + break; + case CallbackPriority.Low: + _low.Remove(callback); + break; + default: + throw new NotSupportedException($"Unsupported callback priority: ${priority}"); + } + } + + public IEnumerator GetEnumerator() + { + foreach (var callback in _high) yield return callback; + foreach (var callback in _normal) yield return callback; + foreach (var callback in _low) yield return callback; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Source/Engine/AGS.Engine/Misc/Events/EventCallbacksSingleList.cs b/Source/Engine/AGS.Engine/Misc/Events/EventCallbacksSingleList.cs new file mode 100644 index 000000000..9e449372a --- /dev/null +++ b/Source/Engine/AGS.Engine/Misc/Events/EventCallbacksSingleList.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AGS.API; + +namespace AGS.Engine +{ + public class EventCallbacksSingleList : IEnumerable + { + private const int MAX_SUBSCRIPTIONS = 10000; + + private readonly IConcurrentHashSet _list; + + public EventCallbacksSingleList() + { + _list = new AGSConcurrentHashSet(fireListChangedEvent: false); + } + + public int Count { get; private set; } + + public void Add(Callback callback) + { + if (!_list.Add(callback)) return; + Count++; + if (Count > MAX_SUBSCRIPTIONS) + { + throw new OverflowException("Event Subscription Overflow"); + } + } + + public void Remove(Callback callback) + { + if (!_list.Remove(callback)) return; + Count--; + } + + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Source/Engine/AGS.Engine/Objects/Interactions/AGSInteractionEvent.cs b/Source/Engine/AGS.Engine/Objects/Interactions/AGSInteractionEvent.cs index fb7c02f01..08a73ef36 100644 --- a/Source/Engine/AGS.Engine/Objects/Interactions/AGSInteractionEvent.cs +++ b/Source/Engine/AGS.Engine/Objects/Interactions/AGSInteractionEvent.cs @@ -28,57 +28,57 @@ public AGSInteractionEvent(List> defaultEvents, string verb, public int SubscribersCount => _ev.SubscribersCount; - public void Subscribe(Action callback) + public void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.Subscribe(callback); + _ev.Subscribe(callback, priority); } - public void Unsubscribe(Action callback) + public void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.Unsubscribe(callback); + _ev.Unsubscribe(callback, priority); } - public void Subscribe(Action callback) + public void Subscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.Subscribe(callback); + _ev.Subscribe(callback, priority); } - public void Unsubscribe(Action callback) + public void Unsubscribe(Action callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.Unsubscribe(callback); + _ev.Unsubscribe(callback, priority); } - public void WaitUntil(Predicate condition) + public void WaitUntil(Predicate condition, CallbackPriority priority = CallbackPriority.Normal) { - Task.Run(async () => await WaitUntilAsync(condition)).Wait(); + Task.Run(async () => await WaitUntilAsync(condition, priority)).Wait(); } - public void SubscribeToAsync(Func callback) + public void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.SubscribeToAsync(callback); + _ev.SubscribeToAsync(callback, priority); } - public void UnsubscribeToAsync(Func callback) + public void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.UnsubscribeToAsync(callback); + _ev.UnsubscribeToAsync(callback, priority); } - public void SubscribeToAsync(Func callback) + public void SubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.SubscribeToAsync(callback); + _ev.SubscribeToAsync(callback, priority); } - public void UnsubscribeToAsync(Func callback) + public void UnsubscribeToAsync(Func callback, CallbackPriority priority = CallbackPriority.Normal) { - _ev.UnsubscribeToAsync(callback); + _ev.UnsubscribeToAsync(callback, priority); } - public async Task WaitUntilAsync(Predicate condition) + public async Task WaitUntilAsync(Predicate condition, CallbackPriority priority = CallbackPriority.Normal) { var ev = getEvent(); if (ev == null) return; if (!await approachHotspot(ev)) return; - await ev.WaitUntilAsync(condition); + await ev.WaitUntilAsync(condition, priority); } public async Task InvokeAsync(TEventArgs args) diff --git a/Source/Tests/Engine/Misc/Events/EventTests.cs b/Source/Tests/Engine/Misc/Events/EventTests.cs index 65b570db4..f3973b19d 100644 --- a/Source/Tests/Engine/Misc/Events/EventTests.cs +++ b/Source/Tests/Engine/Misc/Events/EventTests.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using AGS.Engine; using System.Threading.Tasks; +using AGS.API; namespace Tests { @@ -139,12 +140,60 @@ public void SubscribeUnsubscribeOnDifferentTargetAsyncTest() Assert.AreEqual(1, ev.SubscribersCount); } + [Test] + public void ClaimEventTest() + { + AGSEvent ev = new AGSEvent(); + ev.Subscribe(onEventClaim, CallbackPriority.High); + EventTests target2 = new EventTests(); + ev.Subscribe(target2.onEventClaim); + ev.Invoke(new MockEventArgs(x)); + Assert.AreEqual(2, ev.SubscribersCount); + Assert.AreEqual(1, syncEvents); + Assert.AreEqual(0, target2.syncEvents); + } + + [Test] + public void DontClaimEventTest() + { + AGSEvent ev = new AGSEvent(); + ev.Subscribe(onEventDontClaim); + EventTests target2 = new EventTests(); + ev.Subscribe(target2.onEventDontClaim); + ev.Invoke(new MockEventArgs(x)); + Assert.AreEqual(2, ev.SubscribersCount); + Assert.AreEqual(1, syncEvents); + Assert.AreEqual(1, target2.syncEvents); + } + + [Test] + public void DuplicateSubscribersTest() + { + AGSEvent ev = new AGSEvent(); + ev.Subscribe(onEvent); + ev.Subscribe(onEvent); + Assert.AreEqual(1, ev.SubscribersCount); + } + private void onEvent(MockEventArgs e) { Assert.AreEqual (x, e.X); syncEvents++; } + private void onEventClaim(MockEventArgs e, ref ClaimEventToken token) + { + Assert.AreEqual(x, e.X); + syncEvents++; + token.Claimed = true; + } + + private void onEventDontClaim(MockEventArgs e, ref ClaimEventToken token) + { + Assert.AreEqual(x, e.X); + syncEvents++; + } + private async Task onEventAsync(MockEventArgs e) { await Task.Delay (1);