// Copyright (c) 2023 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. extern alias nuget; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Security; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; using nuget::JetBrains.Annotations; using PerformanceFish.ModCompatibility; using CollectionExtensions = PerformanceFish.Utility.CollectionExtensions; using MethodBody = Mono.Cecil.Cil.MethodBody; namespace PerformanceFish.Prepatching; public static class PrepatchManager { public static bool PatchingFinished { get; private set; } [UsedImplicitly] public static void Start(ModuleDefinition module) { if (PatchingFinished) { ThrowHelper.ThrowInvalidOperationException("Tried running free patches a second time. This should " + "never happen. Cancelling now to avoid further issues."); } Log.Message(LogPatches.PERFORMANCE_FISH_WELCOME_MESSAGE); if (!ActiveMods.Prepatcher) Log.Warning("Prepatches running without prepatcher active. This should normally not happen."); var stopwatch = new Stopwatch(); stopwatch.Start(); // if (Type.GetType("Prepatcher.Process.FreePatcher, PrepatcherImpl") is { } freePatcherType // && AccessTools.Method(freePatcherType, "AddSkipVerificationAttribute") is { } addSkipVerificationMethod // /*Type.GetType("HugsLib.Patches.MainMenuDrawer_Quickstart_Patch, HugsLib") is { } freePatcherType // && AccessTools.Method(freePatcherType, "QuicktestButtonUsesQuickstarter") is { } addSkipVerificationMethod*/) // { // var rimworldAssembly = typeof(Root).Assembly; // var harmonyAssembly = typeof(Harmony).Assembly; // var prepatcherAssembly = freePatcherType.Assembly; // var fishAssembly = typeof(PerformanceFishMod).Assembly; // var monoModAssembly = typeof(MonoMod.Utils.Cil.ILGeneratorShim).Assembly; // // SetReflectionOnly(rimworldAssembly, false, out var previousRimworldValue); // SetReflectionOnly(harmonyAssembly, false, out var previousHarmonyValue); // SetReflectionOnly(prepatcherAssembly, false, out var previousPrepatcherValue); // SetReflectionOnly(monoModAssembly, false, out var previousMonoModValue); // SetReflectionOnly(fishAssembly, false, out var previousFishValue); // // PerformanceFishMod.Harmony.Patch(addSkipVerificationMethod, // prefix: new(methodof(FreePatcherSkipVerificationPatch))); // // SetReflectionOnly(rimworldAssembly, previousRimworldValue, out _); // SetReflectionOnly(harmonyAssembly, previousHarmonyValue, out _); // SetReflectionOnly(prepatcherAssembly, previousPrepatcherValue, out _); // SetReflectionOnly(fishAssembly, previousMonoModValue, out _); // SetReflectionOnly(fishAssembly, previousFishValue, out _); // } Application.logMessageReceivedThreaded -= Verse.Log.Notify_MessageReceivedThreadedInternal; AddAttributes(module); ModifyAllTypes(module); var allPrepatchClasses = PerformanceFishMod.AllPrepatchClasses = PerformanceFishMod.InitializeAllPatchClasses(); _ = FishSettings.Instance; allPrepatchClasses.ApplyPatches(module); FishStash.Get.InitializeActivePrepatchIDs(); PatchingFinished = true; stopwatch.Stop(); Verse.Log.Message($"Performance Fish finished applying prepatches in { stopwatch.ElapsedSecondsAccurate():N7} seconds"); } // private static readonly FieldInfo? _monoAssemblyField = AccessTools.Field(typeof(Assembly), "_mono_assembly"); // // internal static unsafe void SetReflectionOnly(Assembly asm, bool value, out bool previousValue) // { // var refOnlyField = (int*)((IntPtr)_monoAssemblyField!.GetValue(asm) + 0x74); // previousValue = *refOnlyField == 1 || (*refOnlyField == 0 ? false : throw new()); // *refOnlyField = value ? 1 : 0; // } // // public static bool FreePatcherSkipVerificationPatch() => false; private static void ModifyAllTypes(ModuleDefinition module) { var types = module.Types; // Parallel.For(0, types.Count, // about 20% faster than sequential for in testing // i => ModifyAllMethodsIn(types[i])); for (var i = 0; i < types.Count; i++) ModifyAllMethodsIn(types[i]); } private static void ModifyAllMethodsIn(TypeDefinition type) { if (type.Methods is not { } methods) return; for (var i = methods.Count; i-- > 0;) { if (methods[i].Body is { } body) ModifyMethodBody(body); } if (type.NestedTypes is not { } nestedTypes) return; for (var i = nestedTypes.Count; i-- > 0;) ModifyAllMethodsIn(nestedTypes[i]); } private static void ModifyMethodBody(MethodBody body) { ApplySkipLocalsInit(body); ModifyInstructions(body.Method, body.Instructions); } [UsedImplicitly(ImplicitUseTargetFlags.Members)] private enum LinqMethod { None, First, FirstOrDefault, Where, Any, Select, Count, Contains, Sum } [SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "UnusedMember.Local")] private static class LinqStrings { public const string First = "First", FirstOrDefault = "FirstOrDefault", Where = "Where", Any = "Any", Select = "Select", Count = "Count", Contains = "Contains", Sum = "Sum", DeclaringType = "System.Linq.Enumerable"; } private static void ModifyInstructions(MethodDefinition method, Collection instructions) { for (var i = 0; i < instructions.Count; i++) { var instruction = instructions[i]; if (instruction.OpCode != OpCodes.Call) continue; if (i - 4 < 0) continue; if (!(i + 1 < instructions.Count && instruction.Operand is MethodReference { Name: LinqStrings.Where, DeclaringType.FullName: LinqStrings.DeclaringType } && instructions[i + 1].Operand is MethodReference { Name: LinqStrings.FirstOrDefault or LinqStrings.First, DeclaringType.FullName: LinqStrings.DeclaringType }) && instruction.Operand is not MethodReference { Name: LinqStrings.FirstOrDefault or LinqStrings.First, DeclaringType.FullName: LinqStrings.DeclaringType }) { continue; } var linqTarget = LinqMethod.FirstOrDefault; if (((MethodReference)instruction.Operand).Name == LinqStrings.First || (i + 1 < instructions.Count && ((MethodReference)instruction.Operand).Name == LinqStrings.Where && ((MethodReference)instructions[i + 1].Operand).Name == LinqStrings.First)) { linqTarget = LinqMethod.First; } if (!(instructions[i - 1].OpCode == OpCodes.Newobj && instructions[i - 2].OpCode == OpCodes.Ldftn) && !(instructions[i - 1].OpCode == OpCodes.Stsfld && instructions[i - 2].OpCode == OpCodes.Dup && instructions[i - 3].OpCode == OpCodes.Newobj && instructions[i - 4].OpCode == OpCodes.Ldftn)) continue; var inputMethodInstruction = instructions[i - 4]; var inputMethod = inputMethodInstruction.OpCode == OpCodes.Call ? inputMethodInstruction.Operand as MethodReference : null; if (inputMethod is null && i - 6 < 0) continue; if ((inputMethod ??= (inputMethodInstruction = instructions[i - 6]).Operand as MethodReference) is null) continue; var module = method.Module; if (inputMethod!.Name == "get_AllDefs") { inputMethodInstruction.Operand = inputMethod = module.ImportReference((inputMethod.DeclaringType.ContainsGenericParameter ? typeof(DefDatabase<>).GetProperty(nameof(DefDatabase.AllDefsListForReading)) : typeof(DefDatabase<>) .MakeGenericType(AccessTools.TypeByName(((GenericInstanceType)inputMethod.DeclaringType) .GenericArguments[0].FullName)) .GetProperty(nameof(DefDatabase.AllDefsListForReading)))!.GetMethod, inputMethod); } var inputReturnType = inputMethod.ReturnType; if (inputReturnType.Name == "List`1") { var containsGenericParameter = inputReturnType.ContainsGenericParameter && (inputReturnType.TryGetGenericArguments() ?.Any(a => a.TryGetGenericParameterType(inputMethod) is null) ?? true); var listFindMethod = linqTarget == LinqMethod.FirstOrDefault ? containsGenericParameter ? typeof(List<>).GetMethod(nameof(List.Find)) : typeof(List<>) .MakeGenericType(AccessTools.TypeByName(inputReturnType .TryGetGenericArguments(inputMethod)![0].FullName)).GetMethod(nameof(List.Find)) : methodof((Func, Predicate, object>)CollectionExtensions.First) .GetGenericMethodDefinition(); if (linqTarget == LinqMethod.First && !containsGenericParameter) { listFindMethod = listFindMethod!.MakeGenericMethod(AccessTools.TypeByName(inputReturnType .TryGetGenericArguments(inputMethod)![0].FullName)); } var newOperand = containsGenericParameter ? module.ImportReference(listFindMethod, method) : module.ImportReference(listFindMethod); // Log.Message($"Performance Fish successfully optimized { // ((MethodReference)instruction.Operand).FullName} within method { // method.FullName} by applying new operand {newOperand.FullName}"); var instructionsToRemove = 0; if (instruction.Operand is MethodReference { Name: LinqStrings.Where }) instructionsToRemove++; instruction.Operand = newOperand; while (instructionsToRemove-- > 0) instructions.RemoveAt(i + 1); } } } private static void ApplySkipLocalsInit(MethodBody body) => body.InitLocals = false; private static void AddAttributes(ModuleDefinition module) { AddModuleAttributes(module); AddAssemblyAttributes(module); // AddSecurityAttributes(module.Assembly.SecurityDeclarations, module); } private static void AddAssemblyAttributes(ModuleDefinition module) { var rimworldCustomAttributes = module.Assembly.CustomAttributes; if (TryAddAttribute(module, rimworldCustomAttributes, typeof(IgnoresAccessChecksToAttribute), typeof(string)) is { } ignoresAccessChecksToAttribute) { ignoresAccessChecksToAttribute.ConstructorArguments.Add(new(module.TypeSystem.String, "mscorlib")); } TryAddAttribute(module, rimworldCustomAttributes, typeof(AllowPartiallyTrustedCallersAttribute)); TryAddAttribute(module, rimworldCustomAttributes, typeof(SecurityTransparentAttribute)); if (TryAddAttribute(module, rimworldCustomAttributes, typeof(SecurityRulesAttribute), typeof(SecurityRuleSet)) is { } securityRulesAttribute) { securityRulesAttribute.ConstructorArguments.Add(new(module.ImportReference(typeof(SecurityRuleSet)), SecurityRuleSet.Level2)); securityRulesAttribute.Properties.Add(new(nameof(SecurityRulesAttribute.SkipVerificationInFullTrust), new(module.TypeSystem.Boolean, true))); } var debuggableAttribute = rimworldCustomAttributes.First(static a => a.AttributeType.Name == nameof(DebuggableAttribute)); debuggableAttribute.Constructor = module.ImportReference(typeof(DebuggableAttribute) .GetConstructor([typeof(bool), typeof(bool)])); debuggableAttribute.ConstructorArguments.Clear(); debuggableAttribute.ConstructorArguments.Add(new(module.TypeSystem.Boolean, false)); debuggableAttribute.ConstructorArguments.Add(new(module.TypeSystem.Boolean, false)); } private static void AddModuleAttributes(ModuleDefinition module) => TryAddAttribute(module, module.CustomAttributes, typeof(UnverifiableCodeAttribute)); // internal static void AddSecurityAttributes(Collection securityDeclarations, // ModuleDefinition module, [CallerArgumentExpression(nameof(securityDeclarations))] string? targetName = null) // { // if (securityDeclarations.Any(static s => s.SecurityAttributes // .Any(static a => a.AttributeType.Name == nameof(SecurityPermissionAttribute) // && a.Properties.Any(static p => p.Name == nameof(SecurityPermissionAttribute.SkipVerification))))) // { // return; // } // // if (targetName != null) // Log.Message($"Added skipVerification attribute to {targetName}"); // // securityDeclarations.Add(new(Mono.Cecil.SecurityAction.RequestMinimum) // { // SecurityAttributes = // { // new SecurityAttribute(module.ImportReference(typeof(SecurityPermissionAttribute))) // { // Properties = // { // new(nameof(SecurityPermissionAttribute.SkipVerification), // new(module.TypeSystem.Boolean, true)) // } // } // } // }); // } public static CustomAttribute? TryAddAttribute(ModuleDefinition module, IList attributes, Type attributeType, params Type[] parameters) { if (ContainsAttribute(attributes, attributeType.Name)) return null; var attribute = ImportAttribute(module, attributeType, parameters); attributes.Add(attribute); return attribute; } private static CustomAttribute ImportAttribute(ModuleDefinition module, Type attributeType, params Type[] parameters) => new(module.ImportReference(AccessTools.Constructor(attributeType, parameters))); private static bool ContainsAttribute(IList attributes, string name) { for (var i = attributes.Count; i-- > 0;) { if (attributes[i].AttributeType.Name == name) return true; } return false; } }