From 54b0dd360b7a014ba5c4d60afc52ae4c9324dad0 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:51:24 -0400 Subject: [PATCH 01/12] Prevent file corruption --- IPA.Injector/Injector.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 75ba1d8d..e78b10c3 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -247,8 +247,11 @@ namespace IPA.Injector if (modified) { + string tempFilePath = Path.GetTempFileName(); bkp?.Add(unityPath); - unityAsmDef.Write(unityPath); + unityAsmDef.Write(tempFilePath); + File.Delete(unityPath); + File.Move(tempFilePath, unityPath); } } endPatchCoreModule: @@ -279,7 +282,10 @@ namespace IPA.Injector var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); deleter.Methods.Clear(); // delete all methods - ascAsmDef.Write(ascPath); + string tempFilePath = Path.GetTempFileName(); + ascAsmDef.Write(tempFilePath); + File.Delete(ascPath); + File.Move(tempFilePath, ascPath); isFirst = false; } @@ -333,4 +339,4 @@ namespace IPA.Injector _ = PluginComponent.Create(); } } -} \ No newline at end of file +} From 7dc39c0d236934f30d41cdd64e07837541db6e4a Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:51:41 -0400 Subject: [PATCH 02/12] Fix game assembly backup process --- IPA.Injector/Injector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index e78b10c3..138fdab9 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -283,6 +283,7 @@ namespace IPA.Injector deleter.Methods.Clear(); // delete all methods string tempFilePath = Path.GetTempFileName(); + bkp?.Add(ascPath); ascAsmDef.Write(tempFilePath); File.Delete(ascPath); File.Move(tempFilePath, ascPath); From 0550637d79e0b108ed26aab5aee917f90e8e9322 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:40:01 -0400 Subject: [PATCH 03/12] Rework bootstrapper a bit --- IPA.Injector/Injector.cs | 217 +++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 114 deletions(-) diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 138fdab9..f62b263e 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -142,9 +142,9 @@ namespace IPA.Injector var sw = Stopwatch.StartNew(); var cAsmName = Assembly.GetExecutingAssembly().GetName(); - var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; - var dataDir = new DirectoryInfo(managedPath).Parent.Name; + var dataDir = new DirectoryInfo(managedPath).Parent!.Name; var gameName = dataDir.Substring(0, dataDir.Length - 5); Logging.Logger.Injector.Debug("Finding backup"); @@ -153,152 +153,141 @@ namespace IPA.Injector if (bkp == null) Logging.Logger.Injector.Warn("No backup found! Was BSIPA installed using the installer?"); + // TODO: Investigate if this ever worked properly. + // this is a critical section because if you exit in here, assembly can die + using var critSec = CriticalSection.ExecuteSection(); + + var readerParameters = new ReaderParameters + { + ReadWrite = false, + InMemory = true, + ReadingMode = ReadingMode.Immediate + }; + Logging.Logger.Injector.Debug("Ensuring patch on UnityEngine.CoreModule exists"); #region Insert patch into UnityEngine.CoreModule.dll - { - var unityPath = Path.Combine(managedPath, - "UnityEngine.CoreModule.dll"); + var unityPath = Path.Combine(managedPath, "UnityEngine.CoreModule.dll"); - // this is a critical section because if you exit in here, CoreModule can die - using var critSec = CriticalSection.ExecuteSection(); + using var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, readerParameters); + var unityModDef = unityAsmDef.MainModule; - using var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters - { - ReadWrite = false, - InMemory = true, - ReadingMode = ReadingMode.Immediate - }); - var unityModDef = unityAsmDef.MainModule; - - bool modified = false; - foreach (var asmref in unityModDef.AssemblyReferences) + bool modified = false; + foreach (var asmref in unityModDef.AssemblyReferences) + { + if (asmref.Name == cAsmName.Name) { - if (asmref.Name == cAsmName.Name) + if (asmref.Version != cAsmName.Version) { - if (asmref.Version != cAsmName.Version) - { - asmref.Version = cAsmName.Version; - modified = true; - } + asmref.Version = cAsmName.Version; + modified = true; } } + } - var application = unityModDef.GetType("UnityEngine", "Camera"); + var application = unityModDef.GetType("UnityEngine", "Camera"); - if (application == null) - { - Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!" - + "Nothing to patch to get ourselves into the Unity run cycle!"); - goto endPatchCoreModule; - } + if (application == null) + { + Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!" + + "Nothing to patch to get ourselves into the Unity run cycle!"); + goto endPatchCoreModule; + } - MethodDefinition? cctor = null; - foreach (var m in application.Methods) - if (m.IsRuntimeSpecialName && m.Name == ".cctor") - cctor = m; + MethodDefinition? cctor = null; + foreach (var m in application.Methods) + if (m.IsRuntimeSpecialName && m.Name == ".cctor") + cctor = m; - var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method); + var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method); - if (cctor == null) - { - cctor = new MethodDefinition(".cctor", - MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, - unityModDef.TypeSystem.Void); - application.Methods.Add(cctor); - modified = true; - - var ilp = cctor.Body.GetILProcessor(); - ilp.Emit(OpCodes.Call, cbs); - ilp.Emit(OpCodes.Ret); - } - else + if (cctor == null) + { + cctor = new MethodDefinition(".cctor", + MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, + unityModDef.TypeSystem.Void); + application.Methods.Add(cctor); + modified = true; + + var ilp = cctor.Body.GetILProcessor(); + ilp.Emit(OpCodes.Call, cbs); + ilp.Emit(OpCodes.Ret); + } + else + { + var ilp = cctor.Body.GetILProcessor(); + for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++) { - var ilp = cctor.Body.GetILProcessor(); - for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++) + var ins = cctor.Body.Instructions[i]; + switch (i) { - var ins = cctor.Body.Instructions[i]; - switch (i) - { - case 0 when ins.OpCode != OpCodes.Call: - ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); - modified = true; - break; + case 0 when ins.OpCode != OpCodes.Call: + ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); + modified = true; + break; - case 0: + case 0: + { + var methodRef = ins.Operand as MethodReference; + if (methodRef?.FullName != cbs.FullName) { - var methodRef = ins.Operand as MethodReference; - if (methodRef?.FullName != cbs.FullName) - { - ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); - modified = true; - } - - break; + ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); + modified = true; } - case 1 when ins.OpCode != OpCodes.Ret: - ilp.Replace(ins, ilp.Create(OpCodes.Ret)); - modified = true; + break; - } + } + case 1 when ins.OpCode != OpCodes.Ret: + ilp.Replace(ins, ilp.Create(OpCodes.Ret)); + modified = true; + break; } } + } - if (modified) - { - string tempFilePath = Path.GetTempFileName(); - bkp?.Add(unityPath); - unityAsmDef.Write(tempFilePath); - File.Delete(unityPath); - File.Move(tempFilePath, unityPath); - } + if (modified) + { + string tempFilePath = Path.GetTempFileName(); + bkp?.Add(unityPath); + unityAsmDef.Write(tempFilePath); + File.Delete(unityPath); + File.Move(tempFilePath, unityPath); } endPatchCoreModule: #endregion Insert patch into UnityEngine.CoreModule.dll - bool isFirst = true; - foreach (var name in SelfConfig.GameAssemblies_) +#if BeatSaber + Logging.Logger.Injector.Debug("Ensuring anti-yeet patch exists"); + + var name = SelfConfig.GameAssemblies_.FirstOrDefault() ?? SelfConfig.GetDefaultGameAssemblies().First(); + var ascPath = Path.Combine(managedPath, name); + + try { - var ascPath = Path.Combine(managedPath, name); + using var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, readerParameters); + var ascModDef = ascAsmDef.MainModule; - using var execSec = CriticalSection.ExecuteSection(); + var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); -#if BeatSaber - if (isFirst) + if (deleter.Methods.Count > 0) { - try - { - Logging.Logger.Injector.Debug("Applying anti-yeet patch"); - - using var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, new ReaderParameters - { - ReadWrite = false, - InMemory = true, - ReadingMode = ReadingMode.Immediate - }); - var ascModDef = ascAsmDef.MainModule; - - var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); - deleter.Methods.Clear(); // delete all methods - - string tempFilePath = Path.GetTempFileName(); - bkp?.Add(ascPath); - ascAsmDef.Write(tempFilePath); - File.Delete(ascPath); - File.Move(tempFilePath, ascPath); - - isFirst = false; - } - catch (Exception e) - { - Logging.Logger.Injector.Warn($"Could not apply anti-yeet patch to {ascPath}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logging.Logger.Injector.Warn(e); - } + deleter.Methods.Clear(); // delete all methods + + string tempFilePath = Path.GetTempFileName(); + bkp?.Add(ascPath); + ascAsmDef.Write(tempFilePath); + File.Delete(ascPath); + File.Move(tempFilePath, ascPath); } -#endif } + catch (Exception e) + { + Logging.Logger.Injector.Warn($"Could not apply anti-yeet patch to {ascPath}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + Logging.Logger.Injector.Warn(e); + } +#endif sw.Stop(); Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}"); From be3a02d53f59286f979abe2457cdcd81607a483a Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sat, 16 Sep 2023 01:22:25 -0400 Subject: [PATCH 04/12] Fix nullable value being serialized as non-nullable --- .../Config/Stores/GeneratedStoreImpl/Serialization.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs index 358bef88..d25f8c35 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs @@ -52,16 +52,19 @@ namespace IPA.Config.Stores il.MarkLabel(passedNull); } - if (member.IsNullable) + if (member is { IsNullable: true, HasConverter: false }) il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod()); var memberConversionType = member.ConversionType; var targetType = GetExpectedValueTypeForType(memberConversionType); if (member.HasConverter) { - using var stlocal = GetLocal.Allocate(memberConversionType); + using var stlocal = GetLocal.Allocate(member.IsNullable ? member.Type : memberConversionType); using var valLocal = GetLocal.Allocate(typeof(Value)); + if (member.IsNullable) + il.Emit(OpCodes.Ldloc_S, valueTypeLocal.Local); + il.Emit(OpCodes.Stloc, stlocal); il.BeginExceptionBlock(); il.Emit(OpCodes.Ldsfld, member.ConverterField); From 6a4d9ec922e6299979771b94a86dad7553338ad5 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Wed, 20 Sep 2023 04:57:49 -0400 Subject: [PATCH 05/12] Bring back `HarmonyProtector` and make it more robust --- IPA.Injector/Injector.cs | 7 +++ IPA.Loader/Loader/HarmonyProtector.cs | 79 +++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 IPA.Loader/Loader/HarmonyProtector.cs diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index f62b263e..1565770b 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -137,6 +137,11 @@ namespace IPA.Injector Loader.LibLoader.Configure(); } + private static void InstallHarmonyProtections() + { // proxy function to delay resolution + HarmonyProtectorProxy.ProtectNull(); + } + private static void InstallBootstrapPatch() { var sw = Stopwatch.StartNew(); @@ -312,6 +317,8 @@ namespace IPA.Injector // need to reinit streams singe Unity seems to redirect stdout StdoutInterceptor.RedirectConsole(); + InstallHarmonyProtections(); + var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); bootstrapper.Destroyed += Bootstrapper_Destroyed; } diff --git a/IPA.Loader/Loader/HarmonyProtector.cs b/IPA.Loader/Loader/HarmonyProtector.cs new file mode 100644 index 00000000..549b74f1 --- /dev/null +++ b/IPA.Loader/Loader/HarmonyProtector.cs @@ -0,0 +1,79 @@ +using HarmonyLib; +using IPA.Logging; +using MonoMod.RuntimeDetour; +using System; +using System.Reflection; + +namespace IPA.Loader +{ + internal static class HarmonyProtectorProxy + { + public static void ProtectNull() => HarmonyProtector.Protect(); + } + + internal static class HarmonyProtector + { + public static void Protect() + { + var guid = Guid.NewGuid().ToString("N"); + var id = guid.Remove(new Random().Next(7, guid.Length - 1)); + var harmony = new Harmony(id); + + var unpatchByTypeOrId = AccessTools.Method(typeof(PatchProcessor), nameof(PatchProcessor.Unpatch), new[] { typeof(HarmonyPatchType), typeof(string) }); + var unpatchMethod = AccessTools.Method(typeof(PatchProcessor), nameof(PatchProcessor.Unpatch), new[] { typeof(MethodInfo) }); + var processPatchJob = AccessTools.Method(typeof(PatchClassProcessor), "ProcessPatchJob"); + var patch = AccessTools.Method(typeof(PatchProcessor), nameof(PatchProcessor.Patch)); + + var unpatchPrefix = AccessTools.Method(typeof(HarmonyProtector), nameof(PatchProcessor_Unpatch_Prefix)); + var processPatchJobPrefix = AccessTools.Method(typeof(HarmonyProtector), nameof(PatchClassProcessor_ProcessPatchJob_Prefix)); + var patchPrefix = AccessTools.Method(typeof(HarmonyProtector), nameof(PatchProcessor_Patch_Prefix)); + + harmony.Patch(unpatchByTypeOrId, new HarmonyMethod(unpatchPrefix)); + harmony.Patch(unpatchMethod, new HarmonyMethod(unpatchPrefix)); + harmony.Patch(processPatchJob, new HarmonyMethod(processPatchJobPrefix)); + harmony.Patch(patch, new HarmonyMethod(patchPrefix)); + } + + private static bool ShouldBlockExecution(MethodBase methodBase) + { + var getIdentifiable = AccessTools.Method(typeof(DetourHelper), nameof(DetourHelper.GetIdentifiable)).GetIdentifiable(); + var getValue = AccessTools.Method(typeof(FieldInfo), nameof(FieldInfo.GetValue)).GetIdentifiable(); + var declaringTypeGetter = AccessTools.PropertyGetter(typeof(MemberInfo), nameof(MemberInfo.DeclaringType)).GetIdentifiable(); + var methodBaseEquals = AccessTools.Method(typeof(MethodBase), nameof(MethodBase.Equals)).GetIdentifiable(); + var assemblyEquals = AccessTools.Method(typeof(Assembly), nameof(Assembly.Equals)).GetIdentifiable(); + var assemblyGetter = AccessTools.PropertyGetter(typeof(Type), nameof(Type.Assembly)).GetIdentifiable(); + var getExecutingAssembly = AccessTools.Method(typeof(Assembly), nameof(Assembly.GetExecutingAssembly)).GetIdentifiable(); + var method = methodBase.GetIdentifiable(); + var assembly = method.DeclaringType!.Assembly; + return method.Equals(getIdentifiable) || + method.Equals(getValue) || + method.Equals(declaringTypeGetter) || + method.Equals(methodBaseEquals) || + method.Equals(assemblyEquals) || + method.Equals(assemblyGetter) || + method.Equals(getExecutingAssembly) || + assembly.Equals(Assembly.GetExecutingAssembly()) || + assembly.Equals(typeof(Harmony).Assembly); + } + + private static bool PatchProcessor_Patch_Prefix(MethodBase ___original, ref MethodInfo __result) + { + if (ShouldBlockExecution(___original)) + { + __result = ___original as MethodInfo; + return false; + } + + return true; + } + + private static bool PatchClassProcessor_ProcessPatchJob_Prefix(object job) + { + var original = AccessTools.Field(job.GetType(), "original"); + var methodBase = (MethodBase)original!.GetValue(job); + return !ShouldBlockExecution(methodBase); + } + + private static bool PatchProcessor_Unpatch_Prefix(MethodBase ___original) => !ShouldBlockExecution(___original); + } +} From 3ccb5df3462b43a8faccd178aa2dd8ef9a7d93d6 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Wed, 20 Sep 2023 17:23:02 -0400 Subject: [PATCH 06/12] Fix Beat Saber version parsing --- IPA.Loader/Utilities/AlmostVersion.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/IPA.Loader/Utilities/AlmostVersion.cs b/IPA.Loader/Utilities/AlmostVersion.cs index a9527503..494090e9 100644 --- a/IPA.Loader/Utilities/AlmostVersion.cs +++ b/IPA.Loader/Utilities/AlmostVersion.cs @@ -10,7 +10,7 @@ using Version = Hive.Versioning.Version; namespace IPA.Utilities { /// - /// A type that wraps so that the string of the version is stored when the string is + /// A type that wraps so that the string of the version is stored when the string is /// not a valid . /// public class AlmostVersion : IComparable, IComparable, @@ -61,7 +61,7 @@ namespace IPA.Utilities public AlmostVersion(SVersion ver) : this(ver?.UnderlyingVersion ?? throw new ArgumentNullException(nameof(ver))) { } /// - /// Creates an from the version string in stored using + /// Creates an from the version string in stored using /// the storage mode specified in . /// /// the text to parse as an @@ -92,7 +92,13 @@ namespace IPA.Utilities if (mode == StoredAs.SemVer) { StorageMode = StoredAs.SemVer; +#if BeatSaber + var index = str.IndexOf('_'); + var versionString = index >= 0 ? str.Substring(0, index) : str; + var result = Version.TryParse(versionString, out var version); +#else var result = Version.TryParse(str, out var version); +#endif SemverValue = version; return result; } From 4e9cc1e6838f5ea393ccd05dd03fdf00c49bb7d5 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Wed, 20 Sep 2023 23:23:34 -0400 Subject: [PATCH 07/12] Do not mark disabled mods as ignored --- IPA.Loader/Loader/PluginLoader.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index 04930963..f20d428d 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -598,7 +598,7 @@ namespace IPA.Loader Logger.Loader.Trace($"- Found already processed"); return true; } - if (metadataCache!.TryGetValue(id, out var plugin)) + if (metadataCache.TryGetValue(id, out var plugin)) { Logger.Loader.Trace($"- In metadata cache"); if (partial) @@ -609,11 +609,11 @@ namespace IPA.Loader disabled = !plugin.Enabled; meta = plugin.Meta; + ignored = false; if (!disabled) { try { - ignored = false; Resolve(plugin.Meta, ref disabled, out ignored); } catch (Exception e) @@ -715,8 +715,7 @@ namespace IPA.Loader if (depDisabled) { Logger.Loader.Warn($"Dependency '{id}' for '{plugin.Id}' disabled; disabling"); - disabledPlugins!.Add(plugin); - _ = disabledIds!.Add(plugin.Id); + disabledPlugins.Add(plugin); disabled = true; } @@ -770,7 +769,6 @@ namespace IPA.Loader && range.Matches(meta.HVersion) && !conflIgnored && !conflDisabled) // the conflict is only *actually* a problem if it is both not ignored and not disabled { - Logger.Loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.HVersion}; ignoring '{plugin.Id}'"); ignoredPlugins.Add(plugin, new(Reason.Conflict) { From e8ee5d60d8c7ab34b880bf88c66c4ed3e2bf3a43 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Fri, 29 Sep 2023 22:29:17 -0400 Subject: [PATCH 08/12] Fix Unity logging not being redirected when off-main thread --- IPA.Injector/Injector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 1565770b..25d8bd74 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -305,7 +305,7 @@ namespace IPA.Injector if (bootstrapped) return; bootstrapped = true; - Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type) + Application.logMessageReceivedThreaded += delegate (string condition, string stackTrace, LogType type) { var level = UnityLogRedirector.LogTypeToLevel(type); UnityLogProvider.UnityLogger.Log(level, $"{condition}"); From 5cd7b83077b27bca0c2f510aa8e3683e8e2b5b8f Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:12:01 -0400 Subject: [PATCH 09/12] Hopefully latest wave of serialization fixes --- .../Stores/GeneratedStoreImpl/Correction.cs | 30 ++++++---- .../GeneratedStoreImpl/Deserialization.cs | 15 +++-- .../GeneratedStoreImpl/ObjectStructure.cs | 4 +- .../GeneratedStoreImpl/Serialization.cs | 59 +++++++++++-------- 4 files changed, 62 insertions(+), 46 deletions(-) diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs index 2d69d585..0af170ad 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Correction.cs @@ -1,17 +1,8 @@ #nullable enable using IPA.Config.Data; -using IPA.Config.Stores.Attributes; -using IPA.Logging; using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; using System.Reflection.Emit; -using System.Text; -using System.Threading; -using System.Threading.Tasks; #if NET3 using Net3_Proxy; using Array = Net3_Proxy.Array; @@ -46,12 +37,27 @@ namespace IPA.Config.Stores var endLabel = il.DefineLabel(); - // TODO: when we have a nullable, we need to save to a local to call methods if (member.IsNullable) { - il.Emit(OpCodes.Dup); + using var valueTypeLocal = GetLocal.Allocate(member.Type); + var passedNull = il.DefineLabel(); + + il.Emit(OpCodes.Stloc, valueTypeLocal); + il.Emit(OpCodes.Ldloca, valueTypeLocal); il.Emit(OpCodes.Call, member.Nullable_HasValue.GetGetMethod()); - il.Emit(OpCodes.Brfalse, endLabel); + il.Emit(OpCodes.Brtrue, passedNull); + + if (member.ConversionType.IsValueType) + { + il.Emit(OpCodes.Ldloca, valueTypeLocal); + il.Emit(OpCodes.Initobj, member.Type); + } + il.Emit(OpCodes.Ldloc, valueTypeLocal); + il.Emit(OpCodes.Br, endLabel); + + il.MarkLabel(passedNull); + + il.Emit(OpCodes.Ldloca, valueTypeLocal); il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod()); } diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs index 07904fce..18744fed 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Deserialization.cs @@ -41,7 +41,7 @@ namespace IPA.Config.Stores il.Emit(OpCodes.Callvirt, IGeneratedStore_Deserialize); } - private static void EmitDeserializeNullable(ILGenerator il, SerializedMemberInfo member, Type expected, LocalAllocator GetLocal, + private static void EmitDeserializeNullable(ILGenerator il, SerializedMemberInfo member, Type expected, LocalAllocator GetLocal, Action thisarg, Action parentobj) { if (!member.IsNullable) throw new InvalidOperationException("EmitDeserializeNullable called for non-nullable!"); @@ -53,7 +53,7 @@ namespace IPA.Config.Stores // top of stack is the Value to deserialize; the type will be as returned from GetExpectedValueTypeForType // after, top of stack will be thing to write to field - private static void EmitDeserializeValue(ILGenerator il, SerializedMemberInfo member, Type targetType, Type expected, LocalAllocator GetLocal, + private static void EmitDeserializeValue(ILGenerator il, SerializedMemberInfo member, Type targetType, Type expected, LocalAllocator GetLocal, Action thisarg, Action parentobj) { if (typeof(Value).IsAssignableFrom(targetType)) return; // do nothing @@ -96,14 +96,14 @@ namespace IPA.Config.Stores { using var mapLocal = GetLocal.Allocate(typeof(Map)); - using var resultLocal = GetLocal.Allocate(targetType); + using var resultLocal = GetLocal.Allocate(member.IsNullable ? member.Type : targetType); using var valueLocal = GetLocal.Allocate(typeof(Value)); var structure = ReadObjectMembers(targetType); if (!structure.Any()) { - Logger.Config.Warn($"Custom value type {targetType.FullName} (when compiling serialization of" + - $" {member.Name} on {member.Member.DeclaringType.FullName}) has no accessible members"); + Logger.Config.Warn($"Custom value type {targetType.FullName} (when compiling deserialization of" + + $" {member.Name} on {member.Member.DeclaringType.FullName}) has no accessible members, might need a converter"); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldloca, resultLocal); il.Emit(OpCodes.Initobj, targetType); @@ -112,7 +112,6 @@ namespace IPA.Config.Stores { il.Emit(OpCodes.Stloc, mapLocal); - // TODO: handle creating a nullable, when applicable EmitLoad(il, member, thisarg); il.Emit(OpCodes.Stloc, resultLocal); @@ -130,7 +129,7 @@ namespace IPA.Config.Stores } } - private static void EmitDeserializeStructure(ILGenerator il, IEnumerable structure, + private static void EmitDeserializeStructure(ILGenerator il, IEnumerable structure, LocalBuilder mapLocal, LocalBuilder valueLocal, LocalAllocator GetLocal, Action thisobj, Action parentobj) { @@ -204,7 +203,7 @@ namespace IPA.Config.Stores } // emit takes the value being deserialized, logs on error, leaves nothing on stack - private static void EmitDeserializeMember(ILGenerator il, SerializedMemberInfo member, Label nextLabel, Action getValue, LocalAllocator GetLocal, + private static void EmitDeserializeMember(ILGenerator il, SerializedMemberInfo member, Label nextLabel, Action getValue, LocalAllocator GetLocal, Action thisobj, Action parentobj) { var Object_GetType = typeof(object).GetMethod(nameof(Object.GetType)); diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs index 54595d76..b5a9040b 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/ObjectStructure.cs @@ -78,7 +78,7 @@ namespace IPA.Config.Stores if (converterAttr.UseDefaultConverterForType) converterAttr = new UseConverterAttribute(Converter.GetDefaultConverterType(member.Type)); if (converterAttr.UseDefaultConverterForType) - throw new InvalidOperationException("How did we get here?"); + throw new InvalidOperationException("How did we get here?"); member.Converter = converterAttr.ConverterType; member.IsGenericConverter = converterAttr.IsGenericConverter; @@ -175,7 +175,7 @@ namespace IPA.Config.Stores // only look at public/protected fields foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { - if (field.IsPrivate) + if (field.IsPrivate || field.IsAssembly) continue; var smi = new SerializedMemberInfo diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs index d25f8c35..63703cf8 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs @@ -28,47 +28,58 @@ namespace IPA.Config.Stores ? GetLocal.Allocate(member.Type) : default; - if (member.IsNullable) - { - il.Emit(OpCodes.Stloc, valueTypeLocal.Local); - il.Emit(OpCodes.Ldloca, valueTypeLocal.Local); - } - var endSerialize = il.DefineLabel(); if (member.AllowNull) { var passedNull = il.DefineLabel(); - il.Emit(OpCodes.Dup); if (member.IsNullable) + { + il.Emit(OpCodes.Stloc, valueTypeLocal.Local); + il.Emit(OpCodes.Ldloca, valueTypeLocal.Local); il.Emit(OpCodes.Call, member.Nullable_HasValue.GetGetMethod()); + } il.Emit(OpCodes.Brtrue, passedNull); - il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Br, endSerialize); il.MarkLabel(passedNull); - } - if (member is { IsNullable: true, HasConverter: false }) - il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod()); + if (member.IsNullable) + { + if (!member.HasConverter) + { + il.Emit(OpCodes.Ldloca, valueTypeLocal.Local); + il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod()); + } + } + else + { + EmitLoad(il, member, thisarg); + } + } var memberConversionType = member.ConversionType; var targetType = GetExpectedValueTypeForType(memberConversionType); if (member.HasConverter) { - using var stlocal = GetLocal.Allocate(member.IsNullable ? member.Type : memberConversionType); - using var valLocal = GetLocal.Allocate(typeof(Value)); - if (member.IsNullable) - il.Emit(OpCodes.Ldloc_S, valueTypeLocal.Local); + { + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldsfld, member.ConverterField); + il.Emit(OpCodes.Ldloc, valueTypeLocal.Local); + } + else + { + using var stlocal = GetLocal.Allocate(memberConversionType); - il.Emit(OpCodes.Stloc, stlocal); - il.BeginExceptionBlock(); - il.Emit(OpCodes.Ldsfld, member.ConverterField); - il.Emit(OpCodes.Ldloc, stlocal); + il.Emit(OpCodes.Stloc, stlocal); + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldsfld, member.ConverterField); + il.Emit(OpCodes.Ldloc, stlocal); + } if (member.IsGenericConverter) { @@ -90,18 +101,18 @@ namespace IPA.Config.Stores il.Emit(OpCodes.Call, toValue); } - il.Emit(OpCodes.Stloc, valLocal); + il.Emit(OpCodes.Stloc_1); il.BeginCatchBlock(typeof(Exception)); EmitWarnException(il, "Error serializing member using converter"); il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Stloc, valLocal); + il.Emit(OpCodes.Stloc_1); il.EndExceptionBlock(); - il.Emit(OpCodes.Ldloc, valLocal); + il.Emit(OpCodes.Ldloc_1); } else if (targetType == typeof(Text)) { // only happens when arg is a string or char var TextCreate = typeof(Value).GetMethod(nameof(Value.Text)); - if (member.Type == typeof(char)) + if (memberConversionType == typeof(char)) { var strFromChar = typeof(char).GetMethod(nameof(char.ToString), new[] { typeof(char) }); il.Emit(OpCodes.Call, strFromChar); @@ -172,7 +183,7 @@ namespace IPA.Config.Stores if (!structure.Any()) { Logger.Config.Warn($"Custom value type {memberConversionType.FullName} (when compiling serialization of" + - $" {member.Name} on {member.Member.DeclaringType.FullName}) has no accessible members"); + $" {member.Name} on {member.Member.DeclaringType.FullName}) has no accessible members, might need a converter"); il.Emit(OpCodes.Pop); } else From 5d30ef0a24d6a05d4b83911c147f3065e0b5f5cd Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:16:28 -0400 Subject: [PATCH 10/12] Catch potential exceptions --- .../GeneratedStoreImpl/GeneratedStoreImpl.cs | 13 ++++++++++--- IPA/Program.cs | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs index 7efa987a..fdb83d43 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/GeneratedStoreImpl.cs @@ -34,8 +34,8 @@ namespace IPA.Config.Stores public static IConfigStore Create(Type type) => Create(type, null); - private static readonly MethodInfo CreateGParent = - typeof(GeneratedStoreImpl).GetMethod(nameof(Create), BindingFlags.NonPublic | BindingFlags.Static, null, + private static readonly MethodInfo CreateGParent = + typeof(GeneratedStoreImpl).GetMethod(nameof(Create), BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.Any, new[] { typeof(IGeneratedStore) }, Array.Empty()); internal static T Create(IGeneratedStore? parent) where T : class => (T)Create(typeof(T), parent); @@ -72,7 +72,14 @@ namespace IPA.Config.Stores internal static void DebugSaveAssembly(string file) { - Assembly.Save(file); + try + { + Assembly.Save(file); + } + catch (Exception ex) + { + Logger.Config.Error(ex); + } } private static ModuleBuilder? module; diff --git a/IPA/Program.cs b/IPA/Program.cs index ef524c67..f2f5f49d 100644 --- a/IPA/Program.cs +++ b/IPA/Program.cs @@ -72,7 +72,7 @@ namespace IPA } PatchContext? context = null; - + Assembly? AssemblyLibLoader(object? source, ResolveEventArgs e) { // ReSharper disable AccessToModifiedClosure @@ -329,8 +329,16 @@ namespace IPA Debug.Assert(targetFile.Directory != null, "targetFile.Directory != null"); targetFile.Directory?.Create(); - LineBack(); - ClearLine(); + try + { + LineBack(); + ClearLine(); + } + catch (Exception ex) + { + // Might throw IOException due to an invalid handle when accessing IsConsole from a MSBuild task. + } + Console.WriteLine(@"Copying {0}", targetFile.FullName); backup.Add(targetFile); _ = fi.CopyTo(targetFile.FullName, true); @@ -366,7 +374,7 @@ namespace IPA /// Encodes an argument for passing into a program /// /// The value_ that should be received by the program - /// The value_ which needs to be passed to the program for the original value_ + /// The value_ which needs to be passed to the program for the original value_ /// to come through public static string EncodeParameterArgument(string original) { From efdfee21dc71bcb497d854c94142368bdd6f017d Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:38:16 -0400 Subject: [PATCH 11/12] Fix Unity hard crash when serializing a nullable `decimal` --- IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs index 63703cf8..af0d04ad 100644 --- a/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs +++ b/IPA.Loader/Config/Stores/GeneratedStoreImpl/Serialization.cs @@ -127,13 +127,13 @@ namespace IPA.Config.Stores else if (targetType == typeof(Integer)) { var IntCreate = typeof(Value).GetMethod(nameof(Value.Integer)); - EmitNumberConvertTo(il, IntCreate.GetParameters()[0].ParameterType, member.Type); + EmitNumberConvertTo(il, IntCreate.GetParameters()[0].ParameterType, memberConversionType); il.Emit(OpCodes.Call, IntCreate); } else if (targetType == typeof(FloatingPoint)) { var FloatCreate = typeof(Value).GetMethod(nameof(Value.Float)); - EmitNumberConvertTo(il, FloatCreate.GetParameters()[0].ParameterType, member.Type); + EmitNumberConvertTo(il, FloatCreate.GetParameters()[0].ParameterType, memberConversionType); il.Emit(OpCodes.Call, FloatCreate); } else if (targetType == typeof(List)) From 7851d12b23956bff50191fa6f56de6f8df810004 Mon Sep 17 00:00:00 2001 From: Meivyn <793322+Meivyn@users.noreply.github.com> Date: Sat, 11 Nov 2023 17:08:10 -0500 Subject: [PATCH 12/12] Revert "Bring back `HarmonyProtector` and make it more robust" This reverts commit 6a4d9ec922e6299979771b94a86dad7553338ad5. --- IPA.Injector/Injector.cs | 7 --- IPA.Loader/Loader/HarmonyProtector.cs | 79 --------------------------- 2 files changed, 86 deletions(-) delete mode 100644 IPA.Loader/Loader/HarmonyProtector.cs diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 25d8bd74..6c3fc030 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -137,11 +137,6 @@ namespace IPA.Injector Loader.LibLoader.Configure(); } - private static void InstallHarmonyProtections() - { // proxy function to delay resolution - HarmonyProtectorProxy.ProtectNull(); - } - private static void InstallBootstrapPatch() { var sw = Stopwatch.StartNew(); @@ -317,8 +312,6 @@ namespace IPA.Injector // need to reinit streams singe Unity seems to redirect stdout StdoutInterceptor.RedirectConsole(); - InstallHarmonyProtections(); - var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); bootstrapper.Destroyed += Bootstrapper_Destroyed; } diff --git a/IPA.Loader/Loader/HarmonyProtector.cs b/IPA.Loader/Loader/HarmonyProtector.cs deleted file mode 100644 index 549b74f1..00000000 --- a/IPA.Loader/Loader/HarmonyProtector.cs +++ /dev/null @@ -1,79 +0,0 @@ -using HarmonyLib; -using IPA.Logging; -using MonoMod.RuntimeDetour; -using System; -using System.Reflection; - -namespace IPA.Loader -{ - internal static class HarmonyProtectorProxy - { - public static void ProtectNull() => HarmonyProtector.Protect(); - } - - internal static class HarmonyProtector - { - public static void Protect() - { - var guid = Guid.NewGuid().ToString("N"); - var id = guid.Remove(new Random().Next(7, guid.Length - 1)); - var harmony = new Harmony(id); - - var unpatchByTypeOrId = AccessTools.Method(typeof(PatchProcessor), nameof(PatchProcessor.Unpatch), new[] { typeof(HarmonyPatchType), typeof(string) }); - var unpatchMethod = AccessTools.Method(typeof(PatchProcessor), nameof(PatchProcessor.Unpatch), new[] { typeof(MethodInfo) }); - var processPatchJob = AccessTools.Method(typeof(PatchClassProcessor), "ProcessPatchJob"); - var patch = AccessTools.Method(typeof(PatchProcessor), nameof(PatchProcessor.Patch)); - - var unpatchPrefix = AccessTools.Method(typeof(HarmonyProtector), nameof(PatchProcessor_Unpatch_Prefix)); - var processPatchJobPrefix = AccessTools.Method(typeof(HarmonyProtector), nameof(PatchClassProcessor_ProcessPatchJob_Prefix)); - var patchPrefix = AccessTools.Method(typeof(HarmonyProtector), nameof(PatchProcessor_Patch_Prefix)); - - harmony.Patch(unpatchByTypeOrId, new HarmonyMethod(unpatchPrefix)); - harmony.Patch(unpatchMethod, new HarmonyMethod(unpatchPrefix)); - harmony.Patch(processPatchJob, new HarmonyMethod(processPatchJobPrefix)); - harmony.Patch(patch, new HarmonyMethod(patchPrefix)); - } - - private static bool ShouldBlockExecution(MethodBase methodBase) - { - var getIdentifiable = AccessTools.Method(typeof(DetourHelper), nameof(DetourHelper.GetIdentifiable)).GetIdentifiable(); - var getValue = AccessTools.Method(typeof(FieldInfo), nameof(FieldInfo.GetValue)).GetIdentifiable(); - var declaringTypeGetter = AccessTools.PropertyGetter(typeof(MemberInfo), nameof(MemberInfo.DeclaringType)).GetIdentifiable(); - var methodBaseEquals = AccessTools.Method(typeof(MethodBase), nameof(MethodBase.Equals)).GetIdentifiable(); - var assemblyEquals = AccessTools.Method(typeof(Assembly), nameof(Assembly.Equals)).GetIdentifiable(); - var assemblyGetter = AccessTools.PropertyGetter(typeof(Type), nameof(Type.Assembly)).GetIdentifiable(); - var getExecutingAssembly = AccessTools.Method(typeof(Assembly), nameof(Assembly.GetExecutingAssembly)).GetIdentifiable(); - var method = methodBase.GetIdentifiable(); - var assembly = method.DeclaringType!.Assembly; - return method.Equals(getIdentifiable) || - method.Equals(getValue) || - method.Equals(declaringTypeGetter) || - method.Equals(methodBaseEquals) || - method.Equals(assemblyEquals) || - method.Equals(assemblyGetter) || - method.Equals(getExecutingAssembly) || - assembly.Equals(Assembly.GetExecutingAssembly()) || - assembly.Equals(typeof(Harmony).Assembly); - } - - private static bool PatchProcessor_Patch_Prefix(MethodBase ___original, ref MethodInfo __result) - { - if (ShouldBlockExecution(___original)) - { - __result = ___original as MethodInfo; - return false; - } - - return true; - } - - private static bool PatchClassProcessor_ProcessPatchJob_Prefix(object job) - { - var original = AccessTools.Field(job.GetType(), "original"); - var methodBase = (MethodBase)original!.GetValue(job); - return !ShouldBlockExecution(methodBase); - } - - private static bool PatchProcessor_Unpatch_Prefix(MethodBase ___original) => !ShouldBlockExecution(___original); - } -}