From 5f5fcfa4bf56b527803cfc46231ac5b39cce815f Mon Sep 17 00:00:00 2001 From: notfood Date: Thu, 14 Oct 2021 18:39:11 -0500 Subject: [PATCH] Allow referenced assemblies - To request a reference assembly to be generated add an empty txt file under `References` with the name of the dll you want to target, then run MultiplayerCompat without Multiplayer. - Generated assemblies are referenced in the Source_Referenced project, compat classes in this project are loaded dynamically while avoiding TypeLoadException issues. - It's discouraged to abuse this feature as it's expensive to maintain, consider submitting code with proper Reflection first. --- .gitignore | 1 + ...layer_Compat.sln => Multiplayer_Compat.sln | 8 +- Source/MpCompat.cs | 9 +- Source/MpCompatLoader.cs | 36 ++++++++ Source/Multiplayer_Compat.csproj | 1 + Source/ReferenceBuilder.cs | 82 +++++++++++++++++++ .../Multiplayer_Compat_Referenced.csproj | 33 ++++++++ 7 files changed, 168 insertions(+), 2 deletions(-) rename Source/Multiplayer_Compat.sln => Multiplayer_Compat.sln (54%) create mode 100644 Source/ReferenceBuilder.cs create mode 100644 Source_Referenced/Multiplayer_Compat_Referenced.csproj diff --git a/.gitignore b/.gitignore index b34a2808..1bc5b749 100644 --- a/.gitignore +++ b/.gitignore @@ -330,3 +330,4 @@ ASALocalRun/ .mfractor/ Assemblies/ +Referenced/ diff --git a/Source/Multiplayer_Compat.sln b/Multiplayer_Compat.sln similarity index 54% rename from Source/Multiplayer_Compat.sln rename to Multiplayer_Compat.sln index 621e893d..8ae06925 100644 --- a/Source/Multiplayer_Compat.sln +++ b/Multiplayer_Compat.sln @@ -1,7 +1,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer_Compat", "Multiplayer_Compat.csproj", "{163F72FB-89D2-4FA7-B392-D31513C473BE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer_Compat", "Source\Multiplayer_Compat.csproj", "{163F72FB-89D2-4FA7-B392-D31513C473BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer_Compat_Referenced", "Source_Referenced\Multiplayer_Compat_Referenced.csproj", "{65FF869B-3A3E-418A-8734-4D70FE2D0B1F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -13,5 +15,9 @@ Global {163F72FB-89D2-4FA7-B392-D31513C473BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {163F72FB-89D2-4FA7-B392-D31513C473BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {163F72FB-89D2-4FA7-B392-D31513C473BE}.Release|Any CPU.Build.0 = Release|Any CPU + {65FF869B-3A3E-418A-8734-4D70FE2D0B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65FF869B-3A3E-418A-8734-4D70FE2D0B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65FF869B-3A3E-418A-8734-4D70FE2D0B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65FF869B-3A3E-418A-8734-4D70FE2D0B1F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Source/MpCompat.cs b/Source/MpCompat.cs index 0d304d84..76c582d2 100644 --- a/Source/MpCompat.cs +++ b/Source/MpCompat.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using HarmonyLib; @@ -10,11 +11,17 @@ namespace Multiplayer.Compat { public class MpCompat : Mod { + const string REFERENCES_FOLDER = "References"; internal static readonly Harmony harmony = new Harmony("rimworld.multiplayer.compat"); public MpCompat(ModContentPack content) : base(content) { - if (!MP.enabled) return; + if (!MP.enabled) { + Log.Warning($"MPCompat :: Multiplayer is disabled. Running in Reference Building mode.\nPut any reference building requests under {REFERENCES_FOLDER} as {{DLLNAME}}.txt"); + ReferenceBuilder.Restore(Path.Combine(content.RootDir, REFERENCES_FOLDER)); + Log.Warning($"MPCompat :: Done Rebuilding. Bailing out..."); + return; + } MpCompatLoader.Load(content); harmony.PatchAll(); diff --git a/Source/MpCompatLoader.cs b/Source/MpCompatLoader.cs index 08b6fd47..ddebc904 100644 --- a/Source/MpCompatLoader.cs +++ b/Source/MpCompatLoader.cs @@ -1,6 +1,8 @@ using System; +using System.IO; using System.Linq; using System.Reflection; +using Mono.Cecil; using Verse; namespace Multiplayer.Compat @@ -9,9 +11,43 @@ public static class MpCompatLoader { internal static void Load(ModContentPack content) { + LoadConditional(content); + foreach (var asm in content.assemblies.loadedAssemblies) InitCompatInAsm(asm); } + + static void LoadConditional(ModContentPack content) + { + var asmPath = ModContentPack + .GetAllFilesForModPreserveOrder(content, "Referenced/", f => f.ToLower() == ".dll") + .FirstOrDefault(f => f.Item2.Name == "Multiplayer_Compat_Referenced.dll")?.Item2; + + if (asmPath == null) + { + return; + } + + var asm = AssemblyDefinition.ReadAssembly(asmPath.FullName); + + foreach (var t in asm.MainModule.GetTypes().ToArray()) + { + var attr = t.CustomAttributes + .FirstOrDefault(a => a.Constructor.DeclaringType.Name == nameof(MpCompatForAttribute)); + if (attr == null) continue; + + var modId = (string)attr.ConstructorArguments.First().Value; + var mod = LoadedModManager.RunningMods.FirstOrDefault(m => m.PackageId.NoModIdSuffix() == modId); + if (mod == null) + asm.MainModule.Types.Remove(t); + } + + var stream = new MemoryStream(); + asm.Write(stream); + + var loadedAsm = AppDomain.CurrentDomain.Load(stream.ToArray()); + InitCompatInAsm(loadedAsm); + } static void InitCompatInAsm(Assembly asm) { diff --git a/Source/Multiplayer_Compat.csproj b/Source/Multiplayer_Compat.csproj index 681b9eb3..4495294b 100644 --- a/Source/Multiplayer_Compat.csproj +++ b/Source/Multiplayer_Compat.csproj @@ -10,6 +10,7 @@ false None + 2.1.* diff --git a/Source/ReferenceBuilder.cs b/Source/ReferenceBuilder.cs new file mode 100644 index 00000000..7d0e3196 --- /dev/null +++ b/Source/ReferenceBuilder.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Mono.Cecil; +using Verse; + +namespace Multiplayer.Compat +{ + static class ReferenceBuilder + { + public static void Restore(string refsFolder) + { + var requestedFiles = Directory.CreateDirectory(refsFolder).GetFiles("*.txt"); + + foreach(var request in requestedFiles) + { + BuildReference(refsFolder, request); + } + } + + static void BuildReference(string refsFolder, FileInfo request) + { + var asmId = Path.GetFileNameWithoutExtension(request.Name); + + var assembly = LoadedModManager.RunningModsListForReading + .SelectMany(m => ModContentPack.GetAllFilesForModPreserveOrder(m, "Assemblies/", f => f.ToLower() == ".dll")) + .FirstOrDefault(f => f.Item2.Name == asmId + ".dll")?.Item2; + + var hash = ComputeHash(assembly.FullName); + var hashFile = Path.Combine(refsFolder, asmId + ".txt"); + + if (File.Exists(hashFile) && File.ReadAllText(hashFile) == hash) + return; + + Console.WriteLine($"MpCompat References: Writing {asmId}.dll"); + + var outFile = Path.Combine(refsFolder, asmId + ".dll"); + var asmDef = AssemblyDefinition.ReadAssembly(assembly.FullName); + + foreach (var t in asmDef.MainModule.GetTypes()) + { + if (t.IsNested) + t.IsNestedPublic = true; + else + t.IsPublic = true; + + foreach (var m in t.Methods) + { + m.IsPublic = true; + m.Body = new Mono.Cecil.Cil.MethodBody(m); + } + + foreach (var f in t.Fields) + { + f.IsInitOnly = false; + f.IsPublic = true; + } + } + + asmDef.Write(outFile); + File.WriteAllText(hashFile, hash); + } + + static string ComputeHash(string assemblyPath) + { + var res = new StringBuilder(); + + using var hash = SHA1.Create(); + using FileStream file = File.Open(assemblyPath, FileMode.Open, FileAccess.Read); + + hash.ComputeHash(file); + + foreach (byte b in hash.Hash) + res.Append(b.ToString("X2")); + + return res.ToString(); + } + } +} \ No newline at end of file diff --git a/Source_Referenced/Multiplayer_Compat_Referenced.csproj b/Source_Referenced/Multiplayer_Compat_Referenced.csproj new file mode 100644 index 00000000..6ed87e40 --- /dev/null +++ b/Source_Referenced/Multiplayer_Compat_Referenced.csproj @@ -0,0 +1,33 @@ + + + + net472 + 9.0 + false + false + ..\Referenced\ + false + false + None + + + + false + + + 2.1.* + runtime + + + 0.3.0 + runtime + + + 1.3.* + runtime + + + false + + +