- 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)tags/v2.1.0
@@ -9,9 +9,9 @@ namespace TechbloxModdingAPI.App | |||
{ | |||
public class AppEngine : IFactoryEngine | |||
{ | |||
public event EventHandler<MenuEventArgs> EnterMenu; | |||
public WrappedHandler<MenuEventArgs> EnterMenu; | |||
public event EventHandler<MenuEventArgs> ExitMenu; | |||
public WrappedHandler<MenuEventArgs> 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 | |||
@@ -28,7 +28,7 @@ namespace TechbloxModdingAPI.App | |||
/// </summary> | |||
public static event EventHandler<MenuEventArgs> EnterMenu | |||
{ | |||
add => appEngine.EnterMenu += ExceptionUtil.WrapHandler(value); | |||
add => appEngine.EnterMenu += value; | |||
remove => appEngine.EnterMenu -= value; | |||
} | |||
@@ -37,7 +37,7 @@ namespace TechbloxModdingAPI.App | |||
/// </summary> | |||
public static event EventHandler<MenuEventArgs> ExitMenu | |||
{ | |||
add => appEngine.ExitMenu += ExceptionUtil.WrapHandler(value); | |||
add => appEngine.ExitMenu += value; | |||
remove => appEngine.ExitMenu -= value; | |||
} | |||
@@ -93,7 +93,7 @@ namespace TechbloxModdingAPI.App | |||
/// </summary> | |||
public static event EventHandler<GameEventArgs> Simulate | |||
{ | |||
add => buildSimEventEngine.SimulationMode += ExceptionUtil.WrapHandler(value); | |||
add => buildSimEventEngine.SimulationMode += value; | |||
remove => buildSimEventEngine.SimulationMode -= value; | |||
} | |||
@@ -103,7 +103,7 @@ namespace TechbloxModdingAPI.App | |||
/// </summary> | |||
public static event EventHandler<GameEventArgs> Edit | |||
{ | |||
add => buildSimEventEngine.BuildMode += ExceptionUtil.WrapHandler(value); | |||
add => buildSimEventEngine.BuildMode += value; | |||
remove => buildSimEventEngine.BuildMode -= value; | |||
} | |||
@@ -112,7 +112,7 @@ namespace TechbloxModdingAPI.App | |||
/// </summary> | |||
public static event EventHandler<GameEventArgs> Enter | |||
{ | |||
add => gameEngine.EnterGame += ExceptionUtil.WrapHandler(value); | |||
add => gameEngine.EnterGame += value; | |||
remove => gameEngine.EnterGame -= value; | |||
} | |||
@@ -122,7 +122,7 @@ namespace TechbloxModdingAPI.App | |||
/// </summary> | |||
public static event EventHandler<GameEventArgs> Exit | |||
{ | |||
add => gameEngine.ExitGame += ExceptionUtil.WrapHandler(value); | |||
add => gameEngine.ExitGame += value; | |||
remove => gameEngine.ExitGame -= value; | |||
} | |||
@@ -11,9 +11,9 @@ namespace TechbloxModdingAPI.App | |||
{ | |||
public class GameBuildSimEventEngine : IApiEngine, IUnorderedInitializeOnTimeRunningModeEntered, IUnorderedInitializeOnTimeStoppedModeEntered | |||
{ | |||
public event EventHandler<GameEventArgs> SimulationMode; | |||
public WrappedHandler<GameEventArgs> SimulationMode; | |||
public event EventHandler<GameEventArgs> BuildMode; | |||
public WrappedHandler<GameEventArgs> 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; | |||
} | |||
} | |||
@@ -17,9 +17,9 @@ namespace TechbloxModdingAPI.App | |||
{ | |||
public class GameGameEngine : IApiEngine | |||
{ | |||
public event EventHandler<GameEventArgs> EnterGame; | |||
public WrappedHandler<GameEventArgs> EnterGame; | |||
public event EventHandler<GameEventArgs> ExitGame; | |||
public WrappedHandler<GameEventArgs> 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; | |||
} | |||
@@ -78,8 +78,8 @@ namespace TechbloxModdingAPI | |||
/// An event that fires each time a block is placed. | |||
/// </summary> | |||
public static event EventHandler<BlockPlacedRemovedEventArgs> 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 | |||
/// </summary> | |||
public static event EventHandler<BlockPlacedRemovedEventArgs> 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))} | |||
}; | |||
/// <summary> | |||
/// 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. | |||
/// </summary> | |||
/// <param name="egid">The EGID of the block</param> | |||
/// <param name="signaling">Whether the block is definitely a signaling block</param> | |||
/// <returns></returns> | |||
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) | |||
@@ -10,8 +10,8 @@ namespace TechbloxModdingAPI.Blocks.Engines | |||
{ | |||
public class BlockEventsEngine : IReactionaryEngine<BlockTagEntityStruct> | |||
{ | |||
public event EventHandler<BlockPlacedRemovedEventArgs> Placed; | |||
public event EventHandler<BlockPlacedRemovedEventArgs> Removed; | |||
public WrappedHandler<BlockPlacedRemovedEventArgs> Placed; | |||
public WrappedHandler<BlockPlacedRemovedEventArgs> 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); | |||
} | |||
} |
@@ -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 | |||
/// <summary> | |||
/// The wire's in-game id. | |||
/// </summary> | |||
public EGID Id | |||
public override EGID Id | |||
{ | |||
get => wireEGID; | |||
} | |||
@@ -279,7 +279,7 @@ namespace TechbloxModdingAPI.Blocks | |||
/// <returns>A copy of the wire object.</returns> | |||
public Wire OutputToInputCopy() | |||
{ | |||
return new Wire(wireEGID); | |||
return GetInstance(wireEGID, egid => new Wire(egid)); | |||
} | |||
/// <summary> | |||
@@ -23,6 +23,22 @@ namespace TechbloxModdingAPI | |||
return _instances.TryGetValue(type, out var dict) ? dict : null; | |||
} | |||
/// <summary> | |||
/// 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. | |||
/// </summary> | |||
/// <param name="egid">The EGID of the entity</param> | |||
/// <param name="constructor">The constructor to construct the object</param> | |||
/// <typeparam name="T">The object type</typeparam> | |||
/// <returns></returns> | |||
internal static T GetInstance<T>(EGID egid, Func<EGID, T> 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 | |||
@@ -21,7 +21,7 @@ namespace TechbloxModdingAPI | |||
/// An in-game player character. Any Leo you see is a player. | |||
/// </summary> | |||
public class Player : IEquatable<Player>, IEquatable<EGID> | |||
{ | |||
{ //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<WiresGroup>.Group)) | |||
? EcsObjectBase.GetInstance(new EGID(egid.entityID, NamedExclusiveGroup<WiresGroup>.Group), | |||
e => new Wire(e)) | |||
: null; | |||
} | |||
@@ -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. | |||
/// </summary> | |||
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; | |||
@@ -1,43 +0,0 @@ | |||
using System; | |||
using TechbloxModdingAPI.Events; | |||
namespace TechbloxModdingAPI.Utility | |||
{ | |||
public static class ExceptionUtil | |||
{ | |||
/// <summary> | |||
/// Invokes an event with a null-check. | |||
/// </summary> | |||
/// <param name="handler">The event to emit, can be null</param> | |||
/// <param name="sender">Event sender</param> | |||
/// <param name="args">Event arguments</param> | |||
/// <typeparam name="T">Type of the event arguments</typeparam> | |||
public static void InvokeEvent<T>(EventHandler<T> handler, object sender, T args) | |||
{ | |||
handler?.Invoke(sender, args); | |||
} | |||
/// <summary> | |||
/// Wraps the event handler in a try-catch block to avoid propagating exceptions. | |||
/// </summary> | |||
/// <param name="handler">The handler to wrap (not null)</param> | |||
/// <typeparam name="T">Type of the event arguments</typeparam> | |||
/// <returns>The wrapped handler</returns> | |||
public static EventHandler<T> WrapHandler<T>(EventHandler<T> 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()); | |||
} | |||
}; | |||
} | |||
} | |||
} |
@@ -1,6 +1,5 @@ | |||
using Svelto.ECS; | |||
using Svelto.ECS.Hybrid; | |||
using TechbloxModdingAPI.Blocks; | |||
namespace TechbloxModdingAPI.Utility | |||
{ | |||
@@ -23,32 +22,36 @@ namespace TechbloxModdingAPI.Utility | |||
} | |||
/// <summary> | |||
/// 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. | |||
/// </summary> | |||
/// <param name="entitiesDB"></param> | |||
/// <param name="obj"></param> | |||
/// <typeparam name="T"></typeparam> | |||
/// <returns></returns> | |||
public static OptionalRef<T> QueryEntityOptional<T>(this EntitiesDB entitiesDB, EcsObjectBase obj) | |||
/// <param name="entitiesDB">The entities DB to query from</param> | |||
/// <param name="obj">The ECS object to query</param> | |||
/// <param name="group">The group of the entity if the object can have multiple</param> | |||
/// <typeparam name="T">The component to query</typeparam> | |||
/// <returns>A reference to the component or a dummy value</returns> | |||
public static OptionalRef<T> QueryEntityOptional<T>(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) | |||
where T : struct, IEntityViewComponent | |||
{ | |||
var opt = QueryEntityOptional<T>(entitiesDB, obj.Id); | |||
return opt ? opt : new OptionalRef<T>(obj, true); | |||
EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); | |||
var opt = QueryEntityOptional<T>(entitiesDB, id); | |||
return opt ? opt : new OptionalRef<T>(obj, false); | |||
} | |||
/// <summary> | |||
/// Attempts to query an entity and returns the result or a dummy value that can be modified. | |||
/// </summary> | |||
/// <param name="entitiesDB"></param> | |||
/// <param name="obj"></param> | |||
/// <typeparam name="T"></typeparam> | |||
/// <returns></returns> | |||
public static ref T QueryEntityOrDefault<T>(this EntitiesDB entitiesDB, EcsObjectBase obj) | |||
/// <param name="entitiesDB">The entities DB to query from</param> | |||
/// <param name="obj">The ECS object to query</param> | |||
/// <param name="group">The group of the entity if the object can have multiple</param> | |||
/// <typeparam name="T">The component to query</typeparam> | |||
/// <returns>A reference to the component or a dummy value</returns> | |||
public static ref T QueryEntityOrDefault<T>(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) | |||
where T : struct, IEntityViewComponent | |||
{ | |||
var opt = QueryEntityOptional<T>(entitiesDB, obj.Id); | |||
EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); | |||
var opt = QueryEntityOptional<T>(entitiesDB, id); | |||
if (opt) return ref opt.Get(); | |||
if (obj.InitData.Valid) return ref obj.InitData.Initializer(obj.Id).GetOrCreate<T>(); | |||
if (obj.InitData.Valid) return ref obj.InitData.Initializer(id).GetOrCreate<T>(); | |||
return ref opt.Get(); //Default value | |||
} | |||
} |
@@ -21,32 +21,36 @@ namespace TechbloxModdingAPI.Utility | |||
} | |||
/// <summary> | |||
/// 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. | |||
/// </summary> | |||
/// <param name="entitiesDB"></param> | |||
/// <param name="obj"></param> | |||
/// <typeparam name="T"></typeparam> | |||
/// <returns></returns> | |||
public static OptionalRef<T> QueryEntityOptional<T>(this EntitiesDB entitiesDB, EcsObjectBase obj) | |||
/// <param name="entitiesDB">The entities DB to query from</param> | |||
/// <param name="obj">The ECS object to query</param> | |||
/// <param name="group">The group of the entity if the object can have multiple</param> | |||
/// <typeparam name="T">The component to query</typeparam> | |||
/// <returns>A reference to the component or a dummy value</returns> | |||
public static OptionalRef<T> QueryEntityOptional<T>(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) | |||
where T : unmanaged, IEntityComponent | |||
{ | |||
var opt = QueryEntityOptional<T>(entitiesDB, obj.Id); | |||
EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); | |||
var opt = QueryEntityOptional<T>(entitiesDB, id); | |||
return opt ? opt : new OptionalRef<T>(obj, true); | |||
} | |||
/// <summary> | |||
/// Attempts to query an entity and returns the result or a dummy value that can be modified. | |||
/// </summary> | |||
/// <param name="entitiesDB"></param> | |||
/// <param name="obj"></param> | |||
/// <typeparam name="T"></typeparam> | |||
/// <returns></returns> | |||
public static ref T QueryEntityOrDefault<T>(this EntitiesDB entitiesDB, EcsObjectBase obj) | |||
/// <param name="entitiesDB">The entities DB to query from</param> | |||
/// <param name="obj">The ECS object to query</param> | |||
/// <param name="group">The group of the entity if the object can have multiple</param> | |||
/// <typeparam name="T">The component to query</typeparam> | |||
/// <returns>A reference to the component or a dummy value</returns> | |||
public static ref T QueryEntityOrDefault<T>(this EntitiesDB entitiesDB, EcsObjectBase obj, ExclusiveGroupStruct group = default) | |||
where T : unmanaged, IEntityComponent | |||
{ | |||
var opt = QueryEntityOptional<T>(entitiesDB, obj.Id); | |||
EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); | |||
var opt = QueryEntityOptional<T>(entitiesDB, id); | |||
if (opt) return ref opt.Get(); | |||
if (obj.InitData.Valid) return ref obj.InitData.Initializer(obj.Id).GetOrCreate<T>(); | |||
if (obj.InitData.Valid) return ref obj.InitData.Initializer(id).GetOrCreate<T>(); | |||
return ref opt.Get(); //Default value | |||
} | |||
} |
@@ -0,0 +1,53 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using TechbloxModdingAPI.Events; | |||
namespace TechbloxModdingAPI.Utility | |||
{ | |||
/// <summary> | |||
/// Wraps the event handler in a try-catch block to avoid propagating exceptions. | |||
/// </summary> | |||
/// <typeparam name="T">The event arguments type</typeparam> | |||
public struct WrappedHandler<T> | |||
{ | |||
private EventHandler<T> eventHandler; | |||
/// <summary> | |||
/// Store wrappers so we can unregister them properly | |||
/// </summary> | |||
private static Dictionary<EventHandler<T>, EventHandler<T>> wrappers = | |||
new Dictionary<EventHandler<T>, EventHandler<T>>(); | |||
public static WrappedHandler<T> operator +(WrappedHandler<T> original, EventHandler<T> added) | |||
{ | |||
EventHandler<T> 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<T> { eventHandler = original.eventHandler + wrapped }; | |||
} | |||
public static WrappedHandler<T> operator -(WrappedHandler<T> original, EventHandler<T> removed) | |||
{ | |||
if (!wrappers.TryGetValue(removed, out var wrapped)) return original; | |||
wrappers.Remove(removed); | |||
return new WrappedHandler<T> { eventHandler = original.eventHandler - wrapped }; | |||
} | |||
public void Invoke(object sender, T args) | |||
{ | |||
eventHandler?.Invoke(sender, args); | |||
} | |||
} | |||
} |