Merge branch 'event-mgr' into staging

This commit is contained in:
Westin Miller
2017-10-22 03:31:46 -07:00
26 changed files with 987 additions and 181 deletions

View 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
View 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; }
}
}

View 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
{
}
}

View 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);
}
}

View File

@@ -120,6 +120,11 @@ namespace Torch.API
/// </summary> /// </summary>
public interface ITorchServer : ITorchBase public interface ITorchServer : ITorchBase
{ {
/// <summary>
/// The current <see cref="ServerState"/>
/// </summary>
ServerState State { get; }
/// <summary> /// <summary>
/// Path of the dedicated instance folder. /// Path of the dedicated instance folder.
/// </summary> /// </summary>

View File

@@ -163,10 +163,14 @@
<Compile Include="ITorchConfig.cs" /> <Compile Include="ITorchConfig.cs" />
<Compile Include="Managers\DependencyManagerExtensions.cs" /> <Compile Include="Managers\DependencyManagerExtensions.cs" />
<Compile Include="Managers\DependencyProviderExtensions.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\IChatManagerClient.cs" />
<Compile Include="Managers\IChatManagerServer.cs" /> <Compile Include="Managers\IChatManagerServer.cs" />
<Compile Include="Managers\IDependencyManager.cs" /> <Compile Include="Managers\IDependencyManager.cs" />
<Compile Include="Managers\IDependencyProvider.cs" /> <Compile Include="Managers\IDependencyProvider.cs" />
<Compile Include="Event\IEventManager.cs" />
<Compile Include="Managers\IManager.cs" /> <Compile Include="Managers\IManager.cs" />
<Compile Include="Managers\IMultiplayerManagerClient.cs" /> <Compile Include="Managers\IMultiplayerManagerClient.cs" />
<Compile Include="Managers\IMultiplayerManagerBase.cs" /> <Compile Include="Managers\IMultiplayerManagerBase.cs" />

View File

@@ -26,8 +26,7 @@ namespace Torch.Server
login anonymous login anonymous
app_update 298740 app_update 298740
quit"; quit";
private TorchAssemblyResolver _resolver;
private TorchConfig _config; private TorchConfig _config;
private TorchServer _server; private TorchServer _server;
private string _basePath; private string _basePath;
@@ -50,7 +49,6 @@ quit";
if (!args.Contains("-noupdate")) if (!args.Contains("-noupdate"))
RunSteamCmd(); RunSteamCmd();
_resolver = new TorchAssemblyResolver(Path.Combine(_basePath, "DedicatedServer64"));
_config = InitConfig(); _config = InitConfig();
if (!_config.Parse(args)) if (!_config.Parse(args))
return false; return false;
@@ -94,8 +92,6 @@ quit";
} }
else else
_server.Start(); _server.Start();
_resolver?.Dispose();
} }
private TorchConfig InitConfig() private TorchConfig InitConfig()

View File

@@ -44,13 +44,16 @@ namespace Torch.Server
var binDir = Path.Combine(workingDir, "DedicatedServer64"); var binDir = Path.Combine(workingDir, "DedicatedServer64");
Directory.SetCurrentDirectory(workingDir); Directory.SetCurrentDirectory(workingDir);
if (!TorchLauncher.IsTorchWrapped())
{
TorchLauncher.Launch(Assembly.GetEntryAssembly().FullName,args, binDir);
return;
}
if (!Environment.UserInteractive) if (!Environment.UserInteractive)
{ {
using (var service = new TorchService()) using (var service = new TorchService())
using (new TorchAssemblyResolver(binDir))
{
ServiceBase.Run(service); ServiceBase.Run(service);
}
return; return;
} }

View File

@@ -3,12 +3,14 @@ using Sandbox.Engine.Utils;
using Sandbox.Game; using Sandbox.Game;
using Sandbox.Game.World; using Sandbox.Game.World;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Principal; using System.Security.Principal;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Xml.Serialization.GeneratedAssembly; using Microsoft.Xml.Serialization.GeneratedAssembly;
@@ -194,16 +196,62 @@ namespace Torch.Server
((TorchServer)state).Invoke(() => mre.Set()); ((TorchServer)state).Invoke(() => mre.Set());
if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout))) if (!mre.WaitOne(TimeSpan.FromSeconds(Instance.Config.TickTimeout)))
{ {
var mainThread = MySandboxGame.Static.UpdateThread; Log.Error(DumpFrozenThread(MySandboxGame.Static.UpdateThread));
if (mainThread.IsAlive) throw new TimeoutException($"Server watchdog detected that the server was frozen for at least {((TorchServer)state).Config.TickTimeout} seconds.");
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.Debug("Server watchdog responded"); 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 /> /// <inheritdoc />
public override void Stop() public override void Stop()
{ {
@@ -253,28 +301,32 @@ namespace Torch.Server
/// <param name="callerId">Caller of the save operation</param> /// <param name="callerId">Caller of the save operation</param>
private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0) private void SaveCompleted(SaveGameStatus statusCode, long callerId = 0)
{ {
string response = null;
switch (statusCode) switch (statusCode)
{ {
case SaveGameStatus.Success: case SaveGameStatus.Success:
Log.Info("Save completed."); Log.Info("Save completed.");
// TODO response = "Saved game.";
// Multiplayer.SendMessage("Saved game.", playerId: callerId);
break; break;
case SaveGameStatus.SaveInProgress: case SaveGameStatus.SaveInProgress:
Log.Error("Save failed, a save is already in progress."); 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; break;
case SaveGameStatus.GameNotReady: case SaveGameStatus.GameNotReady:
Log.Error("Save failed, game was not ready."); 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; break;
case SaveGameStatus.TimedOut: case SaveGameStatus.TimedOut:
Log.Error("Save failed, save timed out."); 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; break;
default: default:
break; 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
View 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
View 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;
}
}
}
}

View 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
View 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);
}
}

View File

@@ -79,8 +79,9 @@ namespace Torch.Managers.PatchManager
} }
private const string INSTANCE_PARAMETER = "__instance"; public const string INSTANCE_PARAMETER = "__instance";
private const string RESULT_PARAMETER = "__result"; public const string RESULT_PARAMETER = "__result";
public const string PREFIX_SKIPPED_PARAMETER = "__prefixSkipped";
#pragma warning disable 649 #pragma warning disable 649
[ReflectedStaticMethod(Type = typeof(RuntimeHelpers), Name = "_CompileMethod", OverrideTypeNames = new[] { "System.IRuntimeMethodInfo" })] [ReflectedStaticMethod(Type = typeof(RuntimeHelpers), Name = "_CompileMethod", OverrideTypeNames = new[] { "System.IRuntimeMethodInfo" })]
@@ -118,7 +119,7 @@ namespace Torch.Managers.PatchManager
var specialVariables = new Dictionary<string, LocalBuilder>(); var specialVariables = new Dictionary<string, LocalBuilder>();
Label labelAfterOriginalContent = target.DefineLabel(); Label labelAfterOriginalContent = target.DefineLabel();
Label labelAfterOriginalReturn = target.DefineLabel(); Label labelSkipMethodContent = target.DefineLabel();
Type returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void); Type returnType = _method is MethodInfo meth ? meth.ReturnType : typeof(void);
@@ -131,6 +132,13 @@ namespace Torch.Managers.PatchManager
resultVariable = target.DeclareLocal(returnType); resultVariable = target.DeclareLocal(returnType);
} }
resultVariable?.SetToDefault(target); 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) if (resultVariable != null)
specialVariables.Add(RESULT_PARAMETER, resultVariable); specialVariables.Add(RESULT_PARAMETER, resultVariable);
@@ -140,7 +148,7 @@ namespace Torch.Managers.PatchManager
{ {
EmitMonkeyCall(target, prefix, specialVariables); EmitMonkeyCall(target, prefix, specialVariables);
if (prefix.ReturnType == typeof(bool)) if (prefix.ReturnType == typeof(bool))
target.Emit(OpCodes.Brfalse, labelAfterOriginalReturn); target.Emit(OpCodes.Brfalse, labelSkipMethodContent);
else if (prefix.ReturnType != typeof(void)) else if (prefix.ReturnType != typeof(void))
throw new Exception( throw new Exception(
$"Prefixes must return void or bool. {prefix.DeclaringType?.FullName}.{prefix.Name} returns {prefix.ReturnType}"); $"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); target.MarkLabel(labelAfterOriginalContent);
if (resultVariable != null) if (resultVariable != null)
target.Emit(OpCodes.Stloc, resultVariable); 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"); target.EmitComment("Suffixes Begin");
foreach (MethodInfo suffix in Suffixes) 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"); throw new Exception("Can't use an instance parameter for a static method");
target.Emit(OpCodes.Ldarg_0); target.Emit(OpCodes.Ldarg_0);
break; 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: case RESULT_PARAMETER:
var retType = param.ParameterType.IsByRef Type retType = param.ParameterType.IsByRef
? param.ParameterType.GetElementType() ? param.ParameterType.GetElementType()
: param.ParameterType; : param.ParameterType;
if (retType == null || !retType.IsAssignableFrom(specialVariables[RESULT_PARAMETER].LocalType)) 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]); target.Emit(param.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, specialVariables[RESULT_PARAMETER]);
break; break;
default: default:
var declParam = _method.GetParameters().FirstOrDefault(x => x.Name == param.Name); ParameterInfo declParam = _method.GetParameters().FirstOrDefault(x => x.Name == param.Name);
if (declParam == null) if (declParam == null)
throw new Exception($"Parameter name {param.Name} not found"); 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; bool patchByRef = param.IsOut || param.ParameterType.IsByRef;
var declByRef = declParam.IsOut || declParam.ParameterType.IsByRef; bool declByRef = declParam.IsOut || declParam.ParameterType.IsByRef;
if (patchByRef == declByRef) if (patchByRef == declByRef)
target.Emit(OpCodes.Ldarg, paramIdx); target.Emit(OpCodes.Ldarg, paramIdx);
else if (patchByRef) else if (patchByRef)

View File

@@ -7,7 +7,6 @@ using System.Reflection.Emit;
using System.Text; using System.Text;
using Torch.Managers.PatchManager.Transpile; using Torch.Managers.PatchManager.Transpile;
using Torch.Utils; using Torch.Utils;
using Label = System.Windows.Controls.Label;
namespace Torch.Managers.PatchManager.MSIL namespace Torch.Managers.PatchManager.MSIL
{ {

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Torch.Managers.PatchManager namespace Torch.Managers.PatchManager
@@ -6,14 +7,12 @@ namespace Torch.Managers.PatchManager
/// <summary> /// <summary>
/// Represents a set of common patches that can all be reversed in a single step. /// Represents a set of common patches that can all be reversed in a single step.
/// </summary> /// </summary>
public class PatchContext public sealed class PatchContext
{ {
private readonly PatchManager _replacer;
private readonly Dictionary<MethodBase, MethodRewritePattern> _rewritePatterns = new Dictionary<MethodBase, MethodRewritePattern>(); private readonly Dictionary<MethodBase, MethodRewritePattern> _rewritePatterns = new Dictionary<MethodBase, MethodRewritePattern>();
internal PatchContext(PatchManager replacer) internal PatchContext()
{ {
_replacer = replacer;
} }
/// <summary> /// <summary>
@@ -25,12 +24,22 @@ namespace Torch.Managers.PatchManager
{ {
if (_rewritePatterns.TryGetValue(method, out MethodRewritePattern pattern)) if (_rewritePatterns.TryGetValue(method, out MethodRewritePattern pattern))
return pattern; return pattern;
MethodRewritePattern parent = _replacer.GetPattern(method); MethodRewritePattern parent = PatchManager.GetPatternInternal(method);
var res = new MethodRewritePattern(parent); var res = new MethodRewritePattern(parent);
_rewritePatterns.Add(method, res); _rewritePatterns.Add(method, res);
return 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() internal void RemoveAll()
{ {
foreach (MethodRewritePattern pattern in _rewritePatterns.Values) foreach (MethodRewritePattern pattern in _rewritePatterns.Values)

View File

@@ -1,6 +1,10 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using NLog;
using Torch.API; using Torch.API;
using Torch.Managers.PatchManager.Transpile;
namespace Torch.Managers.PatchManager namespace Torch.Managers.PatchManager
{ {
@@ -9,16 +13,69 @@ namespace Torch.Managers.PatchManager
/// </summary> /// </summary>
public class PatchManager : Manager 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> /// <summary>
/// Creates a new patch manager. Only have one active at a time. /// Creates a new patch manager.
/// </summary> /// </summary>
/// <param name="torchInstance"></param> /// <param name="torchInstance"></param>
public PatchManager(ITorchBase torchInstance) : base(torchInstance) public PatchManager(ITorchBase torchInstance) : base(torchInstance)
{ {
} }
private readonly Dictionary<MethodBase, DecoratedMethod> _rewritePatterns = new Dictionary<MethodBase, DecoratedMethod>(); private static readonly Dictionary<MethodBase, DecoratedMethod> _rewritePatterns = new Dictionary<MethodBase, DecoratedMethod>();
private readonly HashSet<PatchContext> _contexts = new HashSet<PatchContext>(); 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> /// <summary>
/// Gets the rewrite pattern for the given method, creating one if it doesn't exist. /// 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> /// <returns></returns>
public MethodRewritePattern GetPattern(MethodBase method) public MethodRewritePattern GetPattern(MethodBase method)
{ {
if (_rewritePatterns.TryGetValue(method, out DecoratedMethod pattern)) return GetPatternInternal(method);
return pattern;
var res = new DecoratedMethod(method);
_rewritePatterns.Add(method, res);
return res;
} }
/// <summary> /// <summary>
/// Creates a new <see cref="PatchContext"/> used for tracking changes. A call to <see cref="Commit"/> will apply the patches. /// Creates a new <see cref="PatchContext"/> used for tracking changes. A call to <see cref="Commit"/> will apply the patches.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public PatchContext AcquireContext() public PatchContext AcquireContext()
{ {
var context = new PatchContext(this); Assembly assembly = Assembly.GetCallingAssembly();
_contexts.Add(context); 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; 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. /// Frees the given context, and unregister all patches from it. A call to <see cref="Commit"/> will apply the unpatching operation.
/// </summary> /// </summary>
/// <param name="context">Context to remove</param> /// <param name="context">Context to remove</param>
[MethodImpl(MethodImplOptions.NoInlining)]
public void FreeContext(PatchContext context) public void FreeContext(PatchContext context)
{ {
Assembly assembly = Assembly.GetCallingAssembly();
context.RemoveAll(); 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> /// <summary>
@@ -60,8 +161,7 @@ namespace Torch.Managers.PatchManager
/// </summary> /// </summary>
public void Commit() public void Commit()
{ {
foreach (DecoratedMethod m in _rewritePatterns.Values) CommitInternal();
m.Commit();
} }
/// <summary> /// <summary>
@@ -77,10 +177,15 @@ namespace Torch.Managers.PatchManager
/// </summary> /// </summary>
public override void Detach() public override void Detach()
{ {
foreach (DecoratedMethod m in _rewritePatterns.Values) lock (_contexts)
m.Revert(); {
_rewritePatterns.Clear(); foreach (List<PatchContext> set in _contexts.Values)
_contexts.Clear(); foreach (PatchContext ctx in set)
ctx.RemoveAll();
_contexts.Clear();
foreach (DecoratedMethod m in _rewritePatterns.Values)
m.Revert();
}
} }
} }
} }

View 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
{
}
}

View 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;
}
}
}

View File

@@ -239,7 +239,9 @@ namespace Torch.Managers
{ {
var data = new byte[stream.Length]; var data = new byte[stream.Length];
stream.Read(data, 0, data.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]; var data = new byte[entry.Length];
stream.Read(data, 0, data.Length); stream.Read(data, 0, data.Length);
assemblies.Add(Assembly.Load(data)); Assembly asm = Assembly.Load(data);
TorchBase.RegisterAuxAssembly(asm);
} }
} }
} }

View File

@@ -155,11 +155,15 @@
<Link>Properties\AssemblyVersion.cs</Link> <Link>Properties\AssemblyVersion.cs</Link>
</Compile> </Compile>
<Compile Include="Collections\ObservableList.cs" /> <Compile Include="Collections\ObservableList.cs" />
<Compile Include="Event\EventShimAttribute.cs" />
<Compile Include="Managers\ChatManager\ChatManagerClient.cs" /> <Compile Include="Managers\ChatManager\ChatManagerClient.cs" />
<Compile Include="Managers\ChatManager\ChatManagerServer.cs" /> <Compile Include="Managers\ChatManager\ChatManagerServer.cs" />
<Compile Include="Extensions\DispatcherExtensions.cs" /> <Compile Include="Extensions\DispatcherExtensions.cs" />
<Compile Include="Extensions\ICollectionExtensions.cs" /> <Compile Include="Extensions\ICollectionExtensions.cs" />
<Compile Include="Managers\DependencyManager.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\KeenLogManager.cs" />
<Compile Include="Managers\PatchManager\AssemblyMemory.cs" /> <Compile Include="Managers\PatchManager\AssemblyMemory.cs" />
<Compile Include="Managers\PatchManager\DecoratedMethod.cs" /> <Compile Include="Managers\PatchManager\DecoratedMethod.cs" />
@@ -175,12 +179,14 @@
<Compile Include="Managers\PatchManager\MSIL\MsilOperandInline.cs" /> <Compile Include="Managers\PatchManager\MSIL\MsilOperandInline.cs" />
<Compile Include="Managers\PatchManager\MSIL\MsilOperandSwitch.cs" /> <Compile Include="Managers\PatchManager\MSIL\MsilOperandSwitch.cs" />
<Compile Include="Managers\PatchManager\MethodRewritePattern.cs" /> <Compile Include="Managers\PatchManager\MethodRewritePattern.cs" />
<Compile Include="Managers\PatchManager\PatchShimAttribute.cs" />
<Compile Include="Managers\PatchManager\PatchContext.cs" /> <Compile Include="Managers\PatchManager\PatchContext.cs" />
<Compile Include="Managers\PatchManager\PatchManager.cs" /> <Compile Include="Managers\PatchManager\PatchManager.cs" />
<Compile Include="Managers\PatchManager\PatchPriorityAttribute.cs" /> <Compile Include="Managers\PatchManager\PatchPriorityAttribute.cs" />
<Compile Include="Managers\PatchManager\Transpile\LoggingILGenerator.cs" /> <Compile Include="Managers\PatchManager\Transpile\LoggingILGenerator.cs" />
<Compile Include="Managers\PatchManager\Transpile\MethodContext.cs" /> <Compile Include="Managers\PatchManager\Transpile\MethodContext.cs" />
<Compile Include="Managers\PatchManager\Transpile\MethodTranspiler.cs" /> <Compile Include="Managers\PatchManager\Transpile\MethodTranspiler.cs" />
<Compile Include="Patches\GameStatePatchShim.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SaveGameStatus.cs" /> <Compile Include="SaveGameStatus.cs" />
<Compile Include="Collections\KeyTree.cs" /> <Compile Include="Collections\KeyTree.cs" />
@@ -208,6 +214,7 @@
<Compile Include="Plugins\PluginManifest.cs" /> <Compile Include="Plugins\PluginManifest.cs" />
<Compile Include="Utils\Reflection.cs" /> <Compile Include="Utils\Reflection.cs" />
<Compile Include="Managers\ScriptingManager.cs" /> <Compile Include="Managers\ScriptingManager.cs" />
<Compile Include="Utils\StringUtils.cs" />
<Compile Include="Utils\TorchAssemblyResolver.cs" /> <Compile Include="Utils\TorchAssemblyResolver.cs" />
<Compile Include="Utils\ReflectedManager.cs" /> <Compile Include="Utils\ReflectedManager.cs" />
<Compile Include="Session\TorchSessionManager.cs" /> <Compile Include="Session\TorchSessionManager.cs" />
@@ -215,6 +222,7 @@
<Compile Include="SteamService.cs" /> <Compile Include="SteamService.cs" />
<Compile Include="TorchPluginBase.cs" /> <Compile Include="TorchPluginBase.cs" />
<Compile Include="Session\TorchSession.cs" /> <Compile Include="Session\TorchSession.cs" />
<Compile Include="Utils\TorchLauncher.cs" />
<Compile Include="ViewModels\ModViewModel.cs" /> <Compile Include="ViewModels\ModViewModel.cs" />
<Compile Include="Collections\MTObservableCollection.cs" /> <Compile Include="Collections\MTObservableCollection.cs" />
<Compile Include="Extensions\MyPlayerCollectionExtensions.cs" /> <Compile Include="Extensions\MyPlayerCollectionExtensions.cs" />

View File

@@ -22,9 +22,11 @@ using Torch.API.Managers;
using Torch.API.ModAPI; using Torch.API.ModAPI;
using Torch.API.Session; using Torch.API.Session;
using Torch.Commands; using Torch.Commands;
using Torch.Event;
using Torch.Managers; using Torch.Managers;
using Torch.Managers.ChatManager; using Torch.Managers.ChatManager;
using Torch.Managers.PatchManager; using Torch.Managers.PatchManager;
using Torch.Patches;
using Torch.Utils; using Torch.Utils;
using Torch.Session; using Torch.Session;
using VRage.Collections; using VRage.Collections;
@@ -45,8 +47,13 @@ namespace Torch
{ {
static TorchBase() static TorchBase()
{ {
// We can safely never detach this since we don't reload assemblies. ReflectedManager.Process(typeof(TorchBase).Assembly);
new ReflectedManager().Attach(); ReflectedManager.Process(typeof(ITorchBase).Assembly);
PatchManager.AddPatchShim(typeof(GameStatePatchShim));
PatchManager.CommitInternal();
RegisterCoreAssembly(typeof(ITorchBase).Assembly);
RegisterCoreAssembly(typeof(TorchBase).Assembly);
RegisterCoreAssembly(Assembly.GetEntryAssembly());
} }
/// <summary> /// <summary>
@@ -100,6 +107,7 @@ namespace Torch
/// <exception cref="InvalidOperationException">Thrown if a TorchBase instance already exists.</exception> /// <exception cref="InvalidOperationException">Thrown if a TorchBase instance already exists.</exception>
protected TorchBase() protected TorchBase()
{ {
RegisterCoreAssembly(GetType().Assembly);
if (Instance != null) if (Instance != null)
throw new InvalidOperationException("A TorchBase instance already exists."); throw new InvalidOperationException("A TorchBase instance already exists.");
@@ -120,20 +128,11 @@ namespace Torch
sessionManager.AddFactory((x) => new EntityManager(this)); sessionManager.AddFactory((x) => new EntityManager(this));
Managers.AddManager(sessionManager); Managers.AddManager(sessionManager);
var patcher = new PatchManager(this); Managers.AddManager(new PatchManager(this));
GameStateInjector.Inject(patcher.AcquireContext());
Managers.AddManager(patcher);
// Managers.AddManager(new KeenLogManager(this));
Managers.AddManager(new FilesystemManager(this)); Managers.AddManager(new FilesystemManager(this));
Managers.AddManager(new UpdateManager(this)); Managers.AddManager(new UpdateManager(this));
Managers.AddManager(new EventManager(this));
Managers.AddManager(Plugins); 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; TorchAPI.Instance = this;
} }
@@ -390,7 +389,7 @@ namespace Torch
/// <inheritdoc /> /// <inheritdoc />
public virtual void Update() public virtual void Update()
{ {
GetManager<IPluginManager>().UpdatePlugins(); Managers.GetManager<IPluginManager>().UpdatePlugins();
} }
@@ -400,7 +399,7 @@ namespace Torch
public TorchGameState GameState public TorchGameState GameState
{ {
get => _gameState; get => _gameState;
private set internal set
{ {
_gameState = value; _gameState = value;
GameStateChanged?.Invoke(MySandboxGame.Static, _gameState); GameStateChanged?.Invoke(MySandboxGame.Static, _gameState);
@@ -410,71 +409,37 @@ namespace Torch
/// <inheritdoc/> /// <inheritdoc/>
public event TorchGameStateChangedDel GameStateChanged; public event TorchGameStateChangedDel GameStateChanged;
#region GameStateInjecting private static readonly HashSet<Assembly> _registeredCoreAssemblies = new HashSet<Assembly>();
private static class GameStateInjector /// <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 lock (_registeredCoreAssemblies)
[ReflectedMethodInfo(typeof(MySandboxGame), nameof(MySandboxGame.Dispose))] if (_registeredCoreAssemblies.Add(asm))
private static MethodInfo _sandboxGameDispose; {
[ReflectedMethodInfo(typeof(MySandboxGame), "Initialize")] ReflectedManager.Process(asm);
private static MethodInfo _sandboxGameInit; EventManager.AddDispatchShims(asm);
#pragma warning restore 649 PatchManager.AddPatchShims(asm);
}
internal static void Inject(PatchContext target) }
{
ConstructorInfo ctor = typeof(MySandboxGame).GetConstructor(new[] {typeof(string[])}); private static readonly HashSet<Assembly> _registeredAuxAssemblies = new HashSet<Assembly>();
if (ctor == null)
throw new ArgumentException("Can't find constructor MySandboxGame(string[])"); /// <summary>
target.GetPattern(ctor).Prefixes.Add(MethodRef(nameof(PrefixConstructor))); /// Registers an auxillary (plugin) assembly with the system, including its
target.GetPattern(ctor).Suffixes.Add(MethodRef(nameof(SuffixConstructor))); /// <see cref="ReflectedManager"/> related components.
target.GetPattern(_sandboxGameInit).Prefixes.Add(MethodRef(nameof(PrefixInit))); /// </summary>
target.GetPattern(_sandboxGameInit).Suffixes.Add(MethodRef(nameof(SuffixInit))); /// <param name="asm">Assembly to register</param>
target.GetPattern(_sandboxGameDispose).Prefixes.Add(MethodRef(nameof(PrefixDispose))); internal static void RegisterAuxAssembly(Assembly asm)
target.GetPattern(_sandboxGameDispose).Suffixes.Add(MethodRef(nameof(SuffixDispose))); {
} lock (_registeredAuxAssemblies)
if (_registeredAuxAssemblies.Add(asm))
private static MethodInfo MethodRef(string name) {
{ ReflectedManager.Process(asm);
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;
}
} }
#endregion
} }
} }

View File

@@ -398,36 +398,9 @@ namespace Torch.Utils
/// <summary> /// <summary>
/// Automatically calls <see cref="ReflectedManager.Process(Assembly)"/> for every assembly already loaded, and every assembly that is loaded in the future. /// Automatically calls <see cref="ReflectedManager.Process(Assembly)"/> for every assembly already loaded, and every assembly that is loaded in the future.
/// </summary> /// </summary>
public class ReflectedManager public static class ReflectedManager
{ {
private static readonly Logger _log = LogManager.GetCurrentClassLogger(); 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>(); private static readonly HashSet<Type> _processedTypes = new HashSet<Type>();
/// <summary> /// <summary>
@@ -438,11 +411,6 @@ namespace Torch.Utils
{ {
if (_processedTypes.Add(t)) 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 | foreach (FieldInfo field in t.GetFields(BindingFlags.Static | BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic)) BindingFlags.Public | BindingFlags.NonPublic))
{ {
@@ -636,6 +604,7 @@ namespace Torch.Utils
field.SetValue(null, field.SetValue(null,
Expression.Lambda(Expression.Call(paramExp[0], methodInstance, argExp), paramExp) Expression.Lambda(Expression.Call(paramExp[0], methodInstance, argExp), paramExp)
.Compile()); .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()); field.SetValue(null, Expression.Lambda(impl, paramExp).Compile());
_log.Trace($"Reflecting field {field.DeclaringType?.FullName}#{field.Name} with {field.DeclaringType?.FullName}#{field.Name}");
} }
} }
} }

View 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() ?? "";
}
}
}

View File

@@ -42,23 +42,16 @@ namespace Torch.Utils
{ {
string assemblyName = new AssemblyName(args.Name).Name; string assemblyName = new AssemblyName(args.Name).Name;
lock (_assemblies) lock (_assemblies)
{
if (_assemblies.TryGetValue(assemblyName, out Assembly asm)) if (_assemblies.TryGetValue(assemblyName, out Assembly asm))
return asm; return asm;
} foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
lock (AppDomain.CurrentDomain) if (asm.GetName().Name.Equals(assemblyName))
{ {
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) lock (_assemblies)
if (asm.GetName().Name.Equals(assemblyName)) _assemblies.Add(assemblyName, asm);
{ return asm;
lock (this) }
{ lock (_assemblies)
_assemblies.Add(assemblyName, asm);
return asm;
}
}
}
lock (this)
{ {
foreach (string path in _paths) foreach (string path in _paths)
{ {

View 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);
}
}
}