diff --git a/Barotrauma/BarotraumaShared/LuaCsDependencies.props b/Barotrauma/BarotraumaShared/LuaCsDependencies.props index 21439206a..dde50c674 100644 --- a/Barotrauma/BarotraumaShared/LuaCsDependencies.props +++ b/Barotrauma/BarotraumaShared/LuaCsDependencies.props @@ -3,6 +3,6 @@ - + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs index 08729c8f2..344856c46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs @@ -91,7 +91,7 @@ namespace Barotrauma } // Copied from https://github.com/evilfactory/moonsharp/blob/5264656c6442e783f3c75082cce69a93d66d4cc0/src/MoonSharp.Interpreter/Interop/Converters/ScriptToClrConversions.cs#L79-L99 - private static MethodInfo HasImplicitConversion(Type baseType, Type targetType) + private static MethodInfo GetImplicitOperatorMethod(Type baseType, Type targetType) { try { @@ -101,12 +101,12 @@ namespace Barotrauma { if (baseType.BaseType != null) { - return HasImplicitConversion(baseType.BaseType, targetType); + return GetImplicitOperatorMethod(baseType.BaseType, targetType); } if (targetType.BaseType != null) { - return HasImplicitConversion(baseType, targetType.BaseType); + return GetImplicitOperatorMethod(baseType, targetType.BaseType); } return null; @@ -141,18 +141,18 @@ namespace Barotrauma il.Call(typeof(object).GetMethod("GetType")); il.StoreLocal(baseType); - // IL: var implicitConversionMethod = SigilExtensions.HasImplicitConversion(baseType, ); - var implicitConversionMethod = il.DeclareLocal(typeof(MethodInfo), $"cast_implicitConversionMethod_{guid}"); + // IL: var implicitOperatorMethod = SigilExtensions.GetImplicitOperatorMethod(baseType, ); + var implicitOperatorMethod = il.DeclareLocal(typeof(MethodInfo), $"cast_implicitOperatorMethod_{guid}"); il.LoadLocal(baseType); il.LoadType(targetType); - il.Call(typeof(SigilExtensions).GetMethod(nameof(HasImplicitConversion), BindingFlags.NonPublic | BindingFlags.Static)); - il.StoreLocal(implicitConversionMethod); + il.Call(typeof(SigilExtensions).GetMethod(nameof(GetImplicitOperatorMethod), BindingFlags.NonPublic | BindingFlags.Static)); + il.StoreLocal(implicitOperatorMethod); // IL: castValue; var castValue = il.DeclareLocal(targetType, $"cast_castValue_{guid}"); // IL: if (implicitConversionMethod != null) - il.LoadLocal(implicitConversionMethod); + il.LoadLocal(implicitOperatorMethod); il.Branch((il) => { // IL: var methodInvokeParams = new object[1]; @@ -168,7 +168,7 @@ namespace Barotrauma il.StoreElement(); // IL: castValue = ()implicitConversionMethod.Invoke(null, methodInvokeParams); - il.LoadLocal(implicitConversionMethod); + il.LoadLocal(implicitOperatorMethod); il.LoadNull(); // first parameter is null because implicit cast operators are static il.LoadLocal(methodInvokeParams); il.Call(typeof(MethodInfo).GetMethod("Invoke", new[] { typeof(object), typeof(object[]) })); @@ -519,9 +519,9 @@ namespace Barotrauma "ContentPackageManager", }; - private static void ValidatePatchTarget(MethodInfo methodInfo) + private static void ValidatePatchTarget(MethodBase method) { - if (prohibitedHooks.Any(h => methodInfo.DeclaringType.FullName.StartsWith(h))) + if (prohibitedHooks.Any(h => method.DeclaringType.FullName.StartsWith(h))) { throw new ArgumentException("Hooks into the modding environment are prohibited."); } @@ -575,7 +575,7 @@ namespace Barotrauma return !(left == right); } - public static MethodKey Create(MethodInfo method) => new MethodKey + public static MethodKey Create(MethodBase method) => new MethodKey { ModuleHandle = method.Module.ModuleHandle, MetadataToken = method.MetadataToken, @@ -814,30 +814,37 @@ namespace Barotrauma public object Call(string name, params object[] args) => Call(name, args); - private static MethodInfo ResolveMethod(string className, string methodName, string[] parameterNames) + private static MethodBase ResolveMethod(string className, string methodName, string[] parameters) { var classType = LuaUserData.GetType(className); if (classType == null) throw new InvalidOperationException($"Invalid class name '{className}'"); const BindingFlags BINDING_FLAGS = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - MethodInfo methodInfo = null; - if (parameterNames != null) + const string CTOR = ".ctor"; + + MethodBase method = null; + if (parameters != null) { - var parameterTypes = parameterNames.Select(x => LuaUserData.GetType(x)).ToArray(); - methodInfo = classType.GetMethod(methodName, BINDING_FLAGS, null, parameterTypes, null); + var parameterTypes = parameters.Select(x => LuaUserData.GetType(x)).ToArray(); + // TODO: remove the casts once we can use C# 9 features + method = methodName == CTOR + ? (MethodBase)classType.GetConstructor(BINDING_FLAGS, null, parameterTypes, null) + : (MethodBase)classType.GetMethod(methodName, BINDING_FLAGS, null, parameterTypes, null); } else { - methodInfo = classType.GetMethod(methodName, BINDING_FLAGS); + method = methodName == CTOR + ? (MethodBase)classType.GetConstructor(BINDING_FLAGS, null, Array.Empty(), null) + : (MethodBase)classType.GetMethod(methodName, BINDING_FLAGS); } - if (methodInfo == null) + if (method == null) { - var parameterNamesStr = parameterNames == null ? "" : string.Join(", ", parameterNames); + var parameterNamesStr = parameters == null ? "" : string.Join(", ", parameters); throw new InvalidOperationException($"Method '{methodName}({parameterNamesStr})' not found in class '{className}'"); } - return methodInfo; + return method; } private class DynamicParameterMapping @@ -863,7 +870,7 @@ namespace Barotrauma // If you need to debug this: // - use https://sharplab.io ; it's a very useful for resource for writing IL by hand. // - use il.NewMessage("") or il.WriteLine("") to see where the IL crashes at runtime. - private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodInfo original, HookMethodType hookType) + private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase original, HookMethodType hookType) { var parameters = new List { @@ -871,7 +878,7 @@ namespace Barotrauma new DynamicParameterMapping("__instance", null, typeof(object)), }; - var hasReturnType = original.ReturnType != typeof(void); + var hasReturnType = original is MethodInfo mi && mi.ReturnType != typeof(void); if (hasReturnType) { parameters.Add(new DynamicParameterMapping("__result", null, typeof(object).MakeByRefType())); @@ -919,7 +926,7 @@ namespace Barotrauma // IL: var patchKey = MethodKey.Create(__originalMethod); var patchKey = il.DeclareLocal("patchKey"); il.LoadArgument(0); // load __originalMethod - il.CastClass(); + il.CastClass(); il.Call(typeof(MethodKey).GetMethod(nameof(MethodKey.Create))); il.StoreLocal(patchKey); @@ -1032,10 +1039,10 @@ namespace Barotrauma { // IL: var csReturnType = Type.GetTypeFromHandle(); var csReturnType = il.DeclareLocal("csReturnType"); - il.LoadType(original.ReturnType); + il.LoadType(((MethodInfo)original).ReturnType); il.StoreLocal(csReturnType); - // IL: var csReturnValue = luaReturnValue.ToObject(csReturnValueType); + // IL: var csReturnValue = luaReturnValue.ToObject(csReturnType); var csReturnValue = il.DeclareLocal("csReturnValue"); il.LoadLocal(luaReturnValue); il.LoadLocal(csReturnType); @@ -1149,7 +1156,7 @@ namespace Barotrauma return type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static); } - private string Patch(string identifier, MethodInfo method, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) + private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) { if (method == null) throw new ArgumentNullException(nameof(method)); if (patch == null) throw new ArgumentNullException(nameof(patch)); @@ -1199,29 +1206,29 @@ namespace Barotrauma public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) { - var methodInfo = ResolveMethod(className, methodName, parameterTypes); - return Patch(identifier, methodInfo, patch, hookType); + var method = ResolveMethod(className, methodName, parameterTypes); + return Patch(identifier, method, patch, hookType); } public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) { - var methodInfo = ResolveMethod(className, methodName, null); - return Patch(identifier, methodInfo, patch, hookType); + var method = ResolveMethod(className, methodName, null); + return Patch(identifier, method, patch, hookType); } public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) { - var methodInfo = ResolveMethod(className, methodName, parameterTypes); - return Patch(null, methodInfo, patch, hookType); + var method = ResolveMethod(className, methodName, parameterTypes); + return Patch(null, method, patch, hookType); } public string Patch(string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before) { - var methodInfo = ResolveMethod(className, methodName, null); - return Patch(null, methodInfo, patch, hookType); + var method = ResolveMethod(className, methodName, null); + return Patch(null, method, patch, hookType); } - private bool RemovePatch(string identifier, MethodInfo method, HookMethodType hookType) + private bool RemovePatch(string identifier, MethodBase method, HookMethodType hookType) { if (identifier == null) throw new ArgumentNullException(nameof(identifier)); identifier = NormalizeIdentifier(identifier); @@ -1242,14 +1249,14 @@ namespace Barotrauma public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, HookMethodType hookType) { - var methodInfo = ResolveMethod(className, methodName, parameterTypes); - return RemovePatch(identifier, methodInfo, hookType); + var method = ResolveMethod(className, methodName, parameterTypes); + return RemovePatch(identifier, method, hookType); } public bool RemovePatch(string identifier, string className, string methodName, HookMethodType hookType) { - var methodInfo = ResolveMethod(className, methodName, null); - return RemovePatch(identifier, methodInfo, hookType); + var method = ResolveMethod(className, methodName, null); + return RemovePatch(identifier, method, hookType); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs index b3a4eb9be..dd8f39261 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHookCompat.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Linq; using System.Reflection; using HarmonyLib; using System.Collections.Generic; using MoonSharp.Interpreter; -using static Barotrauma.LuaCsSetup; using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch; namespace Barotrauma @@ -122,7 +121,7 @@ namespace Barotrauma private static MethodInfo _miHookLuaCsPatchRetPostfix = typeof(LuaCsHook).GetMethod("HookLuaCsPatchRetPostfix", BindingFlags.NonPublic | BindingFlags.Static); // TODO: deprecate this - public void HookMethod(string identifier, MethodInfo method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, ACsMod owner = null) + public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, ACsMod owner = null) { if (identifier == null || method == null || patch == null) { @@ -136,7 +135,7 @@ namespace Barotrauma if (hookType == HookMethodType.Before) { - if (method.ReturnType != typeof(void)) + if (method is MethodInfo mi && mi.ReturnType != typeof(void)) { if (patches == null || patches.Prefixes == null || patches.Prefixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchRetPrefix) == null) { @@ -168,7 +167,7 @@ namespace Barotrauma } else if (hookType == HookMethodType.After) { - if (method.ReturnType != typeof(void)) + if (method is MethodInfo mi && mi.ReturnType != typeof(void)) { if (patches == null || patches.Postfixes == null || patches.Postfixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchRetPostfix) == null) { @@ -200,13 +199,13 @@ namespace Barotrauma } protected void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, HookMethodType hookMethodType = HookMethodType.Before) { - var methodInfo = ResolveMethod(className, methodName, parameterNames); - if (methodInfo == null) return; - if (methodInfo.GetParameters().Any(x => x.ParameterType.IsByRef)) + var method = ResolveMethod(className, methodName, parameterNames); + if (method == null) return; + if (method.GetParameters().Any(x => x.ParameterType.IsByRef)) { throw new InvalidOperationException($"{nameof(HookMethod)} doesn't support ByRef parameters; use {nameof(Patch)} instead."); } - HookMethod(identifier, methodInfo, patch, hookMethodType); + HookMethod(identifier, method, patch, hookMethodType); } protected void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, HookMethodType hookMethodType = HookMethodType.Before) => HookMethod(identifier, className, methodName, null, patch, hookMethodType); @@ -216,9 +215,9 @@ namespace Barotrauma HookMethod("", className, methodName, parameterNames, patch, hookMethodType); - public void UnhookMethod(string identifier, MethodInfo method, HookMethodType hookType = HookMethodType.Before) + public void UnhookMethod(string identifier, MethodBase method, HookMethodType hookType = HookMethodType.Before) { - var funcAddr = ((long)method.MethodHandle.GetFunctionPointer()); + var funcAddr = (long)method.MethodHandle.GetFunctionPointer(); Dictionary> methods; if (hookType == HookMethodType.Before) methods = compatHookPrefixMethods; @@ -229,9 +228,9 @@ namespace Barotrauma } protected void UnhookMethod(string identifier, string className, string methodName, string[] parameterNames, HookMethodType hookType = HookMethodType.Before) { - var methodInfo = ResolveMethod(className, methodName, parameterNames); - if (methodInfo == null) return; - UnhookMethod(identifier, methodInfo, hookType); + var method = ResolveMethod(className, methodName, parameterNames); + if (method == null) return; + UnhookMethod(identifier, method, hookType); } } -} \ No newline at end of file +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index d473f6a9d..29ff74305 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -320,6 +320,10 @@ namespace Barotrauma public DynValue CallLuaFunction(object function, params object[] args) { + // XXX: `lua` might be null if `LuaCsSetup.Stop()` is called while + // a patched function is still running. + if (lua == null) return null; + lock (lua) { try diff --git a/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs b/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs new file mode 100644 index 000000000..632ceca38 --- /dev/null +++ b/Barotrauma/BarotraumaTest/LuaCs/HookPatchHelpers.cs @@ -0,0 +1,159 @@ +using Barotrauma; +using MoonSharp.Interpreter; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Xunit; + +namespace TestProject.LuaCs +{ + internal static class HookPatchHelpers + { + public class PatchHandle : IDisposable + { + private readonly Action disposeAction; + + public PatchHandle(string patchId, Action disposeAction) + { + this.disposeAction = disposeAction; + this.PatchId = patchId; + } + + public string PatchId { get; } + + public void Dispose() => disposeAction(); + } + + private static List BuildHookPatchArgsList( + string? patchId, + string className, + string methodName, + string[]? parameters) + { + static string Stringify(object value) => + "\"" + value.ToString()!.Replace(@"\", @"\\").Replace("\"", "\\\"") + "\""; + + var args = new List(); + if (patchId != null) args.Add(Stringify(patchId)); + args.Add(Stringify(className)); + args.Add(Stringify(methodName)); + if (parameters != null && parameters.Length > 0) + { + var sb = new StringBuilder(); + sb.Append("{ "); + foreach (var param in parameters) + { + sb.Append(Stringify(param)); + sb.Append(", "); + } + sb.Append(" }"); + args.Add(sb.ToString()); + } + return args; + } + + private static DynValue DoHookPatch( + this LuaCsSetup luaCs, + string? patchId, + string className, + string methodName, + string[]? parameters, + string function, + LuaCsHook.HookMethodType patchType) + { + var args = BuildHookPatchArgsList(patchId, className, methodName, parameters); + args.Add(function); + args.Add(patchType switch + { + LuaCsHook.HookMethodType.Before => "Hook.HookMethodType.Before", + LuaCsHook.HookMethodType.After => "Hook.HookMethodType.After", + _ => throw new NotImplementedException(), + }); + return luaCs.Lua.DoString($"return Hook.Patch({string.Join(", ", args)})"); + } + + private static DynValue DoHookRemovePatch( + this LuaCsSetup luaCs, + string? patchId, + string className, + string methodName, + string[]? parameters, + LuaCsHook.HookMethodType patchType) + { + var args = BuildHookPatchArgsList(patchId, className, methodName, parameters); + args.Add(patchType switch + { + LuaCsHook.HookMethodType.Before => "Hook.HookMethodType.Before", + LuaCsHook.HookMethodType.After => "Hook.HookMethodType.After", + _ => throw new NotImplementedException(), + }); + return luaCs.Lua.DoString($"return Hook.RemovePatch({string.Join(", ", args)})"); + } + + public static PatchHandle AddPrefix(this LuaCsSetup luaCs, string body, string methodName, string[]? parameters = null, string? patchId = null) + { + var className = typeof(T).FullName!; + var returnValue = luaCs.DoHookPatch(patchId, className, methodName, parameters, @$" + function(instance, ptable) + {body} + end + ", LuaCsHook.HookMethodType.Before); + Assert.Equal(DataType.String, returnValue.Type); + return new(returnValue.String, () => luaCs.RemovePrefix(returnValue.String, methodName)); + } + + public static PatchHandle AddPostfix(this LuaCsSetup luaCs, string body, string methodName, string[]? parameters = null, string? patchId = null) + { + var className = typeof(T).FullName!; + var returnValue = luaCs.DoHookPatch(patchId, className, methodName, parameters, @$" + function(instance, ptable) + {body} + end + ", LuaCsHook.HookMethodType.After); + Assert.Equal(DataType.String, returnValue.Type); + return new(returnValue.String, () => luaCs.RemovePostfix(returnValue.String, methodName)); + } + + public static bool RemovePrefix(this LuaCsSetup luaCs, string patchId, string methodName, string[]? parameters = null) + { + var className = typeof(T).FullName!; + var returnValue = luaCs.DoHookRemovePatch(patchId, className, methodName, parameters, LuaCsHook.HookMethodType.Before); + Assert.Equal(DataType.Boolean, returnValue.Type); + return returnValue.Boolean; + } + + public static bool RemovePostfix(this LuaCsSetup luaCs, string patchId, string methodName, string[]? parameters = null) + { + var className = typeof(T).FullName!; + var returnValue = luaCs.DoHookRemovePatch(patchId, className, methodName, parameters, LuaCsHook.HookMethodType.After); + Assert.Equal(DataType.Boolean, returnValue.Type); + return returnValue.Boolean; + } + + public class PatchTargetHandle : IDisposable + { + private readonly SemaphoreSlim @lock; + + public PatchTargetHandle(SemaphoreSlim @lock) + { + this.@lock = @lock; + } + + public void Dispose() => @lock.Release(); + } + + private static readonly ConcurrentDictionary PatchTargetLocks = new(); + + public static PatchTargetHandle LockPatchTarget() + { + if (!PatchTargetLocks.TryGetValue(typeof(T), out var @lock)) + { + PatchTargetLocks[typeof(T)] = @lock = new SemaphoreSlim(1); + } + @lock.Wait(); + return new(@lock); + } + } +} diff --git a/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs b/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs index 9a27df57c..bd69ff172 100644 --- a/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs +++ b/Barotrauma/BarotraumaTest/LuaCs/HookPatchTests.cs @@ -1,104 +1,45 @@ using Barotrauma; using Microsoft.Xna.Framework; using MoonSharp.Interpreter; -using System.Runtime.ExceptionServices; using Xunit; using Xunit.Abstractions; namespace TestProject.LuaCs { - public class HookPatchTests + [Collection("LuaCs")] + public class HookPatchTests : IClassFixture { - private readonly LuaCsSetup luaCs = new(); + private readonly LuaCsSetup luaCs; - public HookPatchTests(ITestOutputHelper output) + public HookPatchTests(LuaCsFixture luaCsFixture, ITestOutputHelper output) { - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); + // XXX: we can't have multiple instances of LuaCs patching the + // same methods, otherwise we get script ownership exceptions. + luaCs = luaCsFixture.LuaCs; luaCs.MessageLogger = (prefix, o) => { o ??= "null"; - output.WriteLine(prefix + o); - }; - luaCs.ExceptionHandler = (ex, _) => - { - // Pretend we never caught the exception in the first place - // (this allows us to preserve the stack trace) - var di = ExceptionDispatchInfo.Capture(ex); - di.Throw(); + output?.WriteLine(prefix + o); }; + + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + UserData.RegisterType(); + luaCs.Initialize(); luaCs.Lua.Globals["TestValueType"] = UserData.CreateStatic(); luaCs.Lua.Globals["InterfaceImplementingType"] = UserData.CreateStatic(); } - private DynValue AddPrefix(string body, string testMethod = "Run", string? patchId = null) - { - var className = typeof(T).FullName; - if (patchId != null) - { - return luaCs.Lua.DoString(@$" - return Hook.Patch('{patchId}', '{className}', '{testMethod}', function(instance, ptable) - {body} - end, Hook.HookMethodType.Before) - "); - } - else - { - return luaCs.Lua.DoString(@$" - return Hook.Patch('{className}', '{testMethod}', function(instance, ptable) - {body} - end, Hook.HookMethodType.Before) - "); - } - } - - private DynValue AddPostfix(string body, string testMethod = "Run", string? patchId = null) - { - var className = typeof(T).FullName; - if (patchId != null) - { - return luaCs.Lua.DoString(@$" - return Hook.Patch('{patchId}', '{className}', '{testMethod}', function(instance, ptable) - {body} - end, Hook.HookMethodType.After) - "); - } - else - { - return luaCs.Lua.DoString(@$" - return Hook.Patch('{className}', '{testMethod}', function(instance, ptable) - {body} - end, Hook.HookMethodType.After) - "); - } - } - - private DynValue RemovePrefix(string patchName, string testMethod = "Run") - { - var className = typeof(T).FullName; - return luaCs.Lua.DoString($@" - return Hook.RemovePatch('{patchName}', '{className}', '{testMethod}', Hook.HookMethodType.Before) - "); - } - - private DynValue RemovePostfix(string patchName, string testMethod = "Run") - { - var className = typeof(T).FullName; - return luaCs.Lua.DoString($@" - return Hook.RemovePatch('{patchName}', '{className}', '{testMethod}', Hook.HookMethodType.After) - "); - } - - public class PatchTarget1 + private class PatchTargetSimple { public bool ran; @@ -111,8 +52,11 @@ namespace TestProject.LuaCs [Fact] public void TestFullMethodReplacement() { - var target = new PatchTarget1(); - AddPrefix("ptable.PreventExecution = true"); + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetSimple(); + using var patchHandle = luaCs.AddPrefix(@" + ptable.PreventExecution = true + ", nameof(PatchTargetSimple.Run)); target.Run(); Assert.False(target.ran); } @@ -120,11 +64,12 @@ namespace TestProject.LuaCs [Fact] public void TestOverrideExistingPatch() { - var target = new PatchTarget1(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetSimple(); + using var patchHandle = luaCs.AddPrefix(@" ptable.PreventExecution = true originalPatchRan = true - ", patchId: "test"); + ", nameof(PatchTargetSimple.Run), patchId: "test"); target.Run(); Assert.False(target.ran); Assert.True(luaCs.Lua.Globals["originalPatchRan"] as bool?); @@ -134,7 +79,9 @@ namespace TestProject.LuaCs luaCs.Lua.Globals["originalPatchRan"] = false; // Replace the existing prefix, but don't prevent execution this time - AddPrefix("replacementPatchRan = true", patchId: "test"); + luaCs.AddPrefix(@" + replacementPatchRan = true + ", nameof(PatchTargetSimple.Run), patchId: "test"); target.Run(); Assert.True(target.ran); @@ -148,19 +95,20 @@ namespace TestProject.LuaCs [Fact] public void TestRemovePrefix() { - var target = new PatchTarget1(); - var patchId = AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetSimple(); + using (var patchHandle = luaCs.AddPrefix(@" ptable.PreventExecution = true patchRan = true - "); - target.Run(); - Assert.False(target.ran); - Assert.True(luaCs.Lua.Globals["patchRan"] as bool?); + ", nameof(PatchTargetSimple.Run))) + { + target.Run(); + Assert.False(target.ran); + Assert.True(luaCs.Lua.Globals["patchRan"] as bool?); - luaCs.Lua.Globals["patchRan"] = false; + luaCs.Lua.Globals["patchRan"] = false; + } - Assert.Equal(DataType.String, patchId.Type); - RemovePrefix(patchId.String); target.Run(); Assert.True(target.ran); Assert.False(luaCs.Lua.Globals["patchRan"] as bool?); @@ -169,19 +117,20 @@ namespace TestProject.LuaCs [Fact] public void TestRemovePostfix() { - var target = new PatchTarget1(); - var patchId = AddPostfix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetSimple(); + using (var patchHandle = luaCs.AddPostfix(@" patchRan = true - "); - target.Run(); - Assert.True(target.ran); - Assert.True(luaCs.Lua.Globals["patchRan"] as bool?); + ", nameof(PatchTargetSimple.Run))) + { + target.Run(); + Assert.True(target.ran); + Assert.True(luaCs.Lua.Globals["patchRan"] as bool?); - target.ran = false; - luaCs.Lua.Globals["patchRan"] = false; + target.ran = false; + luaCs.Lua.Globals["patchRan"] = false; + } - Assert.Equal(DataType.String, patchId.Type); - RemovePostfix(patchId.String); target.Run(); Assert.True(target.ran); Assert.False(luaCs.Lua.Globals["patchRan"] as bool?); @@ -197,7 +146,7 @@ namespace TestProject.LuaCs } } - public class PatchTarget2 + private class PatchTargetReturnsObject { public bool ran; @@ -213,7 +162,7 @@ namespace TestProject.LuaCs int GetFoo(); } - public class InterfaceImplementingType : IBogusInterface + private class InterfaceImplementingType : IBogusInterface { private readonly int foo; @@ -228,11 +177,12 @@ namespace TestProject.LuaCs [Fact] public void TestReturnBoxed() { - var target = new PatchTarget2(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetReturnsObject(); + using var patchHandle = luaCs.AddPrefix(@" ptable.PreventExecution = true return 123 - "); + ", nameof(PatchTargetReturnsObject.Run)); var returnValue = target.Run(); Assert.False(target.ran); Assert.Equal(123, (int)(double)returnValue); @@ -241,9 +191,12 @@ namespace TestProject.LuaCs [Fact] public void TestReturnVoid() { - var target = new PatchTarget2(); + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetReturnsObject(); // This should have no effect - AddPrefix("return"); + using var patchHandle = luaCs.AddPrefix(@" + return + ", nameof(PatchTargetReturnsObject.Run)); var returnValue = target.Run(); Assert.True(target.ran); Assert.Equal(5, returnValue); @@ -252,9 +205,12 @@ namespace TestProject.LuaCs [Fact] public void TestReturnNil() { - var target = new PatchTarget2(); + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetReturnsObject(); // This should modify the return value to "null" - AddPostfix("return nil"); + using var patchHandle = luaCs.AddPostfix(@" + return nil + ", nameof(PatchTargetReturnsObject.Run)); var returnValue = target.Run(); Assert.True(target.ran); Assert.Null(returnValue); @@ -263,17 +219,18 @@ namespace TestProject.LuaCs [Fact] public void TestReturnValueType() { - var target = new PatchTarget2(); - AddPostfix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetReturnsObject(); + using var patchHandle = luaCs.AddPostfix(@" return TestValueType.__new(100) - "); + ", nameof(PatchTargetSimple.Run)); var returnValue = target.Run(); Assert.True(target.ran); Assert.IsType(returnValue); Assert.Equal(100, ((TestValueType)returnValue).foo); } - public class PatchTarget3 + private class PatchTargetReturnsInterface { public bool ran; @@ -287,16 +244,17 @@ namespace TestProject.LuaCs [Fact] public void TestReturnInterfaceImplementingType() { - var target = new PatchTarget3(); - AddPostfix(@" - return InterfaceImplementingType.__new(100); - "); + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetReturnsInterface(); + using var patchHandle = luaCs.AddPostfix(@" + return InterfaceImplementingType.__new(100) + ", nameof(PatchTargetReturnsInterface.Run)); var returnValue = target.Run()!; Assert.True(target.ran); Assert.Equal(100, returnValue.GetFoo()); } - public class PatchTarget4 + private class PatchTargetModifyParams { public bool ran; @@ -310,19 +268,20 @@ namespace TestProject.LuaCs [Fact] public void TestModifyParameters() { - var target = new PatchTarget4(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetModifyParams(); + using var patchHandle = luaCs.AddPrefix(@" ptable['a'] = Int32(100) ptable['b'] = 'abc' ptable['refByte'] = Byte(4) - "); + ", nameof(PatchTargetModifyParams.Run)); byte refByte = 123; target.Run(5, out var outString, ref refByte, "foo"); Assert.True(target.ran); Assert.Equal("100abc4", outString); } - public class PatchTarget5 + private class PatchTargetVector2 { public bool ran; @@ -336,15 +295,91 @@ namespace TestProject.LuaCs [Fact] public void TestParameterValueType() { - var target = new PatchTarget5(); - AddPrefix("patchRan = true"); + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetVector2(); + using var patchHandle = luaCs.AddPrefix(@" + patchRan = true + ", nameof(PatchTargetVector2.Run)); var returnValue = target.Run(new Vector2(1, 2)); Assert.True(target.ran); Assert.True(luaCs.Lua.Globals["patchRan"] as bool?); Assert.Equal("{X:1 Y:2}", returnValue); } - public class PatchTarget6 + private class PatchTargetConstructor + { + public enum CtorType + { + None, + Patched, + Default, + Int, + StringString, + } + + public CtorType Ctor { get; set; } + + public bool PrefixRan { get; set; } + + public PatchTargetConstructor() + { + Ctor = CtorType.Default; + } + + public PatchTargetConstructor(int a = default) + { + Ctor = CtorType.Int; + } + + public PatchTargetConstructor(string a, string b) + { + Ctor = CtorType.StringString; + } + } + + [Fact] + public void TestPatchConstructor() + { + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + + { + using var postfixHandle = luaCs.AddPostfix(@$" + instance.Ctor = {(int)PatchTargetConstructor.CtorType.Patched} + ", ".ctor"); + using var prefixHandle = luaCs.AddPrefix(@$" + instance.PrefixRan = true + ", ".ctor"); + var target = new PatchTargetConstructor(); + Assert.Equal(PatchTargetConstructor.CtorType.Patched, target.Ctor); + Assert.True(target.PrefixRan); + } + + { + using var postfixHandle = luaCs.AddPostfix(@$" + instance.Ctor = {(int)PatchTargetConstructor.CtorType.Patched} + ", ".ctor", new[] { typeof(int).FullName! }); + using var prefixHandle = luaCs.AddPrefix(@$" + instance.PrefixRan = true + ", ".ctor", new[] { typeof(int).FullName! }); + var target = new PatchTargetConstructor(1); + Assert.Equal(PatchTargetConstructor.CtorType.Patched, target.Ctor); + Assert.True(target.PrefixRan); + } + + { + using var postfixHandle = luaCs.AddPostfix(@$" + instance.Ctor = {(int)PatchTargetConstructor.CtorType.Patched} + ", ".ctor", new[] { typeof(string).FullName!, typeof(string).FullName! }); + using var prefixHandle = luaCs.AddPrefix(@$" + instance.PrefixRan = true + ", ".ctor", new[] { typeof(string).FullName!, typeof(string).FullName! }); + var target = new PatchTargetConstructor("", ""); + Assert.Equal(PatchTargetConstructor.CtorType.Patched, target.Ctor); + Assert.True(target.PrefixRan); + } + } + + private class PatchTargetNumbers { public bool ran; @@ -410,120 +445,130 @@ namespace TestProject.LuaCs } [Fact] - public void TestCastPrimitiveWrapperSByte() + public void TestCastSByte() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = SByte(-6) - ", testMethod: nameof(PatchTarget6.RunSByte)); + ", nameof(PatchTargetNumbers.RunSByte)); var returnValue = target.RunSByte(-5); Assert.True(target.ran); Assert.Equal(-6, returnValue); } [Fact] - public void TestCastPrimitiveWrapperByte() + public void TestCastByte() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = Byte(6) - ", testMethod: nameof(PatchTarget6.RunByte)); + ", nameof(PatchTargetNumbers.RunByte)); var returnValue = target.RunByte(5); Assert.True(target.ran); Assert.Equal(6, returnValue); } [Fact] - public void TestCastPrimitiveWrapperInt16() + public void TestCastInt16() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = Int16(-25000) - ", testMethod: nameof(PatchTarget6.RunInt16)); + ", nameof(PatchTargetNumbers.RunInt16)); var returnValue = target.RunInt16(30000); Assert.True(target.ran); Assert.Equal(-25000, returnValue); } [Fact] - public void TestCastPrimitiveWrapperUInt16() + public void TestCastUInt16() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = UInt16(60000) - ", testMethod: nameof(PatchTarget6.RunUInt16)); + ", nameof(PatchTargetNumbers.RunUInt16)); var returnValue = target.RunUInt16(50000); Assert.True(target.ran); Assert.Equal(60000, returnValue); } [Fact] - public void TestCastPrimitiveWrapperInt32() + public void TestCastInt32() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = Int32('7FFFFF00', 16) - ", testMethod: nameof(PatchTarget6.RunInt32)); + ", nameof(PatchTargetNumbers.RunInt32)); var returnValue = target.RunInt32(900000); Assert.True(target.ran); Assert.Equal(0x7FFFFF00, returnValue); } [Fact] - public void TestCastPrimitiveWrapperUInt32() + public void TestCastUInt32() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = UInt32('AFFFFFFF', 16) - ", testMethod: nameof(PatchTarget6.RunUInt32)); + ", nameof(PatchTargetNumbers.RunUInt32)); var returnValue = target.RunUInt32(300500); Assert.True(target.ran); Assert.Equal(0xAFFFFFFF, returnValue); } [Fact] - public void TestCastPrimitiveWrapperInt64() + public void TestCastInt64() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = Int64('7555555555555555', 16) - ", testMethod: nameof(PatchTarget6.RunInt64)); + ", nameof(PatchTargetNumbers.RunInt64)); var returnValue = target.RunInt64(0x7FFFFFFF00000000); Assert.True(target.ran); Assert.Equal(0x7555555555555555, returnValue); } [Fact] - public void TestCastPrimitiveWrapperUInt64() + public void TestCastUInt64() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = UInt64('F555555555555555', 16) - ", testMethod: nameof(PatchTarget6.RunUInt64)); + ", nameof(PatchTargetNumbers.RunUInt64)); var returnValue = target.RunUInt64(0xFFFFFFFF00000000); Assert.True(target.ran); Assert.Equal(0xF555555555555555, returnValue); } [Fact] - public void TestCastPrimitiveWrapperSingle() + public void TestCastSingle() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = Single(123.456) - ", testMethod: nameof(PatchTarget6.RunSingle)); + ", nameof(PatchTargetNumbers.RunSingle)); var returnValue = target.RunSingle(111.111f); Assert.True(target.ran); Assert.Equal(123.456f, returnValue); } [Fact] - public void TestCastPrimitiveWrapperDouble() + public void TestCastDouble() { - var target = new PatchTarget6(); - AddPrefix(@" + using var patchTargetHandle = HookPatchHelpers.LockPatchTarget(); + var target = new PatchTargetNumbers(); + using var patchHandle = luaCs.AddPrefix(@" ptable['v'] = Double(123.456) - ", testMethod: nameof(PatchTarget6.RunDouble)); + ", nameof(PatchTargetNumbers.RunDouble)); var returnValue = target.RunDouble(111.111d); Assert.True(target.ran); Assert.Equal(123.456d, returnValue); diff --git a/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs b/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs new file mode 100644 index 000000000..28a0dd9f2 --- /dev/null +++ b/Barotrauma/BarotraumaTest/LuaCs/LuaCsFixture.cs @@ -0,0 +1,31 @@ +using Barotrauma; +using System; +using System.Runtime.ExceptionServices; + +namespace TestProject.LuaCs +{ + /// + /// Shared LuaCsSetup instance. + /// + /// + /// Don't use this unless you need to test logic that makes use of + /// a shared state (static variables). + /// + public class LuaCsFixture : IDisposable + { + public LuaCsFixture() + { + LuaCs.ExceptionHandler = (ex, _) => + { + // Pretend we never caught the exception in the first place + // (this allows us to preserve the stack trace) + var di = ExceptionDispatchInfo.Capture(ex); + di.Throw(); + }; + } + + internal LuaCsSetup LuaCs { get; } = new(); + + void IDisposable.Dispose() => LuaCs.Stop(); + } +} diff --git a/Libraries/moonsharp b/Libraries/moonsharp index b9fc22da9..9366bba2f 160000 --- a/Libraries/moonsharp +++ b/Libraries/moonsharp @@ -1 +1 @@ -Subproject commit b9fc22da9e79427206e4f9ca241382aaefa03532 +Subproject commit 9366bba2f24a95ebbbdb4e714d377deac406d2a0 diff --git a/LinuxSolution.sln b/LinuxSolution.sln index 16e6b7939..e970112ea 100644 --- a/LinuxSolution.sln +++ b/LinuxSolution.sln @@ -42,7 +42,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Framework.Linux.Ne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxTest", "Barotrauma\BarotraumaTest\LinuxTest.csproj", "{F1B80D94-8BD6-48CE-8D17-BB2A5C98BCA3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter.netcore", "Libraries\moonsharp\src\MoonSharp.Interpreter\_Projects\MoonSharp.Interpreter.netcore\MoonSharp.Interpreter.netcore.csproj", "{382DFA63-78FC-41AC-BA85-630960A56E5C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{382DFA63-78FC-41AC-BA85-630960A56E5C}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/MacSolution.sln b/MacSolution.sln index a801ae405..f3f6acae3 100644 --- a/MacSolution.sln +++ b/MacSolution.sln @@ -39,7 +39,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks.Posix" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacTest", "Barotrauma\BarotraumaTest\MacTest.csproj", "{20BC9336-B439-4BF1-8B65-D587DBF421D1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter.netcore", "Libraries\moonsharp\src\MoonSharp.Interpreter\_Projects\MoonSharp.Interpreter.netcore\MoonSharp.Interpreter.netcore.csproj", "{40BDE83D-61D5-481C-A53E-E0F5B23881E2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{40BDE83D-61D5-481C-A53E-E0F5B23881E2}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/WindowsSolution.sln b/WindowsSolution.sln index b0d437056..efba0f9be 100644 --- a/WindowsSolution.sln +++ b/WindowsSolution.sln @@ -42,7 +42,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpFont.NetStandard", "Li EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsTest", "Barotrauma\BarotraumaTest\WindowsTest.csproj", "{C7212AE2-A925-4225-A639-AE0653EF65B0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter.netcore", "Libraries\moonsharp\src\MoonSharp.Interpreter\_Projects\MoonSharp.Interpreter.netcore\MoonSharp.Interpreter.netcore.csproj", "{2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoonSharp.Interpreter", "Libraries\moonsharp\MoonSharp.Interpreter\MoonSharp.Interpreter.csproj", "{2EEF2610-64A3-4E5D-95ED-0E181C1A34ED}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution