using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using Mono.Cecil; using NStrip; using UnityEditor; using UnityEngine; namespace MeatKit { /// /// Assembly importer class to get the managed assemblies from the game into the Unity editor without /// the editor wanting to crash itself. Original implementation by Nolenz. /// https://github.com/WurstModders/WurstMod-Reloaded/blob/2e33e83284b3a9f39c8df210ad907925d1d7d9d8/WMRWorkbench/Assets/Editor/Manglers/AssemblyMangler.cs /// public static partial class MeatKit { private const string AssemblyName = "Assembly-CSharp"; private const string AssemblyRename = "H3VRCode-CSharp"; private const string AssemblyFirstpassName = "Assembly-CSharp-firstpass"; private const string AssemblyFirstpassRename = "H3VRCode-CSharp-passfirst"; // Types we want to strip from the main Unity assembly private static readonly string[] StripAssemblyTypes = { // Alloy classes "MaterialMapChannelPackerDefinition", "Alloy.PackedMapDefinition", "Alloy.BaseTextureChannelMapping", "Alloy.MapChannel", "Alloy.TextureValueChannelMode", "Alloy.NormalMapChannelTextureChannelMapping", "Alloy.TextureImportConfig", "Alloy.MapTextureChannelMapping", "AlloyUtils", "Alloy.EnumExtension", "MinValueAttribute", "MaxValueAttribute", "AlloyEffectsManager", "Alloy.EnumFlagsAttribute", // Bakery MonoBehaviours "BakeryAlwaysRender", "BakeryDirectLight", "BakeryLightmapGroup", "BakeryLightmapGroupSelector", "BakeryLightmappedPrefab", "BakeryLightMesh", "BakeryPointLight", "BakerySkyLight", "BakeryVolume", "BakeryVolumeReceiver", "BakeryVolumeTrigger", "BakeryProjectSettings", "VolumeTestScene2", "BakeryPackAsSingleSquare", "BakerySector", "BakerySectorCapture", "ftGlobalStorage", "ftLightmaps", "ftLightmapsStorage", "ftLocalStorage", // Bakery supporting types "ftUniqueIDRegistry", "BakeryLightmapGroupPlain", //Editor Tool Scripts "IconCamera" }; // Array of the extra assemblies that need to come with the main Unity assemblies private static readonly string[] ExtraAssemblies = { "DinoFracture.dll", "ES2.dll" }; private static void ImportAssemblies(string assembliesDirectory, string destinationDirectory) { // Remove whatever was there before and make the folder again if (!Directory.Exists(destinationDirectory)) Directory.CreateDirectory(destinationDirectory); // Load all of our modifiers var editors = Extensions.GetAllInstances(); foreach (var editor in editors) editor.Applied = false; // We need a custom assembly resolver that sometimes points to different directories. var rParams = new ReaderParameters { AssemblyResolver = new RedirectedAssemblyResolver(assembliesDirectory, destinationDirectory) }; // Rename the game's firstpass assembly { var firstpassAssembly = AssemblyDefinition.ReadAssembly(Path.Combine(assembliesDirectory, AssemblyFirstpassName + ".dll")); firstpassAssembly.Name = new AssemblyNameDefinition(AssemblyFirstpassRename, firstpassAssembly.Name.Version); firstpassAssembly.MainModule.Name = AssemblyFirstpassRename + ".dll"; // Apply modifications foreach (var editor in editors) editor.ApplyModification(firstpassAssembly); // Publicize Assembly AssemblyStripper.MakePublic(firstpassAssembly, new string[0], false, false); firstpassAssembly.Write(Path.Combine(destinationDirectory, AssemblyFirstpassRename + ".dll")); firstpassAssembly.Dispose(); } // Main assembly { // Rename the main assembly var mainAssembly = AssemblyDefinition.ReadAssembly(Path.Combine(assembliesDirectory, AssemblyName + ".dll"), rParams); mainAssembly.Name = new AssemblyNameDefinition(AssemblyRename, mainAssembly.Name.Version); mainAssembly.MainModule.Name = AssemblyRename + ".dll"; // Change the firstpass reference in this assembly mainAssembly.MainModule.AssemblyReferences .First(x => x.Name == AssemblyFirstpassName) .Name = AssemblyFirstpassRename; // Strip some types from the assembly to prevent doubles in the editor foreach (var typename in StripAssemblyTypes) { var type = mainAssembly.MainModule.GetType(typename); if (type != null) mainAssembly.MainModule.Types.Remove(type); else Debug.LogWarning("Type " + typename + " was not found in assembly."); } // Apply modifications foreach (var editor in editors) editor.ApplyModification(mainAssembly); // Publicize assembly AssemblyStripper.MakePublic(mainAssembly, new string[0], false, false); // Write the main assembly out into the destination folder and dispose it mainAssembly.Write(Path.Combine(destinationDirectory, AssemblyRename + ".dll")); } // Then lastly copy the other assemblies to the destination folder foreach (var file in ExtraAssemblies) { var path = Path.Combine(assembliesDirectory, file); if (File.Exists(path)) ImportSingleAssembly(path, destinationDirectory); } // Check if anything didn't apply foreach (var editor in editors) if (!editor.Applied) Debug.LogWarning(editor.name + " was not applied while importing.", editor); // When we're done importing assemblies, let Unity refresh the asset database PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, "H3VR_IMPORTED"); NormalizeMetaFileGUIDs(); } private static void ImportSingleAssembly(string assemblyPath, string destinationDirectory) { var rParams = new ReaderParameters { AssemblyResolver = new RedirectedAssemblyResolver(Path.GetDirectoryName(assemblyPath), destinationDirectory) }; // We just want to rename the references to the game's assemblies here var asm = AssemblyDefinition.ReadAssembly(assemblyPath, rParams); foreach (var reference in asm.MainModule.AssemblyReferences) switch (reference.Name) { case AssemblyName: reference.Name = AssemblyRename; break; case AssemblyFirstpassName: reference.Name = AssemblyFirstpassRename; break; } asm.Write(Path.Combine(destinationDirectory, Path.GetFileName(assemblyPath))); NormalizeMetaFileGUIDs(); } private static void NormalizeMetaFileGUIDs() { // This is a really important step. We need to make sure that the meta files for the assemblies are generated // WITH THE SAME GUIDs each time. Otherwise, if you lose one and didn't have a backup, all your scripts will be missing // and that is of course no bueno. Unity expects 32 hexadecimal digits for the guid so we'll use md5. // We need every meta file to exist already. AssetDatabase.Refresh(); var hashFunction = MD5.Create(); var replaceWith = new Regex(@"^guid: [0-9a-f]{32}$", RegexOptions.Multiline); foreach (var metaFile in Directory.GetFiles(ManagedDirectory, "*.meta")) { // First we get the hash var assemblyName = Path.GetFileName(metaFile.Substring(0, metaFile.Length - 5)); var hash = hashFunction.ComputeHash(Encoding.UTF8.GetBytes(assemblyName)); var hexHash = Extensions.ByteArrayToString(hash).ToLower(); // Then we need to replace the hash in the meta file with it. var metaText = File.ReadAllText(metaFile); metaText = replaceWith.Replace(metaText, "guid: " + hexHash); File.WriteAllText(metaFile, metaText); } // If anything was changed we need Unity to apply it immediately. AssetDatabase.Refresh(); } /// /// Assembly resolver that redirects references to another path if not found. /// private class RedirectedAssemblyResolver : BaseAssemblyResolver { private readonly DefaultAssemblyResolver _defaultResolver = new DefaultAssemblyResolver(); private readonly string[] _redirectPaths; public RedirectedAssemblyResolver(params string[] redirectPath) { _redirectPaths = redirectPath; } public override AssemblyDefinition Resolve(AssemblyNameReference name) { AssemblyDefinition asm = null; try { asm = _defaultResolver.Resolve(name); } catch (AssemblyResolutionException) { foreach (var path in _redirectPaths) try { var asmPath = Path.Combine(path, name.Name + ".dll"); Debug.Log("Assembly path: " + asmPath); if (File.Exists(asmPath)) asm = AssemblyDefinition.ReadAssembly(asmPath, new ReaderParameters {AssemblyResolver = this}); } catch (AssemblyResolutionException) { // Ignored } } if (asm != null) return asm; throw new AssemblyResolutionException(name); } } } }