From 4bd636b8edf75c9032e1f1e4e3a51d6acf39ab83 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Fri, 8 Oct 2021 03:58:01 +0200 Subject: [PATCH] Add wrapped event handler, using the existing ECS object instances - Added a wrapper class that handles the individual wrapping of event handlers to individually handle exceptions - now it tracks the wrapped event handlers so it can unregister them properly - Fixed an exception that happened when two ECS objects were created with the same EGID - Added support for returning an existing ECS object if it exists instead of always creating a new one - Added a parameter to the entity query extension methods to override the group of the ECS object (could be used for the player properties) --- TechbloxModdingAPI/App/AppEngine.cs | 8 +-- TechbloxModdingAPI/App/Client.cs | 4 +- TechbloxModdingAPI/App/Game.cs | 8 +-- .../App/GameBuildSimEventEngine.cs | 8 +-- TechbloxModdingAPI/App/GameGameEngine.cs | 8 +-- TechbloxModdingAPI/Block.cs | 28 +++++++--- .../Blocks/Engines/BlockEventsEngine.cs | 12 ++--- TechbloxModdingAPI/Blocks/Wire.cs | 6 +-- TechbloxModdingAPI/EcsObjectBase.cs | 22 +++++++- TechbloxModdingAPI/Player.cs | 7 +-- TechbloxModdingAPI/SimBody.cs | 5 +- TechbloxModdingAPI/Utility/ExceptionUtil.cs | 43 --------------- .../Utility/ManagedApiExtensions.cs | 35 ++++++------ .../Utility/NativeApiExtensions.cs | 32 ++++++----- TechbloxModdingAPI/Utility/WrappedHandler.cs | 53 +++++++++++++++++++ 15 files changed, 164 insertions(+), 115 deletions(-) delete mode 100644 TechbloxModdingAPI/Utility/ExceptionUtil.cs create mode 100644 TechbloxModdingAPI/Utility/WrappedHandler.cs diff --git a/TechbloxModdingAPI/App/AppEngine.cs b/TechbloxModdingAPI/App/AppEngine.cs index 741c576..3f89f26 100644 --- a/TechbloxModdingAPI/App/AppEngine.cs +++ b/TechbloxModdingAPI/App/AppEngine.cs @@ -9,9 +9,9 @@ namespace TechbloxModdingAPI.App { public class AppEngine : IFactoryEngine { - public event EventHandler EnterMenu; + public WrappedHandler EnterMenu; - public event EventHandler ExitMenu; + public WrappedHandler ExitMenu; public IEntityFactory Factory { set; private get; } @@ -24,13 +24,13 @@ namespace TechbloxModdingAPI.App public void Dispose() { IsInMenu = false; - ExceptionUtil.InvokeEvent(ExitMenu, this, new MenuEventArgs { }); + ExitMenu.Invoke(this, new MenuEventArgs { }); } public void Ready() { IsInMenu = true; - ExceptionUtil.InvokeEvent(EnterMenu, this, new MenuEventArgs { }); + EnterMenu.Invoke(this, new MenuEventArgs { }); } // app functionality diff --git a/TechbloxModdingAPI/App/Client.cs b/TechbloxModdingAPI/App/Client.cs index 9d636eb..026b1a2 100644 --- a/TechbloxModdingAPI/App/Client.cs +++ b/TechbloxModdingAPI/App/Client.cs @@ -28,7 +28,7 @@ namespace TechbloxModdingAPI.App /// public static event EventHandler EnterMenu { - add => appEngine.EnterMenu += ExceptionUtil.WrapHandler(value); + add => appEngine.EnterMenu += value; remove => appEngine.EnterMenu -= value; } @@ -37,7 +37,7 @@ namespace TechbloxModdingAPI.App /// public static event EventHandler ExitMenu { - add => appEngine.ExitMenu += ExceptionUtil.WrapHandler(value); + add => appEngine.ExitMenu += value; remove => appEngine.ExitMenu -= value; } diff --git a/TechbloxModdingAPI/App/Game.cs b/TechbloxModdingAPI/App/Game.cs index 5a9cccc..ad7ba33 100644 --- a/TechbloxModdingAPI/App/Game.cs +++ b/TechbloxModdingAPI/App/Game.cs @@ -93,7 +93,7 @@ namespace TechbloxModdingAPI.App /// public static event EventHandler Simulate { - add => buildSimEventEngine.SimulationMode += ExceptionUtil.WrapHandler(value); + add => buildSimEventEngine.SimulationMode += value; remove => buildSimEventEngine.SimulationMode -= value; } @@ -103,7 +103,7 @@ namespace TechbloxModdingAPI.App /// public static event EventHandler Edit { - add => buildSimEventEngine.BuildMode += ExceptionUtil.WrapHandler(value); + add => buildSimEventEngine.BuildMode += value; remove => buildSimEventEngine.BuildMode -= value; } @@ -112,7 +112,7 @@ namespace TechbloxModdingAPI.App /// public static event EventHandler Enter { - add => gameEngine.EnterGame += ExceptionUtil.WrapHandler(value); + add => gameEngine.EnterGame += value; remove => gameEngine.EnterGame -= value; } @@ -122,7 +122,7 @@ namespace TechbloxModdingAPI.App /// public static event EventHandler Exit { - add => gameEngine.ExitGame += ExceptionUtil.WrapHandler(value); + add => gameEngine.ExitGame += value; remove => gameEngine.ExitGame -= value; } diff --git a/TechbloxModdingAPI/App/GameBuildSimEventEngine.cs b/TechbloxModdingAPI/App/GameBuildSimEventEngine.cs index f710d6f..d4697ff 100644 --- a/TechbloxModdingAPI/App/GameBuildSimEventEngine.cs +++ b/TechbloxModdingAPI/App/GameBuildSimEventEngine.cs @@ -11,9 +11,9 @@ namespace TechbloxModdingAPI.App { public class GameBuildSimEventEngine : IApiEngine, IUnorderedInitializeOnTimeRunningModeEntered, IUnorderedInitializeOnTimeStoppedModeEntered { - public event EventHandler SimulationMode; + public WrappedHandler SimulationMode; - public event EventHandler BuildMode; + public WrappedHandler BuildMode; public string Name => "TechbloxModdingAPIBuildSimEventGameEngine"; @@ -27,13 +27,13 @@ namespace TechbloxModdingAPI.App public JobHandle OnInitializeTimeRunningMode(JobHandle inputDeps) { - ExceptionUtil.InvokeEvent(SimulationMode, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + SimulationMode.Invoke(this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); return inputDeps; } public JobHandle OnInitializeTimeStoppedMode(JobHandle inputDeps) { - ExceptionUtil.InvokeEvent(BuildMode, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + BuildMode.Invoke(this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); return inputDeps; } } diff --git a/TechbloxModdingAPI/App/GameGameEngine.cs b/TechbloxModdingAPI/App/GameGameEngine.cs index 8ae2aa3..a773d61 100644 --- a/TechbloxModdingAPI/App/GameGameEngine.cs +++ b/TechbloxModdingAPI/App/GameGameEngine.cs @@ -17,9 +17,9 @@ namespace TechbloxModdingAPI.App { public class GameGameEngine : IApiEngine { - public event EventHandler EnterGame; + public WrappedHandler EnterGame; - public event EventHandler ExitGame; + public WrappedHandler ExitGame; public string Name => "TechbloxModdingAPIGameInfoMenuEngine"; @@ -29,13 +29,13 @@ namespace TechbloxModdingAPI.App public void Dispose() { - ExceptionUtil.InvokeEvent(ExitGame, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + ExitGame.Invoke(this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); IsInGame = false; } public void Ready() { - ExceptionUtil.InvokeEvent(EnterGame, this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); + EnterGame.Invoke(this, new GameEventArgs { GameName = GameMode.SaveGameDetails.Name, GamePath = GameMode.SaveGameDetails.Folder }); IsInGame = true; } diff --git a/TechbloxModdingAPI/Block.cs b/TechbloxModdingAPI/Block.cs index 44bbc92..2cd995b 100644 --- a/TechbloxModdingAPI/Block.cs +++ b/TechbloxModdingAPI/Block.cs @@ -78,8 +78,8 @@ namespace TechbloxModdingAPI /// An event that fires each time a block is placed. /// public static event EventHandler Placed - { - add => BlockEventsEngine.Placed += ExceptionUtil.WrapHandler(value); + { //TODO: Rename and add instance version in 3.0 + add => BlockEventsEngine.Placed += value; remove => BlockEventsEngine.Placed -= value; } @@ -88,7 +88,7 @@ namespace TechbloxModdingAPI /// public static event EventHandler Removed { - add => BlockEventsEngine.Removed += ExceptionUtil.WrapHandler(value); + add => BlockEventsEngine.Removed += value; remove => BlockEventsEngine.Removed -= value; } @@ -105,13 +105,25 @@ namespace TechbloxModdingAPI {CommonExclusiveGroups.WHEELRIG_BLOCK_BUILD_GROUP, (id => new WheelRig(id), typeof(WheelRig))} }; + /// + /// Returns a correctly typed instance of this block. The instances are shared for a specific block. + /// If an instance is no longer referenced a new instance is returned. + /// + /// The EGID of the block + /// Whether the block is definitely a signaling block + /// internal static Block New(EGID egid, bool signaling = false) { - return GroupToConstructor.ContainsKey(egid.groupID) - ? GroupToConstructor[egid.groupID].Constructor(egid) - : signaling - ? new SignalingBlock(egid) - : new Block(egid); + if (egid == default) return null; + if (GroupToConstructor.ContainsKey(egid.groupID)) + { + var (constructor, type) = GroupToConstructor[egid.groupID]; + return GetInstance(egid, constructor, type); + } + + return signaling + ? GetInstance(egid, e => new SignalingBlock(e)) + : GetInstance(egid, e => new Block(e)); } public Block(EGID id) diff --git a/TechbloxModdingAPI/Blocks/Engines/BlockEventsEngine.cs b/TechbloxModdingAPI/Blocks/Engines/BlockEventsEngine.cs index 776f162..4da3f65 100644 --- a/TechbloxModdingAPI/Blocks/Engines/BlockEventsEngine.cs +++ b/TechbloxModdingAPI/Blocks/Engines/BlockEventsEngine.cs @@ -10,8 +10,8 @@ namespace TechbloxModdingAPI.Blocks.Engines { public class BlockEventsEngine : IReactionaryEngine { - public event EventHandler Placed; - public event EventHandler Removed; + public WrappedHandler Placed; + public WrappedHandler Removed; public void Ready() { @@ -31,16 +31,14 @@ namespace TechbloxModdingAPI.Blocks.Engines { if (!(shouldAddRemove = !shouldAddRemove)) return; - ExceptionUtil.InvokeEvent(Placed, this, - new BlockPlacedRemovedEventArgs {ID = egid}); + Placed.Invoke(this, new BlockPlacedRemovedEventArgs {ID = egid}); } public void Remove(ref BlockTagEntityStruct entityComponent, EGID egid) { if (!(shouldAddRemove = !shouldAddRemove)) return; - ExceptionUtil.InvokeEvent(Removed, this, - new BlockPlacedRemovedEventArgs {ID = egid}); + Removed.Invoke(this, new BlockPlacedRemovedEventArgs {ID = egid}); } } @@ -49,6 +47,6 @@ namespace TechbloxModdingAPI.Blocks.Engines public EGID ID; private Block block; - public Block Block => block ?? (block = Block.New(ID)); + public Block Block => block ??= Block.New(ID); } } \ No newline at end of file diff --git a/TechbloxModdingAPI/Blocks/Wire.cs b/TechbloxModdingAPI/Blocks/Wire.cs index 4413b4f..0c66557 100644 --- a/TechbloxModdingAPI/Blocks/Wire.cs +++ b/TechbloxModdingAPI/Blocks/Wire.cs @@ -8,7 +8,7 @@ using TechbloxModdingAPI.Blocks.Engines; namespace TechbloxModdingAPI.Blocks { - public class Wire + public class Wire : EcsObjectBase { internal static SignalEngine signalEngine; @@ -154,7 +154,7 @@ namespace TechbloxModdingAPI.Blocks /// /// The wire's in-game id. /// - public EGID Id + public override EGID Id { get => wireEGID; } @@ -279,7 +279,7 @@ namespace TechbloxModdingAPI.Blocks /// A copy of the wire object. public Wire OutputToInputCopy() { - return new Wire(wireEGID); + return GetInstance(wireEGID, egid => new Wire(egid)); } /// diff --git a/TechbloxModdingAPI/EcsObjectBase.cs b/TechbloxModdingAPI/EcsObjectBase.cs index 6a4db65..f01561a 100644 --- a/TechbloxModdingAPI/EcsObjectBase.cs +++ b/TechbloxModdingAPI/EcsObjectBase.cs @@ -23,6 +23,22 @@ namespace TechbloxModdingAPI return _instances.TryGetValue(type, out var dict) ? dict : null; } + /// + /// Returns a cached instance if there's an actively used instance of the object already. + /// Objects still get garbage collected and then they will be removed from the cache. + /// + /// The EGID of the entity + /// The constructor to construct the object + /// The object type + /// + internal static T GetInstance(EGID egid, Func constructor, Type type = null) where T : EcsObjectBase + { + var instances = GetInstances(type ?? typeof(T)); + if (instances == null || !instances.TryGetValue(egid, out var instance)) + return constructor(egid); // It will be added by the constructor + return (T)instance; + } + protected EcsObjectBase() { if (!_instances.TryGetValue(GetType(), out var dict)) @@ -31,9 +47,11 @@ namespace TechbloxModdingAPI _instances.Add(GetType(), dict); } - // ReSharper disable once VirtualMemberCallInConstructor + // ReSharper disable VirtualMemberCallInConstructor // The ID should not depend on the constructor - dict.Add(Id, this); + if (!dict.ContainsKey(Id)) // Multiple instances may be created + dict.Add(Id, this); + // ReSharper enable VirtualMemberCallInConstructor } #region ECS initializer stuff diff --git a/TechbloxModdingAPI/Player.cs b/TechbloxModdingAPI/Player.cs index 67d36ca..3dad9ea 100644 --- a/TechbloxModdingAPI/Player.cs +++ b/TechbloxModdingAPI/Player.cs @@ -21,7 +21,7 @@ namespace TechbloxModdingAPI /// An in-game player character. Any Leo you see is a player. /// public class Player : IEquatable, IEquatable - { + { //TODO: Inherit EcsObjectBase and make Id an EGID, useful for caching // static functionality private static PlayerEngine playerEngine = new PlayerEngine(); private static Player localPlayer; @@ -448,7 +448,7 @@ namespace TechbloxModdingAPI { var egid = playerEngine.GetThingLookedAt(Id, maxDistance); return egid != default && egid.groupID == CommonExclusiveGroups.SIMULATION_BODIES_GROUP - ? new SimBody(egid) + ? EcsObjectBase.GetInstance(egid, e => new SimBody(e)) : null; } @@ -461,7 +461,8 @@ namespace TechbloxModdingAPI { var egid = playerEngine.GetThingLookedAt(Id, maxDistance); return egid != default && egid.groupID == WiresGUIExclusiveGroups.WireGroup - ? new Wire(new EGID(egid.entityID, NamedExclusiveGroup.Group)) + ? EcsObjectBase.GetInstance(new EGID(egid.entityID, NamedExclusiveGroup.Group), + e => new Wire(e)) : null; } diff --git a/TechbloxModdingAPI/SimBody.cs b/TechbloxModdingAPI/SimBody.cs index 55c6a9e..2f2f8b6 100644 --- a/TechbloxModdingAPI/SimBody.cs +++ b/TechbloxModdingAPI/SimBody.cs @@ -20,7 +20,10 @@ namespace TechbloxModdingAPI /// The cluster this chunk belongs to, or null if no cluster destruction manager present or the chunk doesn't exist. /// Get the SimBody from a Block if possible for good performance here. /// - public Cluster Cluster => cluster ?? (cluster = clusterId == uint.MaxValue ? Block.BlockEngine.GetCluster(Id.entityID) : new Cluster(clusterId)); + public Cluster Cluster => cluster ??= clusterId == uint.MaxValue // Return cluster or if it's null then set it + ? Block.BlockEngine.GetCluster(Id.entityID) // If we don't have a clusterId set then get it from the game + : GetInstance(new EGID(clusterId, CommonExclusiveGroups.SIMULATION_CLUSTERS_GROUP), + egid => new Cluster(egid)); // Otherwise get the cluster from the ID private Cluster cluster; private readonly uint clusterId = uint.MaxValue; diff --git a/TechbloxModdingAPI/Utility/ExceptionUtil.cs b/TechbloxModdingAPI/Utility/ExceptionUtil.cs deleted file mode 100644 index d11508b..0000000 --- a/TechbloxModdingAPI/Utility/ExceptionUtil.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using TechbloxModdingAPI.Events; - -namespace TechbloxModdingAPI.Utility -{ - public static class ExceptionUtil - { - /// - /// Invokes an event with a null-check. - /// - /// The event to emit, can be null - /// Event sender - /// Event arguments - /// Type of the event arguments - public static void InvokeEvent(EventHandler handler, object sender, T args) - { - handler?.Invoke(sender, args); - } - - /// - /// Wraps the event handler in a try-catch block to avoid propagating exceptions. - /// - /// The handler to wrap (not null) - /// Type of the event arguments - /// The wrapped handler - public static EventHandler WrapHandler(EventHandler handler) - { - return (sender, e) => - { - try - { - handler(sender, e); - } - catch (Exception e1) - { - EventRuntimeException wrappedException = - new EventRuntimeException($"EventHandler with arg type {typeof(T).Name} threw an exception", e1); - Logging.LogWarning(wrappedException.ToString()); - } - }; - } - } -} \ No newline at end of file diff --git a/TechbloxModdingAPI/Utility/ManagedApiExtensions.cs b/TechbloxModdingAPI/Utility/ManagedApiExtensions.cs index 2616ab7..6829b2d 100644 --- a/TechbloxModdingAPI/Utility/ManagedApiExtensions.cs +++ b/TechbloxModdingAPI/Utility/ManagedApiExtensions.cs @@ -1,6 +1,5 @@ using Svelto.ECS; using Svelto.ECS.Hybrid; -using TechbloxModdingAPI.Blocks; namespace TechbloxModdingAPI.Utility { @@ -23,32 +22,36 @@ namespace TechbloxModdingAPI.Utility } /// - /// Attempts to query an entity and returns the result or a dummy value that can be modified. + /// Attempts to query an entity and returns the result in an optional reference. /// - /// - /// - /// - /// - public static OptionalRef QueryEntityOptional(this EntitiesDB entitiesDB, EcsObjectBase obj) + /// The entities DB to query from + /// The ECS object to query + /// The group of the entity if the object can have multiple + /// The component to query + /// A reference to the component or a dummy value + public static OptionalRef QueryEntityOptional(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) where T : struct, IEntityViewComponent { - var opt = QueryEntityOptional(entitiesDB, obj.Id); - return opt ? opt : new OptionalRef(obj, true); + EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); + var opt = QueryEntityOptional(entitiesDB, id); + return opt ? opt : new OptionalRef(obj, false); } /// /// Attempts to query an entity and returns the result or a dummy value that can be modified. /// - /// - /// - /// - /// - public static ref T QueryEntityOrDefault(this EntitiesDB entitiesDB, EcsObjectBase obj) + /// The entities DB to query from + /// The ECS object to query + /// The group of the entity if the object can have multiple + /// The component to query + /// A reference to the component or a dummy value + public static ref T QueryEntityOrDefault(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) where T : struct, IEntityViewComponent { - var opt = QueryEntityOptional(entitiesDB, obj.Id); + EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); + var opt = QueryEntityOptional(entitiesDB, id); if (opt) return ref opt.Get(); - if (obj.InitData.Valid) return ref obj.InitData.Initializer(obj.Id).GetOrCreate(); + if (obj.InitData.Valid) return ref obj.InitData.Initializer(id).GetOrCreate(); return ref opt.Get(); //Default value } } diff --git a/TechbloxModdingAPI/Utility/NativeApiExtensions.cs b/TechbloxModdingAPI/Utility/NativeApiExtensions.cs index adc9ee5..8ccb251 100644 --- a/TechbloxModdingAPI/Utility/NativeApiExtensions.cs +++ b/TechbloxModdingAPI/Utility/NativeApiExtensions.cs @@ -21,32 +21,36 @@ namespace TechbloxModdingAPI.Utility } /// - /// Attempts to query an entity and returns the result or a dummy value that can be modified. + /// Attempts to query an entity and returns the result in an optional reference. /// - /// - /// - /// - /// - public static OptionalRef QueryEntityOptional(this EntitiesDB entitiesDB, EcsObjectBase obj) + /// The entities DB to query from + /// The ECS object to query + /// The group of the entity if the object can have multiple + /// The component to query + /// A reference to the component or a dummy value + public static OptionalRef QueryEntityOptional(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) where T : unmanaged, IEntityComponent { - var opt = QueryEntityOptional(entitiesDB, obj.Id); + EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); + var opt = QueryEntityOptional(entitiesDB, id); return opt ? opt : new OptionalRef(obj, true); } /// /// Attempts to query an entity and returns the result or a dummy value that can be modified. /// - /// - /// - /// - /// - public static ref T QueryEntityOrDefault(this EntitiesDB entitiesDB, EcsObjectBase obj) + /// The entities DB to query from + /// The ECS object to query + /// The group of the entity if the object can have multiple + /// The component to query + /// A reference to the component or a dummy value + public static ref T QueryEntityOrDefault(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) where T : unmanaged, IEntityComponent { - var opt = QueryEntityOptional(entitiesDB, obj.Id); + EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); + var opt = QueryEntityOptional(entitiesDB, id); if (opt) return ref opt.Get(); - if (obj.InitData.Valid) return ref obj.InitData.Initializer(obj.Id).GetOrCreate(); + if (obj.InitData.Valid) return ref obj.InitData.Initializer(id).GetOrCreate(); return ref opt.Get(); //Default value } } diff --git a/TechbloxModdingAPI/Utility/WrappedHandler.cs b/TechbloxModdingAPI/Utility/WrappedHandler.cs new file mode 100644 index 0000000..57ff3ba --- /dev/null +++ b/TechbloxModdingAPI/Utility/WrappedHandler.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using TechbloxModdingAPI.Events; + +namespace TechbloxModdingAPI.Utility +{ + /// + /// Wraps the event handler in a try-catch block to avoid propagating exceptions. + /// + /// The event arguments type + public struct WrappedHandler + { + private EventHandler eventHandler; + + /// + /// Store wrappers so we can unregister them properly + /// + private static Dictionary, EventHandler> wrappers = + new Dictionary, EventHandler>(); + + public static WrappedHandler operator +(WrappedHandler original, EventHandler added) + { + EventHandler wrapped = (sender, e) => + { + try + { + added(sender, e); + } + catch (Exception e1) + { + EventRuntimeException wrappedException = + new EventRuntimeException($"EventHandler with arg type {typeof(T).Name} threw an exception", + e1); + Logging.LogWarning(wrappedException.ToString()); + } + }; + wrappers.Add(added, wrapped); + return new WrappedHandler { eventHandler = original.eventHandler + wrapped }; + } + + public static WrappedHandler operator -(WrappedHandler original, EventHandler removed) + { + if (!wrappers.TryGetValue(removed, out var wrapped)) return original; + wrappers.Remove(removed); + return new WrappedHandler { eventHandler = original.eventHandler - wrapped }; + } + + public void Invoke(object sender, T args) + { + eventHandler?.Invoke(sender, args); + } + } +} \ No newline at end of file