From b325a01eeaf93ad8c09d8783b683146223762d6f Mon Sep 17 00:00:00 2001 From: Maplewheels Date: Mon, 2 Feb 2026 02:38:10 -0500 Subject: [PATCH] Rewrote the EventService. --- .../LuaCs/Services/EventService.cs | 479 +++++++----------- .../LuaCs/Services/Safe/ILuaEventService.cs | 4 +- .../Services/_Interfaces/IEventService.cs | 4 +- 3 files changed, 195 insertions(+), 292 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs index 38743666d..42a025c2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs @@ -8,11 +8,12 @@ using Barotrauma.LuaCs.Events; using Barotrauma.LuaCs.Services.Compatibility; using FluentResults; using FluentResults.LuaCs; +using Microsoft.Toolkit.Diagnostics; using OneOf; namespace Barotrauma.LuaCs.Services; -public partial class EventService : IEventService, IEventAssemblyContextUnloading +public partial class EventService : IEventService { private readonly record struct TypeStringKey : IEqualityComparer, IEquatable { @@ -49,338 +50,240 @@ public partial class EventService : IEventService, IEventAssemblyContextUnloadin public static implicit operator TypeStringKey(Type type) => new(type); public static implicit operator TypeStringKey(string typeName) => new(typeName); } - /// - /// Contains subscriber delegates by event and identifier. - /// Structure:
- /// - Key: Type or String, TypeName == String Equality.
- /// - Value: Dictionary
- /// ---- Key: Either string identifier or subscriber instance pointer
- /// ---- Value: Subscriber delegate
- ///
- private readonly ConcurrentDictionary, IEvent>> _subscriptions = new(); - private readonly ConcurrentDictionary _eventTypeNameAliases = new(); - private readonly Lazy _pluginManagementService; - private readonly ConcurrentDictionary>> _luaSubscriptionFactories = new(); - /// - /// A collection of factories to produce subscribers from a single lua function handle. For legacy Add() API. - /// - private readonly Dictionary> _luaLegacySubscriptionFactories = new(); - /// - /// A collection of lua event subscribers from Add() that had neither a valid event name nor an event alias pointing to one. - /// Only actionable via Call(). - /// - private readonly Dictionary> _luaOrphanSubscribers = new(); - public EventService(Lazy pluginManagementService) - { - _pluginManagementService = pluginManagementService ?? throw new ArgumentNullException(nameof(pluginManagementService)); - this.Subscribe(this); + private readonly AsyncReaderWriterLock _operationsLock = new(); + private readonly ConcurrentDictionary, IEvent>> _subscribers = new(); + private readonly ConcurrentDictionary RunnerFactory)> _luaAliasEventFactory = new(); + private readonly ConcurrentDictionary> _luaLegacyEventsSubscribers = new(); - InitPatcher(); - } + #region Disposal - public bool IsDisposed { get; private set; } = false; - - #region Compatibility - - [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] - void ILuaCsHook.Add(string eventName, string identifier, LuaCsFunc callback, ACsMod mod = null) + public void Dispose() { - Add(eventName, identifier, callback); - } - [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] - void ILuaCsHook.Add(string eventName, LuaCsFunc callback, ACsMod mod = null) - { - Add(eventName, callback); - } - - public bool Exists(string eventName, string identifier) - { - ((IService)this).CheckDisposed(); - if (_subscriptions.ContainsKey(eventName) && _subscriptions[eventName].ContainsKey(identifier)) - return true; - if (_luaOrphanSubscribers.ContainsKey(eventName)) - return true; - return false; - } - - [Obsolete("Part of the legacy events API, only works for Lua-only custom events.")] - public T Call(string eventName, params object[] args) - { - ((IService)this).CheckDisposed(); - if (!_luaOrphanSubscribers.TryGetValue(eventName, out var dict)) - return default; - T returnValue = default; - foreach (var sub in dict.Values) + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) { - try - { - var r = sub(args); - if (r != default) - returnValue = (T)r; - } - catch - { - continue; - } + return; } - return returnValue; + + _luaLegacyEventsSubscribers.Clear(); + _luaAliasEventFactory.Clear(); + _subscribers.Clear(); + } + + private int _isDisposed; + + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + public FluentResults.Result Reset() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _luaLegacyEventsSubscribers.Clear(); + _luaAliasEventFactory.Clear(); + _subscribers.Clear(); + return FluentResults.Result.Ok(); } - - [Obsolete("Part of the legacy events API, only works for Lua-only custom events.")] - public object Call(string eventName, params object[] args) => Call(eventName, args); #endregion + #region LuaEventSystem + public void Add(string eventName, string identifier, LuaCsFunc callback) { - var eventKey = eventName; - if (_eventTypeNameAliases.TryGetValue(eventName, out var aliasType)) - eventKey = aliasType; - if (_luaLegacySubscriptionFactories.TryGetValue(eventKey, out var factory)) + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + Guard.IsNotNull(callback, nameof(callback)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc)) { - factory(identifier, callback); - return; + var eventSubs = _subscribers.GetOrAdd(eventFunc.Event, key => new ConcurrentDictionary, IEvent>()); + eventSubs[identifier] = eventFunc.RunnerFactory(callback); + } + else + { + var eventSubs = _luaLegacyEventsSubscribers.GetOrAdd(eventName, key => new ConcurrentDictionary()); + eventSubs[identifier] = callback; } - _luaOrphanSubscribers.TryGetOrSet(eventName, () => new Dictionary()) - .Add(identifier.IsNullOrWhiteSpace() ? string.Empty : identifier, callback); } public void Add(string eventName, LuaCsFunc callback) { - Add(eventName, string.Empty, callback); - } - - public void Remove(string eventName, string identifier) - { - if (_luaOrphanSubscribers.TryGetValue(eventName, out var dict)) - dict.Remove(identifier); - if (_subscriptions.TryGetValue(eventName, out var dict2)) - dict2.Remove(identifier); - } - - public void PublishLuaEvent(string interfaceName, LuaCsFunc runner) - { - ((IService)this).CheckDisposed(); - if (interfaceName.IsNullOrWhiteSpace()) - return; - if (!_subscriptions.TryGetValue(interfaceName, out var dict)) - return; - - var type = _subscriptions - .Select(x => x.Key) - .FirstOrNull(x => x.Type?.Name == interfaceName)?.Type; - - var errors = new Queue(); - foreach (var eventSub in dict.Values) - { - try - { - runner(type is null ? eventSub : Convert.ChangeType(eventSub, type)); // cast if possible - } - catch - { - continue; - } - } + // random ident, we hope for no conflicts :barodev:. + Add(eventName, Random.Shared.NextInt64().ToString() ,callback); } - public FluentResults.Result RegisterSafeEvent() where T : IEvent + public object Call(string eventName, params object[] args) { - ((IService)this).CheckDisposed(); - var type = typeof(T); - if (_luaSubscriptionFactories.ContainsKey(type)) - return FluentResults.Result.Ok().WithReason(new Success($"The event {type.Name} is already registered.")); - try + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubscribers) + || eventSubscribers.IsEmpty) { - _luaSubscriptionFactories.TryAdd(type, (ident, funcDict) => - { - var runner = T.GetLuaRunner(funcDict); - var dict = _subscriptions.TryGetOrSet(type, () => new Dictionary, IEvent>()); - if (!ident.IsNullOrWhiteSpace()) - dict[ident] = runner; - else - dict[runner] = runner; - }); - return FluentResults.Result.Ok(); - } - catch (NullReferenceException e) - { - return FluentResults.Result.Fail(new Error($"The lua runner for {type.Name} is not registered.") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, type)); - } - } - - public FluentResults.Result UnregisterSafeEvent() where T : IEvent - { - ((IService)this).CheckDisposed(); - _luaSubscriptionFactories.TryRemove(typeof(T), out _); - if (!_subscriptions.TryGetValue(typeof(T), out var dict)) - return FluentResults.Result.Ok(); - dict.Values.Where(value => value.IsLuaRunner()).ToImmutableArray().ForEach(Unsubscribe); - return FluentResults.Result.Ok(); - } - - // lua subscribe - public void Subscribe(string interfaceName, string identifier, IDictionary callbacks) - { - ((IService)this).CheckDisposed(); - if (_luaSubscriptionFactories.TryGetValue(interfaceName, out var subFactory)) - subFactory(identifier, callbacks); - } - - public FluentResults.Result SetLegacyLuaRunnerFactory(Func runnerFactory) where T : IEvent - { - var type = typeof(T); - if (!_luaSubscriptionFactories.TryGetValue(type, out var dict)) - return FluentResults.Result.Fail(new Error($"Tried to add legacy lua factory for an event not registered for lua subscriptions.")); - - _luaLegacySubscriptionFactories[type] = (ident, func) => - { - var runner = runnerFactory(func); - _subscriptions.TryGetOrSet(type, () => new Dictionary, IEvent>())[ident] = runner; - }; - return FluentResults.Result.Ok(); - } - - public void RemoveLegacyLuaRunnerFactory() where T : IEvent - { - _luaLegacySubscriptionFactories.Remove(typeof(T)); - } - - public void SetAliasToEvent(string alias) where T : IEvent - { - if (alias.IsNullOrWhiteSpace()) - return; - _eventTypeNameAliases[alias] = typeof(T).Name; - } - - public void RemoveEventAlias(string alias) - { - _eventTypeNameAliases.TryRemove(alias, out _); - } - - public void RemoveAllEventAliases() where T : IEvent - { - foreach (var keys in _eventTypeNameAliases - .Where(kvp => kvp.Value.IsNullOrWhiteSpace() || kvp.Value == typeof(T).Name) - .Select(kvp => kvp.Key).ToImmutableArray()) - { - _eventTypeNameAliases.TryRemove(keys, out _); - } - } - - public FluentResults.Result Subscribe(T subscriber) where T : IEvent - { - ((IService)this).CheckDisposed(); - var eventType = typeof(T); - var dict = _subscriptions.TryGetOrSet(eventType, () => new Dictionary, IEvent>()); - if (dict.ContainsKey(OneOf.FromT1(subscriber))) - { - return FluentResults.Result.Fail( - new Error($"The subscriber for {eventType.Name} is already registered to the event.") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, subscriber)); - } - dict[subscriber] = subscriber; - return FluentResults.Result.Ok(); - } - - public void Unsubscribe(T subscriber) where T : IEvent - { - ((IService)this).CheckDisposed(); - if (!_subscriptions.TryGetValue(typeof(T), out var dict)) - return; - dict.Remove(OneOf.FromT1(subscriber)); - } - - public void ClearAllEventSubscribers() where T : IEvent - { - _subscriptions.TryRemove(typeof(T), out _); - if (typeof(IEventAssemblyContextUnloading) == typeof(T)) - { - this.Subscribe(this); - } - } - public void ClearAllSubscribers() - { - _subscriptions.Clear(); - this.Subscribe(this); - } - - public FluentResults.Result PublishEvent(Action action) where T : IEvent - { - ((IService)this).CheckDisposed(); - var eventType = typeof(T); - if (!_subscriptions.TryGetValue(eventType, out var dict)) - { - return FluentResults.Result.Fail(new Error($"The event {eventType.Name} is not registered.") - .WithMetadata(MetadataType.ExceptionObject, this)); + return null; } - var errors = new Queue(); - foreach (var eventSub in dict.Values) + object returnValue = null; + + foreach (var subscriber in eventSubscribers) { try { - action((T)eventSub); + returnValue = subscriber.Value.Invoke(args); } catch (Exception e) { #if DEBUG - throw; //make errors apparent + throw; #endif - errors.Enqueue(new Error($"Error while executing runner for {eventType.Name} on type {eventSub.GetType().Name}.") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, eventSub) - .WithMetadata(MetadataType.ExceptionDetails, e.Message) - .WithMetadata(MetadataType.StackTrace, e.StackTrace)); } } - - var result = errors.Count > 0 ? FluentResults.Result.Fail($"Errors while executing event type {eventType.Name}") : FluentResults.Result.Ok(); - while (errors.Count > 0) - result = result.WithError(errors.Dequeue()); - return result; + + return returnValue; } - public void Dispose() - { - Reset(); - IsDisposed = true; - GC.SuppressFinalize(this); - } - - public FluentResults.Result Reset() + public void Subscribe(string identifier, IDictionary callbacks) where T : IEvent { + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + Guard.IsNotNull(callbacks, nameof(callbacks)); + Guard.IsNotEmpty(callbacks, nameof(callbacks)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); - _subscriptions.Clear(); - _luaSubscriptionFactories.Clear(); - _eventTypeNameAliases.Clear(); - _luaLegacySubscriptionFactories.Clear(); - _luaOrphanSubscribers.Clear(); - ResetPatcher(); - InitPatcher(); + + var eventSubs = _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary, IEvent>()); + eventSubs[identifier] = T.GetLuaRunner(callbacks); + } + + public void Remove(string eventName, string identifier) + { + Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName)); + Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + + if (!_subscribers.TryGetValue(eventName, out var evtSubscribers)) + { + return; + } + + evtSubscribers.TryRemove(identifier, out _); + } + + public void PublishLuaEvent(LuaCsFunc subscriberRunner) where T : IEvent + { + this.PublishEvent(sub => subscriberRunner(sub)); + } + + public FluentResults.Result RegisterLuaEventAlias(string luaEventName, string targetMethod) where T : IEvent + { + Guard.IsNotNullOrWhiteSpace(luaEventName, nameof(luaEventName)); + Guard.IsNotNullOrWhiteSpace(targetMethod, nameof(targetMethod)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (_luaAliasEventFactory.ContainsKey(luaEventName)) + { +#if DEBUG + ThrowHelper.ThrowInvalidOperationException($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}."); +#endif + return FluentResults.Result.Fail($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}."); + } + + var eventRunnerFactory = (LuaCsFunc function) => (IEvent)T.GetLuaRunner(new Dictionary + { + { targetMethod, function } + }); + + _luaAliasEventFactory[luaEventName] = (Event: typeof(T), RunnerFactory: eventRunnerFactory); + // create the group + _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary, IEvent>()); return FluentResults.Result.Ok(); } - public void OnAssemblyUnloading(WeakReference loaderService) + #endregion + + public FluentResults.Result Subscribe(T subscriber) where T : class, IEvent { - if (!loaderService.TryGetTarget(out var loader)) - return; - foreach (var assembly in loader.Assemblies) + Guard.IsNotNull(subscriber, nameof(subscriber)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + var eventSubs = + _subscribers.GetOrAdd(typeof(T), (type) => new ConcurrentDictionary, IEvent>()); + + if (eventSubs.ContainsKey(subscriber)) { - var types = assembly.GetSafeTypes() - .Where(t => typeof(IEvent).IsAssignableFrom(t)) - .ToImmutableArray(); - if (!types.Any()) - continue; - foreach (var type in types) + ThrowHelper.ThrowInvalidOperationException($"{nameof(Subscribe)}: The instance is already registered!"); + } + + return eventSubs.TryAdd(subscriber, subscriber) + ? FluentResults.Result.Ok() + : FluentResults.Result.Fail($"{nameof(Subscribe)}: Failed to add subscriber."); + } + + public void Unsubscribe(T subscriber) where T : class, IEvent + { + Guard.IsNotNull(subscriber, nameof(subscriber)); + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_subscribers.TryGetValue(typeof(T), out var evtSubscribers)) + { + return; + } + + evtSubscribers.TryRemove(subscriber, out _); + } + + public void ClearAllEventSubscribers() where T : IEvent + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _subscribers.TryRemove(typeof(T), out _); + } + + public void ClearAllSubscribers() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + _subscribers.Clear(); + } + + public FluentResults.Result PublishEvent(Action action) where T : IEvent + { + Guard.IsNotNull(action, nameof(action)); + using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + if (!_subscribers.TryGetValue(typeof(T), out var subs) || subs.IsEmpty) + { + return FluentResults.Result.Ok(); + } + + var results = new FluentResults.Result(); + + foreach (var sub in subs) + { + try { - _subscriptions.TryRemove(type, out _); - _luaSubscriptionFactories.TryRemove(type, out _); + action.Invoke((T)sub.Value); + } + catch (Exception e) + { + results.WithError(new ExceptionalError(e)); +#if DEBUG + throw; +#endif + continue; } } + + return results; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs index e5dd9178d..50d470965 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs @@ -13,7 +13,7 @@ public interface ILuaSafeEventService : ILuaService, ILuaCsHook /// /// /// A 'method name'=='signature action' dictionary matching the interface method list. - void Subscribe(string identifier, IDictionary callbacks); + void Subscribe(string identifier, IDictionary callbacks) where T : IEvent; /// /// Removes a subscriber from an event that subscribed under the given identifier. /// @@ -26,7 +26,7 @@ public interface ILuaSafeEventService : ILuaService, ILuaCsHook /// Interface type. /// Execution runner, the subscriber is provided as the first argument in the lua runner. /// - void PublishLuaEvent(LuaCsFunc subscriberRunner); + void PublishLuaEvent(LuaCsFunc subscriberRunner) where T : IEvent; /// /// Defines the target method name for legacy to target on new diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs index 1dc2117d1..a5ef5ce67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs @@ -14,13 +14,13 @@ public interface IEventService : IReusableService, ILuaEventService /// /// /// - FluentResults.Result Subscribe(T subscriber) where T : IEvent; + FluentResults.Result Subscribe(T subscriber) where T : class, IEvent; /// /// /// /// /// - void Unsubscribe(T subscriber) where T : IEvent; + void Unsubscribe(T subscriber) where T : class, IEvent; /// /// Clears all subscribers for a given event type and removes any registration to the type. ///