diff --git a/Torch/Managers/NetworkManager/NetworkManager.cs b/Torch/Managers/NetworkManager/NetworkManager.cs index 0fd8d69..0ce57a3 100644 --- a/Torch/Managers/NetworkManager/NetworkManager.cs +++ b/Torch/Managers/NetworkManager/NetworkManager.cs @@ -17,7 +17,7 @@ using VRageMath; namespace Torch.Managers { - public class NetworkManager : Manager, INetworkManager + public partial class NetworkManager : Manager, INetworkManager { private static Logger _log = LogManager.GetCurrentClassLogger(); @@ -26,18 +26,8 @@ namespace Torch.Managers private readonly HashSet _networkHandlers = new HashSet(); private bool _init; - private const int MAX_ARGUMENT = 6; - private const int GENERIC_PARAMETERS = 8; - private const int DISPATCH_PARAMETERS = 10; - private static readonly DBNull DbNull = DBNull.Value; - private static MethodInfo _dispatchInfo; - - private static MethodInfo DispatchEventInfo => _dispatchInfo ?? (_dispatchInfo = typeof(MyReplicationLayerBase).GetMethod("DispatchEvent", BindingFlags.NonPublic | BindingFlags.Instance)); - [ReflectedGetter(Name = "m_typeTable")] private static Func _typeTableGetter; - [ReflectedGetter(Name = "m_methodInfoLookup")] - private static Func> _methodInfoLookupGetter; [ReflectedMethod(Type = typeof(MyReplicationLayer), Name = "GetObjectByNetworkId")] private static Func _getObjectByNetworkId; @@ -45,7 +35,7 @@ namespace Torch.Managers { } - + private static bool ReflectionUnitTest(bool suppress = false) { try @@ -116,281 +106,112 @@ namespace Torch.Managers // TODO reverse what was done in Attach } - #region Network Intercept - - //TODO: Change this to a method patch so I don't have to try to keep up with Keen. - /// - /// This is the main body of the network intercept system. When messages come in from clients, they are processed here - /// before being passed on to the game server. - /// - /// DO NOT modify this method unless you're absolutely sure of what you're doing. This can very easily destabilize the game! - /// - /// - private void OnEvent(MyPacket packet) - { - if (_networkHandlers.Count == 0) - { - //pass the message back to the game server - try - { - ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).OnEvent(packet); - } - catch (Exception ex) - { - _log.Error(ex); - //crash after logging, bad things could happen if we continue on with bad data - throw; - } - return; - } - - var stream = new BitStream(); - stream.ResetRead(packet.BitStream); - - var networkId = stream.ReadNetworkId(); - //this value is unused, but removing this line corrupts the rest of the stream - var blockedNetworkId = stream.ReadNetworkId(); - var eventId = (uint)stream.ReadUInt16(); - bool flag = stream.ReadBool(); - Vector3D? position = new Vector3D?(); - if (flag) - position = new Vector3D?(stream.ReadVector3D()); - - CallSite site; - object obj; - if (networkId.IsInvalid) // Static event - { - site = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).StaticEventTable.Get(eventId); - obj = null; - } - else // Instance event - { - //var sendAs = ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).GetObjectByNetworkId(networkId); - var sendAs = _getObjectByNetworkId((MyReplicationLayer)MyMultiplayer.ReplicationLayer, networkId); - if (sendAs == null) - { - return; - } - var typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(sendAs.GetType()); - var eventCount = typeInfo.EventTable.Count; - if (eventId < eventCount) // Directly - { - obj = sendAs; - site = typeInfo.EventTable.Get(eventId); - } - else // Through proxy - { - obj = ((IMyProxyTarget)sendAs).Target; - typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(obj.GetType()); - site = typeInfo.EventTable.Get(eventId - (uint)eventCount); // Subtract max id of Proxy - } - } - - //we're handling the network live in the game thread, this needs to go as fast as possible - var discard = false; - foreach (var handler in _networkHandlers) - //Parallel.ForEach(_networkHandlers, handler => - { - try - { - if (handler.CanHandle(site)) - discard |= handler.Handle(packet.Sender.Id.Value, site, stream, obj, packet); - } - catch (Exception ex) - { - //ApplicationLog.Error(ex.ToString()); - _log.Error(ex); - } - } - - //one of the handlers wants us to discard this packet - if (discard) - return; - - //pass the message back to the game server - try - { - ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).OnEvent(packet); - } - catch (Exception ex) - { - _log.Error(ex, "Error processing network event!"); - _log.Error(ex); - //crash after logging, bad things could happen if we continue on with bad data - throw; - } - } - - - /// - public void RegisterNetworkHandler(INetworkHandler handler) - { - var handlerType = handler.GetType().FullName; - var toRemove = new List(); - foreach (var item in _networkHandlers) - { - if (item.GetType().FullName == handlerType) - { - //if (ExtenderOptions.IsDebugging) - _log.Error("Network handler already registered! " + handlerType); - toRemove.Add(item); - } - } - - foreach (var oldHandler in toRemove) - _networkHandlers.Remove(oldHandler); - - _networkHandlers.Add(handler); - } - - /// - public bool UnregisterNetworkHandler(INetworkHandler handler) - { - return _networkHandlers.Remove(handler); - } - - public void RegisterNetworkHandlers(params INetworkHandler[] handlers) - { - foreach (var handler in handlers) - RegisterNetworkHandler(handler); - } - - #endregion - #region Network Injection - - - /// - /// Broadcasts an event to all connected clients - /// - /// - /// - /// - public void RaiseEvent(MethodInfo method, object obj, params object[] args) + + private static Func GetDelegate(MethodInfo method) where TA : class { - //default(EndpointId) tells the network to broadcast the message - RaiseEvent(method, obj, default(EndpointId), args); + return x => Delegate.CreateDelegate(typeof(TA), x, method) as TA; } - /// - /// Sends an event to one client by SteamId - /// - /// - /// - /// - /// - public void RaiseEvent(MethodInfo method, object obj, ulong steamId, params object[] args) + public static void RaiseEvent(T1 instance, MethodInfo method, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - RaiseEvent(method, obj, new EndpointId(steamId), args); + var del = GetDelegate(method); + + MyMultiplayer.RaiseEvent(instance, del, target); } - /// - /// Sends an event to one client - /// - /// - /// - /// - /// - public void RaiseEvent(MethodInfo method, object obj, EndpointId endpoint, params object[] args) + public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - if (method == null) - throw new ArgumentNullException(nameof(method), "MethodInfo cannot be null!"); + var del = GetDelegate> (method); - if (args.Length > MAX_ARGUMENT) - throw new ArgumentOutOfRangeException(nameof(args), $"Cannot pass more than {MAX_ARGUMENT} arguments!"); - - var owner = obj as IMyEventOwner; - if (obj != null && owner == null) - throw new InvalidCastException("Provided event target is not of type IMyEventOwner!"); - - if (!method.HasAttribute()) - throw new CustomAttributeFormatException("Provided event target does not have the Event attribute! Replication will not succeed!"); - - //array to hold arguments to pass into DispatchEvent - object[] arguments = new object[DISPATCH_PARAMETERS]; - - - arguments[0] = obj == null ? TryGetStaticCallSite(method) : TryGetCallSite(method, obj); - arguments[1] = endpoint; - arguments[2] = owner; - - //copy supplied arguments into the reflection arguments - for (var i = 0; i < args.Length; i++) - arguments[i + 3] = args[i]; - - //pad the array out with DBNull, skip last element - //last element should stay null (this is for blocking events -- not used?) - for (var j = args.Length + 3; j < arguments.Length - 1; j++) - arguments[j] = DbNull; - - //create an array of Types so we can create a generic method - var argTypes = new Type[GENERIC_PARAMETERS]; - - //any null arguments (not DBNull) must be of type IMyEventOwner - for (var k = 2; k < arguments.Length; k++) - argTypes[k - 2] = arguments[k]?.GetType() ?? typeof(IMyEventOwner); - - var parameters = method.GetParameters(); - for (var i = 0; i < parameters.Length; i++) - { - if (argTypes[i + 1] != parameters[i].ParameterType) - throw new TypeLoadException($"Type mismatch on method parameters. Expected {string.Join(", ", parameters.Select(p => p.ParameterType.ToString()))} got {string.Join(", ", argTypes.Select(t => t.ToString()))}"); - } - - //create a generic method of DispatchEvent and invoke to inject our data into the network - var dispatch = DispatchEventInfo.MakeGenericMethod(argTypes); - dispatch.Invoke(MyMultiplayer.ReplicationLayer, arguments); + MyMultiplayer.RaiseEvent(instance, del, arg1, target); } - /// - /// Broadcasts a static event to all connected clients - /// - /// - /// - public void RaiseStaticEvent(MethodInfo method, params object[] args) + public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - //default(EndpointId) tells the network to broadcast the message - RaiseStaticEvent(method, default(EndpointId), args); + var del = GetDelegate>(method); + + MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, target); } - /// - /// Sends a static event to one client by SteamId - /// - /// - /// - /// - public void RaiseStaticEvent(MethodInfo method, ulong steamId, params object[] args) + public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - RaiseEvent(method, null, new EndpointId(steamId), args); + var del = GetDelegate>(method); + + MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, target); } - /// - /// Sends a static event to one client - /// - /// - /// - /// - public void RaiseStaticEvent(MethodInfo method, EndpointId endpoint, params object[] args) + public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - RaiseEvent(method, null, endpoint, args); + var del = GetDelegate>(method); + + MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, target); } - private CallSite TryGetStaticCallSite(MethodInfo method) + public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - MyTypeTable typeTable = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer); - if (!_methodInfoLookupGetter.Invoke(typeTable.StaticEventTable).TryGetValue(method, out CallSite result)) - throw new MissingMemberException("Provided event target not found!"); - return result; + var del = GetDelegate>(method); + + MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, arg5, target); } - private CallSite TryGetCallSite(MethodInfo method, object arg) + public static void RaiseEvent(T1 instance, MethodInfo method, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, EndpointId target = default(EndpointId)) where T1 : IMyEventOwner { - MySynchronizedTypeInfo typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(arg.GetType()); - if (!_methodInfoLookupGetter.Invoke(typeInfo.EventTable).TryGetValue(method, out CallSite result)) - throw new MissingMemberException("Provided event target not found!"); - return result; + var del = GetDelegate>(method); + + MyMultiplayer.RaiseEvent(instance, del, arg1, arg2, arg3, arg4, arg5, arg6, target); } + public static void RaiseStaticEvent(MethodInfo method, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate(method); + + MyMultiplayer.RaiseStaticEvent(del, target, position); + } + + public static void RaiseStaticEvent(MethodInfo method, T1 arg1, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate>(method); + + MyMultiplayer.RaiseStaticEvent(del, arg1, target, position); + } + + public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate>(method); + + MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, target, position); + } + + public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate>(method); + + MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, target, position); + } + + public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate>(method); + + MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, target, position); + } + + public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate>(method); + + MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, arg5, target, position); + } + + public static void RaiseStaticEvent(MethodInfo method, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, EndpointId target = default(EndpointId), Vector3D? position = null) + { + var del = GetDelegate>(method); + + MyMultiplayer.RaiseStaticEvent(del, arg1, arg2, arg3, arg4, arg5, arg6, target, position); + } #endregion + + } } diff --git a/Torch/Managers/NetworkManager/NetworkManager_Deprecated.cs b/Torch/Managers/NetworkManager/NetworkManager_Deprecated.cs new file mode 100644 index 0000000..1e25769 --- /dev/null +++ b/Torch/Managers/NetworkManager/NetworkManager_Deprecated.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Sandbox.Engine.Multiplayer; +using Torch.API.Managers; +using Torch.Utils; +using VRage; +using VRage.Library.Collections; +using VRage.Network; +using VRageMath; + +namespace Torch.Managers +{ + //Everything in this file is deprecated and should be deleted Eventually(tm) + public partial class NetworkManager + { + [ReflectedGetter(Name = "m_methodInfoLookup")] + private static Func> _methodInfoLookupGetter; + private const int MAX_ARGUMENT = 6; + private const int GENERIC_PARAMETERS = 8; + private const int DISPATCH_PARAMETERS = 11; + private static readonly DBNull DbNull = DBNull.Value; + private static MethodInfo _dispatchInfo; + + private static MethodInfo DispatchEventInfo => _dispatchInfo ?? (_dispatchInfo = typeof(MyReplicationLayerBase).GetMethod("DispatchEvent", BindingFlags.NonPublic | BindingFlags.Instance)); + + #region Network Injection + + + /// + /// Broadcasts an event to all connected clients + /// + /// + /// + /// + [Obsolete("Old injection system deprecated. Use the generic methods instead.")] + public void RaiseEvent(MethodInfo method, object obj, params object[] args) + { + //default(EndpointId) tells the network to broadcast the message + RaiseEvent(method, obj, default(EndpointId), args); + } + + /// + /// Sends an event to one client by SteamId + /// + /// + /// + /// + /// + [Obsolete("Old injection system deprecated. Use the generic methods instead.")] + public void RaiseEvent(MethodInfo method, object obj, ulong steamId, params object[] args) + { + RaiseEvent(method, obj, new EndpointId(steamId), args); + } + + /// + /// Sends an event to one client + /// + /// + /// + /// + /// + [Obsolete("Old injection system deprecated. Use the generic methods instead.")] + public void RaiseEvent(MethodInfo method, object obj, EndpointId endpoint, params object[] args) + { + if (method == null) + throw new ArgumentNullException(nameof(method), "MethodInfo cannot be null!"); + + if (args.Length > MAX_ARGUMENT) + throw new ArgumentOutOfRangeException(nameof(args), $"Cannot pass more than {MAX_ARGUMENT} arguments!"); + + var owner = obj as IMyEventOwner; + if (obj != null && owner == null) + throw new InvalidCastException("Provided event target is not of type IMyEventOwner!"); + + if (!method.HasAttribute()) + throw new CustomAttributeFormatException("Provided event target does not have the Event attribute! Replication will not succeed!"); + + //array to hold arguments to pass into DispatchEvent + object[] arguments = new object[DISPATCH_PARAMETERS]; + + + arguments[0] = obj == null ? TryGetStaticCallSite(method) : TryGetCallSite(method, obj); + arguments[1] = endpoint; + arguments[2] = new Vector3D?(); + arguments[3] = owner; + + //copy supplied arguments into the reflection arguments + for (var i = 0; i < args.Length; i++) + arguments[i + 4] = args[i]; + + //pad the array out with DBNull, skip last element + //last element should stay null (this is for blocking events -- not used?) + for (var j = args.Length + 4; j < arguments.Length - 1; j++) + arguments[j] = DbNull; + + //create an array of Types so we can create a generic method + var argTypes = new Type[GENERIC_PARAMETERS]; + + //any null arguments (not DBNull) must be of type IMyEventOwner + for (var k = 2; k < arguments.Length; k++) + argTypes[k - 2] = arguments[k]?.GetType() ?? typeof(IMyEventOwner); + + var parameters = method.GetParameters(); + for (var i = 0; i < parameters.Length; i++) + { + if (argTypes[i + 1] != parameters[i].ParameterType) + throw new TypeLoadException($"Type mismatch on method parameters. Expected {string.Join(", ", parameters.Select(p => p.ParameterType.ToString()))} got {string.Join(", ", argTypes.Select(t => t.ToString()))}"); + } + + //create a generic method of DispatchEvent and invoke to inject our data into the network + var dispatch = DispatchEventInfo.MakeGenericMethod(argTypes); + dispatch.Invoke(MyMultiplayer.ReplicationLayer, arguments); + } + + /// + /// Broadcasts a static event to all connected clients + /// + /// + /// + [Obsolete("Old injection system deprecated. Use the generic methods instead.")] + public void RaiseStaticEvent(MethodInfo method, params object[] args) + { + //default(EndpointId) tells the network to broadcast the message + RaiseStaticEvent(method, default(EndpointId), args); + } + + /// + /// Sends a static event to one client by SteamId + /// + /// + /// + /// + [Obsolete("Old injection system deprecated. Use the generic methods instead.")] + public void RaiseStaticEvent(MethodInfo method, ulong steamId, params object[] args) + { + RaiseEvent(method, (object)null, new EndpointId(steamId), args); + } + + /// + /// Sends a static event to one client + /// + /// + /// + /// + [Obsolete("Old injection system deprecated. Use the generic methods instead.")] + public void RaiseStaticEvent(MethodInfo method, EndpointId endpoint, params object[] args) + { + RaiseEvent(method, (object)null, endpoint, args); + } + + private CallSite TryGetStaticCallSite(MethodInfo method) + { + MyTypeTable typeTable = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer); + if (!_methodInfoLookupGetter.Invoke(typeTable.StaticEventTable).TryGetValue(method, out CallSite result)) + throw new MissingMemberException("Provided event target not found!"); + return result; + } + + private CallSite TryGetCallSite(MethodInfo method, object arg) + { + MySynchronizedTypeInfo typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(arg.GetType()); + if (!_methodInfoLookupGetter.Invoke(typeInfo.EventTable).TryGetValue(method, out CallSite result)) + throw new MissingMemberException("Provided event target not found!"); + return result; + } + + #endregion + + #region Network Intercept + + [Obsolete] + //TODO: Change this to a method patch so I don't have to try to keep up with Keen. + /// + /// This is the main body of the network intercept system. When messages come in from clients, they are processed here + /// before being passed on to the game server. + /// + /// DO NOT modify this method unless you're absolutely sure of what you're doing. This can very easily destabilize the game! + /// + /// + private void OnEvent(MyPacket packet) + { + if (_networkHandlers.Count == 0) + { + //pass the message back to the game server + try + { + ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).OnEvent(packet); + } + catch (Exception ex) + { + _log.Error(ex); + //crash after logging, bad things could happen if we continue on with bad data + throw; + } + return; + } + + var stream = new BitStream(); + stream.ResetRead(packet.BitStream); + + var networkId = stream.ReadNetworkId(); + //this value is unused, but removing this line corrupts the rest of the stream + var blockedNetworkId = stream.ReadNetworkId(); + var eventId = (uint)stream.ReadUInt16(); + bool flag = stream.ReadBool(); + Vector3D? position = new Vector3D?(); + if (flag) + position = new Vector3D?(stream.ReadVector3D()); + + CallSite site; + object obj; + if (networkId.IsInvalid) // Static event + { + site = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).StaticEventTable.Get(eventId); + obj = null; + } + else // Instance event + { + //var sendAs = ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).GetObjectByNetworkId(networkId); + var sendAs = _getObjectByNetworkId((MyReplicationLayer)MyMultiplayer.ReplicationLayer, networkId); + if (sendAs == null) + { + return; + } + var typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(sendAs.GetType()); + var eventCount = typeInfo.EventTable.Count; + if (eventId < eventCount) // Directly + { + obj = sendAs; + site = typeInfo.EventTable.Get(eventId); + } + else // Through proxy + { + obj = ((IMyProxyTarget)sendAs).Target; + typeInfo = _typeTableGetter.Invoke(MyMultiplayer.ReplicationLayer).Get(obj.GetType()); + site = typeInfo.EventTable.Get(eventId - (uint)eventCount); // Subtract max id of Proxy + } + } + + //we're handling the network live in the game thread, this needs to go as fast as possible + var discard = false; + foreach (var handler in _networkHandlers) + //Parallel.ForEach(_networkHandlers, handler => + { + try + { + if (handler.CanHandle(site)) + discard |= handler.Handle(packet.Sender.Id.Value, site, stream, obj, packet); + } + catch (Exception ex) + { + //ApplicationLog.Error(ex.ToString()); + _log.Error(ex); + } + } + + //one of the handlers wants us to discard this packet + if (discard) + return; + + //pass the message back to the game server + try + { + ((MyReplicationLayer)MyMultiplayer.ReplicationLayer).OnEvent(packet); + } + catch (Exception ex) + { + _log.Error(ex, "Error processing network event!"); + _log.Error(ex); + //crash after logging, bad things could happen if we continue on with bad data + throw; + } + } + + [Obsolete("Deprecated. Use a method patch instead.")] + /// + public void RegisterNetworkHandler(INetworkHandler handler) + { + var handlerType = handler.GetType().FullName; + var toRemove = new List(); + foreach (var item in _networkHandlers) + { + if (item.GetType().FullName == handlerType) + { + //if (ExtenderOptions.IsDebugging) + _log.Error("Network handler already registered! " + handlerType); + toRemove.Add(item); + } + } + + foreach (var oldHandler in toRemove) + _networkHandlers.Remove(oldHandler); + + _networkHandlers.Add(handler); + } + + [Obsolete("Deprecated. Use a method patch instead.")] + /// + public bool UnregisterNetworkHandler(INetworkHandler handler) + { + return _networkHandlers.Remove(handler); + } + + [Obsolete("Deprecated. Use a method patch instead.")] + public void RegisterNetworkHandlers(params INetworkHandler[] handlers) + { + foreach (var handler in handlers) + RegisterNetworkHandler(handler); + } + + #endregion + + } +} diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index ca444dd..747f50b 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -194,6 +194,7 @@ +