diff --git a/FastTrack/FastCmps.cs b/FastTrack/FastCmps.cs index bcd9d061..20d39fc3 100644 --- a/FastTrack/FastCmps.cs +++ b/FastTrack/FastCmps.cs @@ -1,388 +1,389 @@ -/* - * Copyright 2024 Peter Han - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -using HarmonyLib; -using PeterHan.PLib.Core; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; - -using OldList = System.Collections.IList; -using TranspiledMethod = System.Collections.Generic.IEnumerable; - -namespace PeterHan.FastTrack { - /// - /// Patches the Components.Cmps class to avoid the KCompactedVector altogether. - /// - public static class FastCmps { - /// - /// The generic types to patch. - /// - private static readonly ISet TYPES_TO_PATCH = new HashSet(); - - /// - /// Generates method bodies for Add and Remove. - /// - private delegate TranspiledMethod GenerateBody(Type t, FieldInfo itemsField, - Label end); - - /// - /// Computes the types that will be patched since Harmony can only patch concrete - /// implementations. - /// - private static void ComputeTargetTypes() { - var target = typeof(Components.Cmps<>); - var container = typeof(Components.CmpsByWorld<>); - if (TYPES_TO_PATCH.Count < 1) - foreach (var field in typeof(Components).GetFields(PPatchTools.BASE_FLAGS | - BindingFlags.Static | BindingFlags.Instance)) { - var type = field.FieldType; - if (type.IsConstructedGenericType && !type.ContainsGenericParameters) { - var def = type.GetGenericTypeDefinition(); - if (def == target || def == container) { - // Components.CmpsByWorld<...> - var subtype = type.GenericTypeArguments[0]; - TYPES_TO_PATCH.Add(subtype); - } - } - } - } - - /// - /// Shares common code between the Add and Remove transpilers. - /// - /// The original method to transpile. - /// The Cmps method being transpiled. - /// The current IL generator. - /// The event field name to invoke on success. - /// A delegate which generates the meat of the method. - /// The replacement method. - private static TranspiledMethod TranspilerBase(TranspiledMethod instructions, - MethodBase originalMethod, ILGenerator generator, string evtName, - GenerateBody methodBody) { - var cmpType = originalMethod.DeclaringType; - bool patched = false; - var result = instructions; - if (cmpType != null && cmpType.IsConstructedGenericType && cmpType. - GetGenericTypeDefinition() == typeof(Components.Cmps<>)) { - // Components.Cmps - var t = cmpType.GenericTypeArguments[0]; - var itemsField = cmpType.GetFieldSafe(nameof(Components.Cmps.items), - false); - var eventField = cmpType.GetFieldSafe(evtName, false); - var kcv = typeof(KCompactedVector<>).MakeGenericType(t); - // Need concrete versions of the methods in that class - var getDataList = kcv.GetMethodSafe(nameof(KCompactedVector. - GetDataList), false); - var invokeAction = eventField?.FieldType.GetMethodSafe(nameof(Action. - Invoke), false, t); - var newMethod = new List(32); - if (itemsField != null && eventField != null && getDataList != null && - invokeAction != null) { - var evt = generator.DeclareLocal(eventField.FieldType); - var end = generator.DefineLabel(); - // Load items field - newMethod.Add(new CodeInstruction(OpCodes.Ldarg_0)); - newMethod.Add(new CodeInstruction(OpCodes.Ldfld, itemsField)); - // Call GetDataList() - newMethod.Add(new CodeInstruction(OpCodes.Callvirt, getDataList)); - newMethod.Add(new CodeInstruction(OpCodes.Ldarg_1)); - newMethod.AddRange(methodBody.Invoke(t, itemsField, end)); - // Load event field - newMethod.Add(new CodeInstruction(OpCodes.Ldarg_0)); - newMethod.Add(new CodeInstruction(OpCodes.Ldfld, eventField)); - // Exit if null - newMethod.Add(new CodeInstruction(OpCodes.Dup)); - newMethod.Add(new CodeInstruction(OpCodes.Stloc_S, (byte)evt.LocalIndex)); - newMethod.Add(new CodeInstruction(OpCodes.Brfalse_S, end)); - // Call Invoke - newMethod.Add(new CodeInstruction(OpCodes.Ldloc_S, (byte)evt.LocalIndex)); - newMethod.Add(new CodeInstruction(OpCodes.Ldarg_1)); - newMethod.Add(new CodeInstruction(OpCodes.Callvirt, invokeAction)); - // Return - newMethod.Add(new CodeInstruction(OpCodes.Ret).WithLabels(end)); -#if DEBUG - PUtil.LogDebug("Patched " + cmpType.FullName + "." + originalMethod.Name); -#endif - patched = true; - } - result = newMethod; - } - if (!patched) - PUtil.LogWarning("Unable to patch " + cmpType?.FullName + "." + originalMethod. - Name); - return result; - } - - /// - /// Applied to Components.Cmps to speed up Add. - /// - [HarmonyPatch] - internal static class Add_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.MinimalKCV; - - /// - /// Target Add() on each required type. - /// - internal static IEnumerable TargetMethods() { - ComputeTargetTypes(); - foreach (var type in TYPES_TO_PATCH) - yield return typeof(Components.Cmps<>).MakeGenericType(type). - GetMethodSafe(nameof(Components.Cmps.Add), false, type); - } - - /// - /// Generates the Cmps.Add body. - /// - private static TranspiledMethod GenerateMethodBody(Type t, FieldInfo itemsField, - Label _) { - var addToList = typeof(List<>).MakeGenericType(t).GetMethodSafe(nameof( - List.Add), false, t); - if (addToList == null) - throw new ArgumentException("Unable to find List.Add"); - // Add to the list - yield return new CodeInstruction(OpCodes.Callvirt, addToList); - } - - /// - /// Replace the method body entirely with a new one, which unfortunately needs to - /// be done here to handle the generic types. - /// - internal static TranspiledMethod Transpiler(TranspiledMethod instructions, - MethodBase originalMethod, ILGenerator generator) { - return TranspilerBase(instructions, originalMethod, generator, "OnAdd", - GenerateMethodBody); - } - } - - /// - /// Applied to Components.Cmps to reduce allocations in GetWorldItems. - /// - [HarmonyPatch] - internal static class GetWorldItems_Patch { - /// - /// The preallocate size to pass to the factory created Lists. - /// - private static readonly object[] CONSTRUCTOR_ARGS = { 32 }; - - /// - /// The signature of the constructor to call with preallocate size. - /// - private static readonly Type[] CONSTRUCTOR_SIG = { typeof(int) }; - - /// - /// Stores pooled lists to limit allocations. - /// - private static readonly IDictionary POOL = new Dictionary(64); - - internal static bool Prepare() => FastTrackOptions.Instance.AllocOpts; - - /// - /// Gets a shared list instance that works with the specified container type. - /// - /// The container requesting a list. - /// A shared list compatible with that container type. - private static OldList GetList(object container) { - var type = container.GetType(); - var pool = POOL; - if (!pool.TryGetValue(type, out var list)) { - // Due to the Harmony bug, the type that actually is being used has to be - // computed at runtime, as opposed to static pooled lists like ListPool - var types = type.GenericTypeArguments; - var elementType = typeof(KMonoBehaviour); - if (types != null && types.Length > 0) - elementType = types[0]; - // Only called once per type, not worth making a delegate - var constructor = typeof(List<>).MakeGenericType(elementType). - GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Instance, null, - CONSTRUCTOR_SIG, null); - if (constructor != null && constructor.Invoke(CONSTRUCTOR_ARGS) is - OldList newList) - list = newList; - if (list == null) - list = new List(32); -#if DEBUG - PUtil.LogDebug("Created world items list for type " + type); -#endif - pool.Add(type, list); - } - list.Clear(); - return list; - } - - /// - /// Target GetWorldItems() on each required type. - /// - internal static IEnumerable TargetMethods() { - ComputeTargetTypes(); - foreach (var type in TYPES_TO_PATCH) - yield return typeof(Components.Cmps<>).MakeGenericType(type).GetMethodSafe( - nameof(Components.Cmps.GetWorldItems), false, - typeof(int), typeof(bool)); - } - - /// - /// Replace the new list call to get a list from the pool instead. Due to the - /// Harmony bug this transpiler has to pretty much always work. - /// - internal static TranspiledMethod Transpiler(TranspiledMethod instructions, - MethodBase originalMethod) { - var containerType = originalMethod.DeclaringType; - var replacement = typeof(GetWorldItems_Patch).GetMethodSafe(nameof( - GetList), true, typeof(object)); - ConstructorInfo targetConstructor = null; - bool patched = false; - // Find the target List constructor to patch - if (containerType != null && !containerType.ContainsGenericParameters) { - var typeArgs = containerType.GenericTypeArguments; - if (typeArgs != null && typeArgs.Length > 0) { - var targetType = typeof(List<>).MakeGenericType(typeArgs[0]); - targetConstructor = targetType.GetConstructor(PPatchTools. - BASE_FLAGS | BindingFlags.Instance, null, Type.EmptyTypes, null); - } - } - if (targetConstructor != null && replacement != null) - foreach (var instr in instructions) { - if (instr.Is(OpCodes.Newobj, targetConstructor)) { - yield return new CodeInstruction(OpCodes.Ldarg_0); - instr.opcode = OpCodes.Call; - instr.operand = replacement; -#if DEBUG - PUtil.LogDebug("Patched " + containerType + "." + originalMethod. - Name); -#endif - patched = true; - } - yield return instr; - } - else - foreach (var instr in instructions) - yield return instr; - if (!patched) - PUtil.LogWarning("Unable to patch " + containerType + "." + - originalMethod.Name); - } - } - - /// - /// Applied to Components.Cmps to speed up Remove. - /// - [HarmonyPatch] - internal static class Remove_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.MinimalKCV; - - /// - /// Target Add() on each required type. - /// - internal static IEnumerable TargetMethods() { - ComputeTargetTypes(); - foreach (var type in TYPES_TO_PATCH) - yield return typeof(Components.Cmps<>).MakeGenericType(type). - GetMethodSafe(nameof(Components.Cmps.Remove), false, type); - } - - /// - /// Generates the Cmps.Remove body. - /// - private static TranspiledMethod GenerateMethodBody(Type t, FieldInfo itemsField, - Label end) { - var removeFromList = typeof(List<>).MakeGenericType(t).GetMethodSafe(nameof( - List.Remove), false, t); - if (removeFromList == null) - throw new ArgumentException("Unable to find List.Remove"); - // Try removing from the list - yield return new CodeInstruction(OpCodes.Callvirt, removeFromList); - // If failed, skip - yield return new CodeInstruction(OpCodes.Brfalse_S, end); - } - - /// - /// Replace the method body entirely with a new one, which unfortunately needs to - /// be done here to handle the generic types. - /// - internal static TranspiledMethod Transpiler(TranspiledMethod instructions, - MethodBase originalMethod, ILGenerator generator) { - return TranspilerBase(instructions, originalMethod, generator, "OnRemove", - GenerateMethodBody); - } - } - } - - /// - /// Applied to Workable to prevent the use of GetWorldItems in the event handler, which - /// could be called from a delegate in rocket landing that is really hard to patch. - /// - [HarmonyPatch(typeof(Workable), nameof(Workable.UpdateStatusItem))] - public static class Workable_UpdateStatusItem_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.AllocOpts; - - /// - /// Applied before UpdateStatusItem runs. - /// - [HarmonyPriority(Priority.Low)] - internal static bool Prefix(Workable __instance) { - if (__instance.TryGetComponent(out KSelectable selectable)) { - var working = __instance.workingStatusItem; - ref var statusHandle = ref __instance.workStatusItemHandle; - selectable.RemoveStatusItem(statusHandle); - if (__instance.worker == null) - UpdateStatusItem(__instance, selectable, ref statusHandle); - else if (working != null) - statusHandle = selectable.AddStatusItem(working, __instance); - } - return false; - } - - /// - /// Updates the status item shown on each skill-required workable, when Duplicants - /// transfer between colonies (or new skills granted / Duplicants printed). - /// - /// The workable to update. - /// The location where the status item will be added. - /// Stores a reference to the status item so it can be - /// destroyed later. - private static void UpdateStatusItem(Workable instance, KSelectable selectable, - ref Guid statusHandle) { - string perk = instance.requiredSkillPerk; - int worldID = instance.GetMyWorldId(); - var duplicants = Components.LiveMinionIdentities.Items; - var dbb = Db.Get().BuildingStatusItems; - int n = duplicants.Count; - // Manually filter to avoid GetWorldItems mutating the shared list again - bool noMinions = instance.requireMinionToWork; - for (int i = 0; i < n && noMinions; i++) { - var duplicant = duplicants[i]; - noMinions = duplicant == null || duplicant.GetMyWorldId() != worldID; - } - if (noMinions) - statusHandle = selectable.AddStatusItem(dbb.WorkRequiresMinion); - else if (instance.shouldShowSkillPerkStatusItem && !string.IsNullOrEmpty(perk)) { - if (MinionResume.AnyMinionHasPerk(perk, worldID)) - statusHandle = selectable.AddStatusItem(instance. - readyForSkillWorkStatusItem, perk); - else { - var statusItem = DlcManager.FeatureClusterSpaceEnabled() ? dbb. - ClusterColonyLacksRequiredSkillPerk : dbb.ColonyLacksRequiredSkillPerk; - statusHandle = selectable.AddStatusItem(statusItem, perk); - } - } - } - } -} +/* + * Copyright 2024 Peter Han + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using HarmonyLib; +using PeterHan.PLib.Core; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; + +using OldList = System.Collections.IList; +using TranspiledMethod = System.Collections.Generic.IEnumerable; + +namespace PeterHan.FastTrack { + /// + /// Patches the Components.Cmps class to avoid the KCompactedVector altogether. + /// + public static class FastCmps { + /// + /// The generic types to patch. + /// + private static readonly ISet TYPES_TO_PATCH = new HashSet(); + + /// + /// Generates method bodies for Add and Remove. + /// + private delegate TranspiledMethod GenerateBody(Type t, FieldInfo itemsField, + Label end); + + /// + /// Computes the types that will be patched since Harmony can only patch concrete + /// implementations. + /// + private static void ComputeTargetTypes() { + var target = typeof(Components.Cmps<>); + var container = typeof(Components.CmpsByWorld<>); + if (TYPES_TO_PATCH.Count < 1) + foreach (var field in typeof(Components).GetFields(PPatchTools.BASE_FLAGS | + BindingFlags.Static | BindingFlags.Instance)) { + var type = field.FieldType; + if (type.IsConstructedGenericType && !type.ContainsGenericParameters) { + var def = type.GetGenericTypeDefinition(); + if (def == target || def == container) { + // Components.CmpsByWorld<...> + var subtype = type.GenericTypeArguments[0]; + TYPES_TO_PATCH.Add(subtype); + } + } + } + } + + /// + /// Shares common code between the Add and Remove transpilers. + /// + /// The original method to transpile. + /// The Cmps method being transpiled. + /// The current IL generator. + /// The event field name to invoke on success. + /// A delegate which generates the meat of the method. + /// The replacement method. + private static TranspiledMethod TranspilerBase(TranspiledMethod instructions, + MethodBase originalMethod, ILGenerator generator, string evtName, + GenerateBody methodBody) { + var cmpType = originalMethod.DeclaringType; + bool patched = false; + var result = instructions; + if (cmpType != null && cmpType.IsConstructedGenericType && cmpType. + GetGenericTypeDefinition() == typeof(Components.Cmps<>)) { + // Components.Cmps + var t = cmpType.GenericTypeArguments[0]; + var itemsField = cmpType.GetFieldSafe(nameof(Components.Cmps.items), + false); + var eventField = cmpType.GetFieldSafe(evtName, false); + var kcv = typeof(KCompactedVector<>).MakeGenericType(t); + // Need concrete versions of the methods in that class + var getDataList = kcv.GetMethodSafe(nameof(KCompactedVector. + GetDataList), false); + var invokeAction = eventField?.FieldType.GetMethodSafe(nameof(Action. + Invoke), false, t); + var newMethod = new List(32); + if (itemsField != null && eventField != null && getDataList != null && + invokeAction != null) { + var evt = generator.DeclareLocal(eventField.FieldType); + var end = generator.DefineLabel(); + // Load items field + newMethod.Add(new CodeInstruction(OpCodes.Ldarg_0)); + newMethod.Add(new CodeInstruction(OpCodes.Ldfld, itemsField)); + // Call GetDataList() + newMethod.Add(new CodeInstruction(OpCodes.Callvirt, getDataList)); + newMethod.Add(new CodeInstruction(OpCodes.Ldarg_1)); + newMethod.AddRange(methodBody.Invoke(t, itemsField, end)); + // Load event field + newMethod.Add(new CodeInstruction(OpCodes.Ldarg_0)); + newMethod.Add(new CodeInstruction(OpCodes.Ldfld, eventField)); + // Exit if null + newMethod.Add(new CodeInstruction(OpCodes.Dup)); + newMethod.Add(new CodeInstruction(OpCodes.Stloc_S, (byte)evt.LocalIndex)); + newMethod.Add(new CodeInstruction(OpCodes.Brfalse_S, end)); + // Call Invoke + newMethod.Add(new CodeInstruction(OpCodes.Ldloc_S, (byte)evt.LocalIndex)); + newMethod.Add(new CodeInstruction(OpCodes.Ldarg_1)); + newMethod.Add(new CodeInstruction(OpCodes.Callvirt, invokeAction)); + // Return + newMethod.Add(new CodeInstruction(OpCodes.Ret).WithLabels(end)); +#if DEBUG + PUtil.LogDebug("Patched " + cmpType.FullName + "." + originalMethod.Name); +#endif + patched = true; + } + result = newMethod; + } + if (!patched) + PUtil.LogWarning("Unable to patch " + cmpType?.FullName + "." + originalMethod. + Name); + return result; + } + + /// + /// Applied to Components.Cmps to speed up Add. + /// + [HarmonyPatch] + internal static class Add_Patch { + internal static bool Prepare() => FastTrackOptions.Instance.MinimalKCV; + + /// + /// Target Add() on each required type. + /// + internal static IEnumerable TargetMethods() { + ComputeTargetTypes(); + foreach (var type in TYPES_TO_PATCH) + yield return typeof(Components.Cmps<>).MakeGenericType(type). + GetMethodSafe(nameof(Components.Cmps.Add), false, type); + } + + /// + /// Generates the Cmps.Add body. + /// + private static TranspiledMethod GenerateMethodBody(Type t, FieldInfo itemsField, + Label _) { + var addToList = typeof(List<>).MakeGenericType(t).GetMethodSafe(nameof( + List.Add), false, t); + if (addToList == null) + throw new ArgumentException("Unable to find List.Add"); + // Add to the list + yield return new CodeInstruction(OpCodes.Callvirt, addToList); + } + + /// + /// Replace the method body entirely with a new one, which unfortunately needs to + /// be done here to handle the generic types. + /// + internal static TranspiledMethod Transpiler(TranspiledMethod instructions, + MethodBase originalMethod, ILGenerator generator) { + return TranspilerBase(instructions, originalMethod, generator, "OnAdd", + GenerateMethodBody); + } + } + + /// + /// Applied to Components.Cmps to reduce allocations in GetWorldItems. + /// + [HarmonyPatch] + internal static class GetWorldItems_Patch { + /// + /// The preallocate size to pass to the factory created Lists. + /// + private static readonly object[] CONSTRUCTOR_ARGS = { 32 }; + + /// + /// The signature of the constructor to call with preallocate size. + /// + private static readonly Type[] CONSTRUCTOR_SIG = { typeof(int) }; + + /// + /// Stores pooled lists to limit allocations. + /// + private static readonly IDictionary POOL = new Dictionary(64); + + internal static bool Prepare() => FastTrackOptions.Instance.AllocOpts; + + /// + /// Gets a shared list instance that works with the specified container type. + /// + /// The container requesting a list. + /// A shared list compatible with that container type. + private static OldList GetList(object container) { + var type = container.GetType(); + var pool = POOL; + if (!pool.TryGetValue(type, out var list)) { + // Due to the Harmony bug, the type that actually is being used has to be + // computed at runtime, as opposed to static pooled lists like ListPool + var types = type.GenericTypeArguments; + var elementType = typeof(KMonoBehaviour); + if (types != null && types.Length > 0) + elementType = types[0]; + // Only called once per type, not worth making a delegate + var constructor = typeof(List<>).MakeGenericType(elementType). + GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Instance, null, + CONSTRUCTOR_SIG, null); + if (constructor != null && constructor.Invoke(CONSTRUCTOR_ARGS) is + OldList newList) + list = newList; + if (list == null) + list = new List(32); +#if DEBUG + PUtil.LogDebug("Created world items list for type " + type); +#endif + pool.Add(type, list); + } + list.Clear(); + return list; + } + + /// + /// Target GetWorldItems() on each required type. + /// + internal static IEnumerable TargetMethods() { + ComputeTargetTypes(); + foreach (var type in TYPES_TO_PATCH) + yield return typeof(Components.Cmps<>).MakeGenericType(type).GetMethodSafe( + nameof(Components.Cmps.GetWorldItems), false, + typeof(int), typeof(ICollection), typeof(Func<,>). + MakeGenericType(type, typeof(bool))); + } + + /// + /// Replace the new list call to get a list from the pool instead. Due to the + /// Harmony bug this transpiler has to pretty much always work. + /// + internal static TranspiledMethod Transpiler(TranspiledMethod instructions, + MethodBase originalMethod) { + var containerType = originalMethod.DeclaringType; + var replacement = typeof(GetWorldItems_Patch).GetMethodSafe(nameof( + GetList), true, typeof(object)); + ConstructorInfo targetConstructor = null; + bool patched = false; + // Find the target List constructor to patch + if (containerType != null && !containerType.ContainsGenericParameters) { + var typeArgs = containerType.GenericTypeArguments; + if (typeArgs != null && typeArgs.Length > 0) { + var targetType = typeof(List<>).MakeGenericType(typeArgs[0]); + targetConstructor = targetType.GetConstructor(PPatchTools. + BASE_FLAGS | BindingFlags.Instance, null, Type.EmptyTypes, null); + } + } + if (targetConstructor != null && replacement != null) + foreach (var instr in instructions) { + if (instr.Is(OpCodes.Newobj, targetConstructor)) { + yield return new CodeInstruction(OpCodes.Ldarg_0); + instr.opcode = OpCodes.Call; + instr.operand = replacement; +#if DEBUG + PUtil.LogDebug("Patched " + containerType + "." + originalMethod. + Name); +#endif + patched = true; + } + yield return instr; + } + else + foreach (var instr in instructions) + yield return instr; + if (!patched) + PUtil.LogWarning("Unable to patch " + containerType + "." + + originalMethod.Name); + } + } + + /// + /// Applied to Components.Cmps to speed up Remove. + /// + [HarmonyPatch] + internal static class Remove_Patch { + internal static bool Prepare() => FastTrackOptions.Instance.MinimalKCV; + + /// + /// Target Add() on each required type. + /// + internal static IEnumerable TargetMethods() { + ComputeTargetTypes(); + foreach (var type in TYPES_TO_PATCH) + yield return typeof(Components.Cmps<>).MakeGenericType(type). + GetMethodSafe(nameof(Components.Cmps.Remove), false, type); + } + + /// + /// Generates the Cmps.Remove body. + /// + private static TranspiledMethod GenerateMethodBody(Type t, FieldInfo itemsField, + Label end) { + var removeFromList = typeof(List<>).MakeGenericType(t).GetMethodSafe(nameof( + List.Remove), false, t); + if (removeFromList == null) + throw new ArgumentException("Unable to find List.Remove"); + // Try removing from the list + yield return new CodeInstruction(OpCodes.Callvirt, removeFromList); + // If failed, skip + yield return new CodeInstruction(OpCodes.Brfalse_S, end); + } + + /// + /// Replace the method body entirely with a new one, which unfortunately needs to + /// be done here to handle the generic types. + /// + internal static TranspiledMethod Transpiler(TranspiledMethod instructions, + MethodBase originalMethod, ILGenerator generator) { + return TranspilerBase(instructions, originalMethod, generator, "OnRemove", + GenerateMethodBody); + } + } + } + + /// + /// Applied to Workable to prevent the use of GetWorldItems in the event handler, which + /// could be called from a delegate in rocket landing that is really hard to patch. + /// + [HarmonyPatch(typeof(Workable), nameof(Workable.UpdateStatusItem))] + public static class Workable_UpdateStatusItem_Patch { + internal static bool Prepare() => FastTrackOptions.Instance.AllocOpts; + + /// + /// Applied before UpdateStatusItem runs. + /// + [HarmonyPriority(Priority.Low)] + internal static bool Prefix(Workable __instance) { + if (__instance.TryGetComponent(out KSelectable selectable)) { + var working = __instance.workingStatusItem; + ref var statusHandle = ref __instance.workStatusItemHandle; + selectable.RemoveStatusItem(statusHandle); + if (__instance.worker == null) + UpdateStatusItem(__instance, selectable, ref statusHandle); + else if (working != null) + statusHandle = selectable.AddStatusItem(working, __instance); + } + return false; + } + + /// + /// Updates the status item shown on each skill-required workable, when Duplicants + /// transfer between colonies (or new skills granted / Duplicants printed). + /// + /// The workable to update. + /// The location where the status item will be added. + /// Stores a reference to the status item so it can be + /// destroyed later. + private static void UpdateStatusItem(Workable instance, KSelectable selectable, + ref Guid statusHandle) { + string perk = instance.requiredSkillPerk; + int worldID = instance.GetMyWorldId(); + var duplicants = Components.LiveMinionIdentities.Items; + var dbb = Db.Get().BuildingStatusItems; + int n = duplicants.Count; + // Manually filter to avoid GetWorldItems mutating the shared list again + bool noMinions = instance.requireMinionToWork; + for (int i = 0; i < n && noMinions; i++) { + var duplicant = duplicants[i]; + noMinions = duplicant == null || duplicant.GetMyWorldId() != worldID; + } + if (noMinions) + statusHandle = selectable.AddStatusItem(dbb.WorkRequiresMinion); + else if (instance.shouldShowSkillPerkStatusItem && !string.IsNullOrEmpty(perk)) { + if (MinionResume.AnyMinionHasPerk(perk, worldID)) + statusHandle = selectable.AddStatusItem(instance. + readyForSkillWorkStatusItem, perk); + else { + var statusItem = DlcManager.FeatureClusterSpaceEnabled() ? dbb. + ClusterColonyLacksRequiredSkillPerk : dbb.ColonyLacksRequiredSkillPerk; + statusHandle = selectable.AddStatusItem(statusItem, perk); + } + } + } + } +} diff --git a/FastTrack/FastTrack.csproj b/FastTrack/FastTrack.csproj index 38efb855..af925310 100644 --- a/FastTrack/FastTrack.csproj +++ b/FastTrack/FastTrack.csproj @@ -1,35 +1,35 @@ - - - - Fast Track - 0.14.0.0 - PeterHan.FastTrack - Optimizes Oxygen Not Included to improve performance. - 0.15.0.0 - 616718 - Vanilla;Mergedown - true - true - true - -namespace PeterHan.FastTrack { - public static class ModVersion { - public static string BUILD_VERSION = "$(LastWorkingBuild)"%3B - public static string FILE_VERSION = "$(FileVersion)"%3B - } -} - - - - - - - - - - - - - - - + + + + Fast Track + 0.15.0.0 + PeterHan.FastTrack + Optimizes Oxygen Not Included to improve performance. + 0.15.0.0 + 646843 + Vanilla;Mergedown + true + true + true + +namespace PeterHan.FastTrack { + public static class ModVersion { + public static string BUILD_VERSION = "$(LastWorkingBuild)"%3B + public static string FILE_VERSION = "$(FileVersion)"%3B + } +} + + + + + + + + + + + + + + + diff --git a/FastTrack/UIPatches/DetailsPanelWrapper.cs b/FastTrack/UIPatches/DetailsPanelWrapper.cs index 4f4dfde4..7303cda9 100644 --- a/FastTrack/UIPatches/DetailsPanelWrapper.cs +++ b/FastTrack/UIPatches/DetailsPanelWrapper.cs @@ -360,11 +360,11 @@ internal static bool Prefix(DetailsScreen __instance, string newName) { /// Applied to DetailsScreen to make opening the codex entry much faster! /// [HarmonyPatch(typeof(DetailsScreen), nameof(DetailsScreen.CodexEntryButton_OnClick))] - internal static class OpenCodexEntry_Patch { + internal static class CodexEntryButton_OnClick_Patch { internal static bool Prepare() => FastTrackOptions.Instance.SideScreenOpts; /// - /// Applied before OpenCodexEntry runs. + /// Applied before CodexEntryButton_OnClick runs. /// [HarmonyPriority(Priority.Low)] internal static bool Prefix() { diff --git a/FastTrack/UIPatches/UIPatches.cs b/FastTrack/UIPatches/UIPatches.cs index 96476b12..496adb9e 100644 --- a/FastTrack/UIPatches/UIPatches.cs +++ b/FastTrack/UIPatches/UIPatches.cs @@ -1,275 +1,278 @@ -/* - * Copyright 2024 Peter Han - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or - * substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING - * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, - * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -using HarmonyLib; -using PeterHan.PLib.Core; -using System.Collections.Generic; -using System.Reflection.Emit; -using UnityEngine; -using UnityEngine.UI; - -using TranspiledMethod = System.Collections.Generic.IEnumerable; - -namespace PeterHan.FastTrack.UIPatches { - /// - /// Applied to KChildFitter to add an updater to fit it only on layout changes. - /// - [HarmonyPatch(typeof(KChildFitter), nameof(KChildFitter.Awake))] - public static class KChildFitter_Awake_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.VirtualScroll; - - /// - /// Applied after Awake runs. - /// - internal static void Postfix(KChildFitter __instance) { - __instance.gameObject.AddOrGet(); - } - } - - /// - /// Applied to KChildFitter to turn off an expensive fitter method that runs every frame! - /// - [HarmonyPatch(typeof(KChildFitter), nameof(KChildFitter.LateUpdate))] - public static class KChildFitter_LateUpdate_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.VirtualScroll; - - /// - /// Applied before LateUpdate runs. - /// - [HarmonyPriority(Priority.Low)] - internal static bool Prefix() { - return false; - } - } - - /// - /// A layout element that triggers child fitting only if layout has actually changed. - /// - internal sealed class KChildFitterUpdater : KMonoBehaviour, ILayoutElement { - public float minWidth => -1.0f; - - public float preferredWidth => -1.0f; - - public float flexibleWidth => -1.0f; - - public float minHeight => -1.0f; - - public float preferredHeight => -1.0f; - - public float flexibleHeight => -1.0f; - - public int layoutPriority => int.MinValue; - -#pragma warning disable IDE0044 -#pragma warning disable CS0649 - // These fields are automatically populated by KMonoBehaviour - [MyCmpReq] - private KChildFitter fitter; -#pragma warning restore CS0649 -#pragma warning restore IDE0044 - - public void CalculateLayoutInputHorizontal() { - fitter.FitSize(); - } - - public void CalculateLayoutInputVertical() { } - } - - /// - /// Applied to MainMenu to get rid of the 15 MB file write and speed up boot! - /// - [HarmonyPatch(typeof(MainMenu), nameof(MainMenu.OnSpawn))] - public static class MainMenu_OnSpawn_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.LoadOpts; - - /// - /// Transpiles OnSpawn to remove everything in try/catch IOException blocks. - /// - internal static TranspiledMethod Transpiler(TranspiledMethod instructions) { - var method = new List(instructions); - int tryStart = -1, n = method.Count; - var remove = new RangeInt(-1, 1); - bool isIOBlock = false; - for (int i = 0; i < n; i++) { - var blocks = method[i].blocks; - if (blocks != null) - foreach (var block in blocks) - switch (block.blockType) { - case ExceptionBlockType.BeginExceptionBlock: - if (tryStart < 0) { - tryStart = i; - isIOBlock = false; - } - break; - case ExceptionBlockType.BeginCatchBlock: - if (tryStart >= 0) - isIOBlock = true; - break; - case ExceptionBlockType.EndExceptionBlock: - if (tryStart >= 0 && isIOBlock && remove.start < 0) { - remove.start = tryStart; - remove.length = i - tryStart + 1; -#if DEBUG - PUtil.LogDebug("Patched MainMenu.OnSpawn: {0:D} to {1:D}".F( - tryStart, i)); -#endif - tryStart = -1; - isIOBlock = false; - } - break; - } - } - if (remove.start >= 0) - method.RemoveRange(remove.start, remove.length); - else - PUtil.LogWarning("Unable to patch MainMenu.OnSpawn"); - return method; - } - } - - /// - /// Applied to NameDisplayScreen to destroy the thought prefabs if conversations are - /// turned off. - /// - [HarmonyPatch(typeof(NameDisplayScreen), nameof(NameDisplayScreen.RegisterComponent))] - public static class NameDisplayScreen_RegisterComponent_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.NoConversations; - - /// - /// Applied before RegisterComponent runs. - /// - [HarmonyPriority(Priority.Low)] - internal static bool Prefix(object component) { - var options = FastTrackOptions.Instance; - return !(component is ThoughtGraph.Instance && options.NoConversations); - } - } - - /// - /// Applied to TechItems to remove a duplicate Add call. - /// - [HarmonyPatch(typeof(Database.TechItems), nameof(Database.TechItems.AddTechItem))] - public static class TechItems_AddTechItem_Patch { - internal static bool Prepare() => FastTrackOptions.Instance.MiscOpts; - - /// - /// Transpiles AddTechItem to remove an Add call that duplicates every item, as it was - /// already added to the constructor. - /// - internal static TranspiledMethod Transpiler(TranspiledMethod instructions) { - var target = typeof(ResourceSet).GetMethodSafe(nameof( - ResourceSet.Add), false, typeof(TechItem)); - foreach (var instr in instructions) { - if (target != null && instr.Is(OpCodes.Callvirt, target)) { - // Original method was 1 arg to 1 arg and result was ignored, so just rip - // it out - instr.opcode = OpCodes.Nop; - instr.operand = null; -#if DEBUG - PUtil.LogDebug("Patched TechItems.AddTechItem"); -#endif - } - yield return instr; - } - } - } - - /// - /// Applied to TrackerTool to reduce the update rate from 50 trackers/frame to 10/frame. - /// - [HarmonyPatch(typeof(TrackerTool), nameof(TrackerTool.OnSpawn))] - public static class TrackerTool_OnSpawn_Patch { - /// - /// The number of trackers to update every rendered frame (disabled while paused). - /// - private const int UPDATES_PER_FRAME = 10; - - internal static bool Prepare() => FastTrackOptions.Instance.ReduceColonyTracking; - - /// - /// Applied after OnSpawn runs. - /// - internal static void Postfix(ref int ___numUpdatesPerFrame) { - ___numUpdatesPerFrame = UPDATES_PER_FRAME; - } - } - - /// - /// Applied to WorldInventory to fix a bug where Update does not run properly on the first - /// run due to a misplaced "break". - /// - [HarmonyPatch(typeof(WorldInventory), nameof(WorldInventory.Update))] - public static class WorldInventory_UpdateTweak_Patch { - internal static bool Prepare() { - var options = FastTrackOptions.Instance; - return options.MiscOpts && !options.ParallelInventory; - } - - /// - /// Transpiles Update to bypass the "break" if firstUpdate is true. Only matters on - /// the first frame. - /// - internal static TranspiledMethod Transpiler(TranspiledMethod instructions, - ILGenerator generator) { - var markerField = typeof(WorldInventory).GetFieldSafe(nameof(WorldInventory. - accessibleUpdateIndex), false); - var updateField = typeof(WorldInventory).GetFieldSafe(nameof(WorldInventory. - firstUpdate), false); - var getSCS = typeof(SpeedControlScreen).GetPropertySafe(nameof( - SpeedControlScreen.Instance), true)?.GetGetMethod(true); - var isPaused = typeof(SpeedControlScreen).GetMethodSafe(nameof(SpeedControlScreen. - IsPaused), false); - bool storeField = false, done = false; - var end = generator.DefineLabel(); - if (getSCS != null && isPaused != null) { - // Exit the method if the speed screen reports paused - yield return new CodeInstruction(OpCodes.Call, getSCS); - yield return new CodeInstruction(OpCodes.Callvirt, isPaused); - yield return new CodeInstruction(OpCodes.Brtrue, end); - } - foreach (var instr in instructions) { - var opcode = instr.opcode; - if (instr.opcode == OpCodes.Ret) { - // Label the end of the method - var labels = instr.labels; - if (labels == null) - instr.labels = labels = new List