mirror of
https://github.com/muskit/H3VR-TNH-Quality-of-Life-Improvements.git
synced 2026-06-02 20:24:26 -07:00
229 lines
11 KiB
C#
229 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using BepInEx;
|
|
using HarmonyLib;
|
|
using Mono.Cecil;
|
|
using Mono.Cecil.Cil;
|
|
using MethodBody = Mono.Cecil.Cil.MethodBody;
|
|
|
|
namespace MeatKit
|
|
{
|
|
public static partial class MeatKit
|
|
{
|
|
private const string EditorAssemblyPath = "Library/ScriptAssemblies/";
|
|
|
|
private static void ExportEditorAssembly(string folder, string tempFile = null,
|
|
Dictionary<string, List<string>> requiredScripts = null)
|
|
{
|
|
// Make a copy of the file if we aren't already given one
|
|
string editorAssembly = EditorAssemblyPath + AssemblyName + ".dll";
|
|
if (!File.Exists(editorAssembly) && !File.Exists(tempFile))
|
|
{
|
|
throw new MeatKitBuildException("Editor assembly missing! Can't export scripts.");
|
|
}
|
|
if (string.IsNullOrEmpty(tempFile))
|
|
{
|
|
tempFile = Path.GetTempFileName();
|
|
File.Copy(editorAssembly, tempFile, true);
|
|
}
|
|
|
|
// Delete the old file
|
|
var settings = BuildWindow.SelectedProfile;
|
|
var exportPath = folder + settings.PackageName + ".dll";
|
|
if (File.Exists(exportPath)) File.Delete(exportPath);
|
|
|
|
var rParams = new ReaderParameters
|
|
{
|
|
AssemblyResolver =
|
|
new RedirectedAssemblyResolver(Path.GetDirectoryName(typeof(UnityEngine.Object).Assembly.Location), ManagedDirectory)
|
|
};
|
|
|
|
// Get the MeatKitPlugin class and rename it
|
|
using (var asm = AssemblyDefinition.ReadAssembly(tempFile, rParams))
|
|
{
|
|
// Locate the plugin class for this profile and set it's name and namespace
|
|
string mainNamespace = BuildWindow.SelectedProfile.MainNamespace;
|
|
var plugin = FindPluginClass(asm.MainModule, mainNamespace);
|
|
BuildLog.WriteLine("Using plugin class " + plugin.FullName);
|
|
plugin.Namespace = mainNamespace;
|
|
plugin.Name = settings.PackageName + "Plugin";
|
|
|
|
// Watermark the plugin just in case it's useful to someone
|
|
BuildLog.WriteLine("Watermarking plugin class");
|
|
var str = asm.MainModule.TypeSystem.String;
|
|
var descriptionAttributeConstructor = typeof(DescriptionAttribute).GetConstructor(new[] {typeof(string)});
|
|
var descriptionAttributeRef = asm.MainModule.ImportReference(descriptionAttributeConstructor);
|
|
var descriptionAttribute = new CustomAttribute(descriptionAttributeRef);
|
|
descriptionAttribute.ConstructorArguments.Add(new CustomAttributeArgument(str, "Built with MeatKit"));
|
|
plugin.CustomAttributes.Add(descriptionAttribute);
|
|
|
|
// This is some quantum bullshit.
|
|
// If you don't enumerate the constructor arguments for attributes their values aren't updated correctly.
|
|
BuildLog.WriteLine("Performing quantum bullshit");
|
|
foreach (var x in GetAllCustomAttributes(asm).SelectMany(a => a.ConstructorArguments))
|
|
{
|
|
}
|
|
|
|
// Get the BepInPlugin attribute and replace the values in it with our own
|
|
BuildLog.WriteLine("Applying BepInPlugin attribute params");
|
|
var guid = settings.Author + "." + settings.PackageName;
|
|
var pluginAttribute = plugin.CustomAttributes.First(a => a.AttributeType.Name == "BepInPlugin");
|
|
pluginAttribute.ConstructorArguments[0] = new CustomAttributeArgument(str, guid);
|
|
pluginAttribute.ConstructorArguments[1] = new CustomAttributeArgument(str, settings.PackageName);
|
|
pluginAttribute.ConstructorArguments[2] = new CustomAttributeArgument(str, settings.Version);
|
|
|
|
// Get the LoadAssets method and make a new body for it
|
|
BuildLog.WriteLine("Generating LoadAssets()");
|
|
var loadAssetsMethod = plugin.Methods.First(m => m.Name == "LoadAssets");
|
|
loadAssetsMethod.Body = new MethodBody(loadAssetsMethod);
|
|
var il = loadAssetsMethod.Body.GetILProcessor();
|
|
|
|
// If we're automatically applying Harmony patches, do that now
|
|
// This IL translates to Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "PluginGuid");
|
|
if (settings.ApplyHarmonyPatches)
|
|
{
|
|
BuildLog.WriteLine(" Code to apply harmony patches");
|
|
var assemblyGetExecutingAssembly = typeof(Assembly).GetMethod("GetExecutingAssembly");
|
|
var harmonyCreateAndPatchALl = typeof(Harmony).GetMethod("CreateAndPatchAll", new[] {typeof(Assembly), typeof(string)});
|
|
il.Emit(OpCodes.Call, plugin.Module.ImportReference(assemblyGetExecutingAssembly));
|
|
il.Emit(OpCodes.Ldstr, guid);
|
|
il.Emit(OpCodes.Call, plugin.Module.ImportReference(harmonyCreateAndPatchALl));
|
|
il.Emit(OpCodes.Pop);
|
|
}
|
|
|
|
// Let any build items insert their own code in here
|
|
foreach (var item in settings.BuildItems)
|
|
{
|
|
BuildLog.WriteLine(" " + item);
|
|
item.GenerateLoadAssets(plugin, il);
|
|
}
|
|
|
|
// Insert a ret at the end so it's valid
|
|
il.Emit(OpCodes.Ret);
|
|
|
|
// Module name needs to be changed away from Assembly-CSharp.dll because it is a reserved name.
|
|
string newAssemblyName = settings.PackageName + ".dll";
|
|
BuildLog.WriteLine("Renaming assembly (" + newAssemblyName + ")");
|
|
asm.Name = new AssemblyNameDefinition(settings.PackageName, asm.Name.Version);
|
|
asm.MainModule.Name = newAssemblyName;
|
|
|
|
// References to renamed unity code must be swapped out.
|
|
BuildLog.WriteLine("Renaming assembly references");
|
|
foreach (var ii in asm.MainModule.AssemblyReferences)
|
|
{
|
|
// Rename any references to the game's code
|
|
if (ii.Name.Contains("H3VRCode-CSharp"))
|
|
{
|
|
var newReference = ii.Name.Replace("H3VRCode-CSharp", "Assembly-CSharp");
|
|
BuildLog.WriteLine(" " + ii.Name + " -> " + newReference);
|
|
ii.Name = newReference;
|
|
}
|
|
|
|
// And also if we're referencing a MonoMod DLL, we need to fix reference too
|
|
if (ii.Name.EndsWith(".mm"))
|
|
{
|
|
// What the name currently is:
|
|
// Assembly-CSharp.PatchName.mm
|
|
// What we want:
|
|
// Assembly-CSharp
|
|
// So just lop off anything past the second to last dot
|
|
int idx = ii.Name.LastIndexOf('.', ii.Name.Length - 4);
|
|
var newReference = ii.Name.Substring(0, idx);
|
|
BuildLog.WriteLine(" " + ii.Name + " -> " + newReference);
|
|
ii.Name = newReference;
|
|
}
|
|
}
|
|
|
|
if (BuildWindow.SelectedProfile.StripNamespaces)
|
|
{
|
|
// Remove types not in an allowed namespace or the global namespace
|
|
BuildLog.WriteLine("Stripping namespaces");
|
|
string[] allowedNamespaces = BuildWindow.SelectedProfile.GetAllAllowedNamespaces();
|
|
List<TypeDefinition> typesToRemove = new List<TypeDefinition>();
|
|
foreach (var type in asm.MainModule.Types)
|
|
{
|
|
if (type.Namespace == "" || allowedNamespaces.Any(x => type.Namespace.Contains(x)))
|
|
continue;
|
|
BuildLog.WriteLine(" " + type.FullName);
|
|
typesToRemove.Add(type);
|
|
}
|
|
|
|
foreach (var type in typesToRemove) asm.MainModule.Types.Remove(type);
|
|
}
|
|
|
|
// Remove the same types we didn't want to import. This cannot be skipped.
|
|
foreach (var type in StripAssemblyTypes
|
|
.Select(x => asm.MainModule.GetType(x))
|
|
.Where(x => x != null)) asm.MainModule.Types.Remove(type);
|
|
|
|
// Check if we're now missing any scripts from the export
|
|
BuildLog.WriteLine("Checking for missing types");
|
|
List<string> missing = new List<string>();
|
|
string originalAssemblyName = AssemblyName + ".dll";
|
|
if (requiredScripts != null && requiredScripts.ContainsKey(originalAssemblyName))
|
|
{
|
|
missing.AddRange(requiredScripts[originalAssemblyName]
|
|
.Where(typeName => !StripAssemblyTypes.Contains(typeName) && asm.MainModule.GetType(typeName) == null));
|
|
}
|
|
|
|
// If we're missing anything, fail the build.
|
|
if (missing.Count > 0)
|
|
{
|
|
string missingTypes = string.Join("\n", missing.ToArray());
|
|
throw new MeatKitBuildException(
|
|
"Exported objects reference scripts which do not exist in the exported assembly... Did you forget to allow a namespace?\n\nMissing types:\n" +
|
|
missingTypes, null);
|
|
}
|
|
|
|
try
|
|
{
|
|
// Save it
|
|
asm.Write(exportPath);
|
|
}
|
|
catch (ArgumentException e)
|
|
{
|
|
throw new MeatKitBuildException(
|
|
"Unable to write exported scripts file. This is likely due to namespace stripping being enabled and a required namespace is not whitelisted.",
|
|
e);
|
|
}
|
|
}
|
|
|
|
// Delete temp file now that we're done.
|
|
File.Delete(tempFile);
|
|
}
|
|
|
|
private static TypeDefinition FindPluginClass(ModuleDefinition module, string mainNamespace)
|
|
{
|
|
// Get the default MeatKitPlugin class from the module
|
|
var pluginClass = module.GetType("MeatKitPlugin");
|
|
|
|
// Try and locate any alternative plugin classes
|
|
foreach (var type in module.Types)
|
|
{
|
|
// We're looking for types that extend the BaseUnityPlugin class and is in the main namespace of our mod
|
|
if (type.IsSubtypeOf(typeof(BaseUnityPlugin)) && type.Namespace == mainNamespace)
|
|
{
|
|
pluginClass = type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return pluginClass;
|
|
}
|
|
|
|
private static IEnumerable<CustomAttribute> GetAllCustomAttributes(AssemblyDefinition asm)
|
|
{
|
|
foreach (var type in asm.MainModule.Types)
|
|
{
|
|
foreach (var attrib in type.CustomAttributes) yield return attrib;
|
|
foreach (CustomAttribute attrib in type.Methods.SelectMany(method => method.CustomAttributes))
|
|
yield return attrib;
|
|
}
|
|
}
|
|
}
|
|
}
|