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 { public const string AssemblyName = "Assembly-CSharp"; public const string AssemblyRename = "H3VRCode-CSharp"; public const string AssemblyFirstpassName = "Assembly-CSharp-firstpass"; public const string AssemblyFirstpassRename = "H3VRCode-CSharp-firstpass"; // Types we want to strip from the main Unity assembly public 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); // Apply help URLs ApplyWikiHelpAttribute(mainAssembly); // 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) }; // If this assembly uses the Assembly-CSharp name at all for any reason, replace it with H3VRCode-CSharp // This would probably only be done on MonoMod patches but is required to make Unity shut up var asm = AssemblyDefinition.ReadAssembly(assemblyPath, rParams); string name = asm.Name.Name; if (name.Contains("Assembly-CSharp")) { name = name.Replace("Assembly-CSharp", "H3VRCode-CSharp"); asm.Name = new AssemblyNameDefinition(name, asm.Name.Version); asm.MainModule.Name = name + ".dll"; } // Replace all occurrences to references of Assembly-CSharp with H3VRCode-CSharp foreach (var reference in asm.MainModule.AssemblyReferences) { if (reference.Name.Contains("Assembly-CSharp")) { reference.Name = reference.Name.Replace("Assembly-CSharp", "H3VRCode-CSharp"); } } asm.Write(Path.Combine(destinationDirectory, asm.MainModule.Name)); NormalizeMetaFileGUIDs(); } private static void ApplyWikiHelpAttribute(AssemblyDefinition asm) { // For convenience, we can add the Unity HelpURL attribute to the components from the game assembly. // We'll point the url at the wiki and just append the full type name at the end // Iterate over every type in the assembly and just stick the attribute on it // Probably doesn't matter if types that don't need it have it. foreach (var type in asm.MainModule.Types) { // If the type doesn't already have this attribute, add it. if (type.CustomAttributes.Any(a => a.AttributeType.Name == "HelpURLAttribute")) continue; string helpUrl = "https://h3vr-modding.github.io/wiki/docs/h3vr/" + type.FullName + ".html"; var str = asm.MainModule.TypeSystem.String; var attributeConstructor = typeof(HelpURLAttribute).GetConstructor(new[] {typeof(string)}); var attributeRef = asm.MainModule.ImportReference(attributeConstructor); var attribute = new CustomAttribute(attributeRef); attribute.ConstructorArguments.Add(new CustomAttributeArgument(str, helpUrl)); type.CustomAttributes.Add(attribute); } } 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"); if (File.Exists(asmPath)) asm = AssemblyDefinition.ReadAssembly(asmPath, new ReaderParameters {AssemblyResolver = this}); } catch (AssemblyResolutionException) { // Ignored } } if (asm != null) return asm; throw new AssemblyResolutionException(name); } } } }