Merge branch 'event-mgr' into staging
This commit is contained in:
28
Torch.API/Event/EventHandlerAttribute.cs
Normal file
28
Torch.API/Event/EventHandlerAttribute.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Torch.API.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute indicating that a method should be invoked when the event occurs.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class EventHandlerAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Events are executed from low priority to high priority.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// While this may seem unintuitive this gives the high priority events the final say on changing/canceling events.
|
||||
/// </remarks>
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies if this handler should ignore a consumed event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="SkipCancelled"/> is <em>true</em> and the event is cancelled by a lower priority handler this handler won't be invoked.
|
||||
/// </remarks>
|
||||
/// <seealso cref="IEvent.Cancelled"/>
|
||||
public bool SkipCancelled { get; set; } = false;
|
||||
}
|
||||
}
|
11
Torch.API/Event/IEvent.cs
Normal file
11
Torch.API/Event/IEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Torch.API.Event
|
||||
{
|
||||
public interface IEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// An event that has been cancelled will no be processed in the default manner.
|
||||
/// </summary>
|
||||
/// <seealso cref="EventHandlerAttribute.SkipCancelled"/>
|
||||
bool Cancelled { get; }
|
||||
}
|
||||
}
|
9
Torch.API/Event/IEventHandler.cs
Normal file
9
Torch.API/Event/IEventHandler.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Torch.API.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface used to tag an event handler. This does <b>not</b> register it with the event manager.
|
||||
/// </summary>
|
||||
public interface IEventHandler
|
||||
{
|
||||
}
|
||||
}
|
27
Torch.API/Event/IEventManager.cs
Normal file
27
Torch.API/Event/IEventManager.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Torch.API.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Manager class responsible for registration of event handlers.
|
||||
/// </summary>
|
||||
public interface IEventManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers all event handler methods contained in the given instance
|
||||
/// </summary>
|
||||
/// <param name="handler">Instance to register</param>
|
||||
/// <returns><b>true</b> if added, <b>false</b> otherwise</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
bool RegisterHandler(IEventHandler handler);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters all event handler methods contained in the given instance
|
||||
/// </summary>
|
||||
/// <param name="handler">Instance to unregister</param>
|
||||
/// <returns><b>true</b> if removed, <b>false</b> otherwise</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
bool UnregisterHandler(IEventHandler handler);
|
||||
}
|
||||
}
|
@@ -120,6 +120,11 @@ namespace Torch.API
|
||||
/// </summary>
|
||||
public interface ITorchServer : ITorchBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The current <see cref="ServerState"/>
|
||||
/// </summary>
|
||||
ServerState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Path of the dedicated instance folder.
|
||||
/// </summary>
|
||||
|
@@ -163,10 +163,14 @@
|
||||
<Compile Include="ITorchConfig.cs" />
|
||||
<Compile Include="Managers\DependencyManagerExtensions.cs" />
|
||||
<Compile Include="Managers\DependencyProviderExtensions.cs" />
|
||||
<Compile Include="Event\EventHandlerAttribute.cs" />
|
||||
<Compile Include="Event\IEvent.cs" />
|
||||
<Compile Include="Event\IEventHandler.cs" />
|
||||
<Compile Include="Managers\IChatManagerClient.cs" />
|
||||
<Compile Include="Managers\IChatManagerServer.cs" />
|
||||
<Compile Include="Managers\IDependencyManager.cs" />
|
||||
<Compile Include="Managers\IDependencyProvider.cs" />
|
||||
<Compile Include="Event\IEventManager.cs" />
|
||||
<Compile Include="Managers\IManager.cs" />
|
||||
<Compile Include="Managers\IMultiplayerManagerClient.cs" />
|
||||
<Compile Include="Managers\IMultiplayerManagerBase.cs" />
|
||||
|
@@ -26,8 +26,7 @@ namespace Torch.Server
|
||||
login anonymous
|
||||
app_update 298740
|
||||
quit";
|
||||
|
||||
private TorchAssemblyResolver _resolver;
|
||||
|
||||
private TorchConfig _config;
|
||||
private TorchServer _server;
|
||||
private string _basePath;
|
||||
@@ -50,7 +49,6 @@ quit";
|
||||
if (!args.Contains("-noupdate"))
|
||||
RunSteamCmd();
|
||||
|
||||
_resolver = new TorchAssemblyResolver(Path.Combine(_basePath, "DedicatedServer64"));
|
||||
_config = InitConfig();
|
||||
if (!_config.Parse(args))
|
||||
return false;
|
||||
@@ -94,8 +92,6 @@ quit";
|
||||
}
|
||||
else
|
||||
_server.Start();
|
||||
|
||||
_resolver?.Dispose();
|
||||
}
|
||||
|
||||
private TorchConfig InitConfig()
|
||||
|
@@ -44,13 +44,16 @@ namespace Torch.Server
|
||||
var binDir = Path.Combine(workingDir, "DedicatedServer64");
|
||||
Directory.SetCurrentDirectory(workingDir);
|
||||
|
||||
if (!TorchLauncher.IsTorchWrapped())
|
||||
{
|
||||
TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName,args, binDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Environment.UserInteractive)
|
||||
{
|
||||
using (var service = new TorchService())
|
||||
using (new TorchAssemblyResolver(binDir))
|
||||
{
|
||||
ServiceBase.Run(service);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -3,12 +3,14 @@ using Sandbox.Engine.Utils;
|
||||
using Sandbox.Game;
|
||||
using Sandbox.Game.World;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Xml.Serialization.GeneratedAssembly;
|
||||
@@ -194,16 +196,62 @@ namespace Torch.Server
|
||||
((TorchServer)state).Invoke(() => mre.Set());
|
||||
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
|
||||
{
|
||||
var mainThread = MySandboxGame.Static.UpdateThread;
|
||||
if (mainThread.IsAlive)
|
||||
mainThread.Suspend();
|
||||
var stackTrace = new StackTrace(mainThread, true);
|
||||
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.\n{stackTrace}");
|
||||
Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
|
||||
throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.");
|
||||
}
|
||||
|
||||
Log.Debug("Server watchdog responded");
|
||||
}
|
||||
|
||||
private static string DumpFrozenThread(Thread thread, int traces = 3, int pause = 5000)
|
||||
{
|
||||
var stacks = new List<string>(traces);
|
||||
var totalSize = 0;
|
||||
for (var i = 0; i < traces; i++)
|
||||
{
|
||||
string dump = DumpStack(thread).ToString();
|
||||
totalSize += dump.Length;
|
||||
stacks.Add(dump);
|
||||
Thread.Sleep(pause);
|
||||
}
|
||||
string commonPrefix = StringUtils.CommonSuffix(stacks);
|
||||
// Advance prefix to include the line terminator.
|
||||
commonPrefix = commonPrefix.Substring(commonPrefix.IndexOf('\n') + 1);
|
||||
|
||||
var result = new StringBuilder(totalSize - (stacks.Count - 1) * commonPrefix.Length);
|
||||
result.AppendLine($"Frozen thread dump {thread.Name}");
|
||||
result.AppendLine("Common prefix:").AppendLine(commonPrefix);
|
||||
for (var i = 0; i < stacks.Count; i++)
|
||||
if (stacks[i].Length > commonPrefix.Length)
|
||||
{
|
||||
result.AppendLine($"Suffix {i}");
|
||||
result.AppendLine(stacks[i].Substring(0, stacks[i].Length - commonPrefix.Length));
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static StackTrace DumpStack(Thread thread)
|
||||
{
|
||||
try
|
||||
{
|
||||
thread.Suspend();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
var stack = new StackTrace(thread, true);
|
||||
try
|
||||
{
|
||||
thread.Resume();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Stop()
|
||||
{
|
||||
@@ -253,28 +301,32 @@ namespace Torch.Server
|
||||
/// <param name="callerId">Caller of the save operation</param>
|
||||
private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0)
|
||||
{
|
||||
string response = null;
|
||||
switch (statusCode)
|
||||
{
|
||||
case SaveGameStatus.Success:
|
||||
Log.Info("Save completed.");
|
||||
// TODO
|
||||
// Multiplayer.SendMessage("Saved game.", playerId: callerId);
|
||||
response = "Saved game.";
|
||||
break;
|
||||
case SaveGameStatus.SaveInProgress:
|
||||
Log.Error("Save failed, a save is already in progress.");
|
||||
// Multiplayer.SendMessage("Save failed, a save is already in progress.", playerId: callerId, font: MyFontEnum.Red);
|
||||
response = "Save failed, a save is already in progress.";
|
||||
break;
|
||||
case SaveGameStatus.GameNotReady:
|
||||
Log.Error("Save failed, game was not ready.");
|
||||
// Multiplayer.SendMessage("Save failed, game was not ready.", playerId: callerId, font: MyFontEnum.Red);
|
||||
response = "Save failed, game was not ready.";
|
||||
break;
|
||||
case SaveGameStatus.TimedOut:
|
||||
Log.Error("Save failed, save timed out.");
|
||||
// Multiplayer.SendMessage("Save failed, save timed out.", playerId: callerId, font: MyFontEnum.Red);
|
||||
response = "Save failed, save timed out.";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (MySession.Static.Players.TryGetPlayerId(callerId, out MyPlayer.PlayerId result))
|
||||
{
|
||||
Managers.GetManager<IChatManagerServer>()?.SendMessageAsOther("Server", response, statusCode == SaveGameStatus.Success ? MyFontEnum.Green : MyFontEnum.Red, result.SteamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
147
Torch/Event/EventList.cs
Normal file
147
Torch/Event/EventList.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Torch.API.Event;
|
||||
|
||||
namespace Torch.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an ordered list of callbacks.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Event type</typeparam>
|
||||
public class EventList<T> : IEventList where T : IEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate type for this event list
|
||||
/// </summary>
|
||||
/// <param name="evt">Event</param>
|
||||
public delegate void DelEventHandler(ref T evt);
|
||||
|
||||
private struct EventHandlerData
|
||||
{
|
||||
internal readonly DelEventHandler _event;
|
||||
internal readonly EventHandlerAttribute _attribute;
|
||||
|
||||
internal EventHandlerData(MethodInfo method, object instance)
|
||||
{
|
||||
_event = (DelEventHandler)Delegate.CreateDelegate(typeof(DelEventHandler), instance, method, true);
|
||||
_attribute = method.GetCustomAttribute<EventHandlerAttribute>();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void Raise(ref T evt)
|
||||
{
|
||||
if (!_attribute.SkipCancelled || !evt.Cancelled)
|
||||
_event(ref evt);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _dispatchersDirty = false;
|
||||
private readonly List<EventHandlerData> _dispatchers = new List<EventHandlerData>();
|
||||
|
||||
private int _bakedCount;
|
||||
private EventHandlerData[] _bakedDispatcher;
|
||||
|
||||
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddHandler(MethodInfo method, IEventHandler instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
_dispatchers.Add(new EventHandlerData(method, instance));
|
||||
_dispatchersDirty = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int RemoveHandlers(IEventHandler instance)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
var removeCount = 0;
|
||||
for (var i = 0; i < _dispatchers.Count; i++)
|
||||
if (_dispatchers[i]._event.Target == instance)
|
||||
{
|
||||
_dispatchers.RemoveAtFast(i);
|
||||
removeCount++;
|
||||
i--;
|
||||
}
|
||||
if (removeCount > 0)
|
||||
{
|
||||
_dispatchersDirty = true;
|
||||
_dispatchers.RemoveRange(_dispatchers.Count - removeCount, removeCount);
|
||||
}
|
||||
return removeCount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Bake()
|
||||
{
|
||||
if (!_dispatchersDirty && _bakedDispatcher != null)
|
||||
return;
|
||||
if (_bakedDispatcher == null || _dispatchers.Count > _bakedDispatcher.Length
|
||||
|| _bakedDispatcher.Length * 5 / 4 < _dispatchers.Count)
|
||||
_bakedDispatcher = new EventHandlerData[_dispatchers.Count];
|
||||
_bakedCount = _dispatchers.Count;
|
||||
for (var i = 0; i < _dispatchers.Count; i++)
|
||||
_bakedDispatcher[i] = _dispatchers[i];
|
||||
Array.Sort(_bakedDispatcher, 0, _bakedCount, EventHandlerDataComparer.Instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises this event for all event handlers, passing the reference to all of them
|
||||
/// </summary>
|
||||
/// <param name="evt">event to raise</param>
|
||||
public void RaiseEvent(ref T evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lock.EnterUpgradeableReadLock();
|
||||
if (_dispatchersDirty)
|
||||
try
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
Bake();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
for (var i = 0; i < _bakedCount; i++)
|
||||
_bakedDispatcher[i].Raise(ref evt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitUpgradeableReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private class EventHandlerDataComparer : IComparer<EventHandlerData>
|
||||
{
|
||||
internal static readonly EventHandlerDataComparer Instance = new EventHandlerDataComparer();
|
||||
|
||||
/// <inheritdoc cref="IComparer{EventHandlerData}.Compare"/>
|
||||
/// <remarks>
|
||||
/// This sorts event handlers with ascending priority order.
|
||||
/// </remarks>
|
||||
public int Compare(EventHandlerData x, EventHandlerData y)
|
||||
{
|
||||
return x._attribute.Priority.CompareTo(y._attribute.Priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
191
Torch/Event/EventManager.cs
Normal file
191
Torch/Event/EventManager.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NLog;
|
||||
using Torch.API;
|
||||
using Torch.API.Event;
|
||||
using Torch.Managers;
|
||||
|
||||
namespace Torch.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Manager class responsible for managing registration and dispatching of events.
|
||||
/// </summary>
|
||||
public class EventManager : Manager, IEventManager
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private static readonly Dictionary<Type, IEventList> _eventLists = new Dictionary<Type, IEventList>();
|
||||
|
||||
internal static void AddDispatchShims(Assembly asm)
|
||||
{
|
||||
foreach (Type type in asm.GetTypes())
|
||||
if (type.HasAttribute<EventShimAttribute>())
|
||||
AddDispatchShim(type);
|
||||
}
|
||||
|
||||
private static readonly HashSet<Type> _dispatchShims = new HashSet<Type>();
|
||||
private static void AddDispatchShim(Type type)
|
||||
{
|
||||
lock (_dispatchShims)
|
||||
if (!_dispatchShims.Add(type))
|
||||
return;
|
||||
if (!type.IsSealed || !type.IsAbstract)
|
||||
_log.Warn($"Registering type {type.FullName} as an event dispatch type, even though it isn't declared singleton");
|
||||
var listsFound = 0;
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
||||
if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(EventList<>))
|
||||
{
|
||||
Type eventType = field.FieldType.GenericTypeArguments[0];
|
||||
if (_eventLists.ContainsKey(eventType))
|
||||
_log.Error($"Ignore event dispatch list {type.FullName}#{field.Name}; we already have one.");
|
||||
else
|
||||
{
|
||||
_eventLists.Add(eventType, (IEventList)field.GetValue(null));
|
||||
listsFound++;
|
||||
}
|
||||
|
||||
}
|
||||
if (listsFound == 0)
|
||||
_log.Warn($"Registering type {type.FullName} as an event dispatch type, even though it has no event lists.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets all event handler methods declared by the given type and its base types.
|
||||
/// </summary>
|
||||
/// <param name="exploreType">Type to explore</param>
|
||||
/// <returns>All event handler methods</returns>
|
||||
private static IEnumerable<MethodInfo> EventHandlers(Type exploreType)
|
||||
{
|
||||
IEnumerable<MethodInfo> enumerable = exploreType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(x =>
|
||||
{
|
||||
var attr = x.GetCustomAttribute<EventHandlerAttribute>();
|
||||
if (attr == null)
|
||||
return false;
|
||||
ParameterInfo[] ps = x.GetParameters();
|
||||
if (ps.Length != 1)
|
||||
return false;
|
||||
return ps[0].ParameterType.IsByRef && typeof(IEvent).IsAssignableFrom(ps[0].ParameterType);
|
||||
});
|
||||
return exploreType.BaseType != null ? enumerable.Concat(EventHandlers(exploreType.BaseType)) : enumerable;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
private static void RegisterHandlerInternal(IEventHandler instance)
|
||||
{
|
||||
foreach (MethodInfo handler in EventHandlers(instance.GetType()))
|
||||
{
|
||||
Type eventType = handler.GetParameters()[0].ParameterType;
|
||||
if (eventType.IsInterface)
|
||||
{
|
||||
var foundList = false;
|
||||
foreach (KeyValuePair<Type, IEventList> kv in _eventLists)
|
||||
if (eventType.IsAssignableFrom(kv.Key))
|
||||
{
|
||||
kv.Value.AddHandler(handler, instance);
|
||||
foundList = true;
|
||||
}
|
||||
if (foundList)
|
||||
continue;
|
||||
}
|
||||
else if (_eventLists.TryGetValue(eventType, out IEventList list))
|
||||
{
|
||||
list.AddHandler(handler, instance);
|
||||
continue;
|
||||
}
|
||||
_log.Error($"Unable to find event handler list for event type {eventType.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters all handlers owned by the given instance
|
||||
/// </summary>
|
||||
/// <param name="instance">Instance</param>
|
||||
private static void UnregisterHandlerInternal(IEventHandler instance)
|
||||
{
|
||||
foreach (IEventList list in _eventLists.Values)
|
||||
list.RemoveHandlers(instance);
|
||||
}
|
||||
|
||||
private Dictionary<Assembly, HashSet<IEventHandler>> _registeredHandlers = new Dictionary<Assembly, HashSet<IEventHandler>>();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public EventManager(ITorchBase torchInstance) : base(torchInstance)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers all event handler methods contained in the given instance
|
||||
/// </summary>
|
||||
/// <param name="handler">Instance to register</param>
|
||||
/// <returns><b>true</b> if added, <b>false</b> otherwise</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public bool RegisterHandler(IEventHandler handler)
|
||||
{
|
||||
Assembly caller = Assembly.GetCallingAssembly();
|
||||
lock (_registeredHandlers)
|
||||
{
|
||||
if (!_registeredHandlers.TryGetValue(caller, out HashSet<IEventHandler> handlers))
|
||||
_registeredHandlers.Add(caller, handlers = new HashSet<IEventHandler>());
|
||||
if (handlers.Add(handler))
|
||||
{
|
||||
RegisterHandlerInternal(handler);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters all event handler methods contained in the given instance
|
||||
/// </summary>
|
||||
/// <param name="handler">Instance to unregister</param>
|
||||
/// <returns><b>true</b> if removed, <b>false</b> otherwise</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public bool UnregisterHandler(IEventHandler handler)
|
||||
{
|
||||
Assembly caller = Assembly.GetCallingAssembly();
|
||||
lock (_registeredHandlers)
|
||||
{
|
||||
if (!_registeredHandlers.TryGetValue(caller, out HashSet<IEventHandler> handlers))
|
||||
return false;
|
||||
if (handlers.Remove(handler))
|
||||
{
|
||||
UnregisterHandlerInternal(handler);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters all handlers owned by the given assembly.
|
||||
/// </summary>
|
||||
/// <param name="asm">Assembly to unregister</param>
|
||||
/// <param name="callback">Optional callback invoked before a handler is unregistered. Ignored if null</param>
|
||||
/// <returns>the number of handlers that were unregistered</returns>
|
||||
internal int UnregisterAllHandlers(Assembly asm, Action<IEventHandler> callback = null)
|
||||
{
|
||||
lock (_registeredHandlers)
|
||||
{
|
||||
if (!_registeredHandlers.TryGetValue(asm, out HashSet<IEventHandler> handlers))
|
||||
return 0;
|
||||
foreach (IEventHandler k in handlers)
|
||||
{
|
||||
callback?.Invoke(k);
|
||||
UnregisterHandlerInternal(k);
|
||||
}
|
||||
int count = handlers.Count;
|
||||
handlers.Clear();
|
||||
_registeredHandlers.Remove(asm);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
Torch/Event/EventShimAttribute.cs
Normal file
20
Torch/Event/EventShimAttribute.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Torch.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Tagging class used to indicate that the class should be treated as an event shim.
|
||||
/// Only works for core assemblies loaded by Torch (non-plugins).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Event shims should be singleton, and have one (or more) fields that are of type <see cref="EventList{T}"/>.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
internal class EventShimAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
25
Torch/Event/IEventList.cs
Normal file
25
Torch/Event/IEventList.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Reflection;
|
||||
using Torch.API.Event;
|
||||
|
||||
namespace Torch.Event
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the interface for adding and removing from an ordered list of callbacks.
|
||||
/// </summary>
|
||||
public interface IEventList
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an event handler for the given method, on the given instance.
|
||||
/// </summary>
|
||||
/// <param name="method">Handler method</param>
|
||||
/// <param name="instance">Instance to invoke the handler on</param>
|
||||
void AddHandler(MethodInfo method, IEventHandler instance);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all event handlers invoked on the given instance.
|
||||
/// </summary>
|
||||
/// <param name="instance">Instance to remove event handlers for</param>
|
||||
/// <returns>The number of event handlers removed</returns>
|
||||
int RemoveHandlers(IEventHandler instance);
|
||||
}
|
||||
}
|
@@ -79,8 +79,9 @@ namespace Torch.Managers.PatchManager
|
||||
}
|
||||
|
||||
|
||||
private const string INSTANCE_PARAMETER = "__instance";
|
||||
private const string RESULT_PARAMETER = "__result";
|
||||
public const string INSTANCE_PARAMETER = "__instance";
|
||||
public const string RESULT_PARAMETER = "__result";
|
||||
public const string PREFIX_SKIPPED_PARAMETER = "__prefixSkipped";
|
||||
|
||||
#pragma warning disable 649
|
||||
[ReflectedStaticMethod(Type = typeof(RuntimeHelpers), Name = "_CompileMethod", OverrideTypeNames = new[] { "System.IRuntimeMethodInfo" })]
|
||||
@@ -118,7 +119,7 @@ namespace Torch.Managers.PatchManager
|
||||
var specialVariables = new Dictionary<string, LocalBuilder>();
|
||||
|
||||
Label labelAfterOriginalContent = target.DefineLabel();
|
||||
Label labelAfterOriginalReturn = target.DefineLabel();
|
||||
Label labelSkipMethodContent = target.DefineLabel();
|
||||
|
||||
|
||||
Type returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
|
||||
@@ -131,6 +132,13 @@ namespace Torch.Managers.PatchManager
|
||||
resultVariable = target.DeclareLocal(returnType);
|
||||
}
|
||||
resultVariable?.SetToDefault(target);
|
||||
LocalBuilder prefixSkippedVariable = null;
|
||||
if (Prefixes.Count > 0 && Suffixes.Any(x => x.GetParameters()
|
||||
.Any(y => y.Name.Equals(PREFIX_SKIPPED_PARAMETER))))
|
||||
{
|
||||
prefixSkippedVariable = target.DeclareLocal(typeof(bool));
|
||||
specialVariables.Add(PREFIX_SKIPPED_PARAMETER, prefixSkippedVariable);
|
||||
}
|
||||
|
||||
if (resultVariable != null)
|
||||
specialVariables.Add(RESULT_PARAMETER, resultVariable);
|
||||
@@ -140,7 +148,7 @@ namespace Torch.Managers.PatchManager
|
||||
{
|
||||
EmitMonkeyCall(target, prefix, specialVariables);
|
||||
if (prefix.ReturnType == typeof(bool))
|
||||
target.Emit(OpCodes.Brfalse, labelAfterOriginalReturn);
|
||||
target.Emit(OpCodes.Brfalse, labelSkipMethodContent);
|
||||
else if (prefix.ReturnType != typeof(void))
|
||||
throw new Exception(
|
||||
$"Prefixes must return void or bool. {prefix.DeclaringType?.FullName}.{prefix.Name} returns {prefix.ReturnType}");
|
||||
@@ -154,7 +162,15 @@ namespace Torch.Managers.PatchManager
|
||||
target.MarkLabel(labelAfterOriginalContent);
|
||||
if (resultVariable != null)
|
||||
target.Emit(OpCodes.Stloc, resultVariable);
|
||||
target.MarkLabel(labelAfterOriginalReturn);
|
||||
Label notSkip = target.DefineLabel();
|
||||
target.Emit(OpCodes.Br, notSkip);
|
||||
target.MarkLabel(labelSkipMethodContent);
|
||||
if (prefixSkippedVariable != null)
|
||||
{
|
||||
target.Emit(OpCodes.Ldc_I4_1);
|
||||
target.Emit(OpCodes.Stloc, prefixSkippedVariable);
|
||||
}
|
||||
target.MarkLabel(notSkip);
|
||||
|
||||
target.EmitComment("Suffixes Begin");
|
||||
foreach (MethodInfo suffix in Suffixes)
|
||||
@@ -182,8 +198,18 @@ namespace Torch.Managers.PatchManager
|
||||
throw new Exception("Can't use an instance parameter for a static method");
|
||||
target.Emit(OpCodes.Ldarg_0);
|
||||
break;
|
||||
case PREFIX_SKIPPED_PARAMETER:
|
||||
if (param.ParameterType != typeof(bool))
|
||||
throw new Exception($"Prefix skipped parameter {param.ParameterType} must be of type bool");
|
||||
if (param.ParameterType.IsByRef || param.IsOut)
|
||||
throw new Exception($"Prefix skipped parameter {param.ParameterType} can't be a reference type");
|
||||
if (specialVariables.TryGetValue(PREFIX_SKIPPED_PARAMETER, out LocalBuilder prefixSkip))
|
||||
target.Emit(OpCodes.Ldloc, prefixSkip);
|
||||
else
|
||||
target.Emit(OpCodes.Ldc_I4_0);
|
||||
break;
|
||||
case RESULT_PARAMETER:
|
||||
var retType = param.ParameterType.IsByRef
|
||||
Type retType = param.ParameterType.IsByRef
|
||||
? param.ParameterType.GetElementType()
|
||||
: param.ParameterType;
|
||||
if (retType == null || !retType.IsAssignableFrom(specialVariables[RESULT_PARAMETER].LocalType))
|
||||
@@ -191,13 +217,13 @@ namespace Torch.Managers.PatchManager
|
||||
target.Emit(param.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, specialVariables[RESULT_PARAMETER]);
|
||||
break;
|
||||
default:
|
||||
var declParam = _method.GetParameters().FirstOrDefault(x => x.Name == param.Name);
|
||||
ParameterInfo declParam = _method.GetParameters().FirstOrDefault(x => x.Name == param.Name);
|
||||
if (declParam == null)
|
||||
throw new Exception($"Parameter name {param.Name} not found");
|
||||
var paramIdx = (_method.IsStatic ? 0 : 1) + declParam.Position;
|
||||
int paramIdx = (_method.IsStatic ? 0 : 1) + declParam.Position;
|
||||
|
||||
var patchByRef = param.IsOut || param.ParameterType.IsByRef;
|
||||
var declByRef = declParam.IsOut || declParam.ParameterType.IsByRef;
|
||||
bool patchByRef = param.IsOut || param.ParameterType.IsByRef;
|
||||
bool declByRef = declParam.IsOut || declParam.ParameterType.IsByRef;
|
||||
if (patchByRef == declByRef)
|
||||
target.Emit(OpCodes.Ldarg, paramIdx);
|
||||
else if (patchByRef)
|
||||
|
@@ -7,7 +7,6 @@ using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using Torch.Managers.PatchManager.Transpile;
|
||||
using Torch.Utils;
|
||||
using Label = System.Windows.Controls.Label;
|
||||
|
||||
namespace Torch.Managers.PatchManager.MSIL
|
||||
{
|
||||
|
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Torch.Managers.PatchManager
|
||||
@@ -6,14 +7,12 @@ namespace Torch.Managers.PatchManager
|
||||
/// <summary>
|
||||
/// Represents a set of common patches that can all be reversed in a single step.
|
||||
/// </summary>
|
||||
public class PatchContext
|
||||
public sealed class PatchContext
|
||||
{
|
||||
private readonly PatchManager _replacer;
|
||||
private readonly Dictionary<MethodBase, MethodRewritePattern> _rewritePatterns = new Dictionary<MethodBase, MethodRewritePattern>();
|
||||
|
||||
internal PatchContext(PatchManager replacer)
|
||||
internal PatchContext()
|
||||
{
|
||||
_replacer = replacer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -25,12 +24,22 @@ namespace Torch.Managers.PatchManager
|
||||
{
|
||||
if (_rewritePatterns.TryGetValue(method, out MethodRewritePattern pattern))
|
||||
return pattern;
|
||||
MethodRewritePattern parent = _replacer.GetPattern(method);
|
||||
MethodRewritePattern parent = PatchManager.GetPatternInternal(method);
|
||||
var res = new MethodRewritePattern(parent);
|
||||
_rewritePatterns.Add(method, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all methods that this context has patched
|
||||
/// </summary>
|
||||
internal IEnumerable<MethodBase> PatchedMethods => _rewritePatterns
|
||||
.Where(x => x.Value.Prefixes.Count > 0 || x.Value.Suffixes.Count > 0 || x.Value.Transpilers.Count > 0)
|
||||
.Select(x => x.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all patches in this context
|
||||
/// </summary>
|
||||
internal void RemoveAll()
|
||||
{
|
||||
foreach (MethodRewritePattern pattern in _rewritePatterns.Values)
|
||||
|
@@ -1,6 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NLog;
|
||||
using Torch.API;
|
||||
using Torch.Managers.PatchManager.Transpile;
|
||||
|
||||
namespace Torch.Managers.PatchManager
|
||||
{
|
||||
@@ -9,16 +13,69 @@ namespace Torch.Managers.PatchManager
|
||||
/// </summary>
|
||||
public class PatchManager : Manager
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
|
||||
internal static void AddPatchShims(Assembly asm)
|
||||
{
|
||||
foreach (Type t in asm.GetTypes())
|
||||
if (t.HasAttribute<PatchShimAttribute>())
|
||||
AddPatchShim(t);
|
||||
}
|
||||
|
||||
private static readonly HashSet<Type> _patchShims = new HashSet<Type>();
|
||||
// Internal, not static, so the static cctor of TorchBase can hookup the GameStatePatchShim which tells us when
|
||||
// its safe to patch the rest of the game.
|
||||
internal static void AddPatchShim(Type type)
|
||||
{
|
||||
lock (_patchShims)
|
||||
if (!_patchShims.Add(type))
|
||||
return;
|
||||
if (!type.IsSealed || !type.IsAbstract)
|
||||
_log.Warn($"Registering type {type.FullName} as a patch shim type, even though it isn't declared singleton");
|
||||
MethodInfo method = type.GetMethod("Patch", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
|
||||
if (method == null)
|
||||
{
|
||||
_log.Error($"Patch shim type {type.FullName} doesn't have a static Patch method.");
|
||||
return;
|
||||
}
|
||||
ParameterInfo[] ps = method.GetParameters();
|
||||
if (ps.Length != 1 || ps[0].IsOut || ps[0].IsOptional || ps[0].ParameterType.IsByRef ||
|
||||
ps[0].ParameterType != typeof(PatchContext) || method.ReturnType != typeof(void))
|
||||
{
|
||||
_log.Error($"Patch shim type {type.FullName} doesn't have a method with signature `void Patch(PatchContext)`");
|
||||
return;
|
||||
}
|
||||
var context = new PatchContext();
|
||||
lock (_coreContexts)
|
||||
_coreContexts.Add(context);
|
||||
method.Invoke(null, new object[] { context });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new patch manager. Only have one active at a time.
|
||||
/// Creates a new patch manager.
|
||||
/// </summary>
|
||||
/// <param name="torchInstance"></param>
|
||||
public PatchManager(ITorchBase torchInstance) : base(torchInstance)
|
||||
{
|
||||
}
|
||||
|
||||
private readonly Dictionary<MethodBase, DecoratedMethod> _rewritePatterns = new Dictionary<MethodBase, DecoratedMethod>();
|
||||
private readonly HashSet<PatchContext> _contexts = new HashSet<PatchContext>();
|
||||
private static readonly Dictionary<MethodBase, DecoratedMethod> _rewritePatterns = new Dictionary<MethodBase, DecoratedMethod>();
|
||||
private static readonly Dictionary<Assembly, List<PatchContext>> _contexts = new Dictionary<Assembly, List<PatchContext>>();
|
||||
// ReSharper disable once CollectionNeverQueried.Local because we may want this in the future.
|
||||
private static readonly List<PatchContext> _coreContexts = new List<PatchContext>();
|
||||
|
||||
/// <inheritdoc cref="GetPattern"/>
|
||||
internal static MethodRewritePattern GetPatternInternal(MethodBase method)
|
||||
{
|
||||
lock (_rewritePatterns)
|
||||
{
|
||||
if (_rewritePatterns.TryGetValue(method, out DecoratedMethod pattern))
|
||||
return pattern;
|
||||
var res = new DecoratedMethod(method);
|
||||
_rewritePatterns.Add(method, res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rewrite pattern for the given method, creating one if it doesn't exist.
|
||||
@@ -27,21 +84,24 @@ namespace Torch.Managers.PatchManager
|
||||
/// <returns></returns>
|
||||
public MethodRewritePattern GetPattern(MethodBase method)
|
||||
{
|
||||
if (_rewritePatterns.TryGetValue(method, out DecoratedMethod pattern))
|
||||
return pattern;
|
||||
var res = new DecoratedMethod(method);
|
||||
_rewritePatterns.Add(method, res);
|
||||
return res;
|
||||
return GetPatternInternal(method);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PatchContext"/> used for tracking changes. A call to <see cref="Commit"/> will apply the patches.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public PatchContext AcquireContext()
|
||||
{
|
||||
var context = new PatchContext(this);
|
||||
_contexts.Add(context);
|
||||
Assembly assembly = Assembly.GetCallingAssembly();
|
||||
var context = new PatchContext();
|
||||
lock (_contexts)
|
||||
{
|
||||
if (!_contexts.TryGetValue(assembly, out List<PatchContext> localContexts))
|
||||
_contexts.Add(assembly, localContexts = new List<PatchContext>());
|
||||
localContexts.Add(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -49,10 +109,51 @@ namespace Torch.Managers.PatchManager
|
||||
/// Frees the given context, and unregister all patches from it. A call to <see cref="Commit"/> will apply the unpatching operation.
|
||||
/// </summary>
|
||||
/// <param name="context">Context to remove</param>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void FreeContext(PatchContext context)
|
||||
{
|
||||
Assembly assembly = Assembly.GetCallingAssembly();
|
||||
context.RemoveAll();
|
||||
_contexts.Remove(context);
|
||||
lock (_contexts)
|
||||
{
|
||||
if (_contexts.TryGetValue(assembly, out List<PatchContext> localContexts))
|
||||
localContexts.Remove(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees all contexts owned by the given assembly. A call to <see cref="Commit"/> will apply the unpatching operation.
|
||||
/// </summary>
|
||||
/// <param name="assembly">Assembly to retrieve owned contexts for</param>
|
||||
/// <param name="callback">Callback to run for before each context is freed, ignored if null.</param>
|
||||
/// <returns>number of contexts freed</returns>
|
||||
internal int FreeAllContexts(Assembly assembly, Action<PatchContext> callback = null)
|
||||
{
|
||||
List<PatchContext> localContexts;
|
||||
lock (_contexts)
|
||||
{
|
||||
if (!_contexts.TryGetValue(assembly, out localContexts))
|
||||
return 0;
|
||||
_contexts.Remove(assembly);
|
||||
}
|
||||
if (localContexts == null)
|
||||
return 0;
|
||||
int count = localContexts.Count;
|
||||
foreach (PatchContext k in localContexts)
|
||||
{
|
||||
callback?.Invoke(k);
|
||||
k.RemoveAll();
|
||||
}
|
||||
localContexts.Clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Commit"/>
|
||||
internal static void CommitInternal()
|
||||
{
|
||||
lock (_rewritePatterns)
|
||||
foreach (DecoratedMethod m in _rewritePatterns.Values)
|
||||
m.Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,8 +161,7 @@ namespace Torch.Managers.PatchManager
|
||||
/// </summary>
|
||||
public void Commit()
|
||||
{
|
||||
foreach (DecoratedMethod m in _rewritePatterns.Values)
|
||||
m.Commit();
|
||||
CommitInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,10 +177,15 @@ namespace Torch.Managers.PatchManager
|
||||
/// </summary>
|
||||
public override void Detach()
|
||||
{
|
||||
foreach (DecoratedMethod m in _rewritePatterns.Values)
|
||||
m.Revert();
|
||||
_rewritePatterns.Clear();
|
||||
_contexts.Clear();
|
||||
lock (_contexts)
|
||||
{
|
||||
foreach (List<PatchContext> set in _contexts.Values)
|
||||
foreach (PatchContext ctx in set)
|
||||
ctx.RemoveAll();
|
||||
_contexts.Clear();
|
||||
foreach (DecoratedMethod m in _rewritePatterns.Values)
|
||||
m.Revert();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
Torch/Managers/PatchManager/PatchShimAttribute.cs
Normal file
20
Torch/Managers/PatchManager/PatchShimAttribute.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Torch.Managers.PatchManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Tagging class used to indicate that the class should be treated as supplying patch rules.
|
||||
/// Only works for core assemblies loaded by Torch (non-plugins).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Event shims should be singleton, and have one method of signature <i>void Patch(PatchContext)</i>
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
internal class PatchShimAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
75
Torch/Patches/GameStatePatchShim.cs
Normal file
75
Torch/Patches/GameStatePatchShim.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Sandbox;
|
||||
using Torch.API;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Utils;
|
||||
|
||||
namespace Torch.Patches
|
||||
{
|
||||
[PatchShim]
|
||||
internal static class GameStatePatchShim
|
||||
{
|
||||
#pragma warning disable 649
|
||||
[ReflectedMethodInfo(typeof(MySandboxGame), nameof(MySandboxGame.Dispose))]
|
||||
private static MethodInfo _sandboxGameDispose;
|
||||
[ReflectedMethodInfo(typeof(MySandboxGame), "Initialize")]
|
||||
private static MethodInfo _sandboxGameInit;
|
||||
#pragma warning restore 649
|
||||
|
||||
internal static void Patch(PatchContext target)
|
||||
{
|
||||
ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] { typeof(string[]) });
|
||||
if (ctor == null)
|
||||
throw new ArgumentException("Can't find constructor MySandboxGame(string[])");
|
||||
target.GetPattern(ctor).Prefixes.Add(MethodRef(PrefixConstructor));
|
||||
target.GetPattern(ctor).Suffixes.Add(MethodRef(SuffixConstructor));
|
||||
target.GetPattern(_sandboxGameInit).Prefixes.Add(MethodRef(PrefixInit));
|
||||
target.GetPattern(_sandboxGameInit).Suffixes.Add(MethodRef(SuffixInit));
|
||||
target.GetPattern(_sandboxGameDispose).Prefixes.Add(MethodRef(PrefixDispose));
|
||||
target.GetPattern(_sandboxGameDispose).Suffixes.Add(MethodRef(SuffixDispose));
|
||||
}
|
||||
|
||||
private static MethodInfo MethodRef(Action a )
|
||||
{
|
||||
return a.Method;
|
||||
}
|
||||
|
||||
private static void PrefixConstructor()
|
||||
{
|
||||
if (TorchBase.Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Creating;
|
||||
}
|
||||
|
||||
private static void SuffixConstructor()
|
||||
{
|
||||
PatchManager.CommitInternal();
|
||||
if (TorchBase.Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Created;
|
||||
}
|
||||
|
||||
private static void PrefixInit()
|
||||
{
|
||||
if (TorchBase.Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Loading;
|
||||
}
|
||||
|
||||
private static void SuffixInit()
|
||||
{
|
||||
if (TorchBase.Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Loaded;
|
||||
}
|
||||
|
||||
private static void PrefixDispose()
|
||||
{
|
||||
if (TorchBase.Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Unloading;
|
||||
}
|
||||
|
||||
private static void SuffixDispose()
|
||||
{
|
||||
if (TorchBase.Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Unloaded;
|
||||
}
|
||||
}
|
||||
}
|
@@ -239,7 +239,9 @@ namespace Torch.Managers
|
||||
{
|
||||
var data = new byte[stream.Length];
|
||||
stream.Read(data, 0, data.Length);
|
||||
assemblies.Add(Assembly.Load(data));
|
||||
Assembly asm = Assembly.Load(data);
|
||||
assemblies.Add(asm);
|
||||
TorchBase.RegisterAuxAssembly(asm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +270,8 @@ namespace Torch.Managers
|
||||
{
|
||||
var data = new byte[entry.Length];
|
||||
stream.Read(data, 0, data.Length);
|
||||
assemblies.Add(Assembly.Load(data));
|
||||
Assembly asm = Assembly.Load(data);
|
||||
TorchBase.RegisterAuxAssembly(asm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -155,11 +155,15 @@
|
||||
<Link>Properties\AssemblyVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Collections\ObservableList.cs" />
|
||||
<Compile Include="Event\EventShimAttribute.cs" />
|
||||
<Compile Include="Managers\ChatManager\ChatManagerClient.cs" />
|
||||
<Compile Include="Managers\ChatManager\ChatManagerServer.cs" />
|
||||
<Compile Include="Extensions\DispatcherExtensions.cs" />
|
||||
<Compile Include="Extensions\ICollectionExtensions.cs" />
|
||||
<Compile Include="Managers\DependencyManager.cs" />
|
||||
<Compile Include="Event\EventList.cs" />
|
||||
<Compile Include="Event\EventManager.cs" />
|
||||
<Compile Include="Event\IEventList.cs" />
|
||||
<Compile Include="Managers\KeenLogManager.cs" />
|
||||
<Compile Include="Managers\PatchManager\AssemblyMemory.cs" />
|
||||
<Compile Include="Managers\PatchManager\DecoratedMethod.cs" />
|
||||
@@ -175,12 +179,14 @@
|
||||
<Compile Include="Managers\PatchManager\MSIL\MsilOperandInline.cs" />
|
||||
<Compile Include="Managers\PatchManager\MSIL\MsilOperandSwitch.cs" />
|
||||
<Compile Include="Managers\PatchManager\MethodRewritePattern.cs" />
|
||||
<Compile Include="Managers\PatchManager\PatchShimAttribute.cs" />
|
||||
<Compile Include="Managers\PatchManager\PatchContext.cs" />
|
||||
<Compile Include="Managers\PatchManager\PatchManager.cs" />
|
||||
<Compile Include="Managers\PatchManager\PatchPriorityAttribute.cs" />
|
||||
<Compile Include="Managers\PatchManager\Transpile\LoggingILGenerator.cs" />
|
||||
<Compile Include="Managers\PatchManager\Transpile\MethodContext.cs" />
|
||||
<Compile Include="Managers\PatchManager\Transpile\MethodTranspiler.cs" />
|
||||
<Compile Include="Patches\GameStatePatchShim.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SaveGameStatus.cs" />
|
||||
<Compile Include="Collections\KeyTree.cs" />
|
||||
@@ -208,6 +214,7 @@
|
||||
<Compile Include="Plugins\PluginManifest.cs" />
|
||||
<Compile Include="Utils\Reflection.cs" />
|
||||
<Compile Include="Managers\ScriptingManager.cs" />
|
||||
<Compile Include="Utils\StringUtils.cs" />
|
||||
<Compile Include="Utils\TorchAssemblyResolver.cs" />
|
||||
<Compile Include="Utils\ReflectedManager.cs" />
|
||||
<Compile Include="Session\TorchSessionManager.cs" />
|
||||
@@ -215,6 +222,7 @@
|
||||
<Compile Include="SteamService.cs" />
|
||||
<Compile Include="TorchPluginBase.cs" />
|
||||
<Compile Include="Session\TorchSession.cs" />
|
||||
<Compile Include="Utils\TorchLauncher.cs" />
|
||||
<Compile Include="ViewModels\ModViewModel.cs" />
|
||||
<Compile Include="Collections\MTObservableCollection.cs" />
|
||||
<Compile Include="Extensions\MyPlayerCollectionExtensions.cs" />
|
||||
|
@@ -22,9 +22,11 @@ using Torch.API.Managers;
|
||||
using Torch.API.ModAPI;
|
||||
using Torch.API.Session;
|
||||
using Torch.Commands;
|
||||
using Torch.Event;
|
||||
using Torch.Managers;
|
||||
using Torch.Managers.ChatManager;
|
||||
using Torch.Managers.PatchManager;
|
||||
using Torch.Patches;
|
||||
using Torch.Utils;
|
||||
using Torch.Session;
|
||||
using VRage.Collections;
|
||||
@@ -45,8 +47,13 @@ namespace Torch
|
||||
{
|
||||
static TorchBase()
|
||||
{
|
||||
// We can safely never detach this since we don't reload assemblies.
|
||||
new ReflectedManager().Attach();
|
||||
ReflectedManager.Process(typeof(TorchBase).Assembly);
|
||||
ReflectedManager.Process(typeof(ITorchBase).Assembly);
|
||||
PatchManager.AddPatchShim(typeof(GameStatePatchShim));
|
||||
PatchManager.CommitInternal();
|
||||
RegisterCoreAssembly(typeof(ITorchBase).Assembly);
|
||||
RegisterCoreAssembly(typeof(TorchBase).Assembly);
|
||||
RegisterCoreAssembly(Assembly.GetEntryAssembly());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,6 +107,7 @@ namespace Torch
|
||||
/// <exception cref="InvalidOperationException">Thrown if a TorchBase instance already exists.</exception>
|
||||
protected TorchBase()
|
||||
{
|
||||
RegisterCoreAssembly(GetType().Assembly);
|
||||
if (Instance != null)
|
||||
throw new InvalidOperationException("A TorchBase instance already exists.");
|
||||
|
||||
@@ -120,20 +128,11 @@ namespace Torch
|
||||
sessionManager.AddFactory((x) => new EntityManager(this));
|
||||
|
||||
Managers.AddManager(sessionManager);
|
||||
var patcher = new PatchManager(this);
|
||||
GameStateInjector.Inject(patcher.AcquireContext());
|
||||
Managers.AddManager(patcher);
|
||||
// Managers.AddManager(new KeenLogManager(this));
|
||||
Managers.AddManager(new PatchManager(this));
|
||||
Managers.AddManager(new FilesystemManager(this));
|
||||
Managers.AddManager(new UpdateManager(this));
|
||||
Managers.AddManager(new EventManager(this));
|
||||
Managers.AddManager(Plugins);
|
||||
GameStateChanged += (game, state) =>
|
||||
{
|
||||
if (state != TorchGameState.Created)
|
||||
return;
|
||||
// At this point flush the patches; it's safe.
|
||||
patcher.Commit();
|
||||
};
|
||||
TorchAPI.Instance = this;
|
||||
}
|
||||
|
||||
@@ -390,7 +389,7 @@ namespace Torch
|
||||
/// <inheritdoc />
|
||||
public virtual void Update()
|
||||
{
|
||||
GetManager<IPluginManager>().UpdatePlugins();
|
||||
Managers.GetManager<IPluginManager>().UpdatePlugins();
|
||||
}
|
||||
|
||||
|
||||
@@ -400,7 +399,7 @@ namespace Torch
|
||||
public TorchGameState GameState
|
||||
{
|
||||
get => _gameState;
|
||||
private set
|
||||
internal set
|
||||
{
|
||||
_gameState = value;
|
||||
GameStateChanged?.Invoke(MySandboxGame.Static, _gameState);
|
||||
@@ -410,71 +409,37 @@ namespace Torch
|
||||
/// <inheritdoc/>
|
||||
public event TorchGameStateChangedDel GameStateChanged;
|
||||
|
||||
#region GameStateInjecting
|
||||
private static class GameStateInjector
|
||||
private static readonly HashSet<Assembly> _registeredCoreAssemblies = new HashSet<Assembly>();
|
||||
/// <summary>
|
||||
/// Registers a core (Torch) assembly with the system, including its
|
||||
/// <see cref="EventManager"/> shims, <see cref="PatchManager"/> shims, and <see cref="ReflectedManager"/> components.
|
||||
/// </summary>
|
||||
/// <param name="asm">Assembly to register</param>
|
||||
internal static void RegisterCoreAssembly(Assembly asm)
|
||||
{
|
||||
#pragma warning disable 649
|
||||
[ReflectedMethodInfo(typeof(MySandboxGame), nameof(MySandboxGame.Dispose))]
|
||||
private static MethodInfo _sandboxGameDispose;
|
||||
[ReflectedMethodInfo(typeof(MySandboxGame), "Initialize")]
|
||||
private static MethodInfo _sandboxGameInit;
|
||||
#pragma warning restore 649
|
||||
|
||||
internal static void Inject(PatchContext target)
|
||||
{
|
||||
ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] {typeof(string[])});
|
||||
if (ctor == null)
|
||||
throw new ArgumentException("Can't find constructor MySandboxGame(string[])");
|
||||
target.GetPattern(ctor).Prefixes.Add(MethodRef(nameof(PrefixConstructor)));
|
||||
target.GetPattern(ctor).Suffixes.Add(MethodRef(nameof(SuffixConstructor)));
|
||||
target.GetPattern(_sandboxGameInit).Prefixes.Add(MethodRef(nameof(PrefixInit)));
|
||||
target.GetPattern(_sandboxGameInit).Suffixes.Add(MethodRef(nameof(SuffixInit)));
|
||||
target.GetPattern(_sandboxGameDispose).Prefixes.Add(MethodRef(nameof(PrefixDispose)));
|
||||
target.GetPattern(_sandboxGameDispose).Suffixes.Add(MethodRef(nameof(SuffixDispose)));
|
||||
}
|
||||
|
||||
private static MethodInfo MethodRef(string name)
|
||||
{
|
||||
return typeof(GameStateInjector).GetMethod(name,
|
||||
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
}
|
||||
|
||||
private static void PrefixConstructor()
|
||||
{
|
||||
if (Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Creating;
|
||||
}
|
||||
|
||||
private static void SuffixConstructor()
|
||||
{
|
||||
if (Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Created;
|
||||
}
|
||||
|
||||
private static void PrefixInit()
|
||||
{
|
||||
if (Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Loading;
|
||||
}
|
||||
|
||||
private static void SuffixInit()
|
||||
{
|
||||
if (Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Loaded;
|
||||
}
|
||||
|
||||
private static void PrefixDispose()
|
||||
{
|
||||
if (Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Unloading;
|
||||
}
|
||||
|
||||
private static void SuffixDispose()
|
||||
{
|
||||
if (Instance is TorchBase tb)
|
||||
tb.GameState = TorchGameState.Unloaded;
|
||||
}
|
||||
lock (_registeredCoreAssemblies)
|
||||
if (_registeredCoreAssemblies.Add(asm))
|
||||
{
|
||||
ReflectedManager.Process(asm);
|
||||
EventManager.AddDispatchShims(asm);
|
||||
PatchManager.AddPatchShims(asm);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<Assembly> _registeredAuxAssemblies = new HashSet<Assembly>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers an auxillary (plugin) assembly with the system, including its
|
||||
/// <see cref="ReflectedManager"/> related components.
|
||||
/// </summary>
|
||||
/// <param name="asm">Assembly to register</param>
|
||||
internal static void RegisterAuxAssembly(Assembly asm)
|
||||
{
|
||||
lock (_registeredAuxAssemblies)
|
||||
if (_registeredAuxAssemblies.Add(asm))
|
||||
{
|
||||
ReflectedManager.Process(asm);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@@ -398,36 +398,9 @@ namespace Torch.Utils
|
||||
/// <summary>
|
||||
/// Automatically calls <see cref="ReflectedManager.Process(Assembly)"/> for every assembly already loaded, and every assembly that is loaded in the future.
|
||||
/// </summary>
|
||||
public class ReflectedManager
|
||||
public static class ReflectedManager
|
||||
{
|
||||
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
|
||||
private static readonly string[] _namespaceBlacklist = new[] {
|
||||
"System", "VRage", "Sandbox", "SpaceEngineers", "Microsoft"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Registers the assembly load event and loads every already existing assembly.
|
||||
/// </summary>
|
||||
public void Attach()
|
||||
{
|
||||
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
Process(asm);
|
||||
AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deregisters the assembly load event
|
||||
/// </summary>
|
||||
public void Detach()
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyLoad -= CurrentDomain_AssemblyLoad;
|
||||
}
|
||||
|
||||
private void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
|
||||
{
|
||||
Process(args.LoadedAssembly);
|
||||
}
|
||||
|
||||
private static readonly HashSet<Type> _processedTypes = new HashSet<Type>();
|
||||
|
||||
/// <summary>
|
||||
@@ -438,11 +411,6 @@ namespace Torch.Utils
|
||||
{
|
||||
if (_processedTypes.Add(t))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(t.Namespace))
|
||||
return;
|
||||
foreach (string ns in _namespaceBlacklist)
|
||||
if (t.FullName.StartsWith(ns))
|
||||
return;
|
||||
foreach (FieldInfo field in t.GetFields(BindingFlags.Static | BindingFlags.Instance |
|
||||
BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
@@ -636,6 +604,7 @@ namespace Torch.Utils
|
||||
field.SetValue(null,
|
||||
Expression.Lambda(Expression.Call(paramExp[0], methodInstance, argExp), paramExp)
|
||||
.Compile());
|
||||
_log.Trace($"Reflecting field {field.DeclaringType?.FullName}#{field.Name} with {methodInstance.DeclaringType?.FullName}#{methodInstance.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +690,7 @@ namespace Torch.Utils
|
||||
}
|
||||
|
||||
field.SetValue(null, Expression.Lambda(impl, paramExp).Compile());
|
||||
_log.Trace($"Reflecting field {field.DeclaringType?.FullName}#{field.Name} with {field.DeclaringType?.FullName}#{field.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
64
Torch/Utils/StringUtils.cs
Normal file
64
Torch/Utils/StringUtils.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Torch.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for strings
|
||||
/// </summary>
|
||||
public static class StringUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines a common prefix for the given set of strings
|
||||
/// </summary>
|
||||
/// <param name="set">Set of strings</param>
|
||||
/// <returns>Common prefix</returns>
|
||||
public static string CommonPrefix(IEnumerable<string> set)
|
||||
{
|
||||
StringBuilder builder = null;
|
||||
foreach (string other in set)
|
||||
{
|
||||
if (builder == null)
|
||||
builder = new StringBuilder(other);
|
||||
if (builder.Length > other.Length)
|
||||
builder.Remove(other.Length, builder.Length - other.Length);
|
||||
for (var i = 0; i < builder.Length; i++)
|
||||
if (builder[i] != other[i])
|
||||
{
|
||||
builder.Remove(i, builder.Length - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder?.ToString() ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines a common suffix for the given set of strings
|
||||
/// </summary>
|
||||
/// <param name="set">Set of strings</param>
|
||||
/// <returns>Common suffix</returns>
|
||||
public static string CommonSuffix(IEnumerable<string> set)
|
||||
{
|
||||
StringBuilder builder = null;
|
||||
foreach (string other in set)
|
||||
{
|
||||
if (builder == null)
|
||||
builder = new StringBuilder(other);
|
||||
if (builder.Length > other.Length)
|
||||
builder.Remove(0, builder.Length - other.Length);
|
||||
for (var i = 0; i < builder.Length; i++)
|
||||
{
|
||||
if (builder[builder.Length - 1 - i] != other[other.Length - 1 - i])
|
||||
{
|
||||
builder.Remove(0, builder.Length - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder?.ToString() ?? "";
|
||||
}
|
||||
}
|
||||
}
|
@@ -42,23 +42,16 @@ namespace Torch.Utils
|
||||
{
|
||||
string assemblyName = new AssemblyName(args.Name).Name;
|
||||
lock (_assemblies)
|
||||
{
|
||||
if (_assemblies.TryGetValue(assemblyName, out Assembly asm))
|
||||
return asm;
|
||||
}
|
||||
lock (AppDomain.CurrentDomain)
|
||||
{
|
||||
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
if (asm.GetName().Name.Equals(assemblyName))
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_assemblies.Add(assemblyName, asm);
|
||||
return asm;
|
||||
}
|
||||
}
|
||||
}
|
||||
lock (this)
|
||||
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
if (asm.GetName().Name.Equals(assemblyName))
|
||||
{
|
||||
lock (_assemblies)
|
||||
_assemblies.Add(assemblyName, asm);
|
||||
return asm;
|
||||
}
|
||||
lock (_assemblies)
|
||||
{
|
||||
foreach (string path in _paths)
|
||||
{
|
||||
|
51
Torch/Utils/TorchLauncher.cs
Normal file
51
Torch/Utils/TorchLauncher.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Torch.API;
|
||||
|
||||
namespace Torch.Utils
|
||||
{
|
||||
public class TorchLauncher
|
||||
{
|
||||
private const string TorchKey = "TorchWrapper";
|
||||
|
||||
public static bool IsTorchWrapped()
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetData(TorchKey) != null;
|
||||
}
|
||||
|
||||
public static void Launch(string entryPoint, string[] args, params string[] binaryPaths)
|
||||
{
|
||||
if (IsTorchWrapped())
|
||||
throw new Exception("Can't wrap torch twice");
|
||||
string exePath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)?.ToLower().Replace('/', '\\');
|
||||
if (exePath == null)
|
||||
throw new ArgumentException("Unable to determine executing assembly's path");
|
||||
var allPaths = new HashSet<string> { exePath };
|
||||
foreach (string other in binaryPaths)
|
||||
allPaths.Add(other.ToLower().Replace('/', '\\'));
|
||||
var pathPrefix = StringUtils.CommonPrefix(allPaths);
|
||||
AppDomain.CurrentDomain.AppendPrivatePath(String.Join(Path.PathSeparator.ToString(), allPaths));
|
||||
AppDomain.CurrentDomain.SetData(TorchKey, true);
|
||||
AppDomain.CurrentDomain.ExecuteAssemblyByName(entryPoint, args);
|
||||
return;
|
||||
// this would be way better but HAVOK IS UNMANAGED :clang:
|
||||
// exclude application base from probing
|
||||
var setup = new AppDomainSetup
|
||||
{
|
||||
ApplicationBase = pathPrefix.ToString(),
|
||||
PrivateBinPathProbe = "",
|
||||
PrivateBinPath = string.Join(";", allPaths)
|
||||
};
|
||||
AppDomain domain = AppDomain.CreateDomain($"TorchDomain-{Assembly.GetEntryAssembly().GetName().Name}-{new Random().Next():X}", null, setup);
|
||||
domain.SetData(TorchKey, true);
|
||||
domain.ExecuteAssemblyByName(entryPoint, args);
|
||||
AppDomain.Unload(domain);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user