Skip to content

Commit

Permalink
Allow referenced assemblies
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
notfood committed Oct 15, 2021
1 parent bdb7fa4 commit 5f5fcfa
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,4 @@ ASALocalRun/
.mfractor/

Assemblies/
Referenced/
8 changes: 7 additions & 1 deletion Source/Multiplayer_Compat.sln → Multiplayer_Compat.sln
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
9 changes: 8 additions & 1 deletion Source/MpCompat.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using HarmonyLib;
Expand All @@ -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();
Expand Down
36 changes: 36 additions & 0 deletions Source/MpCompatLoader.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
using Verse;

namespace Multiplayer.Compat
Expand All @@ -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)
{
Expand Down
1 change: 1 addition & 0 deletions Source/Multiplayer_Compat.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<DebugType>None</DebugType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Lib.Harmony">
<Version>2.1.*</Version>
Expand Down
82 changes: 82 additions & 0 deletions Source/ReferenceBuilder.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
33 changes: 33 additions & 0 deletions Source_Referenced/Multiplayer_Compat_Referenced.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>9.0</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<OutputPath>..\Referenced\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<DebugType>None</DebugType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Source/Multiplayer_Compat.csproj">
<Private>false</Private>
</ProjectReference>
<PackageReference Include="Lib.Harmony">
<Version>2.1.*</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="RimWorld.MultiplayerAPI">
<Version>0.3.0</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="Krafs.Rimworld.Ref">
<Version>1.3.*</Version>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<Reference Include="..\References\*.dll">
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>

0 comments on commit 5f5fcfa

Please sign in to comment.